pronsole.py 65.1 KB
Newer Older
1
#!/usr/bin/env python
2 3

# This file is part of the Printrun suite.
4
#
5 6 7 8
# Printrun is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
9
#
10 11 12 13
# Printrun is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
14
#
15 16 17
# You should have received a copy of the GNU General Public License
# along with Printrun.  If not, see <http://www.gnu.org/licenses/>.

18 19 20 21
import cmd
import glob
import os
import time
22
import threading
23
import sys
24
import shutil
25 26
import subprocess
import codecs
27
import argparse
28
import locale
29
import logging
30
import traceback
31
import re
32

33 34
from serial import SerialException

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
install_locale('pronterface')
40 41
from .settings import Settings, BuildDimensionsSetting
from .power import powerset_print_start, powerset_print_stop
42
from printrun import gcoder
43
from .rpc import ProntRPC
44

45
if os.name == "nt":
46 47 48 49
    try:
        import _winreg
    except:
        pass
50
READLINE = True
51 52 53
try:
    import readline
    try:
54
        readline.rl.mode.show_all_if_ambiguous = "on"  # config pyreadline on windows
55
    except:
56 57
        pass
except:
58
    READLINE = False  # neither readline module is available
59

60 61
tempreading_exp = re.compile("(^T:| T:)")

62 63 64
REPORT_NONE = 0
REPORT_POS = 1
REPORT_TEMP = 2
65
REPORT_MANUAL = 4
66

67
class Status(object):
68 69

    def __init__(self):
70
        self.extruder_temp = 0
71
        self.extruder_temp_target = 0
72 73 74 75
        self.bed_temp = 0
        self.bed_temp_target = 0
        self.print_job = None
        self.print_job_progress = 1.0
76 77

    def update_tempreading(self, tempstr):
78
        temps = parse_temperature_report(tempstr)
79 80
        if "T0" in temps and temps["T0"][0]: hotend_temp = float(temps["T0"][0])
        elif "T" in temps and temps["T"][0]: hotend_temp = float(temps["T"][0])
81
        else: hotend_temp = None
82 83
        if "T0" in temps and temps["T0"][1]: hotend_setpoint = float(temps["T0"][1])
        elif "T" in temps and temps["T"][1]: hotend_setpoint = float(temps["T"][1])
84 85 86 87 88
        else: hotend_setpoint = None
        if hotend_temp is not None:
            self.extruder_temp = hotend_temp
            if hotend_setpoint is not None:
                self.extruder_temp_target = hotend_setpoint
89
        bed_temp = float(temps["B"][0]) if "B" in temps and temps["B"][0] else None
90 91 92 93 94
        if bed_temp is not None:
            self.bed_temp = bed_temp
            setpoint = temps["B"][1]
            if setpoint:
                self.bed_temp_target = float(setpoint)
95 96 97 98 99 100 101 102 103 104

    @property
    def bed_enabled(self):
        return self.bed_temp != 0

    @property
    def extruder_enabled(self):
        return self.extruder_temp != 0


105 106 107
class pronsole(cmd.Cmd):
    def __init__(self):
        cmd.Cmd.__init__(self)
108
        if not READLINE:
109
            self.completekey = None
110
        self.status = Status()
111
        self.dynamic_temp = False
112
        self.compute_eta = None
113 114
        self.statuscheck = False
        self.status_thread = None
115
        self.monitor_interval = 3
116 117
        self.p = printcore.printcore()
        self.p.recvcb = self.recvcb
118 119
        self.p.startcb = self.startcb
        self.p.endcb = self.endcb
120
        self.p.layerchangecb = self.layer_change_cb
121
        self.p.process_host_command = self.process_host_command
122
        self.recvlisteners = []
123
        self.in_macro = False
124
        self.p.onlinecb = self.online
125
        self.p.errorcb = self.logError
126
        self.fgcode = None
127
        self.filename = None
128
        self.rpc_server = None
129
        self.curlayer = 0
130 131
        self.sdlisting = 0
        self.sdlisting_echo = 0
132
        self.sdfiles = []
133 134
        self.paused = False
        self.sdprinting = 0
135
        self.uploading = 0  # Unused, just for pronterface generalization
136 137
        self.temps = {"pla": "185", "abs": "230", "off": "0"}
        self.bedtemps = {"pla": "60", "abs": "110", "off": "0"}
138
        self.percentdone = 0
139
        self.posreport = ""
140
        self.tempreadings = ""
141 142 143
        self.userm114 = 0
        self.userm105 = 0
        self.m105_waitcycles = 0
144
        self.macros = {}
145
        self.history_file = "~/.pronsole-history"
146 147 148
        self.rc_loaded = False
        self.processing_rc = False
        self.processing_args = False
149
        self.settings = Settings(self)
150
        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)
151 152 153 154 155
        self.settings._port_list = self.scanserial
        self.settings._temperature_abs_cb = self.set_temp_preset
        self.settings._temperature_pla_cb = self.set_temp_preset
        self.settings._bedtemp_abs_cb = self.set_temp_preset
        self.settings._bedtemp_pla_cb = self.set_temp_preset
156
        self.update_build_dimensions(None, self.settings.build_dimensions)
157
        self.update_tcp_streaming_mode(None, self.settings.tcp_streaming_mode)
158
        self.monitoring = 0
159 160
        self.starttime = 0
        self.extra_print_time = 0
161
        self.silent = False
162
        self.commandprefixes = 'MGT$'
163
        self.promptstrs = {"offline": "%(bold)soffline>%(normal)s ",
164 165
                           "fallback": "%(bold)sPC>%(normal)s ",
                           "macro": "%(bold)s..>%(normal)s ",
166
                           "online": "%(bold)sT:%(extruder_temp_fancy)s%(progress_fancy)s>%(normal)s "}
167

Guillaume Seguin's avatar
Guillaume Seguin committed
168 169 170
    #  --------------------------------------------------------------
    #  General console handling
    #  --------------------------------------------------------------
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197

    def postloop(self):
        self.p.disconnect()
        cmd.Cmd.postloop(self)

    def preloop(self):
        self.log(_("Welcome to the printer console! Type \"help\" for a list of available commands."))
        self.prompt = self.promptf()
        cmd.Cmd.preloop(self)

    # We replace this function, defined in cmd.py .
    # It's default behavior with regards to Ctr-C
    # and Ctr-D doesn't make much sense...
    def cmdloop(self, intro=None):
        """Repeatedly issue a prompt, accept input, parse an initial prefix
        off the received input, and dispatch to action methods, passing them
        the remainder of the line as argument.

        """

        self.preloop()
        if self.use_rawinput and self.completekey:
            try:
                import readline
                self.old_completer = readline.get_completer()
                readline.set_completer(self.complete)
                readline.parse_and_bind(self.completekey + ": complete")
198 199 200
                history = os.path.expanduser(self.history_file)
                if os.path.exists(history):
                    readline.read_history_file(history)
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238
            except ImportError:
                pass
        try:
            if intro is not None:
                self.intro = intro
            if self.intro:
                self.stdout.write(str(self.intro) + "\n")
            stop = None
            while not stop:
                if self.cmdqueue:
                    line = self.cmdqueue.pop(0)
                else:
                    if self.use_rawinput:
                        try:
                            line = raw_input(self.prompt)
                        except EOFError:
                            print ""
                            self.do_exit("")
                        except KeyboardInterrupt:
                            print ""
                            line = ""
                    else:
                        self.stdout.write(self.prompt)
                        self.stdout.flush()
                        line = self.stdin.readline()
                        if not len(line):
                            line = ""
                        else:
                            line = line.rstrip('\r\n')
                line = self.precmd(line)
                stop = self.onecmd(line)
                stop = self.postcmd(stop, line)
            self.postloop()
        finally:
            if self.use_rawinput and self.completekey:
                try:
                    import readline
                    readline.set_completer(self.old_completer)
239
                    readline.write_history_file(history)
240 241 242
                except ImportError:
                    pass

243
    def confirm(self):
244 245 246 247 248 249
        y_or_n = raw_input("y/n: ")
        if y_or_n == "y":
            return True
        elif y_or_n != "n":
            return self.confirm()
        return False
250

251
    def log(self, *msg):
252
        print u"".join(unicode(i) for i in msg)
253

254
    def logError(self, *msg):
255 256 257 258
        msg = u"".join(unicode(i) for i in msg)
        logging.error(msg)
        if not self.settings.error_command:
            return
259 260 261 262
        output = get_command_output(self.settings.error_command, {"$m": msg})
        if output:
            self.log("Error command output:")
            self.log(output.rstrip())
