printrun-src/printrun/pronsole.py

changeset 15
0bbb006204fc
child 38
700241b53fe7
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 import cmd
19 import glob
20 import os
21 import time
22 import threading
23 import sys
24 import shutil
25 import subprocess
26 import codecs
27 import argparse
28 import locale
29 import logging
30 import traceback
31 import re
32
33 from serial import SerialException
34
35 from . import printcore
36 from .utils import install_locale, run_command, get_command_output, \
37 format_time, format_duration, RemainingTimeEstimator, \
38 get_home_pos, parse_build_dimensions, parse_temperature_report, \
39 setup_logging
40 install_locale('pronterface')
41 from .settings import Settings, BuildDimensionsSetting
42 from .power import powerset_print_start, powerset_print_stop
43 from printrun import gcoder
44 from .rpc import ProntRPC
45
46 if os.name == "nt":
47 try:
48 import _winreg
49 except:
50 pass
51 READLINE = True
52 try:
53 import readline
54 try:
55 readline.rl.mode.show_all_if_ambiguous = "on" # config pyreadline on windows
56 except:
57 pass
58 except:
59 READLINE = False # neither readline module is available
60
61 tempreading_exp = re.compile("(^T:| T:)")
62
63 REPORT_NONE = 0
64 REPORT_POS = 1
65 REPORT_TEMP = 2
66 REPORT_MANUAL = 4
67
68 class Status(object):
69
70 def __init__(self):
71 self.extruder_temp = 0
72 self.extruder_temp_target = 0
73 self.bed_temp = 0
74 self.bed_temp_target = 0
75 self.print_job = None
76 self.print_job_progress = 1.0
77
78 def update_tempreading(self, tempstr):
79 temps = parse_temperature_report(tempstr)
80 if "T0" in temps and temps["T0"][0]: hotend_temp = float(temps["T0"][0])
81 elif "T" in temps and temps["T"][0]: hotend_temp = float(temps["T"][0])
82 else: hotend_temp = None
83 if "T0" in temps and temps["T0"][1]: hotend_setpoint = float(temps["T0"][1])
84 elif "T" in temps and temps["T"][1]: hotend_setpoint = float(temps["T"][1])
85 else: hotend_setpoint = None
86 if hotend_temp is not None:
87 self.extruder_temp = hotend_temp
88 if hotend_setpoint is not None:
89 self.extruder_temp_target = hotend_setpoint
90 bed_temp = float(temps["B"][0]) if "B" in temps and temps["B"][0] else None
91 if bed_temp is not None:
92 self.bed_temp = bed_temp
93 setpoint = temps["B"][1]
94 if setpoint:
95 self.bed_temp_target = float(setpoint)
96
97 @property
98 def bed_enabled(self):
99 return self.bed_temp != 0
100
101 @property
102 def extruder_enabled(self):
103 return self.extruder_temp != 0
104
105
106 class pronsole(cmd.Cmd):
107 def __init__(self):
108 cmd.Cmd.__init__(self)
109 if not READLINE:
110 self.completekey = None
111 self.status = Status()
112 self.dynamic_temp = False
113 self.compute_eta = None
114 self.statuscheck = False
115 self.status_thread = None
116 self.monitor_interval = 3
117 self.p = printcore.printcore()
118 self.p.recvcb = self.recvcb
119 self.p.startcb = self.startcb
120 self.p.endcb = self.endcb
121 self.p.layerchangecb = self.layer_change_cb
122 self.p.process_host_command = self.process_host_command
123 self.recvlisteners = []
124 self.in_macro = False
125 self.p.onlinecb = self.online
126 self.p.errorcb = self.logError
127 self.fgcode = None
128 self.filename = None
129 self.rpc_server = None
130 self.curlayer = 0
131 self.sdlisting = 0
132 self.sdlisting_echo = 0
133 self.sdfiles = []
134 self.paused = False
135 self.sdprinting = 0
136 self.uploading = 0 # Unused, just for pronterface generalization
137 self.temps = {"pla": "185", "abs": "230", "off": "0"}
138 self.bedtemps = {"pla": "60", "abs": "110", "off": "0"}
139 self.percentdone = 0
140 self.posreport = ""
141 self.tempreadings = ""
142 self.userm114 = 0
143 self.userm105 = 0
144 self.m105_waitcycles = 0
145 self.macros = {}
146 self.history_file = "~/.pronsole-history"
147 self.rc_loaded = False
148 self.processing_rc = False
149 self.processing_args = False
150 self.settings = Settings(self)
151 self.settings._add(BuildDimensionsSetting("build_dimensions", "200x200x100+0+0+0+0+0+0", _("Build dimensions"), _("Dimensions of Build Platform\n & optional offset of origin\n & optional switch position\n\nExamples:\n XXXxYYY\n XXX,YYY,ZZZ\n XXXxYYYxZZZ+OffX+OffY+OffZ\nXXXxYYYxZZZ+OffX+OffY+OffZ+HomeX+HomeY+HomeZ"), "Printer"), self.update_build_dimensions)
152 self.settings._port_list = self.scanserial
153 self.settings._temperature_abs_cb = self.set_temp_preset
154 self.settings._temperature_pla_cb = self.set_temp_preset
155 self.settings._bedtemp_abs_cb = self.set_temp_preset
156 self.settings._bedtemp_pla_cb = self.set_temp_preset
157 self.update_build_dimensions(None, self.settings.build_dimensions)
158 self.update_tcp_streaming_mode(None, self.settings.tcp_streaming_mode)
159 self.monitoring = 0
160 self.starttime = 0
161 self.extra_print_time = 0
162 self.silent = False
163 self.commandprefixes = 'MGT$'
164 self.promptstrs = {"offline": "%(bold)soffline>%(normal)s ",
165 "fallback": "%(bold)sPC>%(normal)s ",
166 "macro": "%(bold)s..>%(normal)s ",
167 "online": "%(bold)sT:%(extruder_temp_fancy)s%(progress_fancy)s>%(normal)s "}
168
169 # --------------------------------------------------------------
170 # General console handling
171 # --------------------------------------------------------------
172
173 def postloop(self):
174 self.p.disconnect()
175 cmd.Cmd.postloop(self)
176
177 def preloop(self):
178 self.log(_("Welcome to the printer console! Type \"help\" for a list of available commands."))
179 self.prompt = self.promptf()
180 cmd.Cmd.preloop(self)
181
182 # We replace this function, defined in cmd.py .
183 # It's default behavior with regards to Ctr-C
184 # and Ctr-D doesn't make much sense...
185 def cmdloop(self, intro=None):
186 """Repeatedly issue a prompt, accept input, parse an initial prefix
187 off the received input, and dispatch to action methods, passing them
188 the remainder of the line as argument.
189
190 """
191
192 self.preloop()
193 if self.use_rawinput and self.completekey:
194 try:
195 import readline
196 self.old_completer = readline.get_completer()
197 readline.set_completer(self.complete)
198 readline.parse_and_bind(self.completekey + ": complete")
199 history = os.path.expanduser(self.history_file)
200 if os.path.exists(history):
201 readline.read_history_file(history)
202 except ImportError:
203 pass
204 try:
205 if intro is not None:
206 self.intro = intro
207 if self.intro:
208 self.stdout.write(str(self.intro) + "\n")
209 stop = None
210 while not stop:
211 if self.cmdqueue:
212 line = self.cmdqueue.pop(0)
213 else:
214 if self.use_rawinput:
215 try:
216 line = raw_input(self.prompt)
217 except EOFError:
218 self.log("")
219 self.do_exit("")
220 except KeyboardInterrupt:
221 self.log("")
222 line = ""
223 else:
224 self.stdout.write(self.prompt)
225 self.stdout.flush()
226 line = self.stdin.readline()
227 if not len(line):
228 line = ""
229 else:
230 line = line.rstrip('\r\n')
231 line = self.precmd(line)
232 stop = self.onecmd(line)
233 stop = self.postcmd(stop, line)
234 self.postloop()
235 finally:
236 if self.use_rawinput and self.completekey:
237 try:
238 import readline
239 readline.set_completer(self.old_completer)
240 readline.write_history_file(history)
241 except ImportError:
242 pass
243
244 def confirm(self):
245 y_or_n = raw_input("y/n: ")
246 if y_or_n == "y":
247 return True
248 elif y_or_n != "n":
249 return self.confirm()
250 return False
251
252 def log(self, *msg):
253 msg = u"".join(unicode(i) for i in msg)
254 logging.info(msg)
255
256 def logError(self, *msg):
257 msg = u"".join(unicode(i) for i in msg)
258 logging.error(msg)
259 if not self.settings.error_command:
260 return
261 output = get_command_output(self.settings.error_command, {"$m": msg})
262 if output:
263 self.log("Error command output:")
264 self.log(output.rstrip())
265
266 def promptf(self):
267 """A function to generate prompts so that we can do dynamic prompts. """
268 if self.in_macro:
269 promptstr = self.promptstrs["macro"]
270 elif not self.p.online:
271 promptstr = self.promptstrs["offline"]
272 elif self.status.extruder_enabled:
273 promptstr = self.promptstrs["online"]
274 else:
275 promptstr = self.promptstrs["fallback"]
276 if "%" not in promptstr:
277 return promptstr
278 else:
279 specials = {}
280 specials["extruder_temp"] = str(int(self.status.extruder_temp))
281 specials["extruder_temp_target"] = str(int(self.status.extruder_temp_target))
282 if self.status.extruder_temp_target == 0:
283 specials["extruder_temp_fancy"] = str(int(self.status.extruder_temp))
284 else:
285 specials["extruder_temp_fancy"] = "%s/%s" % (str(int(self.status.extruder_temp)), str(int(self.status.extruder_temp_target)))
286 if self.p.printing:
287 progress = int(1000 * float(self.p.queueindex) / len(self.p.mainqueue)) / 10
288 elif self.sdprinting:
289 progress = self.percentdone
290 else:
291 progress = 0.0
292 specials["progress"] = str(progress)
293 if self.p.printing or self.sdprinting:
294 specials["progress_fancy"] = " " + str(progress) + "%"
295 else:
296 specials["progress_fancy"] = ""
297 specials["bold"] = "\033[01m"
298 specials["normal"] = "\033[00m"
299 return promptstr % specials
300
301 def postcmd(self, stop, line):
302 """ A hook we override to generate prompts after
303 each command is executed, for the next prompt.
304 We also use it to send M105 commands so that
305 temp info gets updated for the prompt."""
306 if self.p.online and self.dynamic_temp:
307 self.p.send_now("M105")
308 self.prompt = self.promptf()
309 return stop
310
311 def kill(self):
312 self.statuscheck = False
313 if self.status_thread:
314 self.status_thread.join()
315 self.status_thread = None
316 if self.rpc_server is not None:
317 self.rpc_server.shutdown()
318
319 def write_prompt(self):
320 sys.stdout.write(self.promptf())
321 sys.stdout.flush()
322
323 def help_help(self, l = ""):
324 self.do_help("")
325
326 def do_gcodes(self, l = ""):
327 self.help_gcodes()
328
329 def help_gcodes(self):
330 self.log("Gcodes are passed through to the printer as they are")
331
332 def precmd(self, line):
333 if line.upper().startswith("M114"):
334 self.userm114 += 1
335 elif line.upper().startswith("M105"):
336 self.userm105 += 1
337 return line
338
339 def help_shell(self):
340 self.log("Executes a python command. Example:")
341 self.log("! os.listdir('.')")
342
343 def do_shell(self, l):
344 exec(l)
345
346 def emptyline(self):
347 """Called when an empty line is entered - do not remove"""
348 pass
349
350 def default(self, l):
351 if l[0].upper() in self.commandprefixes.upper():
352 if self.p and self.p.online:
353 if not self.p.loud:
354 self.log("SENDING:" + l.upper())
355 self.p.send_now(l.upper())
356 else:
357 self.logError(_("Printer is not online."))
358 return
359 elif l[0] == "@":
360 if self.p and self.p.online:
361 if not self.p.loud:
362 self.log("SENDING:" + l[1:])
363 self.p.send_now(l[1:])
364 else:
365 self.logError(_("Printer is not online."))
366 return
367 else:
368 cmd.Cmd.default(self, l)
369
370 def do_exit(self, l):
371 if self.status.extruder_temp_target != 0:
372 self.log("Setting extruder temp to 0")
373 self.p.send_now("M104 S0.0")
374 if self.status.bed_enabled:
375 if self.status.bed_temp_target != 0:
376 self.log("Setting bed temp to 0")
377 self.p.send_now("M140 S0.0")
378 self.log("Disconnecting from printer...")
379 if self.p.printing:
380 self.log(_("Are you sure you want to exit while printing?\n\
381 (this will terminate the print)."))
382 if not self.confirm():
383 return
384 self.log(_("Exiting program. Goodbye!"))
385 self.p.disconnect()
386 self.kill()
387 sys.exit()
388
389 def help_exit(self):
390 self.log(_("Disconnects from the printer and exits the program."))
391
392 # --------------------------------------------------------------
393 # Macro handling
394 # --------------------------------------------------------------
395
396 def complete_macro(self, text, line, begidx, endidx):
397 if (len(line.split()) == 2 and line[-1] != " ") or (len(line.split()) == 1 and line[-1] == " "):
398 return [i for i in self.macros.keys() if i.startswith(text)]
399 elif len(line.split()) == 3 or (len(line.split()) == 2 and line[-1] == " "):
400 return [i for i in ["/D", "/S"] + self.completenames(text) if i.startswith(text)]
401 else:
402 return []
403
404 def hook_macro(self, l):
405 l = l.rstrip()
406 ls = l.lstrip()
407 ws = l[:len(l) - len(ls)] # just leading whitespace
408 if len(ws) == 0:
409 self.end_macro()
410 # pass the unprocessed line to regular command processor to not require empty line in .pronsolerc
411 return self.onecmd(l)
412 self.cur_macro_def += l + "\n"
413
414 def end_macro(self):
415 if "onecmd" in self.__dict__: del self.onecmd # remove override
416 self.in_macro = False
417 self.prompt = self.promptf()
418 if self.cur_macro_def != "":
419 self.macros[self.cur_macro_name] = self.cur_macro_def
420 macro = self.compile_macro(self.cur_macro_name, self.cur_macro_def)
421 setattr(self.__class__, "do_" + self.cur_macro_name, lambda self, largs, macro = macro: macro(self, *largs.split()))
422 setattr(self.__class__, "help_" + self.cur_macro_name, lambda self, macro_name = self.cur_macro_name: self.subhelp_macro(macro_name))
423 if not self.processing_rc:
424 self.log("Macro '" + self.cur_macro_name + "' defined")
425 # save it
426 if not self.processing_args:
427 macro_key = "macro " + self.cur_macro_name
428 macro_def = macro_key
429 if "\n" in self.cur_macro_def:
430 macro_def += "\n"
431 else:
432 macro_def += " "
433 macro_def += self.cur_macro_def
434 self.save_in_rc(macro_key, macro_def)
435 else:
436 self.logError("Empty macro - cancelled")
437 del self.cur_macro_name, self.cur_macro_def
438
439 def compile_macro_line(self, line):
440 line = line.rstrip()
441 ls = line.lstrip()
442 ws = line[:len(line) - len(ls)] # just leading whitespace
443 if ls == "" or ls.startswith('#'): return "" # no code
444 if ls.startswith('!'):
445 return ws + ls[1:] + "\n" # python mode
446 else:
447 ls = ls.replace('"', '\\"') # need to escape double quotes
448 ret = ws + 'self.precmd("' + ls + '".format(*arg))\n' # parametric command mode
449 return ret + ws + 'self.onecmd("' + ls + '".format(*arg))\n'
450
451 def compile_macro(self, macro_name, macro_def):
452 if macro_def.strip() == "":
453 self.logError("Empty macro - cancelled")
454 return
455 macro = None
456 pycode = "def macro(self,*arg):\n"
457 if "\n" not in macro_def.strip():
458 pycode += self.compile_macro_line(" " + macro_def.strip())
459 else:
460 lines = macro_def.split("\n")
461 for l in lines:
462 pycode += self.compile_macro_line(l)
463 exec pycode
464 return macro
465
466 def start_macro(self, macro_name, prev_definition = "", suppress_instructions = False):
467 if not self.processing_rc and not suppress_instructions:
468 self.logError("Enter macro using indented lines, end with empty line")
469 self.cur_macro_name = macro_name
470 self.cur_macro_def = ""
471 self.onecmd = self.hook_macro # override onecmd temporarily
472 self.in_macro = False
473 self.prompt = self.promptf()
474
475 def delete_macro(self, macro_name):
476 if macro_name in self.macros.keys():
477 delattr(self.__class__, "do_" + macro_name)
478 del self.macros[macro_name]
479 self.log("Macro '" + macro_name + "' removed")
480 if not self.processing_rc and not self.processing_args:
481 self.save_in_rc("macro " + macro_name, "")
482 else:
483 self.logError("Macro '" + macro_name + "' is not defined")
484
485 def do_macro(self, args):
486 if args.strip() == "":
487 self.print_topics("User-defined macros", map(str, self.macros.keys()), 15, 80)
488 return
489 arglist = args.split(None, 1)
490 macro_name = arglist[0]
491 if macro_name not in self.macros and hasattr(self.__class__, "do_" + macro_name):
492 self.logError("Name '" + macro_name + "' is being used by built-in command")
493 return
494 if len(arglist) == 2:
495 macro_def = arglist[1]
496 if macro_def.lower() == "/d":
497 self.delete_macro(macro_name)
498 return
499 if macro_def.lower() == "/s":
500 self.subhelp_macro(macro_name)
501 return
502 self.cur_macro_def = macro_def
503 self.cur_macro_name = macro_name
504 self.end_macro()
505 return
506 if macro_name in self.macros:
507 self.start_macro(macro_name, self.macros[macro_name])
508 else:
509 self.start_macro(macro_name)
510
511 def help_macro(self):
512 self.log("Define single-line macro: macro <name> <definition>")
513 self.log("Define multi-line macro: macro <name>")
514 self.log("Enter macro definition in indented lines. Use {0} .. {N} to substitute macro arguments")
515 self.log("Enter python code, prefixed with ! Use arg[0] .. arg[N] to substitute macro arguments")
516 self.log("Delete macro: macro <name> /d")
517 self.log("Show macro definition: macro <name> /s")
518 self.log("'macro' without arguments displays list of defined macros")
519
520 def subhelp_macro(self, macro_name):
521 if macro_name in self.macros.keys():
522 macro_def = self.macros[macro_name]
523 if "\n" in macro_def:
524 self.log("Macro '" + macro_name + "' defined as:")
525 self.log(self.macros[macro_name] + "----------------")
526 else:
527 self.log("Macro '" + macro_name + "' defined as: '" + macro_def + "'")
528 else:
529 self.logError("Macro '" + macro_name + "' is not defined")
530
531 # --------------------------------------------------------------
532 # Configuration handling
533 # --------------------------------------------------------------
534
535 def set(self, var, str):
536 try:
537 t = type(getattr(self.settings, var))
538 value = self.settings._set(var, str)
539 if not self.processing_rc and not self.processing_args:
540 self.save_in_rc("set " + var, "set %s %s" % (var, value))
541 except AttributeError:
542 logging.debug(_("Unknown variable '%s'") % var)
543 except ValueError, ve:
544 if hasattr(ve, "from_validator"):
545 self.logError(_("Bad value %s for variable '%s': %s") % (str, var, ve.args[0]))
546 else:
547 self.logError(_("Bad value for variable '%s', expecting %s (%s)") % (var, repr(t)[1:-1], ve.args[0]))
548
549 def do_set(self, argl):
550 args = argl.split(None, 1)
551 if len(args) < 1:
552 for k in [kk for kk in dir(self.settings) if not kk.startswith("_")]:
553 self.log("%s = %s" % (k, str(getattr(self.settings, k))))
554 return
555 if len(args) < 2:
556 # Try getting the default value of the setting to check whether it
557 # actually exists
558 try:
559 getattr(self.settings, args[0])
560 except AttributeError:
561 logging.warning("Unknown variable '%s'" % args[0])
562 return
563 self.set(args[0], args[1])
564
565 def help_set(self):
566 self.log("Set variable: set <variable> <value>")
567 self.log("Show variable: set <variable>")
568 self.log("'set' without arguments displays all variables")
569
570 def complete_set(self, text, line, begidx, endidx):
571 if (len(line.split()) == 2 and line[-1] != " ") or (len(line.split()) == 1 and line[-1] == " "):
572 return [i for i in dir(self.settings) if not i.startswith("_") and i.startswith(text)]
573 elif len(line.split()) == 3 or (len(line.split()) == 2 and line[-1] == " "):
574 return [i for i in self.settings._tabcomplete(line.split()[1]) if i.startswith(text)]
575 else:
576 return []
577
578 def load_rc(self, rc_filename):
579 self.processing_rc = True
580 try:
581 rc = codecs.open(rc_filename, "r", "utf-8")
582 self.rc_filename = os.path.abspath(rc_filename)
583 for rc_cmd in rc:
584 if not rc_cmd.lstrip().startswith("#"):
585 self.onecmd(rc_cmd)
586 rc.close()
587 if hasattr(self, "cur_macro_def"):
588 self.end_macro()
589 self.rc_loaded = True
590 finally:
591 self.processing_rc = False
592
593 def load_default_rc(self, rc_filename = ".pronsolerc"):
594 if rc_filename == ".pronsolerc" and hasattr(sys, "frozen") and sys.frozen in ["windows_exe", "console_exe"]:
595 rc_filename = "printrunconf.ini"
596 try:
597 try:
598 self.load_rc(os.path.join(os.path.expanduser("~"), rc_filename))
599 except IOError:
600 self.load_rc(rc_filename)
601 except IOError:
602 # make sure the filename is initialized
603 self.rc_filename = os.path.abspath(os.path.join(os.path.expanduser("~"), rc_filename))
604
605 def save_in_rc(self, key, definition):
606 """
607 Saves or updates macro or other definitions in .pronsolerc
608 key is prefix that determines what is being defined/updated (e.g. 'macro foo')
609 definition is the full definition (that is written to file). (e.g. 'macro foo move x 10')
610 Set key as empty string to just add (and not overwrite)
611 Set definition as empty string to remove it from .pronsolerc
612 To delete line from .pronsolerc, set key as the line contents, and definition as empty string
613 Only first definition with given key is overwritten.
614 Updates are made in the same file position.
615 Additions are made to the end of the file.
616 """
617 rci, rco = None, None
618 if definition != "" and not definition.endswith("\n"):
619 definition += "\n"
620 try:
621 written = False
622 if os.path.exists(self.rc_filename):
623 shutil.copy(self.rc_filename, self.rc_filename + "~bak")
624 rci = codecs.open(self.rc_filename + "~bak", "r", "utf-8")
625 rco = codecs.open(self.rc_filename + "~new", "w", "utf-8")
626 if rci is not None:
627 overwriting = False
628 for rc_cmd in rci:
629 l = rc_cmd.rstrip()
630 ls = l.lstrip()
631 ws = l[:len(l) - len(ls)] # just leading whitespace
632 if overwriting and len(ws) == 0:
633 overwriting = False
634 if not written and key != "" and rc_cmd.startswith(key) and (rc_cmd + "\n")[len(key)].isspace():
635 overwriting = True
636 written = True
637 rco.write(definition)
638 if not overwriting:
639 rco.write(rc_cmd)
640 if not rc_cmd.endswith("\n"): rco.write("\n")
641 if not written:
642 rco.write(definition)
643 if rci is not None:
644 rci.close()
645 rco.close()
646 shutil.move(self.rc_filename + "~new", self.rc_filename)
647 # if definition != "":
648 # self.log("Saved '"+key+"' to '"+self.rc_filename+"'")
649 # else:
650 # self.log("Removed '"+key+"' from '"+self.rc_filename+"'")
651 except Exception, e:
652 self.logError("Saving failed for ", key + ":", str(e))
653 finally:
654 del rci, rco
655
656 # --------------------------------------------------------------
657 # Configuration update callbacks
658 # --------------------------------------------------------------
659
660 def update_build_dimensions(self, param, value):
661 self.build_dimensions_list = parse_build_dimensions(value)
662 self.p.analyzer.home_pos = get_home_pos(self.build_dimensions_list)
663
664 def update_tcp_streaming_mode(self, param, value):
665 self.p.tcp_streaming_mode = self.settings.tcp_streaming_mode
666
667 def update_rpc_server(self, param, value):
668 if value:
669 if self.rpc_server is None:
670 self.rpc_server = ProntRPC(self)
671 else:
672 if self.rpc_server is not None:
673 self.rpc_server.shutdown()
674 self.rpc_server = None
675
676 # --------------------------------------------------------------
677 # Command line options handling
678 # --------------------------------------------------------------
679
680 def add_cmdline_arguments(self, parser):
681 parser.add_argument('-v', '--verbose', help = _("increase verbosity"), action = "store_true")
682 parser.add_argument('-c', '--conf', '--config', help = _("load this file on startup instead of .pronsolerc ; you may chain config files, if so settings auto-save will use the last specified file"), action = "append", default = [])
683 parser.add_argument('-e', '--execute', help = _("executes command after configuration/.pronsolerc is loaded ; macros/settings from these commands are not autosaved"), action = "append", default = [])
684 parser.add_argument('filename', nargs='?', help = _("file to load"))
685
686 def process_cmdline_arguments(self, args):
687 if args.verbose:
688 logger = logging.getLogger()
689 logger.setLevel(logging.DEBUG)
690 for config in args.conf:
691 self.load_rc(config)
692 if not self.rc_loaded:
693 self.load_default_rc()
694 self.processing_args = True
695 for command in args.execute:
696 self.onecmd(command)
697 self.processing_args = False
698 self.update_rpc_server(None, self.settings.rpc_server)
699 if args.filename:
700 filename = args.filename.decode(locale.getpreferredencoding())
701 self.cmdline_filename_callback(filename)
702
703 def cmdline_filename_callback(self, filename):
704 self.do_load(filename)
705
706 def parse_cmdline(self, args):
707 parser = argparse.ArgumentParser(description = 'Printrun 3D printer interface')
708 self.add_cmdline_arguments(parser)
709 args = [arg for arg in args if not arg.startswith("-psn")]
710 args = parser.parse_args(args = args)
711 self.process_cmdline_arguments(args)
712 setup_logging(sys.stdout, self.settings.log_path, True)
713
714 # --------------------------------------------------------------
715 # Printer connection handling
716 # --------------------------------------------------------------
717
718 def connect_to_printer(self, port, baud, dtr):
719 try:
720 self.p.connect(port, baud, dtr)
721 except SerialException as e:
722 # Currently, there is no errno, but it should be there in the future
723 if e.errno == 2:
724 self.logError(_("Error: You are trying to connect to a non-existing port."))
725 elif e.errno == 8:
726 self.logError(_("Error: You don't have permission to open %s.") % port)
727 self.logError(_("You might need to add yourself to the dialout group."))
728 else:
729 self.logError(traceback.format_exc())
730 # Kill the scope anyway
731 return False
732 except OSError as e:
733 if e.errno == 2:
734 self.logError(_("Error: You are trying to connect to a non-existing port."))
735 else:
736 self.logError(traceback.format_exc())
737 return False
738 self.statuscheck = True
739 self.status_thread = threading.Thread(target = self.statuschecker)
740 self.status_thread.start()
741 return True
742
743 def do_connect(self, l):
744 a = l.split()
745 p = self.scanserial()
746 port = self.settings.port
747 if (port == "" or port not in p) and len(p) > 0:
748 port = p[0]
749 baud = self.settings.baudrate or 115200
750 if len(a) > 0:
751 port = a[0]
752 if len(a) > 1:
753 try:
754 baud = int(a[1])
755 except:
756 self.log("Bad baud value '" + a[1] + "' ignored")
757 if len(p) == 0 and not port:
758 self.log("No serial ports detected - please specify a port")
759 return
760 if len(a) == 0:
761 self.log("No port specified - connecting to %s at %dbps" % (port, baud))
762 if port != self.settings.port:
763 self.settings.port = port
764 self.save_in_rc("set port", "set port %s" % port)
765 if baud != self.settings.baudrate:
766 self.settings.baudrate = baud
767 self.save_in_rc("set baudrate", "set baudrate %d" % baud)
768 self.connect_to_printer(port, baud, self.settings.dtr)
769
770 def help_connect(self):
771 self.log("Connect to printer")
772 self.log("connect <port> <baudrate>")
773 self.log("If port and baudrate are not specified, connects to first detected port at 115200bps")
774 ports = self.scanserial()
775 if ports:
776 self.log("Available ports: ", " ".join(ports))
777 else:
778 self.log("No serial ports were automatically found.")
779
780 def complete_connect(self, text, line, begidx, endidx):
781 if (len(line.split()) == 2 and line[-1] != " ") or (len(line.split()) == 1 and line[-1] == " "):
782 return [i for i in self.scanserial() if i.startswith(text)]
783 elif len(line.split()) == 3 or (len(line.split()) == 2 and line[-1] == " "):
784 return [i for i in ["2400", "9600", "19200", "38400", "57600", "115200"] if i.startswith(text)]
785 else:
786 return []
787
788 def scanserial(self):
789 """scan for available ports. return a list of device names."""
790 baselist = []
791 if os.name == "nt":
792 try:
793 key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, "HARDWARE\\DEVICEMAP\\SERIALCOMM")
794 i = 0
795 while(1):
796 baselist += [_winreg.EnumValue(key, i)[1]]
797 i += 1
798 except:
799 pass
800
801 for g in ['/dev/ttyUSB*', '/dev/ttyACM*', "/dev/tty.*", "/dev/cu.*", "/dev/rfcomm*"]:
802 baselist += glob.glob(g)
803 return filter(self._bluetoothSerialFilter, baselist)
804
805 def _bluetoothSerialFilter(self, serial):
806 return not ("Bluetooth" in serial or "FireFly" in serial)
807
808 def online(self):
809 self.log("\rPrinter is now online")
810 self.write_prompt()
811
812 def do_disconnect(self, l):
813 self.p.disconnect()
814
815 def help_disconnect(self):
816 self.log("Disconnects from the printer")
817
818 def do_block_until_online(self, l):
819 while not self.p.online:
820 time.sleep(0.1)
821
822 def help_block_until_online(self, l):
823 self.log("Blocks until printer is online")
824 self.log("Warning: if something goes wrong, this can block pronsole forever")
825
826 # --------------------------------------------------------------
827 # Printer status monitoring
828 # --------------------------------------------------------------
829
830 def statuschecker_inner(self, do_monitoring = True):
831 if self.p.online:
832 if self.p.writefailures >= 4:
833 self.logError(_("Disconnecting after 4 failed writes."))
834 self.status_thread = None
835 self.disconnect()
836 return
837 if do_monitoring:
838 if self.sdprinting and not self.paused:
839 self.p.send_now("M27")
840 if self.m105_waitcycles % 10 == 0:
841 self.p.send_now("M105")
842 self.m105_waitcycles += 1
843 cur_time = time.time()
844 wait_time = 0
845 while time.time() < cur_time + self.monitor_interval - 0.25:
846 if not self.statuscheck:
847 break
848 time.sleep(0.25)
849 # Safeguard: if system time changes and goes back in the past,
850 # we could get stuck almost forever
851 wait_time += 0.25
852 if wait_time > self.monitor_interval - 0.25:
853 break
854 # Always sleep at least a bit, if something goes wrong with the
855 # system time we'll avoid freezing the whole app this way
856 time.sleep(0.25)
857
858 def statuschecker(self):
859 while self.statuscheck:
860 self.statuschecker_inner()
861
862 # --------------------------------------------------------------
863 # File loading handling
864 # --------------------------------------------------------------
865
866 def do_load(self, filename):
867 self._do_load(filename)
868
869 def _do_load(self, filename):
870 if not filename:
871 self.logError("No file name given.")
872 return
873 self.log(_("Loading file: %s") % filename)
874 if not os.path.exists(filename):
875 self.logError("File not found!")
876 return
877 self.load_gcode(filename)
878 self.log(_("Loaded %s, %d lines.") % (filename, len(self.fgcode)))
879 self.log(_("Estimated duration: %d layers, %s") % self.fgcode.estimate_duration())
880
881 def load_gcode(self, filename, layer_callback = None, gcode = None):
882 if gcode is None:
883 self.fgcode = gcoder.LightGCode(deferred = True)
884 else:
885 self.fgcode = gcode
886 self.fgcode.prepare(open(filename, "rU"),
887 get_home_pos(self.build_dimensions_list),
888 layer_callback = layer_callback)
889 self.fgcode.estimate_duration()
890 self.filename = filename
891
892 def complete_load(self, text, line, begidx, endidx):
893 s = line.split()
894 if len(s) > 2:
895 return []
896 if (len(s) == 1 and line[-1] == " ") or (len(s) == 2 and line[-1] != " "):
897 if len(s) > 1:
898 return [i[len(s[1]) - len(text):] for i in glob.glob(s[1] + "*/") + glob.glob(s[1] + "*.g*")]
899 else:
900 return glob.glob("*/") + glob.glob("*.g*")
901
902 def help_load(self):
903 self.log("Loads a gcode file (with tab-completion)")
904
905 def do_slice(self, l):
906 l = l.split()
907 if len(l) == 0:
908 self.logError(_("No file name given."))
909 return
910 settings = 0
911 if l[0] == "set":
912 settings = 1
913 else:
914 self.log(_("Slicing file: %s") % l[0])
915 if not(os.path.exists(l[0])):
916 self.logError(_("File not found!"))
917 return
918 try:
919 if settings:
920 command = self.settings.sliceoptscommand
921 self.log(_("Entering slicer settings: %s") % command)
922 run_command(command, blocking = True)
923 else:
924 command = self.settings.slicecommand
925 stl_name = l[0]
926 gcode_name = stl_name.replace(".stl", "_export.gcode").replace(".STL", "_export.gcode")
927 run_command(command,
928 {"$s": stl_name,
929 "$o": gcode_name},
930 blocking = True)
931 self.log(_("Loading sliced file."))
932 self.do_load(l[0].replace(".stl", "_export.gcode"))
933 except Exception, e:
934 self.logError(_("Slicing failed: %s") % e)
935
936 def complete_slice(self, text, line, begidx, endidx):
937 s = line.split()
938 if len(s) > 2:
939 return []
940 if (len(s) == 1 and line[-1] == " ") or (len(s) == 2 and line[-1] != " "):
941 if len(s) > 1:
942 return [i[len(s[1]) - len(text):] for i in glob.glob(s[1] + "*/") + glob.glob(s[1] + "*.stl")]
943 else:
944 return glob.glob("*/") + glob.glob("*.stl")
945
946 def help_slice(self):
947 self.log(_("Creates a gcode file from an stl model using the slicer (with tab-completion)"))
948 self.log(_("slice filename.stl - create gcode file"))
949 self.log(_("slice filename.stl view - create gcode file and view using skeiniso (if using skeinforge)"))
950 self.log(_("slice set - adjust slicer settings"))
951
952 # --------------------------------------------------------------
953 # Print/upload handling
954 # --------------------------------------------------------------
955
956 def do_upload(self, l):
957 names = l.split()
958 if len(names) == 2:
959 filename = names[0]
960 targetname = names[1]
961 else:
962 self.logError(_("Please enter target name in 8.3 format."))
963 return
964 if not self.p.online:
965 self.logError(_("Not connected to printer."))
966 return
967 self._do_load(filename)
968 self.log(_("Uploading as %s") % targetname)
969 self.log(_("Uploading %s") % self.filename)
970 self.p.send_now("M28 " + targetname)
971 self.log(_("Press Ctrl-C to interrupt upload."))
972 self.p.startprint(self.fgcode)
973 try:
974 sys.stdout.write(_("Progress: ") + "00.0%")
975 sys.stdout.flush()
976 while self.p.printing:
977 time.sleep(0.5)
978 sys.stdout.write("\b\b\b\b\b%04.1f%%" % (100 * float(self.p.queueindex) / len(self.p.mainqueue),))
979 sys.stdout.flush()
980 self.p.send_now("M29 " + targetname)
981 time.sleep(0.2)
982 self.p.clear = True
983 self._do_ls(False)
984 self.log("\b\b\b\b\b100%.")
985 self.log(_("Upload completed. %s should now be on the card.") % targetname)
986 return
987 except (KeyboardInterrupt, Exception) as e:
988 if isinstance(e, KeyboardInterrupt):
989 self.logError(_("...interrupted!"))
990 else:
991 self.logError(_("Something wrong happened while uploading:")
992 + "\n" + traceback.format_exc())
993 self.p.pause()
994 self.p.send_now("M29 " + targetname)
995 time.sleep(0.2)
996 self.p.cancelprint()
997 self.logError(_("A partial file named %s may have been written to the sd card.") % targetname)
998
999 def complete_upload(self, text, line, begidx, endidx):
1000 s = line.split()
1001 if len(s) > 2:
1002 return []
1003 if (len(s) == 1 and line[-1] == " ") or (len(s) == 2 and line[-1] != " "):
1004 if len(s) > 1:
1005 return [i[len(s[1]) - len(text):] for i in glob.glob(s[1] + "*/") + glob.glob(s[1] + "*.g*")]
1006 else:
1007 return glob.glob("*/") + glob.glob("*.g*")
1008
1009 def help_upload(self):
1010 self.log("Uploads a gcode file to the sd card")
1011
1012 def help_print(self):
1013 if not self.fgcode:
1014 self.log(_("Send a loaded gcode file to the printer. Load a file with the load command first."))
1015 else:
1016 self.log(_("Send a loaded gcode file to the printer. You have %s loaded right now.") % self.filename)
1017
1018 def do_print(self, l):
1019 if not self.fgcode:
1020 self.logError(_("No file loaded. Please use load first."))
1021 return
1022 if not self.p.online:
1023 self.logError(_("Not connected to printer."))
1024 return
1025 self.log(_("Printing %s") % self.filename)
1026 self.log(_("You can monitor the print with the monitor command."))
1027 self.sdprinting = False
1028 self.p.startprint(self.fgcode)
1029
1030 def do_pause(self, l):
1031 if self.sdprinting:
1032 self.p.send_now("M25")
1033 else:
1034 if not self.p.printing:
1035 self.logError(_("Not printing, cannot pause."))
1036 return
1037 self.p.pause()
1038 self.paused = True
1039
1040 def help_pause(self):
1041 self.log(_("Pauses a running print"))
1042
1043 def pause(self, event = None):
1044 return self.do_pause(None)
1045
1046 def do_resume(self, l):
1047 if not self.paused:
1048 self.logError(_("Not paused, unable to resume. Start a print first."))
1049 return
1050 self.paused = False
1051 if self.sdprinting:
1052 self.p.send_now("M24")
1053 return
1054 else:
1055 self.p.resume()
1056
1057 def help_resume(self):
1058 self.log(_("Resumes a paused print."))
1059
1060 def listfiles(self, line):
1061 if "Begin file list" in line:
1062 self.sdlisting = 1
1063 elif "End file list" in line:
1064 self.sdlisting = 0
1065 self.recvlisteners.remove(self.listfiles)
1066 if self.sdlisting_echo:
1067 self.log(_("Files on SD card:"))
1068 self.log("\n".join(self.sdfiles))
1069 elif self.sdlisting:
1070 self.sdfiles.append(line.strip().lower())
1071
1072 def _do_ls(self, echo):
1073 # FIXME: this was 2, but I think it should rather be 0 as in do_upload
1074 self.sdlisting = 0
1075 self.sdlisting_echo = echo
1076 self.sdfiles = []
1077 self.recvlisteners.append(self.listfiles)
1078 self.p.send_now("M20")
1079
1080 def do_ls(self, l):
1081 if not self.p.online:
1082 self.logError(_("Printer is not online. Please connect to it first."))
1083 return
1084 self._do_ls(True)
1085
1086 def help_ls(self):
1087 self.log(_("Lists files on the SD card"))
1088
1089 def waitforsdresponse(self, l):
1090 if "file.open failed" in l:
1091 self.logError(_("Opening file failed."))
1092 self.recvlisteners.remove(self.waitforsdresponse)
1093 return
1094 if "File opened" in l:
1095 self.log(l)
1096 if "File selected" in l:
1097 self.log(_("Starting print"))
1098 self.p.send_now("M24")
1099 self.sdprinting = True
1100 # self.recvlisteners.remove(self.waitforsdresponse)
1101 return
1102 if "Done printing file" in l:
1103 self.log(l)
1104 self.sdprinting = False
1105 self.recvlisteners.remove(self.waitforsdresponse)
1106 return
1107 if "SD printing byte" in l:
1108 # M27 handler
1109 try:
1110 resp = l.split()
1111 vals = resp[-1].split("/")
1112 self.percentdone = 100.0 * int(vals[0]) / int(vals[1])
1113 except:
1114 pass
1115
1116 def do_reset(self, l):
1117 self.p.reset()
1118
1119 def help_reset(self):
1120 self.log(_("Resets the printer."))
1121
1122 def do_sdprint(self, l):
1123 if not self.p.online:
1124 self.log(_("Printer is not online. Please connect to it first."))
1125 return
1126 self._do_ls(False)
1127 while self.listfiles in self.recvlisteners:
1128 time.sleep(0.1)
1129 if l.lower() not in self.sdfiles:
1130 self.log(_("File is not present on card. Please upload it first."))
1131 return
1132 self.recvlisteners.append(self.waitforsdresponse)
1133 self.p.send_now("M23 " + l.lower())
1134 self.log(_("Printing file: %s from SD card.") % l.lower())
1135 self.log(_("Requesting SD print..."))
1136 time.sleep(1)
1137
1138 def help_sdprint(self):
1139 self.log(_("Print a file from the SD card. Tab completes with available file names."))
1140 self.log(_("sdprint filename.g"))
1141
1142 def complete_sdprint(self, text, line, begidx, endidx):
1143 if not self.sdfiles and self.p.online:
1144 self._do_ls(False)
1145 while self.listfiles in self.recvlisteners:
1146 time.sleep(0.1)
1147 if (len(line.split()) == 2 and line[-1] != " ") or (len(line.split()) == 1 and line[-1] == " "):
1148 return [i for i in self.sdfiles if i.startswith(text)]
1149
1150 # --------------------------------------------------------------
1151 # Printcore callbacks
1152 # --------------------------------------------------------------
1153
1154 def startcb(self, resuming = False):
1155 self.starttime = time.time()
1156 if resuming:
1157 self.log(_("Print resumed at: %s") % format_time(self.starttime))
1158 else:
1159 self.log(_("Print started at: %s") % format_time(self.starttime))
1160 if not self.sdprinting:
1161 self.compute_eta = RemainingTimeEstimator(self.fgcode)
1162 else:
1163 self.compute_eta = None
1164
1165 if self.settings.start_command:
1166 output = get_command_output(self.settings.start_command,
1167 {"$s": str(self.filename),
1168 "$t": format_time(time.time())})
1169 if output:
1170 self.log("Start command output:")
1171 self.log(output.rstrip())
1172 try:
1173 powerset_print_start(reason = "Preventing sleep during print")
1174 except:
1175 self.logError(_("Failed to set power settings:")
1176 + "\n" + traceback.format_exc())
1177
1178 def endcb(self):
1179 try:
1180 powerset_print_stop()
1181 except:
1182 self.logError(_("Failed to set power settings:")
1183 + "\n" + traceback.format_exc())
1184 if self.p.queueindex == 0:
1185 print_duration = int(time.time() - self.starttime + self.extra_print_time)
1186 self.log(_("Print ended at: %(end_time)s and took %(duration)s") % {"end_time": format_time(time.time()),
1187 "duration": format_duration(print_duration)})
1188
1189 # Update total filament length used
1190 if self.fgcode is not None:
1191 new_total = self.settings.total_filament_used + self.fgcode.filament_length
1192 self.set("total_filament_used", new_total)
1193
1194 if not self.settings.final_command:
1195 return
1196 output = get_command_output(self.settings.final_command,
1197 {"$s": str(self.filename),
1198 "$t": format_duration(print_duration)})
1199 if output:
1200 self.log("Final command output:")
1201 self.log(output.rstrip())
1202
1203 def recvcb_report(self, l):
1204 isreport = REPORT_NONE
1205 if "ok C:" in l or "Count" in l \
1206 or ("X:" in l and len(gcoder.m114_exp.findall(l)) == 6):
1207 self.posreport = l
1208 isreport = REPORT_POS
1209 if self.userm114 > 0:
1210 self.userm114 -= 1
1211 isreport |= REPORT_MANUAL
1212 if "ok T:" in l or tempreading_exp.findall(l):
1213 self.tempreadings = l
1214 isreport = REPORT_TEMP
1215 if self.userm105 > 0:
1216 self.userm105 -= 1
1217 isreport |= REPORT_MANUAL
1218 else:
1219 self.m105_waitcycles = 0
1220 return isreport
1221
1222 def recvcb_actions(self, l):
1223 if l.startswith("!!"):
1224 self.do_pause(None)
1225 msg = l.split(" ", 1)
1226 if len(msg) > 1 and self.silent is False: self.logError(msg[1].ljust(15))
1227 sys.stdout.write(self.promptf())
1228 sys.stdout.flush()
1229 return True
1230 elif l.startswith("//"):
1231 command = l.split(" ", 1)
1232 if len(command) > 1:
1233 command = command[1]
1234 self.log(_("Received command %s") % command)
1235 command = command.split(":")
1236 if len(command) == 2 and command[0] == "action":
1237 command = command[1]
1238 if command == "pause":
1239 self.do_pause(None)
1240 sys.stdout.write(self.promptf())
1241 sys.stdout.flush()
1242 return True
1243 elif command == "resume":
1244 self.do_resume(None)
1245 sys.stdout.write(self.promptf())
1246 sys.stdout.flush()
1247 return True
1248 elif command == "disconnect":
1249 self.do_disconnect(None)
1250 sys.stdout.write(self.promptf())
1251 sys.stdout.flush()
1252 return True
1253 return False
1254
1255 def recvcb(self, l):
1256 l = l.rstrip()
1257 for listener in self.recvlisteners:
1258 listener(l)
1259 if not self.recvcb_actions(l):
1260 report_type = self.recvcb_report(l)
1261 if report_type & REPORT_TEMP:
1262 self.status.update_tempreading(l)
1263 if l != "ok" and not self.sdlisting \
1264 and not self.monitoring and (report_type == REPORT_NONE or report_type & REPORT_MANUAL):
1265 if l[:5] == "echo:":
1266 l = l[5:].lstrip()
1267 if self.silent is False: self.log("\r" + l.ljust(15))
1268 sys.stdout.write(self.promptf())
1269 sys.stdout.flush()
1270
1271 def layer_change_cb(self, newlayer):
1272 layerz = self.fgcode.all_layers[newlayer].z
1273 if layerz is not None:
1274 self.curlayer = layerz
1275 if self.compute_eta:
1276 secondselapsed = int(time.time() - self.starttime + self.extra_print_time)
1277 self.compute_eta.update_layer(newlayer, secondselapsed)
1278
1279 def get_eta(self):
1280 if self.sdprinting or self.uploading:
1281 if self.uploading:
1282 fractioncomplete = float(self.p.queueindex) / len(self.p.mainqueue)
1283 else:
1284 fractioncomplete = float(self.percentdone / 100.0)
1285 secondselapsed = int(time.time() - self.starttime + self.extra_print_time)
1286 # Prevent division by zero
1287 secondsestimate = secondselapsed / max(fractioncomplete, 0.000001)
1288 secondsremain = secondsestimate - secondselapsed
1289 progress = fractioncomplete
1290 elif self.compute_eta is not None:
1291 secondselapsed = int(time.time() - self.starttime + self.extra_print_time)
1292 secondsremain, secondsestimate = self.compute_eta(self.p.queueindex, secondselapsed)
1293 progress = self.p.queueindex
1294 else:
1295 secondsremain, secondsestimate, progress = 1, 1, 0
1296 return secondsremain, secondsestimate, progress
1297
1298 def do_eta(self, l):
1299 if not self.p.printing:
1300 self.logError(_("Printer is not currently printing. No ETA available."))
1301 else:
1302 secondsremain, secondsestimate, progress = self.get_eta()
1303 eta = _("Est: %s of %s remaining") % (format_duration(secondsremain),
1304 format_duration(secondsestimate))
1305 self.log(eta.strip())
1306
1307 def help_eta(self):
1308 self.log(_("Displays estimated remaining print time."))
1309
1310 # --------------------------------------------------------------
1311 # Temperature handling
1312 # --------------------------------------------------------------
1313
1314 def set_temp_preset(self, key, value):
1315 if not key.startswith("bed"):
1316 self.temps["pla"] = str(self.settings.temperature_pla)
1317 self.temps["abs"] = str(self.settings.temperature_abs)
1318 self.log("Hotend temperature presets updated, pla:%s, abs:%s" % (self.temps["pla"], self.temps["abs"]))
1319 else:
1320 self.bedtemps["pla"] = str(self.settings.bedtemp_pla)
1321 self.bedtemps["abs"] = str(self.settings.bedtemp_abs)
1322 self.log("Bed temperature presets updated, pla:%s, abs:%s" % (self.bedtemps["pla"], self.bedtemps["abs"]))
1323
1324 def tempcb(self, l):
1325 if "T:" in l:
1326 self.log(l.strip().replace("T", "Hotend").replace("B", "Bed").replace("ok ", ""))
1327
1328 def do_gettemp(self, l):
1329 if "dynamic" in l:
1330 self.dynamic_temp = True
1331 if self.p.online:
1332 self.p.send_now("M105")
1333 time.sleep(0.75)
1334 if not self.status.bed_enabled:
1335 self.log(_("Hotend: %s/%s") % (self.status.extruder_temp, self.status.extruder_temp_target))
1336 else:
1337 self.log(_("Hotend: %s/%s") % (self.status.extruder_temp, self.status.extruder_temp_target))
1338 self.log(_("Bed: %s/%s") % (self.status.bed_temp, self.status.bed_temp_target))
1339
1340 def help_gettemp(self):
1341 self.log(_("Read the extruder and bed temperature."))
1342
1343 def do_settemp(self, l):
1344 l = l.lower().replace(", ", ".")
1345 for i in self.temps.keys():
1346 l = l.replace(i, self.temps[i])
1347 try:
1348 f = float(l)
1349 except:
1350 self.logError(_("You must enter a temperature."))
1351 return
1352
1353 if f >= 0:
1354 if f > 250:
1355 self.log(_("%s is a high temperature to set your extruder to. Are you sure you want to do that?") % f)
1356 if not self.confirm():
1357 return
1358 if self.p.online:
1359 self.p.send_now("M104 S" + l)
1360 self.log(_("Setting hotend temperature to %s degrees Celsius.") % f)
1361 else:
1362 self.logError(_("Printer is not online."))
1363 else:
1364 self.logError(_("You cannot set negative temperatures. To turn the hotend off entirely, set its temperature to 0."))
1365
1366 def help_settemp(self):
1367 self.log(_("Sets the hotend temperature to the value entered."))
1368 self.log(_("Enter either a temperature in celsius or one of the following keywords"))
1369 self.log(", ".join([i + "(" + self.temps[i] + ")" for i in self.temps.keys()]))
1370
1371 def complete_settemp(self, text, line, begidx, endidx):
1372 if (len(line.split()) == 2 and line[-1] != " ") or (len(line.split()) == 1 and line[-1] == " "):
1373 return [i for i in self.temps.keys() if i.startswith(text)]
1374
1375 def do_bedtemp(self, l):
1376 f = None
1377 try:
1378 l = l.lower().replace(", ", ".")
1379 for i in self.bedtemps.keys():
1380 l = l.replace(i, self.bedtemps[i])
1381 f = float(l)
1382 except:
1383 self.logError(_("You must enter a temperature."))
1384 if f is not None and f >= 0:
1385 if self.p.online:
1386 self.p.send_now("M140 S" + l)
1387 self.log(_("Setting bed temperature to %s degrees Celsius.") % f)
1388 else:
1389 self.logError(_("Printer is not online."))
1390 else:
1391 self.logError(_("You cannot set negative temperatures. To turn the bed off entirely, set its temperature to 0."))
1392
1393 def help_bedtemp(self):
1394 self.log(_("Sets the bed temperature to the value entered."))
1395 self.log(_("Enter either a temperature in celsius or one of the following keywords"))
1396 self.log(", ".join([i + "(" + self.bedtemps[i] + ")" for i in self.bedtemps.keys()]))
1397
1398 def complete_bedtemp(self, text, line, begidx, endidx):
1399 if (len(line.split()) == 2 and line[-1] != " ") or (len(line.split()) == 1 and line[-1] == " "):
1400 return [i for i in self.bedtemps.keys() if i.startswith(text)]
1401
1402 def do_monitor(self, l):
1403 interval = 5
1404 if not self.p.online:
1405 self.logError(_("Printer is not online. Please connect to it first."))
1406 return
1407 if not (self.p.printing or self.sdprinting):
1408 self.logError(_("Printer is not printing. Please print something before monitoring."))
1409 return
1410 self.log(_("Monitoring printer, use ^C to interrupt."))
1411 if len(l):
1412 try:
1413 interval = float(l)
1414 except:
1415 self.logError(_("Invalid period given."))
1416 self.log(_("Updating values every %f seconds.") % (interval,))
1417 self.monitoring = 1
1418 prev_msg_len = 0
1419 try:
1420 while True:
1421 self.p.send_now("M105")
1422 if self.sdprinting:
1423 self.p.send_now("M27")
1424 time.sleep(interval)
1425 if self.p.printing:
1426 preface = _("Print progress: ")
1427 progress = 100 * float(self.p.queueindex) / len(self.p.mainqueue)
1428 elif self.sdprinting:
1429 preface = _("SD print progress: ")
1430 progress = self.percentdone
1431 prev_msg = preface + "%.1f%%" % progress
1432 if self.silent is False:
1433 sys.stdout.write("\r" + prev_msg.ljust(prev_msg_len))
1434 sys.stdout.flush()
1435 prev_msg_len = len(prev_msg)
1436 except KeyboardInterrupt:
1437 if self.silent is False: self.log(_("Done monitoring."))
1438 self.monitoring = 0
1439
1440 def help_monitor(self):
1441 self.log(_("Monitor a machine's temperatures and an SD print's status."))
1442 self.log(_("monitor - Reports temperature and SD print status (if SD printing) every 5 seconds"))
1443 self.log(_("monitor 2 - Reports temperature and SD print status (if SD printing) every 2 seconds"))
1444
1445 # --------------------------------------------------------------
1446 # Manual printer controls
1447 # --------------------------------------------------------------
1448
1449 def do_tool(self, l):
1450 tool = None
1451 try:
1452 tool = int(l.lower().strip())
1453 except:
1454 self.logError(_("You must specify the tool index as an integer."))
1455 if tool is not None and tool >= 0:
1456 if self.p.online:
1457 self.p.send_now("T%d" % tool)
1458 self.log(_("Using tool %d.") % tool)
1459 else:
1460 self.logError(_("Printer is not online."))
1461 else:
1462 self.logError(_("You cannot set negative tool numbers."))
1463
1464 def help_tool(self):
1465 self.log(_("Switches to the specified tool (e.g. doing tool 1 will emit a T1 G-Code)."))
1466
1467 def do_move(self, l):
1468 if len(l.split()) < 2:
1469 self.logError(_("No move specified."))
1470 return
1471 if self.p.printing:
1472 self.logError(_("Printer is currently printing. Please pause the print before you issue manual commands."))
1473 return
1474 if not self.p.online:
1475 self.logError(_("Printer is not online. Unable to move."))
1476 return
1477 l = l.split()
1478 if l[0].lower() == "x":
1479 feed = self.settings.xy_feedrate
1480 axis = "X"
1481 elif l[0].lower() == "y":
1482 feed = self.settings.xy_feedrate
1483 axis = "Y"
1484 elif l[0].lower() == "z":
1485 feed = self.settings.z_feedrate
1486 axis = "Z"
1487 elif l[0].lower() == "e":
1488 feed = self.settings.e_feedrate
1489 axis = "E"
1490 else:
1491 self.logError(_("Unknown axis."))
1492 return
1493 try:
1494 float(l[1]) # check if distance can be a float
1495 except:
1496 self.logError(_("Invalid distance"))
1497 return
1498 try:
1499 feed = int(l[2])
1500 except:
1501 pass
1502 self.p.send_now("G91")
1503 self.p.send_now("G0 " + axis + str(l[1]) + " F" + str(feed))
1504 self.p.send_now("G90")
1505
1506 def help_move(self):
1507 self.log(_("Move an axis. Specify the name of the axis and the amount. "))
1508 self.log(_("move X 10 will move the X axis forward by 10mm at %s mm/min (default XY speed)") % self.settings.xy_feedrate)
1509 self.log(_("move Y 10 5000 will move the Y axis forward by 10mm at 5000mm/min"))
1510 self.log(_("move Z -1 will move the Z axis down by 1mm at %s mm/min (default Z speed)") % self.settings.z_feedrate)
1511 self.log(_("Common amounts are in the tabcomplete list."))
1512
1513 def complete_move(self, text, line, begidx, endidx):
1514 if (len(line.split()) == 2 and line[-1] != " ") or (len(line.split()) == 1 and line[-1] == " "):
1515 return [i for i in ["X ", "Y ", "Z ", "E "] if i.lower().startswith(text)]
1516 elif len(line.split()) == 3 or (len(line.split()) == 2 and line[-1] == " "):
1517 base = line.split()[-1]
1518 rlen = 0
1519 if base.startswith("-"):
1520 rlen = 1
1521 if line[-1] == " ":
1522 base = ""
1523 return [i[rlen:] for i in ["-100", "-10", "-1", "-0.1", "100", "10", "1", "0.1", "-50", "-5", "-0.5", "50", "5", "0.5", "-200", "-20", "-2", "-0.2", "200", "20", "2", "0.2"] if i.startswith(base)]
1524 else:
1525 return []
1526
1527 def do_extrude(self, l, override = None, overridefeed = 300):
1528 length = self.settings.default_extrusion # default extrusion length
1529 feed = self.settings.e_feedrate # default speed
1530 if not self.p.online:
1531 self.logError("Printer is not online. Unable to extrude.")
1532 return
1533 if self.p.printing:
1534 self.logError(_("Printer is currently printing. Please pause the print before you issue manual commands."))
1535 return
1536 ls = l.split()
1537 if len(ls):
1538 try:
1539 length = float(ls[0])
1540 except:
1541 self.logError(_("Invalid length given."))
1542 if len(ls) > 1:
1543 try:
1544 feed = int(ls[1])
1545 except:
1546 self.logError(_("Invalid speed given."))
1547 if override is not None:
1548 length = override
1549 feed = overridefeed
1550 self.do_extrude_final(length, feed)
1551
1552 def do_extrude_final(self, length, feed):
1553 if length > 0:
1554 self.log(_("Extruding %fmm of filament.") % (length,))
1555 elif length < 0:
1556 self.log(_("Reversing %fmm of filament.") % (-length,))
1557 else:
1558 self.log(_("Length is 0, not doing anything."))
1559 self.p.send_now("G91")
1560 self.p.send_now("G1 E" + str(length) + " F" + str(feed))
1561 self.p.send_now("G90")
1562
1563 def help_extrude(self):
1564 self.log(_("Extrudes a length of filament, 5mm by default, or the number of mm given as a parameter"))
1565 self.log(_("extrude - extrudes 5mm of filament at 300mm/min (5mm/s)"))
1566 self.log(_("extrude 20 - extrudes 20mm of filament at 300mm/min (5mm/s)"))
1567 self.log(_("extrude -5 - REVERSES 5mm of filament at 300mm/min (5mm/s)"))
1568 self.log(_("extrude 10 210 - extrudes 10mm of filament at 210mm/min (3.5mm/s)"))
1569
1570 def do_reverse(self, l):
1571 length = self.settings.default_extrusion # default extrusion length
1572 feed = self.settings.e_feedrate # default speed
1573 if not self.p.online:
1574 self.logError(_("Printer is not online. Unable to reverse."))
1575 return
1576 if self.p.printing:
1577 self.logError(_("Printer is currently printing. Please pause the print before you issue manual commands."))
1578 return
1579 ls = l.split()
1580 if len(ls):
1581 try:
1582 length = float(ls[0])
1583 except:
1584 self.logError(_("Invalid length given."))
1585 if len(ls) > 1:
1586 try:
1587 feed = int(ls[1])
1588 except:
1589 self.logError(_("Invalid speed given."))
1590 self.do_extrude("", -length, feed)
1591
1592 def help_reverse(self):
1593 self.log(_("Reverses the extruder, 5mm by default, or the number of mm given as a parameter"))
1594 self.log(_("reverse - reverses 5mm of filament at 300mm/min (5mm/s)"))
1595 self.log(_("reverse 20 - reverses 20mm of filament at 300mm/min (5mm/s)"))
1596 self.log(_("reverse 10 210 - extrudes 10mm of filament at 210mm/min (3.5mm/s)"))
1597 self.log(_("reverse -5 - EXTRUDES 5mm of filament at 300mm/min (5mm/s)"))
1598
1599 def do_home(self, l):
1600 if not self.p.online:
1601 self.logError(_("Printer is not online. Unable to move."))
1602 return
1603 if self.p.printing:
1604 self.logError(_("Printer is currently printing. Please pause the print before you issue manual commands."))
1605 return
1606 if "x" in l.lower():
1607 self.p.send_now("G28 X0")
1608 if "y" in l.lower():
1609 self.p.send_now("G28 Y0")
1610 if "z" in l.lower():
1611 self.p.send_now("G28 Z0")
1612 if "e" in l.lower():
1613 self.p.send_now("G92 E0")
1614 if not len(l):
1615 self.p.send_now("G28")
1616 self.p.send_now("G92 E0")
1617
1618 def help_home(self):
1619 self.log(_("Homes the printer"))
1620 self.log(_("home - homes all axes and zeroes the extruder(Using G28 and G92)"))
1621 self.log(_("home xy - homes x and y axes (Using G28)"))
1622 self.log(_("home z - homes z axis only (Using G28)"))
1623 self.log(_("home e - set extruder position to zero (Using G92)"))
1624 self.log(_("home xyze - homes all axes and zeroes the extruder (Using G28 and G92)"))
1625
1626 def do_off(self, l):
1627 self.off()
1628
1629 def off(self, ignore = None):
1630 if self.p.online:
1631 if self.p.printing: self.pause(None)
1632 self.log(_("; Motors off"))
1633 self.onecmd("M84")
1634 self.log(_("; Extruder off"))
1635 self.onecmd("M104 S0")
1636 self.log(_("; Heatbed off"))
1637 self.onecmd("M140 S0")
1638 self.log(_("; Fan off"))
1639 self.onecmd("M107")
1640 self.log(_("; Power supply off"))
1641 self.onecmd("M81")
1642 else:
1643 self.logError(_("Printer is not online. Unable to turn it off."))
1644
1645 def help_off(self):
1646 self.log(_("Turns off everything on the printer"))
1647
1648 # --------------------------------------------------------------
1649 # Host commands handling
1650 # --------------------------------------------------------------
1651
1652 def process_host_command(self, command):
1653 """Override host command handling"""
1654 command = command.lstrip()
1655 if command.startswith(";@"):
1656 command = command[2:]
1657 self.log(_("G-Code calling host command \"%s\"") % command)
1658 self.onecmd(command)
1659
1660 def do_run_script(self, l):
1661 p = run_command(l, {"$s": str(self.filename)}, stdout = subprocess.PIPE)
1662 for line in p.stdout.readlines():
1663 self.log("<< " + line.strip())
1664
1665 def help_run_script(self):
1666 self.log(_("Runs a custom script. Current gcode filename can be given using $s token."))
1667
1668 def do_run_gcode_script(self, l):
1669 p = run_command(l, {"$s": str(self.filename)}, stdout = subprocess.PIPE)
1670 for line in p.stdout.readlines():
1671 self.onecmd(line.strip())
1672
1673 def help_run_gcode_script(self):
1674 self.log(_("Runs a custom script which output gcode which will in turn be executed. Current gcode filename can be given using $s token."))

mercurial