printrun-src/printrun/pronsole.py

changeset 15
0bbb006204fc
child 38
700241b53fe7
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/printrun-src/printrun/pronsole.py	Fri Jun 03 09:16:07 2016 +0200
@@ -0,0 +1,1674 @@
+#!/usr/bin/env python
+
+# This file is part of the Printrun suite.
+#
+# 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.
+#
+# 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.
+#
+# You should have received a copy of the GNU General Public License
+# along with Printrun.  If not, see <http://www.gnu.org/licenses/>.
+
+import cmd
+import glob
+import os
+import time
+import threading
+import sys
+import shutil
+import subprocess
+import codecs
+import argparse
+import locale
+import logging
+import traceback
+import re
+
+from serial import SerialException
+
+from . import printcore
+from .utils import install_locale, run_command, get_command_output, \
+    format_time, format_duration, RemainingTimeEstimator, \
+    get_home_pos, parse_build_dimensions, parse_temperature_report, \
+    setup_logging
+install_locale('pronterface')
+from .settings import Settings, BuildDimensionsSetting
+from .power import powerset_print_start, powerset_print_stop
+from printrun import gcoder
+from .rpc import ProntRPC
+
+if os.name == "nt":
+    try:
+        import _winreg
+    except:
+        pass
+READLINE = True
+try:
+    import readline
+    try:
+        readline.rl.mode.show_all_if_ambiguous = "on"  # config pyreadline on windows
+    except:
+        pass
+except:
+    READLINE = False  # neither readline module is available
+
+tempreading_exp = re.compile("(^T:| T:)")
+
+REPORT_NONE = 0
+REPORT_POS = 1
+REPORT_TEMP = 2
+REPORT_MANUAL = 4
+
+class Status(object):
+
+    def __init__(self):
+        self.extruder_temp = 0
+        self.extruder_temp_target = 0
+        self.bed_temp = 0
+        self.bed_temp_target = 0
+        self.print_job = None
+        self.print_job_progress = 1.0
+
+    def update_tempreading(self, tempstr):
+        temps = parse_temperature_report(tempstr)
+        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])
+        else: hotend_temp = None
+        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])
+        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
+        bed_temp = float(temps["B"][0]) if "B" in temps and temps["B"][0] else None
+        if bed_temp is not None:
+            self.bed_temp = bed_temp
+            setpoint = temps["B"][1]
+            if setpoint:
+                self.bed_temp_target = float(setpoint)
+
+    @property
+    def bed_enabled(self):
+        return self.bed_temp != 0
+
+    @property
+    def extruder_enabled(self):
+        return self.extruder_temp != 0
+
+
+class pronsole(cmd.Cmd):
+    def __init__(self):
+        cmd.Cmd.__init__(self)
+        if not READLINE:
+            self.completekey = None
+        self.status = Status()
+        self.dynamic_temp = False
+        self.compute_eta = None
+        self.statuscheck = False
+        self.status_thread = None
+        self.monitor_interval = 3
+        self.p = printcore.printcore()
+        self.p.recvcb = self.recvcb
+        self.p.startcb = self.startcb
+        self.p.endcb = self.endcb
+        self.p.layerchangecb = self.layer_change_cb
+        self.p.process_host_command = self.process_host_command
+        self.recvlisteners = []
+        self.in_macro = False
+        self.p.onlinecb = self.online
+        self.p.errorcb = self.logError
+        self.fgcode = None
+        self.filename = None
+        self.rpc_server = None
+        self.curlayer = 0
+        self.sdlisting = 0
+        self.sdlisting_echo = 0
+        self.sdfiles = []
+        self.paused = False
+        self.sdprinting = 0
+        self.uploading = 0  # Unused, just for pronterface generalization
+        self.temps = {"pla": "185", "abs": "230", "off": "0"}
+        self.bedtemps = {"pla": "60", "abs": "110", "off": "0"}
+        self.percentdone = 0
+        self.posreport = ""
+        self.tempreadings = ""
+        self.userm114 = 0
+        self.userm105 = 0
+        self.m105_waitcycles = 0
+        self.macros = {}
+        self.history_file = "~/.pronsole-history"
+        self.rc_loaded = False
+        self.processing_rc = False
+        self.processing_args = False
+        self.settings = Settings(self)
+        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)
+        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
+        self.update_build_dimensions(None, self.settings.build_dimensions)
+        self.update_tcp_streaming_mode(None, self.settings.tcp_streaming_mode)
+        self.monitoring = 0
+        self.starttime = 0
+        self.extra_print_time = 0
+        self.silent = False
+        self.commandprefixes = 'MGT$'
+        self.promptstrs = {"offline": "%(bold)soffline>%(normal)s ",
+                           "fallback": "%(bold)sPC>%(normal)s ",
+                           "macro": "%(bold)s..>%(normal)s ",
+                           "online": "%(bold)sT:%(extruder_temp_fancy)s%(progress_fancy)s>%(normal)s "}
+
+    #  --------------------------------------------------------------
+    #  General console handling
+    #  --------------------------------------------------------------
+
+    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")
+                history = os.path.expanduser(self.history_file)
+                if os.path.exists(history):
+                    readline.read_history_file(history)
+            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:
+                            self.log("")
+                            self.do_exit("")
+                        except KeyboardInterrupt:
+                            self.log("")
+                            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)
+                    readline.write_history_file(history)
+                except ImportError:
+                    pass
+
+    def confirm(self):
+        y_or_n = raw_input("y/n: ")
+        if y_or_n == "y":
+            return True
+        elif y_or_n != "n":
+            return self.confirm()
+        return False
+
+    def log(self, *msg):
+        msg = u"".join(unicode(i) for i in msg)
+        logging.info(msg)
+
+    def logError(self, *msg):
+        msg = u"".join(unicode(i) for i in msg)
+        logging.error(msg)
+        if not self.settings.error_command:
+            return
+        output = get_command_output(self.settings.error_command, {"$m": msg})
+        if output:
+            self.log("Error command output:")
+            self.log(output.rstrip())
+
+    def promptf(self):
+        """A function to generate prompts so that we can do dynamic prompts. """
+        if self.in_macro:
+            promptstr = self.promptstrs["macro"]
+        elif not self.p.online:
+            promptstr = self.promptstrs["offline"]
+        elif self.status.extruder_enabled:
+            promptstr = self.promptstrs["online"]
+        else:
+            promptstr = self.promptstrs["fallback"]
+        if "%" not in promptstr:
+            return promptstr
+        else:
+            specials = {}
+            specials["extruder_temp"] = str(int(self.status.extruder_temp))
+            specials["extruder_temp_target"] = str(int(self.status.extruder_temp_target))
+            if self.status.extruder_temp_target == 0:
+                specials["extruder_temp_fancy"] = str(int(self.status.extruder_temp))
+            else:
+                specials["extruder_temp_fancy"] = "%s/%s" % (str(int(self.status.extruder_temp)), str(int(self.status.extruder_temp_target)))
+            if self.p.printing:
+                progress = int(1000 * float(self.p.queueindex) / len(self.p.mainqueue)) / 10
+            elif self.sdprinting:
+                progress = self.percentdone
+            else:
+                progress = 0.0
+            specials["progress"] = str(progress)
+            if self.p.printing or self.sdprinting:
+                specials["progress_fancy"] = " " + str(progress) + "%"
+            else:
+                specials["progress_fancy"] = ""
+            specials["bold"] = "\033[01m"
+            specials["normal"] = "\033[00m"
+            return promptstr % specials
+
+    def postcmd(self, stop, line):
+        """ A hook we override to generate prompts after
+            each command is executed, for the next prompt.
+            We also use it to send M105 commands so that
+            temp info gets updated for the prompt."""
+        if self.p.online and self.dynamic_temp:
+            self.p.send_now("M105")
+        self.prompt = self.promptf()
+        return stop
+
+    def kill(self):
+        self.statuscheck = False
+        if self.status_thread:
+            self.status_thread.join()
+            self.status_thread = None
+        if self.rpc_server is not None:
+            self.rpc_server.shutdown()
+
+    def write_prompt(self):
+        sys.stdout.write(self.promptf())
+        sys.stdout.flush()
+
+    def help_help(self, l = ""):
+        self.do_help("")
+
+    def do_gcodes(self, l = ""):
+        self.help_gcodes()
+
+    def help_gcodes(self):
+        self.log("Gcodes are passed through to the printer as they are")
+
+    def precmd(self, line):
+        if line.upper().startswith("M114"):
+            self.userm114 += 1
+        elif line.upper().startswith("M105"):
+            self.userm105 += 1
+        return line
+
+    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):
+        if l[0].upper() in self.commandprefixes.upper():
+            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:
+            self.log("Setting extruder temp to 0")
+        self.p.send_now("M104 S0.0")
+        if self.status.bed_enabled:
+            if self.status.bed_temp_target != 0:
+                self.log("Setting bed temp to 0")
+            self.p.send_now("M140 S0.0")
+        self.log("Disconnecting from printer...")
+        if self.p.printing:
+            self.log(_("Are you sure you want to exit while printing?\n\
+(this will terminate the print)."))
+            if not self.confirm():
+                return
+        self.log(_("Exiting program. Goodbye!"))
+        self.p.disconnect()
+        self.kill()
+        sys.exit()
+
+    def help_exit(self):
+        self.log(_("Disconnects from the printer and exits the program."))
+
+    # --------------------------------------------------------------
+    # Macro handling
+    # --------------------------------------------------------------
+
+    def complete_macro(self, text, line, begidx, endidx):
+        if (len(line.split()) == 2 and line[-1] != " ") or (len(line.split()) == 1 and line[-1] == " "):
+            return [i for i in self.macros.keys() if i.startswith(text)]
+        elif len(line.split()) == 3 or (len(line.split()) == 2 and line[-1] == " "):
+            return [i for i in ["/D", "/S"] + self.completenames(text) if i.startswith(text)]
+        else:
+            return []
+
+    def hook_macro(self, l):
+        l = l.rstrip()
+        ls = l.lstrip()
+        ws = l[:len(l) - len(ls)]  # just leading whitespace
+        if len(ws) == 0:
+            self.end_macro()
+            # pass the unprocessed line to regular command processor to not require empty line in .pronsolerc
+            return self.onecmd(l)
+        self.cur_macro_def += l + "\n"
+
+    def end_macro(self):
+        if "onecmd" in self.__dict__: del self.onecmd  # remove override
+        self.in_macro = False
+        self.prompt = self.promptf()
+        if self.cur_macro_def != "":
+            self.macros[self.cur_macro_name] = self.cur_macro_def
+            macro = self.compile_macro(self.cur_macro_name, self.cur_macro_def)
+            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))
+            if not self.processing_rc:
+                self.log("Macro '" + self.cur_macro_name + "' defined")
+                # save it
+                if not self.processing_args:
+                    macro_key = "macro " + self.cur_macro_name
+                    macro_def = macro_key
+                    if "\n" in self.cur_macro_def:
+                        macro_def += "\n"
+                    else:
+                        macro_def += " "
+                    macro_def += self.cur_macro_def
+                    self.save_in_rc(macro_key, macro_def)
+        else:
+            self.logError("Empty macro - cancelled")
+        del self.cur_macro_name, self.cur_macro_def
+
+    def compile_macro_line(self, line):
+        line = line.rstrip()
+        ls = line.lstrip()
+        ws = line[:len(line) - len(ls)]  # just leading whitespace
+        if ls == "" or ls.startswith('#'): return ""  # no code
+        if ls.startswith('!'):
+            return ws + ls[1:] + "\n"  # python mode
+        else:
+            ls = ls.replace('"', '\\"')  # need to escape double quotes
+            ret = ws + 'self.precmd("' + ls + '".format(*arg))\n'  # parametric command mode
+            return ret + ws + 'self.onecmd("' + ls + '".format(*arg))\n'
+
+    def compile_macro(self, macro_name, macro_def):
+        if macro_def.strip() == "":
+            self.logError("Empty macro - cancelled")
+            return
+        macro = None
+        pycode = "def macro(self,*arg):\n"
+        if "\n" not in macro_def.strip():
+            pycode += self.compile_macro_line("  " + macro_def.strip())
+        else:
+            lines = macro_def.split("\n")
+            for l in lines:
+                pycode += self.compile_macro_line(l)
+        exec pycode
+        return macro
+
+    def start_macro(self, macro_name, prev_definition = "", suppress_instructions = False):
+        if not self.processing_rc and not suppress_instructions:
+            self.logError("Enter macro using indented lines, end with empty line")
+        self.cur_macro_name = macro_name
+        self.cur_macro_def = ""
+        self.onecmd = self.hook_macro  # override onecmd temporarily
+        self.in_macro = False
+        self.prompt = self.promptf()
+
+    def delete_macro(self, macro_name):
+        if macro_name in self.macros.keys():
+            delattr(self.__class__, "do_" + macro_name)
+            del self.macros[macro_name]
+            self.log("Macro '" + macro_name + "' removed")
+            if not self.processing_rc and not self.processing_args:
+                self.save_in_rc("macro " + macro_name, "")
+        else:
+            self.logError("Macro '" + macro_name + "' is not defined")
+
+    def do_macro(self, args):
+        if args.strip() == "":
+            self.print_topics("User-defined macros", map(str, self.macros.keys()), 15, 80)
+            return
+        arglist = args.split(None, 1)
+        macro_name = arglist[0]
+        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")
+            return
+        if len(arglist) == 2:
+            macro_def = arglist[1]
+            if macro_def.lower() == "/d":
+                self.delete_macro(macro_name)
+                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()
+            return
+        if macro_name in self.macros:
+            self.start_macro(macro_name, self.macros[macro_name])
+        else:
+            self.start_macro(macro_name)
+
+    def help_macro(self):
+        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")
+
+    def subhelp_macro(self, macro_name):
+        if macro_name in self.macros.keys():
+            macro_def = self.macros[macro_name]
+            if "\n" in macro_def:
+                self.log("Macro '" + macro_name + "' defined as:")
+                self.log(self.macros[macro_name] + "----------------")
+            else:
+                self.log("Macro '" + macro_name + "' defined as: '" + macro_def + "'")
+        else:
+            self.logError("Macro '" + macro_name + "' is not defined")
+
+    # --------------------------------------------------------------
+    # Configuration handling
+    # --------------------------------------------------------------
+
+    def set(self, var, str):
+        try:
+            t = type(getattr(self.settings, var))
+            value = self.settings._set(var, str)
+            if not self.processing_rc and not self.processing_args:
+                self.save_in_rc("set " + var, "set %s %s" % (var, value))
+        except AttributeError:
+            logging.debug(_("Unknown variable '%s'") % var)
+        except ValueError, ve:
+            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]))
+
+    def do_set(self, argl):
+        args = argl.split(None, 1)
+        if len(args) < 1:
+            for k in [kk for kk in dir(self.settings) if not kk.startswith("_")]:
+                self.log("%s = %s" % (k, str(getattr(self.settings, k))))
+            return
+        if len(args) < 2:
+            # Try getting the default value of the setting to check whether it
+            # actually exists
+            try:
+                getattr(self.settings, args[0])
+            except AttributeError:
+                logging.warning("Unknown variable '%s'" % args[0])
+            return
+        self.set(args[0], args[1])
+
+    def help_set(self):
+        self.log("Set variable:   set <variable> <value>")
+        self.log("Show variable:  set <variable>")
+        self.log("'set' without arguments displays all variables")
+
+    def complete_set(self, text, line, begidx, endidx):
+        if (len(line.split()) == 2 and line[-1] != " ") or (len(line.split()) == 1 and line[-1] == " "):
+            return [i for i in dir(self.settings) if not i.startswith("_") and i.startswith(text)]
+        elif len(line.split()) == 3 or (len(line.split()) == 2 and line[-1] == " "):
+            return [i for i in self.settings._tabcomplete(line.split()[1]) if i.startswith(text)]
+        else:
+            return []
+
+    def load_rc(self, rc_filename):
+        self.processing_rc = True
+        try:
+            rc = codecs.open(rc_filename, "r", "utf-8")
+            self.rc_filename = os.path.abspath(rc_filename)
+            for rc_cmd in rc:
+                if not rc_cmd.lstrip().startswith("#"):
+                    self.onecmd(rc_cmd)
+            rc.close()
+            if hasattr(self, "cur_macro_def"):
+                self.end_macro()
+            self.rc_loaded = True
+        finally:
+            self.processing_rc = False
+
+    def load_default_rc(self, rc_filename = ".pronsolerc"):
+        if rc_filename == ".pronsolerc" and hasattr(sys, "frozen") and sys.frozen in ["windows_exe", "console_exe"]:
+            rc_filename = "printrunconf.ini"
+        try:
+            try:
+                self.load_rc(os.path.join(os.path.expanduser("~"), rc_filename))
+            except IOError:
+                self.load_rc(rc_filename)
+        except IOError:
+            # make sure the filename is initialized
+            self.rc_filename = os.path.abspath(os.path.join(os.path.expanduser("~"), rc_filename))
+
+    def save_in_rc(self, key, definition):
+        """
+        Saves or updates macro or other definitions in .pronsolerc
+        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.
+        """
+        rci, rco = None, None
+        if definition != "" and not definition.endswith("\n"):
+            definition += "\n"
+        try:
+            written = False
+            if os.path.exists(self.rc_filename):
+                shutil.copy(self.rc_filename, self.rc_filename + "~bak")
+                rci = codecs.open(self.rc_filename + "~bak", "r", "utf-8")
+            rco = codecs.open(self.rc_filename + "~new", "w", "utf-8")
+            if rci is not None:
+                overwriting = False
+                for rc_cmd in rci:
+                    l = rc_cmd.rstrip()
+                    ls = l.lstrip()
+                    ws = l[:len(l) - len(ls)]  # just leading whitespace
+                    if overwriting and len(ws) == 0:
+                        overwriting = False
+                    if not written and key != "" and rc_cmd.startswith(key) and (rc_cmd + "\n")[len(key)].isspace():
+                        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()
+            shutil.move(self.rc_filename + "~new", self.rc_filename)
+            # if definition != "":
+            #    self.log("Saved '"+key+"' to '"+self.rc_filename+"'")
+            # else:
+            #    self.log("Removed '"+key+"' from '"+self.rc_filename+"'")
+        except Exception, e:
+            self.logError("Saving failed for ", key + ":", str(e))
+        finally:
+            del rci, rco
+
+    #  --------------------------------------------------------------
+    #  Configuration update callbacks
+    #  --------------------------------------------------------------
+
+    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)
+
+    def update_tcp_streaming_mode(self, param, value):
+        self.p.tcp_streaming_mode = self.settings.tcp_streaming_mode
+
+    def update_rpc_server(self, param, value):
+        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
+
+    #  --------------------------------------------------------------
+    #  Command line options handling
+    #  --------------------------------------------------------------
+
+    def add_cmdline_arguments(self, parser):
+        parser.add_argument('-v', '--verbose', help = _("increase verbosity"), action = "store_true")
+        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):
+        if args.verbose:
+            logger = logging.getLogger()
+            logger.setLevel(logging.DEBUG)
+        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
+        self.update_rpc_server(None, self.settings.rpc_server)
+        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)
+        setup_logging(sys.stdout, self.settings.log_path, True)
+
+    #  --------------------------------------------------------------
+    #  Printer connection handling
+    #  --------------------------------------------------------------
+
+    def connect_to_printer(self, port, baud, dtr):
+        try:
+            self.p.connect(port, baud, dtr)
+        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
+        self.statuscheck = True
+        self.status_thread = threading.Thread(target = self.statuschecker)
+        self.status_thread.start()
+        return True
+
+    def do_connect(self, l):
+        a = l.split()
+        p = self.scanserial()
+        port = self.settings.port
+        if (port == "" or port not in p) and len(p) > 0:
+            port = p[0]
+        baud = self.settings.baudrate or 115200
+        if len(a) > 0:
+            port = a[0]
+        if len(a) > 1:
+            try:
+                baud = int(a[1])
+            except:
+                self.log("Bad baud value '" + a[1] + "' ignored")
+        if len(p) == 0 and not port:
+            self.log("No serial ports detected - please specify a port")
+            return
+        if len(a) == 0:
+            self.log("No port specified - connecting to %s at %dbps" % (port, baud))
+        if port != self.settings.port:
+            self.settings.port = port
+            self.save_in_rc("set port", "set port %s" % port)
+        if baud != self.settings.baudrate:
+            self.settings.baudrate = baud
+            self.save_in_rc("set baudrate", "set baudrate %d" % baud)
+        self.connect_to_printer(port, baud, self.settings.dtr)
+
+    def help_connect(self):
+        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")
+        ports = self.scanserial()
+        if ports:
+            self.log("Available ports: ", " ".join(ports))
+        else:
+            self.log("No serial ports were automatically found.")
+
+    def complete_connect(self, text, line, begidx, endidx):
+        if (len(line.split()) == 2 and line[-1] != " ") or (len(line.split()) == 1 and line[-1] == " "):
+            return [i for i in self.scanserial() if i.startswith(text)]
+        elif len(line.split()) == 3 or (len(line.split()) == 2 and line[-1] == " "):
+            return [i for i in ["2400", "9600", "19200", "38400", "57600", "115200"] if i.startswith(text)]
+        else:
+            return []
+
+    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()
+
+    def do_disconnect(self, l):
+        self.p.disconnect()
+
+    def help_disconnect(self):
+        self.log("Disconnects from the printer")
+
+    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")
+
+    #  --------------------------------------------------------------
+    #  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 and not self.paused:
+                    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()
+
+    #  --------------------------------------------------------------
+    #  File loading handling
+    #  --------------------------------------------------------------
+
+    def do_load(self, filename):
+        self._do_load(filename)
+
+    def _do_load(self, filename):
+        if not filename:
+            self.logError("No file name given.")
+            return
+        self.log(_("Loading file: %s") % filename)
+        if not os.path.exists(filename):
+            self.logError("File not found!")
+            return
+        self.load_gcode(filename)
+        self.log(_("Loaded %s, %d lines.") % (filename, len(self.fgcode)))
+        self.log(_("Estimated duration: %d layers, %s") % self.fgcode.estimate_duration())
+
+    def load_gcode(self, filename, layer_callback = None, gcode = None):
+        if gcode is None:
+            self.fgcode = gcoder.LightGCode(deferred = True)
+        else:
+            self.fgcode = gcode
+        self.fgcode.prepare(open(filename, "rU"),
+                            get_home_pos(self.build_dimensions_list),
+                            layer_callback = layer_callback)
+        self.fgcode.estimate_duration()
+        self.filename = filename
+
+    def complete_load(self, text, line, begidx, endidx):
+        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] + "*.g*")]
+            else:
+                return glob.glob("*/") + glob.glob("*.g*")
+
+    def help_load(self):
+        self.log("Loads a gcode file (with tab-completion)")
+
+    def do_slice(self, l):
+        l = l.split()
+        if len(l) == 0:
+            self.logError(_("No file name given."))
+            return
+        settings = 0
+        if l[0] == "set":
+            settings = 1
+        else:
+            self.log(_("Slicing file: %s") % l[0])
+            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)
+
+    def complete_slice(self, text, line, begidx, endidx):
+        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")
+
+    def help_slice(self):
+        self.log(_("Creates a gcode file from an stl model using the slicer (with tab-completion)"))
+        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"))
+
+    #  --------------------------------------------------------------
+    #  Print/upload handling
+    #  --------------------------------------------------------------
+
+    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()
+            while self.p.printing:
+                time.sleep(0.5)
+                sys.stdout.write("\b\b\b\b\b%04.1f%%" % (100 * float(self.p.queueindex) / len(self.p.mainqueue),))
+                sys.stdout.flush()
+            self.p.send_now("M29 " + targetname)
+            time.sleep(0.2)
+            self.p.clear = True
+            self._do_ls(False)
+            self.log("\b\b\b\b\b100%.")
+            self.log(_("Upload completed. %s should now be on the card.") % targetname)
+            return
+        except (KeyboardInterrupt, Exception) as e:
+            if isinstance(e, KeyboardInterrupt):
+                self.logError(_("...interrupted!"))
+            else:
+                self.logError(_("Something wrong happened while uploading:")
+                              + "\n" + traceback.format_exc())
+            self.p.pause()
+            self.p.send_now("M29 " + targetname)
+            time.sleep(0.2)
+            self.p.cancelprint()
+            self.logError(_("A partial file named %s may have been written to the sd card.") % targetname)
+
+    def complete_upload(self, text, line, begidx, endidx):
+        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] + "*.g*")]
+            else:
+                return glob.glob("*/") + glob.glob("*.g*")
+
+    def help_upload(self):
+        self.log("Uploads a gcode file to the sd card")
+
+    def help_print(self):
+        if not self.fgcode:
+            self.log(_("Send a loaded gcode file to the printer. Load a file with the load command first."))
+        else:
+            self.log(_("Send a loaded gcode file to the printer. You have %s loaded right now.") % self.filename)
+
+    def do_print(self, l):
+        if not self.fgcode:
+            self.logError(_("No file loaded. Please use load first."))
+            return
+        if not self.p.online:
+            self.logError(_("Not connected to printer."))
+            return
+        self.log(_("Printing %s") % self.filename)
+        self.log(_("You can monitor the print with the monitor command."))
+        self.sdprinting = False
+        self.p.startprint(self.fgcode)
+
+    def do_pause(self, l):
+        if self.sdprinting:
+            self.p.send_now("M25")
+        else:
+            if not self.p.printing:
+                self.logError(_("Not printing, cannot pause."))
+                return
+            self.p.pause()
+        self.paused = True
+
+    def help_pause(self):
+        self.log(_("Pauses a running print"))
+
+    def pause(self, event = None):
+        return self.do_pause(None)
+
+    def do_resume(self, l):
+        if not self.paused:
+            self.logError(_("Not paused, unable to resume. Start a print first."))
+            return
+        self.paused = False
+        if self.sdprinting:
+            self.p.send_now("M24")
+            return
+        else:
+            self.p.resume()
+
+    def help_resume(self):
+        self.log(_("Resumes a paused print."))
+
+    def listfiles(self, line):
+        if "Begin file list" in line:
+            self.sdlisting = 1
+        elif "End file list" in line:
+            self.sdlisting = 0
+            self.recvlisteners.remove(self.listfiles)
+            if self.sdlisting_echo:
+                self.log(_("Files on SD card:"))
+                self.log("\n".join(self.sdfiles))
+        elif self.sdlisting:
+            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
+        self.sdlisting = 0
+        self.sdlisting_echo = echo
+        self.sdfiles = []
+        self.recvlisteners.append(self.listfiles)
+        self.p.send_now("M20")
+
+    def do_ls(self, l):
+        if not self.p.online:
+            self.logError(_("Printer is not online. Please connect to it first."))
+            return
+        self._do_ls(True)
+
+    def help_ls(self):
+        self.log(_("Lists files on the SD card"))
+
+    def waitforsdresponse(self, l):
+        if "file.open failed" in l:
+            self.logError(_("Opening file failed."))
+            self.recvlisteners.remove(self.waitforsdresponse)
+            return
+        if "File opened" in l:
+            self.log(l)
+        if "File selected" in l:
+            self.log(_("Starting print"))
+            self.p.send_now("M24")
+            self.sdprinting = True
+            # self.recvlisteners.remove(self.waitforsdresponse)
+            return
+        if "Done printing file" in l:
+            self.log(l)
+            self.sdprinting = False
+            self.recvlisteners.remove(self.waitforsdresponse)
+            return
+        if "SD printing byte" in l:
+            # M27 handler
+            try:
+                resp = l.split()
+                vals = resp[-1].split("/")
+                self.percentdone = 100.0 * int(vals[0]) / int(vals[1])
+            except:
+                pass
+
+    def do_reset(self, l):
+        self.p.reset()
+
+    def help_reset(self):
+        self.log(_("Resets the printer."))
+
+    def do_sdprint(self, l):
+        if not self.p.online:
+            self.log(_("Printer is not online. Please connect to it first."))
+            return
+        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."))
+            return
+        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..."))
+        time.sleep(1)
+
+    def help_sdprint(self):
+        self.log(_("Print a file from the SD card. Tab completes with available file names."))
+        self.log(_("sdprint filename.g"))
+
+    def complete_sdprint(self, text, line, begidx, endidx):
+        if not self.sdfiles and self.p.online:
+            self._do_ls(False)
+            while self.listfiles in self.recvlisteners:
+                time.sleep(0.1)
+        if (len(line.split()) == 2 and line[-1] != " ") or (len(line.split()) == 1 and line[-1] == " "):
+            return [i for i in self.sdfiles if i.startswith(text)]
+
+    #  --------------------------------------------------------------
+    #  Printcore callbacks
+    #  --------------------------------------------------------------
+
+    def startcb(self, resuming = False):
+        self.starttime = time.time()
+        if resuming:
+            self.log(_("Print resumed at: %s") % format_time(self.starttime))
+        else:
+            self.log(_("Print started at: %s") % format_time(self.starttime))
+            if not self.sdprinting:
+                self.compute_eta = RemainingTimeEstimator(self.fgcode)
+            else:
+                self.compute_eta = None
+
+            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())
+        try:
+            powerset_print_start(reason = "Preventing sleep during print")
+        except:
+            self.logError(_("Failed to set power settings:")
+                          + "\n" + traceback.format_exc())
+
+    def endcb(self):
+        try:
+            powerset_print_stop()
+        except:
+            self.logError(_("Failed to set power settings:")
+                          + "\n" + traceback.format_exc())
+        if self.p.queueindex == 0:
+            print_duration = int(time.time() - self.starttime + self.extra_print_time)
+            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
+            if self.fgcode is not None:
+                new_total = self.settings.total_filament_used + self.fgcode.filament_length
+                self.set("total_filament_used", new_total)
+
+            if not self.settings.final_command:
+                return
+            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())
+
+    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
+            isreport = REPORT_POS
+            if self.userm114 > 0:
+                self.userm114 -= 1
+                isreport |= REPORT_MANUAL
+        if "ok T:" in l or tempreading_exp.findall(l):
+            self.tempreadings = l
+            isreport = REPORT_TEMP
+            if self.userm105 > 0:
+                self.userm105 -= 1
+                isreport |= REPORT_MANUAL
+            else:
+                self.m105_waitcycles = 0
+        return isreport
+
+    def recvcb_actions(self, l):
+        if l.startswith("!!"):
+            self.do_pause(None)
+            msg = l.split(" ", 1)
+            if len(msg) > 1 and self.silent is False: self.logError(msg[1].ljust(15))
+            sys.stdout.write(self.promptf())
+            sys.stdout.flush()
+            return True
+        elif l.startswith("//"):
+            command = l.split(" ", 1)
+            if len(command) > 1:
+                command = command[1]
+                self.log(_("Received command %s") % command)
+                command = command.split(":")
+                if len(command) == 2 and command[0] == "action":
+                    command = command[1]
+                    if command == "pause":
+                        self.do_pause(None)
+                        sys.stdout.write(self.promptf())
+                        sys.stdout.flush()
+                        return True
+                    elif command == "resume":
+                        self.do_resume(None)
+                        sys.stdout.write(self.promptf())
+                        sys.stdout.flush()
+                        return True
+                    elif command == "disconnect":
+                        self.do_disconnect(None)
+                        sys.stdout.write(self.promptf())
+                        sys.stdout.flush()
+                        return True
+        return False
+
+    def recvcb(self, l):
+        l = l.rstrip()
+        for listener in self.recvlisteners:
+            listener(l)
+        if not self.recvcb_actions(l):
+            report_type = self.recvcb_report(l)
+            if report_type & REPORT_TEMP:
+                self.status.update_tempreading(l)
+            if l != "ok" and not self.sdlisting \
+               and not self.monitoring and (report_type == REPORT_NONE or report_type & REPORT_MANUAL):
+                if l[:5] == "echo:":
+                    l = l[5:].lstrip()
+                if self.silent is False: self.log("\r" + l.ljust(15))
+                sys.stdout.write(self.promptf())
+                sys.stdout.flush()
+
+    def layer_change_cb(self, newlayer):
+        layerz = self.fgcode.all_layers[newlayer].z
+        if layerz is not None:
+            self.curlayer = layerz
+        if self.compute_eta:
+            secondselapsed = int(time.time() - self.starttime + self.extra_print_time)
+            self.compute_eta.update_layer(newlayer, secondselapsed)
+
+    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
+        elif self.compute_eta is not None:
+            secondselapsed = int(time.time() - self.starttime + self.extra_print_time)
+            secondsremain, secondsestimate = self.compute_eta(self.p.queueindex, secondselapsed)
+            progress = self.p.queueindex
+        else:
+            secondsremain, secondsestimate, progress = 1, 1, 0
+        return secondsremain, secondsestimate, progress
+
+    def do_eta(self, l):
+        if not self.p.printing:
+            self.logError(_("Printer is not currently printing. No ETA available."))
+        else:
+            secondsremain, secondsestimate, progress = self.get_eta()
+            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."))
+
+    #  --------------------------------------------------------------
+    #  Temperature handling
+    #  --------------------------------------------------------------
+
+    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"]))
+        else:
+            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"]))
+
+    def tempcb(self, l):
+        if "T:" in l:
+            self.log(l.strip().replace("T", "Hotend").replace("B", "Bed").replace("ok ", ""))
+
+    def do_gettemp(self, l):
+        if "dynamic" in l:
+            self.dynamic_temp = True
+        if self.p.online:
+            self.p.send_now("M105")
+            time.sleep(0.75)
+            if not self.status.bed_enabled:
+                self.log(_("Hotend: %s/%s") % (self.status.extruder_temp, self.status.extruder_temp_target))
+            else:
+                self.log(_("Hotend: %s/%s") % (self.status.extruder_temp, self.status.extruder_temp_target))
+                self.log(_("Bed:    %s/%s") % (self.status.bed_temp, self.status.bed_temp_target))
+
+    def help_gettemp(self):
+        self.log(_("Read the extruder and bed temperature."))
+
+    def do_settemp(self, l):
+        l = l.lower().replace(", ", ".")
+        for i in self.temps.keys():
+            l = l.replace(i, self.temps[i])
+        try:
+            f = float(l)
+        except:
+            self.logError(_("You must enter a temperature."))
+            return
+
+        if f >= 0:
+            if f > 250:
+                self.log(_("%s is a high temperature to set your extruder to. Are you sure you want to do that?") % f)
+                if not self.confirm():
+                    return
+            if self.p.online:
+                self.p.send_now("M104 S" + l)
+                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."))
+
+    def help_settemp(self):
+        self.log(_("Sets the hotend temperature to the value entered."))
+        self.log(_("Enter either a temperature in celsius or one of the following keywords"))
+        self.log(", ".join([i + "(" + self.temps[i] + ")" for i in self.temps.keys()]))
+
+    def complete_settemp(self, text, line, begidx, endidx):
+        if (len(line.split()) == 2 and line[-1] != " ") or (len(line.split()) == 1 and line[-1] == " "):
+            return [i for i in self.temps.keys() if i.startswith(text)]
+
+    def do_bedtemp(self, l):
+        f = None
+        try:
+            l = l.lower().replace(", ", ".")
+            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)
+            else:
+                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."))
+
+    def help_bedtemp(self):
+        self.log(_("Sets the bed temperature to the value entered."))
+        self.log(_("Enter either a temperature in celsius or one of the following keywords"))
+        self.log(", ".join([i + "(" + self.bedtemps[i] + ")" for i in self.bedtemps.keys()]))
+
+    def complete_bedtemp(self, text, line, begidx, endidx):
+        if (len(line.split()) == 2 and line[-1] != " ") or (len(line.split()) == 1 and line[-1] == " "):
+            return [i for i in self.bedtemps.keys() if i.startswith(text)]
+
+    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:
+                    preface = _("SD print progress: ")
+                    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: self.log(_("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"))
+
+    #  --------------------------------------------------------------
+    #  Manual printer controls
+    #  --------------------------------------------------------------
+
+    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)."))
+
+    def do_move(self, l):
+        if len(l.split()) < 2:
+            self.logError(_("No move specified."))
+            return
+        if self.p.printing:
+            self.logError(_("Printer is currently printing. Please pause the print before you issue manual commands."))
+            return
+        if not self.p.online:
+            self.logError(_("Printer is not online. Unable to move."))
+            return
+        l = l.split()
+        if l[0].lower() == "x":
+            feed = self.settings.xy_feedrate
+            axis = "X"
+        elif l[0].lower() == "y":
+            feed = self.settings.xy_feedrate
+            axis = "Y"
+        elif l[0].lower() == "z":
+            feed = self.settings.z_feedrate
+            axis = "Z"
+        elif l[0].lower() == "e":
+            feed = self.settings.e_feedrate
+            axis = "E"
+        else:
+            self.logError(_("Unknown axis."))
+            return
+        try:
+            float(l[1])  # check if distance can be a float
+        except:
+            self.logError(_("Invalid distance"))
+            return
+        try:
+            feed = int(l[2])
+        except:
+            pass
+        self.p.send_now("G91")
+        self.p.send_now("G0 " + axis + str(l[1]) + " F" + str(feed))
+        self.p.send_now("G90")
+
+    def help_move(self):
+        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"))
+        self.log(_("move Z -1 will move the Z axis down by 1mm at %s mm/min (default Z speed)") % self.settings.z_feedrate)
+        self.log(_("Common amounts are in the tabcomplete list."))
+
+    def complete_move(self, text, line, begidx, endidx):
+        if (len(line.split()) == 2 and line[-1] != " ") or (len(line.split()) == 1 and line[-1] == " "):
+            return [i for i in ["X ", "Y ", "Z ", "E "] if i.lower().startswith(text)]
+        elif len(line.split()) == 3 or (len(line.split()) == 2 and line[-1] == " "):
+            base = line.split()[-1]
+            rlen = 0
+            if base.startswith("-"):
+                rlen = 1
+            if line[-1] == " ":
+                base = ""
+            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)]
+        else:
+            return []
+
+    def do_extrude(self, l, override = None, overridefeed = 300):
+        length = self.settings.default_extrusion  # default extrusion length
+        feed = self.settings.e_feedrate  # default speed
+        if not self.p.online:
+            self.logError("Printer is not online. Unable to extrude.")
+            return
+        if self.p.printing:
+            self.logError(_("Printer is currently printing. Please pause the print before you issue manual commands."))
+            return
+        ls = l.split()
+        if len(ls):
+            try:
+                length = float(ls[0])
+            except:
+                self.logError(_("Invalid length given."))
+        if len(ls) > 1:
+            try:
+                feed = int(ls[1])
+            except:
+                self.logError(_("Invalid speed given."))
+        if override is not None:
+            length = override
+            feed = overridefeed
+        self.do_extrude_final(length, feed)
+
+    def do_extrude_final(self, length, feed):
+        if length > 0:
+            self.log(_("Extruding %fmm of filament.") % (length,))
+        elif length < 0:
+            self.log(_("Reversing %fmm of filament.") % (-length,))
+        else:
+            self.log(_("Length is 0, not doing anything."))
+        self.p.send_now("G91")
+        self.p.send_now("G1 E" + str(length) + " F" + str(feed))
+        self.p.send_now("G90")
+
+    def help_extrude(self):
+        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)"))
+
+    def do_reverse(self, l):
+        length = self.settings.default_extrusion  # default extrusion length
+        feed = self.settings.e_feedrate  # default speed
+        if not self.p.online:
+            self.logError(_("Printer is not online. Unable to reverse."))
+            return
+        if self.p.printing:
+            self.logError(_("Printer is currently printing. Please pause the print before you issue manual commands."))
+            return
+        ls = l.split()
+        if len(ls):
+            try:
+                length = float(ls[0])
+            except:
+                self.logError(_("Invalid length given."))
+        if len(ls) > 1:
+            try:
+                feed = int(ls[1])
+            except:
+                self.logError(_("Invalid speed given."))
+        self.do_extrude("", -length, feed)
+
+    def help_reverse(self):
+        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)"))
+
+    def do_home(self, l):
+        if not self.p.online:
+            self.logError(_("Printer is not online. Unable to move."))
+            return
+        if self.p.printing:
+            self.logError(_("Printer is currently printing. Please pause the print before you issue manual commands."))
+            return
+        if "x" in l.lower():
+            self.p.send_now("G28 X0")
+        if "y" in l.lower():
+            self.p.send_now("G28 Y0")
+        if "z" in l.lower():
+            self.p.send_now("G28 Z0")
+        if "e" in l.lower():
+            self.p.send_now("G92 E0")
+        if not len(l):
+            self.p.send_now("G28")
+            self.p.send_now("G92 E0")
+
+    def help_home(self):
+        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)"))
+
+    def do_off(self, l):
+        self.off()
+
+    def off(self, ignore = None):
+        if self.p.online:
+            if self.p.printing: self.pause(None)
+            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")
+        else:
+            self.logError(_("Printer is not online. Unable to turn it off."))
+
+    def help_off(self):
+        self.log(_("Turns off everything on the printer"))
+
+    #  --------------------------------------------------------------
+    #  Host commands handling
+    #  --------------------------------------------------------------
+
+    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)
+
+    def do_run_script(self, l):
+        p = run_command(l, {"$s": str(self.filename)}, stdout = subprocess.PIPE)
+        for line in p.stdout.readlines():
+            self.log("<< " + line.strip())
+
+    def help_run_script(self):
+        self.log(_("Runs a custom script. Current gcode filename can be given using $s token."))
+
+    def do_run_gcode_script(self, l):
+        p = run_command(l, {"$s": str(self.filename)}, stdout = subprocess.PIPE)
+        for line in p.stdout.readlines():
+            self.onecmd(line.strip())
+
+    def help_run_gcode_script(self):
+        self.log(_("Runs a custom script which output gcode which will in turn be executed. Current gcode filename can be given using $s token."))

mercurial