263

264 265 266
    def promptf(self):
        """A function to generate prompts so that we can do dynamic prompts. """
        if self.in_macro:
267
            promptstr = self.promptstrs["macro"]
268
        elif not self.p.online:
269 270 271 272 273
            promptstr = self.promptstrs["offline"]
        elif self.status.extruder_enabled:
            promptstr = self.promptstrs["online"]
        else:
            promptstr = self.promptstrs["fallback"]
Guillaume Seguin's avatar
Guillaume Seguin committed
274
        if "%" not in promptstr:
275 276 277
            return promptstr
        else:
            specials = {}
278
            specials["extruder_temp"] = str(int(self.status.extruder_temp))
279
            specials["extruder_temp_target"] = str(int(self.status.extruder_temp_target))
280
            if self.status.extruder_temp_target == 0:
281
                specials["extruder_temp_fancy"] = str(int(self.status.extruder_temp))
282
            else:
283 284
                specials["extruder_temp_fancy"] = "%s/%s" % (str(int(self.status.extruder_temp)), str(int(self.status.extruder_temp_target)))
            if self.p.printing:
285
                progress = int(1000 * float(self.p.queueindex) / len(self.p.mainqueue)) / 10
286 287 288 289 290 291
            elif self.sdprinting:
                progress = self.percentdone
            else:
                progress = 0.0
            specials["progress"] = str(progress)
            if self.p.printing or self.sdprinting:
292
                specials["progress_fancy"] = " " + str(progress) + "%"
293
            else:
294
                specials["progress_fancy"] = ""
295
            specials["bold"] = "\033[01m"
296 297
            specials["normal"] = "\033[00m"
            return promptstr % specials
298 299

    def postcmd(self, stop, line):
300
        """ A hook we override to generate prompts after
301
            each command is executed, for the next prompt.
302
            We also use it to send M105 commands so that
303
            temp info gets updated for the prompt."""
304
        if self.p.online and self.dynamic_temp:
305
            self.p.send_now("M105")
306 307 308
        self.prompt = self.promptf()
        return stop

309
    def kill(self):
310 311 312 313
        self.statuscheck = False
        if self.status_thread:
            self.status_thread.join()
            self.status_thread = None
314 315 316
        if self.rpc_server is not None:
            self.rpc_server.shutdown()

317
    def write_prompt(self):
318
        sys.stdout.write(self.promptf())
319
        sys.stdout.flush()
320

Guillaume Seguin's avatar
Guillaume Seguin committed
321
    def help_help(self, l = ""):
322
        self.do_help("")
323

Guillaume Seguin's avatar
Guillaume Seguin committed
324
    def do_gcodes(self, l = ""):
325
        self.help_gcodes()
326

327
    def help_gcodes(self):
328
        self.log("Gcodes are passed through to the printer as they are")
329

330 331 332 333 334 335
    def precmd(self, line):
        if line.upper().startswith("M114"):
            self.userm114 += 1
        elif line.upper().startswith("M105"):
            self.userm105 += 1
        return line
336 337 338 339 340 341 342 343 344 345 346 347 348

    def help_shell(self):
        self.log("Executes a python command. Example:")
        self.log("! os.listdir('.')")

    def do_shell(self, l):
        exec(l)

    def emptyline(self):
        """Called when an empty line is entered - do not remove"""
        pass

    def default(self, l):
349
        if l[0].upper() in self.commandprefixes.upper():
350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383
            if self.p and self.p.online:
                if not self.p.loud:
                    self.log("SENDING:" + l.upper())
                self.p.send_now(l.upper())
            else:
                self.logError(_("Printer is not online."))
            return
        elif l[0] == "@":
            if self.p and self.p.online:
                if not self.p.loud:
                    self.log("SENDING:" + l[1:])
                self.p.send_now(l[1:])
            else:
                self.logError(_("Printer is not online."))
            return
        else:
            cmd.Cmd.default(self, l)

    def do_exit(self, l):
        if self.status.extruder_temp_target != 0:
            print "Setting extruder temp to 0"
        self.p.send_now("M104 S0.0")
        if self.status.bed_enabled:
            if self.status.bed_temp_target != 0:
                print "Setting bed temp to 0"
            self.p.send_now("M140 S0.0")
        self.log("Disconnecting from printer...")
        if self.p.printing:
            print "Are you sure you want to exit while printing?"
            print "(this will terminate the print)."
            if not self.confirm():
                return
        self.log(_("Exiting program. Goodbye!"))
        self.p.disconnect()
384
        self.kill()
385 386 387 388 389
        sys.exit()

    def help_exit(self):
        self.log(_("Disconnects from the printer and exits the program."))

Guillaume Seguin's avatar
Guillaume Seguin committed
390 391 392
    # --------------------------------------------------------------
    # Macro handling
    # --------------------------------------------------------------
393

394
    def complete_macro(self, text, line, begidx, endidx):
395
        if (len(line.split()) == 2 and line[-1] != " ") or (len(line.split()) == 1 and line[-1] == " "):
396
            return [i for i in self.macros.keys() if i.startswith(text)]
397
        elif len(line.split()) == 3 or (len(line.split()) == 2 and line[-1] == " "):
398
            return [i for i in ["/D", "/S"] + self.completenames(text) if i.startswith(text)]
399 400
        else:
            return []
401

402
    def hook_macro(self, l):
403
        l = l.rstrip()
Keegi's avatar
Keegi committed
404
        ls = l.lstrip()
405
        ws = l[:len(l) - len(ls)]  # just leading whitespace
406
        if len(ws) == 0:
Keegi's avatar
Keegi committed
407 408
            self.end_macro()
            # pass the unprocessed line to regular command processor to not require empty line in .pronsolerc
Keegi's avatar
Keegi committed
409
            return self.onecmd(l)
Keegi's avatar
Keegi committed
410
        self.cur_macro_def += l + "\n"
411 412

    def end_macro(self):
413
        if "onecmd" in self.__dict__: del self.onecmd  # remove override
414 415
        self.in_macro = False
        self.prompt = self.promptf()
416
        if self.cur_macro_def != "":
417
            self.macros[self.cur_macro_name] = self.cur_macro_def
418
            macro = self.compile_macro(self.cur_macro_name, self.cur_macro_def)
419 420
            setattr(self.__class__, "do_" + self.cur_macro_name, lambda self, largs, macro = macro: macro(self, *largs.split()))
            setattr(self.__class__, "help_" + self.cur_macro_name, lambda self, macro_name = self.cur_macro_name: self.subhelp_macro(macro_name))
Keegi's avatar
Keegi committed
421
            if not self.processing_rc:
422
                self.log("Macro '" + self.cur_macro_name + "' defined")
423
                # save it
424
                if not self.processing_args:
425
                    macro_key = "macro " + self.cur_macro_name
426 427 428 429 430 431
                    macro_def = macro_key
                    if "\n" in self.cur_macro_def:
                        macro_def += "\n"
                    else:
                        macro_def += " "
                    macro_def += self.cur_macro_def
432
                    self.save_in_rc(macro_key, macro_def)
433
        else:
434
            self.logError("Empty macro - cancelled")
435
        del self.cur_macro_name, self.cur_macro_def
436

437
    def compile_macro_line(self, line):
438 439
        line = line.rstrip()
        ls = line.lstrip()
440 441
        ws = line[:len(line) - len(ls)]  # just leading whitespace
        if ls == "" or ls.startswith('#'): return ""  # no code
442
        if ls.startswith('!'):
443
            return ws + ls[1:] + "\n"  # python mode
444
        else:
445
            ls = ls.replace('"', '\\"')  # need to escape double quotes
446
            ret = ws + 'self.precmd("' + ls + '".format(*arg))\n'  # parametric command mode
447
            return ret + ws + 'self.onecmd("' + ls + '".format(*arg))\n'
448

449
    def compile_macro(self, macro_name, macro_def):
450
        if macro_def.strip() == "":
451
            self.logError("Empty macro - cancelled")
452
            return
453
        macro = None
454 455
        pycode = "def macro(self,*arg):\n"
        if "\n" not in macro_def.strip():
456
            pycode += self.compile_macro_line("  " + macro_def.strip())
457 458 459 460 461 462
        else:
            lines = macro_def.split("\n")
            for l in lines:
                pycode += self.compile_macro_line(l)
        exec pycode
        return macro
463

464
    def start_macro(self, macro_name, prev_definition = "", suppress_instructions = False):
465
        if not self.processing_rc and not suppress_instructions:
