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