466
            self.logError("Enter macro using indented lines, end with empty line")
467 468
        self.cur_macro_name = macro_name
        self.cur_macro_def = ""
469
        self.onecmd = self.hook_macro  # override onecmd temporarily
470 471
        self.in_macro = False
        self.prompt = self.promptf()
472

473
    def delete_macro(self, macro_name):
Keegi's avatar
Keegi committed
474
        if macro_name in self.macros.keys():
475
            delattr(self.__class__, "do_" + macro_name)
Keegi's avatar
Keegi committed
476
            del self.macros[macro_name]
477
            self.log("Macro '" + macro_name + "' removed")
Keegi's avatar
Keegi committed
478
            if not self.processing_rc and not self.processing_args:
479
                self.save_in_rc("macro " + macro_name, "")
Keegi's avatar
Keegi committed
480
        else:
481 482
            self.logError("Macro '" + macro_name + "' is not defined")

483
    def do_macro(self, args):
484 485
        if args.strip() == "":
            self.print_topics("User-defined macros", map(str, self.macros.keys()), 15, 80)
Keegi's avatar
Keegi committed
486
            return
487
        arglist = args.split(None, 1)
Keegi's avatar
Keegi committed
488
        macro_name = arglist[0]
489 490
        if macro_name not in self.macros and hasattr(self.__class__, "do_" + macro_name):
            self.logError("Name '" + macro_name + "' is being used by built-in command")
Keegi's avatar
Keegi committed
491
            return
Keegi's avatar
Keegi committed
492 493 494
        if len(arglist) == 2:
            macro_def = arglist[1]
            if macro_def.lower() == "/d":
Keegi's avatar
Keegi committed
495
                self.delete_macro(macro_name)
Keegi's avatar
Keegi committed
496 497 498 499 500 501 502
                return
            if macro_def.lower() == "/s":
                self.subhelp_macro(macro_name)
                return
            self.cur_macro_def = macro_def
            self.cur_macro_name = macro_name
            self.end_macro()
Keegi's avatar
Keegi committed
503
            return
504
        if macro_name in self.macros:
505
            self.start_macro(macro_name, self.macros[macro_name])
506 507
        else:
            self.start_macro(macro_name)
508

Keegi's avatar
Keegi committed
509
    def help_macro(self):
510 511 512 513 514 515 516
        self.log("Define single-line macro: macro <name> <definition>")
        self.log("Define multi-line macro:  macro <name>")
        self.log("Enter macro definition in indented lines. Use {0} .. {N} to substitute macro arguments")
        self.log("Enter python code, prefixed with !  Use arg[0] .. arg[N] to substitute macro arguments")
        self.log("Delete macro:             macro <name> /d")
        self.log("Show macro definition:    macro <name> /s")
        self.log("'macro' without arguments displays list of defined macros")
517

518
    def subhelp_macro(self, macro_name):
Keegi's avatar
Keegi committed
519 520 521
        if macro_name in self.macros.keys():
            macro_def = self.macros[macro_name]
            if "\n" in macro_def:
522 523
                self.log("Macro '" + macro_name + "' defined as:")
                self.log(self.macros[macro_name] + "----------------")
Keegi's avatar
Keegi committed
524
            else:
525
                self.log("Macro '" + macro_name + "' defined as: '" + macro_def + "'")
Keegi's avatar
Keegi committed
526
        else:
527
            self.logError("Macro '" + macro_name + "' is not defined")
528

Guillaume Seguin's avatar
Guillaume Seguin committed
529 530 531
    # --------------------------------------------------------------
    # Configuration handling
    # --------------------------------------------------------------
532

533
    def set(self, var, str):
534
        try:
535 536
            t = type(getattr(self.settings, var))
            value = self.settings._set(var, str)
537
            if not self.processing_rc and not self.processing_args:
538
                self.save_in_rc("set " + var, "set %s %s" % (var, value))
539
        except AttributeError:
540
            logging.debug(_("Unknown variable '%s'") % var)
541
        except ValueError, ve:
542 543 544 545
            if hasattr(ve, "from_validator"):
                self.logError(_("Bad value %s for variable '%s': %s") % (str, var, ve.args[0]))
            else:
                self.logError(_("Bad value for variable '%s', expecting %s (%s)") % (var, repr(t)[1:-1], ve.args[0]))
546

547 548
    def do_set(self, argl):
        args = argl.split(None, 1)
549 550
        if len(args) < 1:
            for k in [kk for kk in dir(self.settings) if not kk.startswith("_")]:
551
                self.log("%s = %s" % (k, str(getattr(self.settings, k))))
552 553 554
            return
        if len(args) < 2:
            try:
555
                self.log("%s = %s" % (args[0], getattr(self.settings, args[0])))
556
            except AttributeError:
557
                logging.warning("Unknown variable '%s'" % args[0])
558
            return
559
        self.set(args[0], args[1])
560

561
    def help_set(self):
562 563 564
        self.log("Set variable:   set <variable> <value>")
        self.log("Show variable:  set <variable>")
        self.log("'set' without arguments displays all variables")
565 566

    def complete_set(self, text, line, begidx, endidx):
567
        if (len(line.split()) == 2 and line[-1] != " ") or (len(line.split()) == 1 and line[-1] == " "):
568
            return [i for i in dir(self.settings) if not i.startswith("_") and i.startswith(text)]
569
        elif len(line.split()) == 3 or (len(line.split()) == 2 and line[-1] == " "):
570 571 572
            return [i for i in self.settings._tabcomplete(line.split()[1]) if i.startswith(text)]
        else:
            return []
573

574 575
    def load_rc(self, rc_filename):
        self.processing_rc = True
Keegi's avatar
Keegi committed
576
        try:
577
            rc = codecs.open(rc_filename, "r", "utf-8")
578
            self.rc_filename = os.path.abspath(rc_filename)
579 580 581 582
            for rc_cmd in rc:
                if not rc_cmd.lstrip().startswith("#"):
                    self.onecmd(rc_cmd)
            rc.close()
583
            if hasattr(self, "cur_macro_def"):
584 585 586
                self.end_macro()
            self.rc_loaded = True
        finally:
587
            self.processing_rc = False
588

589
    def load_default_rc(self, rc_filename = ".pronsolerc"):
590 591
        if rc_filename == ".pronsolerc" and hasattr(sys, "frozen") and sys.frozen in ["windows_exe", "console_exe"]:
            rc_filename = "printrunconf.ini"
592 593
        try:
            try:
594
                self.load_rc(os.path.join(os.path.expanduser("~"), rc_filename))
595 596
            except IOError:
                self.load_rc(rc_filename)
Keegi's avatar
Keegi committed
597
        except IOError:
598
            # make sure the filename is initialized
599
            self.rc_filename = os.path.abspath(os.path.join(os.path.expanduser("~"), rc_filename))
600

601
    def save_in_rc(self, key, definition):
602
        """
603
        Saves or updates macro or other definitions in .pronsolerc
604 605 606 607 608 609 610 611 612
        key is prefix that determines what is being defined/updated (e.g. 'macro foo')
        definition is the full definition (that is written to file). (e.g. 'macro foo move x 10')
        Set key as empty string to just add (and not overwrite)
        Set definition as empty string to remove it from .pronsolerc
        To delete line from .pronsolerc, set key as the line contents, and definition as empty string
        Only first definition with given key is overwritten.
        Updates are made in the same file position.
        Additions are made to the end of the file.
        """
613
        rci, rco = None, None
614 615 616 617 618
        if definition != "" and not definition.endswith("\n"):
            definition += "\n"
        try:
            written = False
            if os.path.exists(self.rc_filename):
619 620
                shutil.copy(self.rc_filename, self.rc_filename + "~bak")
                rci = codecs.open(self.rc_filename + "~bak", "r", "utf-8")
621
            rco = codecs.open(self.rc_filename + "~new", "w", "utf-8")
622
            if rci is not None:
623 624 625 626
                overwriting = False
                for rc_cmd in rci:
                    l = rc_cmd.rstrip()
                    ls = l.lstrip()
627
                    ws = l[:len(l) - len(ls)]  # just leading whitespace
628 629
                    if overwriting and len(ws) == 0:
                        overwriting = False
630
                    if not written and key != "" and rc_cmd.startswith(key) and (rc_cmd + "\n")[len(key)].isspace():
631 632 633 634 635 636 637 638 639 640 641
                        overwriting = True
                        written = True
                        rco.write(definition)
                    if not overwriting:
                        rco.write(rc_cmd)
                        if not rc_cmd.endswith("\n"): rco.write("\n")
            if not written:
                rco.write(definition)
            if rci is not None:
                rci.close()
            rco.close()
642
            shutil.move(self.rc_filename + "~new", self.rc_filename)
Guillaume Seguin's avatar
Guillaume Seguin committed
643
            # if definition != "":
644
            #    self.log("Saved '"+key+"' to '"+self.rc_filename+"'")
Guillaume Seguin's avatar
Guillaume Seguin committed
645
            # else:
646
            #    self.log("Removed '"+key+"' from '"+self.rc_filename+"'")
647
        except Exception, e:
648
            self.logError("Saving failed for ", key + ":", str(e))
649
        finally:
650
            del rci, rco
651

Guillaume Seguin's avatar
Guillaume Seguin committed
652 653 654
    #  --------------------------------------------------------------
    #  Configuration update callbacks
    #  --------------------------------------------------------------
655 656 657 658 659

    def update_build_dimensions(self, param, value):
        self.build_dimensions_list = parse_build_dimensions(value)
        self.p.analyzer.home_pos = get_home_pos(self.build_dimensions_list)

660 661 662
    def update_tcp_streaming_mode(self, param, value):
        self.p.tcp_streaming_mode = self.settings.tcp_streaming_mode

663
    def update_rpc_server(self, param, value):
664 665 666 667 668 669 670
        if value:
            if self.rpc_server is None:
                self.rpc_server = ProntRPC(self)
        else:
            if self.rpc_server is not None:
                self.rpc_server.shutdown()
                self.rpc_server = None
671

Guillaume Seguin's avatar
Guillaume Seguin committed
672 673 674
    #  --------------------------------------------------------------
    #  Command line options handling
    #  --------------------------------------------------------------
675 676

    def add_cmdline_arguments(self, parser):
677
        parser.add_argument('-v', '--verbose', help = _("increase verbosity"), action = "store_true")
678 679 680 681 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 = [])
        parser.add_argument('-e', '--execute', help = _("executes command after configuration/.pronsolerc is loaded ; macros/settings from these commands are not autosaved"), action = "append", default = [])
        parser.add_argument('filename', nargs='?', help = _("file to load"))

    def process_cmdline_arguments(self, args):
683 684 685
        if args.verbose:
            logger = logging.getLogger()
            logger.setLevel(logging.DEBUG)
686 687 688 689 690 691 692 693
        for config in args.conf:
            self.load_rc(config)
        if not self.rc_loaded:
            self.load_default_rc()
        self.processing_args = True
        for command in args.execute:
            self.onecmd(command)
        self.processing_args = False
694
        self.update_rpc_server(None, self.settings.rpc_server)
695 696 697 698 699 700 701 702 703 704 705 706 707 708
        if args.filename:
            filename = args.filename.decode(locale.getpreferredencoding())
            self.cmdline_filename_callback(filename)

    def cmdline_filename_callback(self, filename):
        self.do_load(filename)

    def parse_cmdline(self, args):
        parser = argparse.ArgumentParser(description = 'Printrun 3D printer interface')
        self.add_cmdline_arguments(parser)
        args = [arg for arg in args if not arg.startswith("-psn")]
        args = parser.parse_args(args = args)
        self.process_cmdline_arguments(args)

Guillaume Seguin's avatar
Guillaume Seguin committed
709 710 711
    #  --------------------------------------------------------------
    #  Printer connection handling
    #  --------------------------------------------------------------
712

713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732
    def connect_to_printer(self, port, baud):
        try:
            self.p.connect(port, baud)
        except SerialException as e:
            # Currently, there is no errno, but it should be there in the future
            if e.errno == 2:
                self.logError(_("Error: You are trying to connect to a non-existing port."))
            elif e.errno == 8:
                self.logError(_("Error: You don't have permission to open %s.") % port)
                self.logError(_("You might need to add yourself to the dialout group."))
            else:
                self.logError(traceback.format_exc())
            # Kill the scope anyway
            return False
        except OSError as e:
            if e.errno == 2:
                self.logError(_("Error: You are trying to connect to a non-existing port."))
            else:
                self.logError(traceback.format_exc())
            return False
733 734 735 736
        self.statuscheck = True
        self.status_thread = threading.Thread(target = self.statuschecker)
        self.status_thread.start()
        return True
737

738 739 740 741
    def do_connect(self, l):
        a = l.split()
        p = self.scanserial()
        port = self.settings.port
742
        if (port == "" or port not in p) and len(p) > 0:
743 744
            port = p[0]
        baud = self.settings.baudrate or 115200
745
        if len(a) > 0:
746
            port = a[0]
747
        if len(a) > 1:
748
            try:
749
                baud = int(a[1])
750
            except:
751
                self.log("Bad baud value '" + a[1] + "' ignored")
752
        if len(p) == 0 and not port:
753
            self.log("No serial ports detected - please specify a port")
754
            return
755
        if len(a) == 0:
756
            self.log("No port specified - connecting to %s at %dbps" % (port, baud))
757 758
        if port != self.settings.port:
            self.settings.port = port
759
            self.save_in_rc("set port", "set port %s" % port)
760 761
        if baud != self.settings.baudrate:
            self.settings.baudrate = baud
762
            self.save_in_rc("set baudrate", "set baudrate %d" % baud)
763
        self.connect_to_printer(port, baud)
764

765
    def help_connect(self):
766 767 768
        self.log("Connect to printer")
        self.log("connect <port> <baudrate>")
        self.log("If port and baudrate are not specified, connects to first detected port at 115200bps")
769
        ports = self.scanserial()
770
        if ports:
771
            self.log("Available ports: ", " ".join(ports))
772
        else:
773
            self.log("No serial ports were automatically found.")
774

775
    def complete_connect(self, text, line, begidx, endidx):
776
        if (len(line.split()) == 2 and line[-1] != " ") or (len(line.split()) == 1 and line[-1] == " "):
777
            return [i for i in self.scanserial() if i.startswith(text)]
778
        elif len(line.split()) == 3 or (len(line.split()) == 2 and line[-1] == " "):
779 780 781
            return [i for i in ["2400", "9600", "19200", "38400", "57600", "115200"] if i.startswith(text)]
        else:
            return []
782

783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806
    def scanserial(self):
        """scan for available ports. return a list of device names."""
        baselist = []
        if os.name == "nt":
            try:
                key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, "HARDWARE\\DEVICEMAP\\SERIALCOMM")
                i = 0
                while(1):
                    baselist += [_winreg.EnumValue(key, i)[1]]
                    i += 1
            except:
                pass

        for g in ['/dev/ttyUSB*', '/dev/ttyACM*', "/dev/tty.*", "/dev/cu.*", "/dev/rfcomm*"]:
            baselist += glob.glob(g)
        return filter(self._bluetoothSerialFilter, baselist)

    def _bluetoothSerialFilter(self, serial):
        return not ("Bluetooth" in serial or "FireFly" in serial)

    def online(self):
        self.log("\rPrinter is now online")
        self.write_prompt()

807
    def do_disconnect(self, l):
808
        self.p.disconnect()
809

810
    def help_disconnect(self):
811
        self.log("Disconnects from the printer")
Guillaume Seguin's avatar
Guillaume Seguin committed
812

813 814 815 816 817 818 819 820
    def do_block_until_online(self, l):
        while not self.p.online:
            time.sleep(0.1)

    def help_block_until_online(self, l):
        self.log("Blocks until printer is online")
        self.log("Warning: if something goes wrong, this can block pronsole forever")

821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856
    #  --------------------------------------------------------------
    #  Printer status monitoring
    #  --------------------------------------------------------------

    def statuschecker_inner(self, do_monitoring = True):
        if self.p.online:
            if self.p.writefailures >= 4:
                self.logError(_("Disconnecting after 4 failed writes."))
                self.status_thread = None
                self.disconnect()
                return
            if do_monitoring:
                if self.sdprinting:
                    self.p.send_now("M27")
                if self.m105_waitcycles % 10 == 0:
                    self.p.send_now("M105")
                self.m105_waitcycles += 1
        cur_time = time.time()
        wait_time = 0
        while time.time() < cur_time + self.monitor_interval - 0.25:
            if not self.statuscheck:
                break
            time.sleep(0.25)
            # Safeguard: if system time changes and goes back in the past,
            # we could get stuck almost forever
            wait_time += 0.25
            if wait_time > self.monitor_interval - 0.25:
                break
        # Always sleep at least a bit, if something goes wrong with the
        # system time we'll avoid freezing the whole app this way
        time.sleep(0.25)

    def statuschecker(self):
        while self.statuscheck:
            self.statuschecker_inner()

Guillaume Seguin's avatar
Guillaume Seguin committed
857 858 859
    #  --------------------------------------------------------------
    #  File loading handling
    #  --------------------------------------------------------------
860

861 862
    def do_load(self, filename):
        self._do_load(filename)
863

864 865
    def _do_load(self, filename):
        if not filename:
866
            self.logError("No file name given.")
867
            return
868
        self.log(_("Loading file: %s") % filename)
869
        if not os.path.exists(filename):
870
            self.logError("File not found!")
871
            return
872
        self.load_gcode(filename)
873
        self.log(_("Loaded %s, %d lines.") % (filename, len(self.fgcode)))
874
        self.log(_("Estimated duration: %d layers, %s") % self.fgcode.estimate_duration())
875

876 877
    def load_gcode(self, filename, layer_callback = None, gcode = None):
        if gcode is None:
878
            self.fgcode = gcoder.LightGCode(deferred = True)
879 880 881 882 883
        else:
            self.fgcode = gcode
        self.fgcode.prepare(open(filename, "rU"),
                            get_home_pos(self.build_dimensions_list),
                            layer_callback = layer_callback)
884
        self.fgcode.estimate_duration()
885
        self.filename = filename
886

887
    def complete_load(self, text, line, begidx, endidx):
888
        s = line.split()
889
        if len(s) > 2:
890
            return []
891 892 893
        if (len(s) == 1 and line[-1] == " ") or (len(s) == 2 and line[-1] != " "):
            if len(s) > 1:
                return [i[len(s[1]) - len(text):] for i in glob.glob(s[1] + "*/") + glob.glob(s[1] + "*.g*")]
894
            else:
895
                return glob.glob("*/") + glob.glob("*.g*")
896

897
    def help_load(self):
898
        self.log("Loads a gcode file (with tab-completion)")
899

900
    def do_slice(self, l):
901 902 903
        l = l.split()
        if len(l) == 0:
            self.logError(_("No file name given."))
904
            return
905 906 907 908
        settings = 0
        if l[0] == "set":
            settings = 1
        else:
Guillaume Seguin's avatar
Guillaume Seguin committed
909
            self.log(_("Slicing file: %s") % l[0])
910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930
            if not(os.path.exists(l[0])):
                self.logError(_("File not found!"))
                return
        try:
            if settings:
                command = self.settings.sliceoptscommand
                self.log(_("Entering slicer settings: %s") % command)
                run_command(command, blocking = True)
            else:
                command = self.settings.slicecommand
                stl_name = l[0]
                gcode_name = stl_name.replace(".stl", "_export.gcode").replace(".STL", "_export.gcode")
                run_command(command,
                            {"$s": stl_name,
                             "$o": gcode_name},
                            blocking = True)
                self.log(_("Loading sliced file."))
                self.do_load(l[0].replace(".stl", "_export.gcode"))
        except Exception, e:
            self.logError(_("Slicing failed: %s") % e)

931
    def complete_slice(self, text, line, begidx, endidx):
932 933 934 935 936 937 938 939 940
        s = line.split()
        if len(s) > 2:
            return []
        if (len(s) == 1 and line[-1] == " ") or (len(s) == 2 and line[-1] != " "):
            if len(s) > 1:
                return [i[len(s[1]) - len(text):] for i in glob.glob(s[1] + "*/") + glob.glob(s[1] + "*.stl")]
            else:
                return glob.glob("*/") + glob.glob("*.stl")

941
    def help_slice(self):
942
        self.log(_("Creates a gcode file from an stl model using the slicer (with tab-completion)"))
943 944 945
        self.log(_("slice filename.stl - create gcode file"))
        self.log(_("slice filename.stl view - create gcode file and view using skeiniso (if using skeinforge)"))
        self.log(_("slice set - adjust slicer settings"))
946

Guillaume Seguin's avatar
Guillaume Seguin committed
947 948 949
    #  --------------------------------------------------------------
    #  Print/upload handling
    #  --------------------------------------------------------------
950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970

    def do_upload(self, l):
        names = l.split()
        if len(names) == 2:
            filename = names[0]
            targetname = names[1]
        else:
            self.logError(_("Please enter target name in 8.3 format."))
            return
        if not self.p.online:
            self.logError(_("Not connected to printer."))
            return
        self._do_load(filename)
        self.log(_("Uploading as %s") % targetname)
        self.log(_("Uploading %s") % self.filename)
        self.p.send_now("M28 " + targetname)
        self.log(_("Press Ctrl-C to interrupt upload."))
        self.p.startprint(self.fgcode)
        try:
            sys.stdout.write(_("Progress: ") + "00.0%")
            sys.stdout.flush()
971
            while self.p.printing:
972
                time.sleep(0.5)
973
                sys.stdout.write("\b\b\b\b\b%04.1f%%" % (100 * float(self.p.queueindex) / len(self.p.mainqueue),))
974
                sys.stdout.flush()
975
            self.p.send_now("M29 " + targetname)
976 977
            time.sleep(0.2)
            self.p.clear = True
978 979 980
            self._do_ls(False)
            self.log("\b\b\b\b\b100%.")
            self.log(_("Upload completed. %s should now be on the card.") % targetname)
981
            return
982 983 984 985
        except (KeyboardInterrupt, Exception) as e:
            if isinstance(e, KeyboardInterrupt):
                self.logError(_("...interrupted!"))
            else:
986 987
                self.logError(_("Something wrong happened while uploading:"))
                traceback.print_exc(file = sys.stdout)
988
            self.p.pause()
989
            self.p.send_now("M29 " + targetname)
990
            time.sleep(0.2)
991
            self.p.cancelprint()
992
            self.logError(_("A partial file named %s may have been written to the sd card.") % targetname)
993

994
    def complete_upload(self, text, line, begidx, endidx):
995
        s = line.split()
996
        if len(s) > 2:
997
            return []
998 999 1000
        if (len(s) == 1 and line[-1] == " ") or (len(s) == 2 and line[-1] != " "):
            if len(s) > 1:
                return [i[len(s[1]) - len(text):] for i in glob.glob(s[1] + "*/") + glob.glob(s[1] + "*.g*")]
1001
            else:
1002
                return glob.glob("*/") + glob.glob("*.g*")
1003

1004
    def help_upload(self):
1005
        self.log("Uploads a gcode file to the sd card")
1006

1007
    def help_print(self):
1008
        if not self.fgcode:
1009
            self.log(_("Send a loaded gcode file to the printer. Load a file with the load command first."))
1010
        else:
1011
            self.log(_("Send a loaded gcode file to the printer. You have %s loaded right now.") % self.filename)
1012

1013
    def do_print(self, l):
1014
        if not self.fgcode:
1015
            self.logError(_("No file loaded. Please use load first."))
1016 1017
            return
        if not self.p.online:
1018
            self.logError(_("Not connected to printer."))
1019
            return
1020 1021
        self.log(_("Printing %s") % self.filename)
        self.log(_("You can monitor the print with the monitor command."))
1022
        self.p.startprint(self.fgcode)
1023

1024
    def do_pause(self, l):
1025 1026 1027
        if self.sdprinting:
            self.p.send_now("M25")
        else:
1028
            if not self.p.printing:
1029
                self.logError(_("Not printing, cannot pause."))
1030 1031
                return
            self.p.pause()
1032
        self.paused = True
1033

1034
    def help_pause(self):
1035
        self.log(_("Pauses a running print"))
1036

1037 1038 1039
    def pause(self, event):
        return self.do_pause(None)

1040
    def do_resume(self, l):
1041
        if not self.paused:
1042
            self.logError(_("Not paused, unable to resume. Start a print first."))
1043
            return
1044
        self.paused = False
1045 1046 1047 1048 1049
        if self.sdprinting:
            self.p.send_now("M24")
            return
        else:
            self.p.resume()
1050

1051
    def help_resume(self):
1052
        self.log(_("Resumes a paused print."))
1053

1054
    def listfiles(self, line):
1055
        if "Begin file list" in line:
1056
            self.sdlisting = 1
1057
        elif "End file list" in line:
1058
            self.sdlisting = 0
1059
            self.recvlisteners.remove(self.listfiles)
1060
            if self.sdlisting_echo:
1061 1062
                self.log(_("Files on SD card:"))
                self.log("\n".join(self.sdfiles))
1063
        elif self.sdlisting:
1064 1065 1066 1067
            self.sdfiles.append(line.strip().lower())

    def _do_ls(self, echo):
        # FIXME: this was 2, but I think it should rather be 0 as in do_upload
1068 1069
        self.sdlisting = 0
        self.sdlisting_echo = echo
1070
        self.sdfiles = []
1071
        self.recvlisteners.append(self.listfiles)
1072
        self.p.send_now("M20")
1073

1074
    def do_ls(self, l):
1075
        if not self.p.online:
1076
            self.logError(_("Printer is not online. Please connect to it first."))
1077
            return
1078
        self._do_ls(True)
1079

1080
    def help_ls(self):
1081
        self.log(_("Lists files on the SD card"))
1082

1083
    def waitforsdresponse(self, l):
1084
        if "file.open failed" in l:
1085
            self.logError(_("Opening file failed."))
1086 1087 1088
            self.recvlisteners.remove(self.waitforsdresponse)
            return
        if "File opened" in l:
1089
            self.log(l)
1090
        if "File selected" in l:
1091
            self.log(_("Starting print"))
1092
            self.p.send_now("M24")
1093
            self.sdprinting = 1
Guillaume Seguin's avatar
Guillaume Seguin committed
1094
            # self.recvlisteners.remove(self.waitforsdresponse)
1095 1096
            return
        if "Done printing file" in l:
1097
            self.log(l)
1098
            self.sdprinting = 0
1099 1100
            self.recvlisteners.remove(self.waitforsdresponse)
            return
1101
        if "SD printing byte" in l:
Guillaume Seguin's avatar
Guillaume Seguin committed
1102
            # M27 handler
1103
            try:
1104 1105
                resp = l.split()
                vals = resp[-1].split("/")
1106
                self.percentdone = 100.0 * int(vals[0]) / int(vals[1])
1107 1108
            except:
                pass
1109

1110
    def do_reset(self, l):
1111
        self.p.reset()
1112

1113
    def help_reset(self):
1114
        self.log(_("Resets the printer."))
1115

1116
    def do_sdprint(self, l):
1117
        if not self.p.online:
1118
            self.log(_("Printer is not online. Please connect to it first."))
1119
            return
1120 1121 1122 1123 1124
        self._do_ls(False)
        while self.listfiles in self.recvlisteners:
            time.sleep(0.1)
        if l.lower() not in self.sdfiles:
            self.log(_("File is not present on card. Please upload it first."))
1125
            return
1126 1127 1128 1129
        self.recvlisteners.append(self.waitforsdresponse)
        self.p.send_now("M23 " + l.lower())
        self.log(_("Printing file: %s from SD card.") % l.lower())
        self.log(_("Requesting SD print..."))
1130
        time.sleep(1)
1131

1132
    def help_sdprint(self):
1133 1134
        self.log(_("Print a file from the SD card. Tab completes with available file names."))
        self.log(_("sdprint filename.g"))
1135

1136
    def complete_sdprint(self, text, line, begidx, endidx):
1137 1138 1139 1140
        if not self.sdfiles and self.p.online:
            self._do_ls(False)
            while self.listfiles in self.recvlisteners:
                time.sleep(0.1)
1141
        if (len(line.split()) == 2 and line[-1] != " ") or (len(line.split()) == 1 and line[-1] == " "):
1142
            return [i for i in self.sdfiles if i.startswith(text)]
1143

Guillaume Seguin's avatar
Guillaume Seguin committed
1144 1145 1146
    #  --------------------------------------------------------------
    #  Printcore callbacks
    #  --------------------------------------------------------------
1147

1148 1149 1150 1151 1152 1153
    def startcb(self, resuming = False):
        self.starttime = time.time()
        if resuming:
            print _("Print resumed at: %s") % format_time(self.starttime)
        else:
            print _("Print started at: %s") % format_time(self.starttime)
1154 1155 1156 1157
            if not self.sdprinting:
                self.compute_eta = RemainingTimeEstimator(self.fgcode)
            else:
                self.compute_eta = None
1158 1159 1160 1161 1162 1163 1164 1165

            if self.settings.start_command:
                output = get_command_output(self.settings.start_command,
                                            {"$s": str(self.filename),
                                             "$t": format_time(time.time())})
                if output:
                    self.log("Start command output:")
                    self.log(output.rstrip())
1166 1167 1168
        try:
            powerset_print_start(reason = "Preventing sleep during print")
        except:
1169
            logging.error(_("Failed to set power settings:"))
1170
            traceback.print_exc(file = sys.stdout)
1171 1172

    def endcb(self):
1173 1174 1175
        try:
            powerset_print_stop()
        except:
1176
            logging.error(_("Failed to set power settings:"))
1177
            traceback.print_exc(file = sys.stdout)
1178 1179
        if self.p.queueindex == 0:
            print_duration = int(time.time() - self.starttime + self.extra_print_time)
1180 1181 1182 1183 1184 1185
            self.log(_("Print ended at: %(end_time)s and took %(duration)s") % {"end_time": format_time(time.time()),
                                                                                "duration": format_duration(print_duration)})

            # Update total filament length used
            new_total = self.settings.total_filament_used + self.fgcode.filament_length
            self.set("total_filament_used", new_total)
1186

Guillaume Seguin's avatar
Guillaume Seguin committed
1187
            if not self.settings.final_command:
1188
                return
1189 1190 1191 1192 1193 1194
            output = get_command_output(self.settings.final_command,
                                        {"$s": str(self.filename),
                                         "$t": format_duration(print_duration)})
            if output:
                self.log("Final command output:")
                self.log(output.rstrip())
1195

1196 1197 1198 1199 1200
    def recvcb_report(self, l):
        isreport = REPORT_NONE
        if "ok C:" in l or "Count" in l \
           or ("X:" in l and len(gcoder.m114_exp.findall(l)) == 6):
            self.posreport = l
1201
            isreport = REPORT_POS
1202 1203
            if self.userm114 > 0:
                self.userm114 -= 1
1204
                isreport |= REPORT_MANUAL
1205
        if "ok T:" in l or tempreading_exp.findall(l):
1206
            self.tempreadings = l
1207
            isreport = REPORT_TEMP
1208 1209
            if self.userm105 > 0:
                self.userm105 -= 1
1210
                isreport |= REPORT_MANUAL
1211 1212 1213 1214 1215 1216
            else:
                self.m105_waitcycles = 0
        return isreport

    def recvcb(self, l):
        report_type = self.recvcb_report(l)
1217
        if report_type & REPORT_TEMP:
1218 1219
            self.status.update_tempreading(l)
        tstring = l.rstrip()
1220 1221
        for listener in self.recvlisteners:
            listener(l)
1222
        if tstring != "ok" and not self.sdlisting \
1223
           and not self.monitoring and (report_type == REPORT_NONE or report_type & REPORT_MANUAL):
1224 1225 1226 1227 1228 1229
            if tstring[:5] == "echo:":
                tstring = tstring[5:].lstrip()
            if self.silent is False: print "\r" + tstring.ljust(15)
            sys.stdout.write(self.promptf())
            sys.stdout.flush()

1230
    def layer_change_cb(self, newlayer):
1231 1232 1233
        layerz = self.fgcode.all_layers[newlayer].z
        if layerz is not None:
            self.curlayer = layerz
1234 1235 1236 1237
        if self.compute_eta:
            secondselapsed = int(time.time() - self.starttime + self.extra_print_time)
            self.compute_eta.update_layer(newlayer, secondselapsed)

1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248
    def get_eta(self):
        if self.sdprinting or self.uploading:
            if self.uploading:
                fractioncomplete = float(self.p.queueindex) / len(self.p.mainqueue)
            else:
                fractioncomplete = float(self.percentdone / 100.0)
            secondselapsed = int(time.time() - self.starttime + self.extra_print_time)
            # Prevent division by zero
            secondsestimate = secondselapsed / max(fractioncomplete, 0.000001)
            secondsremain = secondsestimate - secondselapsed
            progress = fractioncomplete
1249
        elif self.compute_eta is not None:
1250 1251 1252
            secondselapsed = int(time.time() - self.starttime + self.extra_print_time)
            secondsremain, secondsestimate = self.compute_eta(self.p.queueindex, secondselapsed)
            progress = self.p.queueindex
1253 1254
        else:
            secondsremain, secondsestimate, progress = 1, 1, 0
1255 1256
        return secondsremain, secondsestimate, progress

1257 1258 1259 1260
    def do_eta(self, l):
        if not self.p.printing:
            self.logError(_("Printer is not currently printing. No ETA available."))
        else:
1261
            secondsremain, secondsestimate, progress = self.get_eta()
1262 1263 1264 1265 1266 1267 1268
            eta = _("Est: %s of %s remaining") % (format_duration(secondsremain),
                                                  format_duration(secondsestimate))
            self.log(eta.strip())

    def help_eta(self):
        self.log(_("Displays estimated remaining print time."))

Guillaume Seguin's avatar
Guillaume Seguin committed
1269 1270 1271
    #  --------------------------------------------------------------
    #  Temperature handling
    #  --------------------------------------------------------------
1272

1273 1274 1275 1276 1277
    def set_temp_preset(self, key, value):
        if not key.startswith("bed"):
            self.temps["pla"] = str(self.settings.temperature_pla)
            self.temps["abs"] = str(self.settings.temperature_abs)
            self.log("Hotend temperature presets updated, pla:%s, abs:%s" % (self.temps["pla"], self.temps["abs"]))
1278
        else:
1279 1280 1281
            self.bedtemps["pla"] = str(self.settings.bedtemp_pla)
            self.bedtemps["abs"] = str(self.settings.bedtemp_abs)
            self.log("Bed temperature presets updated, pla:%s, abs:%s" % (self.bedtemps["pla"], self.bedtemps["abs"]))
1282

1283
    def tempcb(self, l):
1284
        if "T:" in l:
1285
            self.log(l.strip().replace("T", "Hotend").replace("B", "Bed").replace("ok ", ""))
1286

1287
    def do_gettemp(self, l):
1288 1289
        if "dynamic" in l:
            self.dynamic_temp = True
1290 1291
        if self.p.online:
            self.p.send_now("M105")
1292
            time.sleep(0.75)
1293 1294 1295 1296 1297
            if not self.status.bed_enabled:
                print "Hotend: %s/%s" % (self.status.extruder_temp, self.status.extruder_temp_target)
            else:
                print "Hotend: %s/%s" % (self.status.extruder_temp, self.status.extruder_temp_target)
                print "Bed:    %s/%s" % (self.status.bed_temp, self.status.bed_temp_target)
1298

1299
    def help_gettemp(self):
1300
        self.log(_("Read the extruder and bed temperature."))
1301

1302
    def do_settemp(self, l):
1303
        l = l.lower().replace(", ", ".")
1304 1305
        for i in self.temps.keys():
            l = l.replace(i, self.temps[i])
1306
        try:
1307
            f = float(l)
1308
        except:
1309
            self.logError(_("You must enter a temperature."))
1310 1311
            return

1312
        if f >= 0:
1313
            if f > 250:
1314 1315 1316
                print _("%s is a high temperature to set your extruder to. Are you sure you want to do that?") % f
                if not self.confirm():
                    return
1317
            if self.p.online:
1318
                self.p.send_now("M104 S" + l)
1319 1320 1321 1322 1323
                self.log(_("Setting hotend temperature to %s degrees Celsius.") % f)
            else:
                self.logError(_("Printer is not online."))
        else:
            self.logError(_("You cannot set negative temperatures. To turn the hotend off entirely, set its temperature to 0."))
1324

1325
    def help_settemp(self):
1326 1327
        self.log(_("Sets the hotend temperature to the value entered."))
        self.log(_("Enter either a temperature in celsius or one of the following keywords"))
1328
        self.log(", ".join([i + "(" + self.temps[i] + ")" for i in self.temps.keys()]))
1329

1330
    def complete_settemp(self, text, line, begidx, endidx):
1331
        if (len(line.split()) == 2 and line[-1] != " ") or (len(line.split()) == 1 and line[-1] == " "):
1332
            return [i for i in self.temps.keys() if i.startswith(text)]
1333

1334
    def do_bedtemp(self, l):
1335 1336
        f = None
        try:
1337
            l = l.lower().replace(", ", ".")
1338 1339 1340 1341 1342 1343 1344 1345 1346
            for i in self.bedtemps.keys():
                l = l.replace(i, self.bedtemps[i])
            f = float(l)
        except:
            self.logError(_("You must enter a temperature."))
        if f is not None and f >= 0:
            if self.p.online:
                self.p.send_now("M140 S" + l)
                self.log(_("Setting bed temperature to %s degrees Celsius.") % f)
1347
            else:
1348 1349 1350
                self.logError(_("Printer is not online."))
        else:
            self.logError(_("You cannot set negative temperatures. To turn the bed off entirely, set its temperature to 0."))
1351

1352
    def help_bedtemp(self):
1353 1354
        self.log(_("Sets the bed temperature to the value entered."))
        self.log(_("Enter either a temperature in celsius or one of the following keywords"))
1355
        self.log(", ".join([i + "(" + self.bedtemps[i] + ")" for i in self.bedtemps.keys()]))
1356

1357
    def complete_bedtemp(self, text, line, begidx, endidx):
1358
        if (len(line.split()) == 2 and line[-1] != " ") or (len(line.split()) == 1 and line[-1] == " "):
1359
            return [i for i in self.bedtemps.keys() if i.startswith(text)]
1360

1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387
    def do_monitor(self, l):
        interval = 5
        if not self.p.online:
            self.logError(_("Printer is not online. Please connect to it first."))
            return
        if not (self.p.printing or self.sdprinting):
            self.logError(_("Printer is not printing. Please print something before monitoring."))
            return
        self.log(_("Monitoring printer, use ^C to interrupt."))
        if len(l):
            try:
                interval = float(l)
            except:
                self.logError(_("Invalid period given."))
        self.log(_("Updating values every %f seconds.") % (interval,))
        self.monitoring = 1
        prev_msg_len = 0
        try:
            while True:
                self.p.send_now("M105")
                if self.sdprinting:
                    self.p.send_now("M27")
                time.sleep(interval)
                if self.p.printing:
                    preface = _("Print progress: ")
                    progress = 100 * float(self.p.queueindex) / len(self.p.mainqueue)
                elif self.sdprinting:
Guillaume Seguin's avatar
Guillaume Seguin committed
1388
                    preface = _("SD print progress: ")
1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403
                    progress = self.percentdone
                prev_msg = preface + "%.1f%%" % progress
                if self.silent is False:
                    sys.stdout.write("\r" + prev_msg.ljust(prev_msg_len))
                    sys.stdout.flush()
                prev_msg_len = len(prev_msg)
        except KeyboardInterrupt:
            if self.silent is False: print _("Done monitoring.")
        self.monitoring = 0

    def help_monitor(self):
        self.log(_("Monitor a machine's temperatures and an SD print's status."))
        self.log(_("monitor - Reports temperature and SD print status (if SD printing) every 5 seconds"))
        self.log(_("monitor 2 - Reports temperature and SD print status (if SD printing) every 2 seconds"))

Guillaume Seguin's avatar
Guillaume Seguin committed
1404 1405 1406
    #  --------------------------------------------------------------
    #  Manual printer controls
    #  --------------------------------------------------------------
1407

1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425
    def do_tool(self, l):
        tool = None
        try:
            tool = int(l.lower().strip())
        except:
            self.logError(_("You must specify the tool index as an integer."))
        if tool is not None and tool >= 0:
            if self.p.online:
                self.p.send_now("T%d" % tool)
                self.log(_("Using tool %d.") % tool)
            else:
                self.logError(_("Printer is not online."))
        else:
            self.logError(_("You cannot set negative tool numbers."))

    def help_tool(self):
        self.log(_("Switches to the specified tool (e.g. doing tool 1 will emit a T1 G-Code)."))

1426
    def do_move(self, l):
1427
        if len(l.split()) < 2:
1428
            self.logError(_("No move specified."))
1429 1430
            return
        if self.p.printing:
1431
            self.logError(_("Printer is currently printing. Please pause the print before you issue manual commands."))
1432 1433
            return
        if not self.p.online:
1434
            self.logError(_("Printer is not online. Unable to move."))
1435
            return
1436
        l = l.split()
1437
        if l[0].lower() == "x":
1438 1439
            feed = self.settings.xy_feedrate
            axis = "X"
1440
        elif l[0].lower() == "y":
1441 1442
            feed = self.settings.xy_feedrate
            axis = "Y"
1443
        elif l[0].lower() == "z":
1444 1445
            feed = self.settings.z_feedrate
            axis = "Z"
1446
        elif l[0].lower() == "e":
1447 1448
            feed = self.settings.e_feedrate
            axis = "E"
1449
        else:
1450
            self.logError(_("Unknown axis."))
1451 1452
            return
        try:
1453
            float(l[1])  # check if distance can be a float
1454
        except:
1455
            self.logError(_("Invalid distance"))
1456
            return
kliment's avatar
kliment committed
1457
        try:
1458
            feed = int(l[2])
kliment's avatar
kliment committed
1459 1460
        except:
            pass
1461
        self.p.send_now("G91")
1462
        self.p.send_now("G0 " + axis + str(l[1]) + " F" + str(feed))
1463
        self.p.send_now("G90")
1464

1465
    def help_move(self):
1466 1467 1468
        self.log(_("Move an axis. Specify the name of the axis and the amount. "))
        self.log(_("move X 10 will move the X axis forward by 10mm at %s mm/min (default XY speed)") % self.settings.xy_feedrate)
        self.log(_("move Y 10 5000 will move the Y axis forward by 10mm at 5000mm/min"))
1469
        self.log(_("move Z -1 will move the Z axis down by 1mm at %s mm/min (default Z speed)") % self.settings.z_feedrate)
1470
        self.log(_("Common amounts are in the tabcomplete list."))
1471

1472
    def complete_move(self, text, line, begidx, endidx):
1473
        if (len(line.split()) == 2 and line[-1] != " ") or (len(line.split()) == 1 and line[-1] == " "):
1474
            return [i for i in ["X ", "Y ", "Z ", "E "] if i.lower().startswith(text)]
1475
        elif len(line.split()) == 3 or (len(line.split()) == 2 and line[-1] == " "):
1476 1477
            base = line.split()[-1]
            rlen = 0
1478
            if base.startswith("-"):
1479
                rlen = 1
1480
            if line[-1] == " ":
1481
                base = ""
1482
            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)]
1483 1484
        else:
            return []
1485

1486
    def do_extrude(self, l, override = None, overridefeed = 300):
1487
        length = self.settings.default_extrusion  # default extrusion length
1488
        feed = self.settings.e_feedrate  # default speed
1489
        if not self.p.online:
1490
            self.logError("Printer is not online. Unable to extrude.")
1491 1492
            return
        if self.p.printing:
1493
            self.logError(_("Printer is currently printing. Please pause the print before you issue manual commands."))
1494
            return
1495
        ls = l.split()
1496 1497
        if len(ls):
            try:
1498
                length = float(ls[0])
1499
            except:
1500
                self.logError(_("Invalid length given."))
1501
        if len(ls) > 1:
1502
            try:
1503
                feed = int(ls[1])
1504
            except:
1505
                self.logError(_("Invalid speed given."))
1506
        if override is not None:
1507 1508
            length = override
            feed = overridefeed
Guillaume Seguin's avatar
Guillaume Seguin committed
1509 1510 1511
        self.do_extrude_final(length, feed)

    def do_extrude_final(self, length, feed):
1512
        if length > 0:
1513 1514
            self.log(_("Extruding %fmm of filament.") % (length,))
        elif length < 0:
1515
            self.log(_("Reversing %fmm of filament.") % (-length,))
1516
        else:
1517
            self.log(_("Length is 0, not doing anything."))
1518
        self.p.send_now("G91")
1519
        self.p.send_now("G1 E" + str(length) + " F" + str(feed))
1520
        self.p.send_now("G90")
1521

1522
    def help_extrude(self):
1523 1524 1525 1526 1527
        self.log(_("Extrudes a length of filament, 5mm by default, or the number of mm given as a parameter"))
        self.log(_("extrude - extrudes 5mm of filament at 300mm/min (5mm/s)"))
        self.log(_("extrude 20 - extrudes 20mm of filament at 300mm/min (5mm/s)"))
        self.log(_("extrude -5 - REVERSES 5mm of filament at 300mm/min (5mm/s)"))
        self.log(_("extrude 10 210 - extrudes 10mm of filament at 210mm/min (3.5mm/s)"))
1528

1529
    def do_reverse(self, l):
1530
        length = self.settings.default_extrusion  # default extrusion length
1531
        feed = self.settings.e_feedrate  # default speed
1532
        if not self.p.online:
1533
            self.logError(_("Printer is not online. Unable to reverse."))
1534 1535
            return
        if self.p.printing:
1536
            self.logError(_("Printer is currently printing. Please pause the print before you issue manual commands."))
1537
            return
1538
        ls = l.split()
1539 1540
        if len(ls):
            try:
1541
                length = float(ls[0])
1542
            except:
1543
                self.logError(_("Invalid length given."))
1544
        if len(ls) > 1:
1545
            try:
1546
                feed = int(ls[1])
1547
            except:
1548
                self.logError(_("Invalid speed given."))
1549
        self.do_extrude("", -length, feed)
1550

1551
    def help_reverse(self):
1552 1553 1554 1555 1556
        self.log(_("Reverses the extruder, 5mm by default, or the number of mm given as a parameter"))
        self.log(_("reverse - reverses 5mm of filament at 300mm/min (5mm/s)"))
        self.log(_("reverse 20 - reverses 20mm of filament at 300mm/min (5mm/s)"))
        self.log(_("reverse 10 210 - extrudes 10mm of filament at 210mm/min (3.5mm/s)"))
        self.log(_("reverse -5 - EXTRUDES 5mm of filament at 300mm/min (5mm/s)"))
1557

1558
    def do_home(self, l):
1559
        if not self.p.online:
1560
            self.logError(_("Printer is not online. Unable to move."))
1561 1562
            return
        if self.p.printing:
1563
            self.logError(_("Printer is currently printing. Please pause the print before you issue manual commands."))
1564 1565
            return
        if "x" in l.lower():
1566
            self.p.send_now("G28 X0")
1567
        if "y" in l.lower():
1568
            self.p.send_now("G28 Y0")
1569
        if "z" in l.lower():
1570
            self.p.send_now("G28 Z0")
kliment's avatar
kliment committed
1571 1572
        if "e" in l.lower():
            self.p.send_now("G92 E0")
1573
        if not len(l):
kliment's avatar
kliment committed
1574
            self.p.send_now("G28")
kliment's avatar
kliment committed
1575
            self.p.send_now("G92 E0")
1576

1577
    def help_home(self):
1578 1579 1580 1581 1582 1583
        self.log(_("Homes the printer"))
        self.log(_("home - homes all axes and zeroes the extruder(Using G28 and G92)"))
        self.log(_("home xy - homes x and y axes (Using G28)"))
        self.log(_("home z - homes z axis only (Using G28)"))
        self.log(_("home e - set extruder position to zero (Using G92)"))
        self.log(_("home xyze - homes all axes and zeroes the extruder (Using G28 and G92)"))
1584

1585
    def do_off(self, l):
Guillaume Seguin's avatar
Guillaume Seguin committed
1586 1587
        self.off()

Guillaume Seguin's avatar
Guillaume Seguin committed
1588
    def off(self, ignore = None):
1589 1590
        if self.p.online:
            if self.p.printing: self.pause(None)
1591 1592 1593 1594 1595 1596 1597 1598 1599 1600
            self.log(_("; Motors off"))
            self.onecmd("M84")
            self.log(_("; Extruder off"))
            self.onecmd("M104 S0")
            self.log(_("; Heatbed off"))
            self.onecmd("M140 S0")
            self.log(_("; Fan off"))
            self.onecmd("M107")
            self.log(_("; Power supply off"))
            self.onecmd("M81")
1601 1602 1603 1604 1605 1606
        else:
            self.logError(_("Printer is not online. Unable to turn it off."))

    def help_off(self):
        self.log(_("Turns off everything on the printer"))

Guillaume Seguin's avatar
Guillaume Seguin committed
1607 1608 1609
    #  --------------------------------------------------------------
    #  Host commands handling
    #  --------------------------------------------------------------
1610

1611 1612 1613 1614 1615 1616 1617
    def process_host_command(self, command):
        """Override host command handling"""
        command = command.lstrip()
        if command.startswith(";@"):
            command = command[2:]
            self.log(_("G-Code calling host command \"%s\"") % command)
            self.onecmd(command)
1618

1619
    def do_run_script(self, l):
1620
        p = run_command(l, {"$s": str(self.filename)}, stdout = subprocess.PIPE)
1621 1622 1623 1624
        for line in p.stdout.readlines():
            self.log("<< " + line.strip())

    def help_run_script(self):
1625
        self.log(_("Runs a custom script. Current gcode filename can be given using $s token."))
1626

1627
    def do_run_gcode_script(self, l):
1628
        p = run_command(l, {"$s": str(self.filename)}, stdout = subprocess.PIPE)
1629 1630 1631 1632
        for line in p.stdout.readlines():
            self.onecmd(line.strip())

    def help_run_gcode_script(self):
1633
        self.log(_("Runs a custom script which output gcode which will in turn be executed. Current gcode filename can be given using $s token."))