pronsole.py 66.8 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 39
    get_home_pos, parse_build_dimensions, parse_temperature_report, \
    setup_logging
40
install_locale('pronterface')
41 42
from .settings import Settings, BuildDimensionsSetting
from .power import powerset_print_start, powerset_print_stop
43
from printrun import gcoder
44
from .rpc import ProntRPC
45

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

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

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

68
class Status(object):
69 70

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

    def update_tempreading(self, tempstr):
79
        temps = parse_temperature_report(tempstr)
80 81
        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])
82
        else: hotend_temp = None
83 84
        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])
85 86 87 88 89
        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
90
        bed_temp = float(temps["B"][0]) if "B" in temps and temps["B"][0] else None
91 92 93 94 95
        if bed_temp is not None:
            self.bed_temp = bed_temp
            setpoint = temps["B"][1]
            if setpoint:
                self.bed_temp_target = float(setpoint)
96 97 98 99 100 101 102 103 104 105

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

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


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

Guillaume Seguin's avatar
Guillaume Seguin committed
169 170 171
    #  --------------------------------------------------------------
    #  General console handling
    #  --------------------------------------------------------------
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 198

    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")
199 200 201
                history = os.path.expanduser(self.history_file)
                if os.path.exists(history):
                    readline.read_history_file(history)
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217
            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:
218
                            self.log("")
219 220
                            self.do_exit("")
                        except KeyboardInterrupt:
221
                            self.log("")
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
                            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)
240
                    readline.write_history_file(history)
241 242 243
                except ImportError:
                    pass

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

252
    def log(self, *msg):
253 254
        msg = u"".join(unicode(i) for i in msg)
        logging.info(msg)
255

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

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

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

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

319
    def write_prompt(self):
320
        sys.stdout.write(self.promptf())
321
        sys.stdout.flush()
322

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

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

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

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

    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):
351
        if l[0].upper() in self.commandprefixes.upper():
352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371
            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:
372
            self.log("Setting extruder temp to 0")
373 374 375
        self.p.send_now("M104 S0.0")
        if self.status.bed_enabled:
            if self.status.bed_temp_target != 0:
376
                self.log("Setting bed temp to 0")
377 378 379
            self.p.send_now("M140 S0.0")
        self.log("Disconnecting from printer...")
        if self.p.printing:
380 381
            self.log(_("Are you sure you want to exit while printing?\n\
(this will terminate the print)."))
382 383 384 385
            if not self.confirm():
                return
        self.log(_("Exiting program. Goodbye!"))
        self.p.disconnect()
386
        self.kill()
387 388 389 390 391
        sys.exit()

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

Guillaume Seguin's avatar
Guillaume Seguin committed
392 393 394
    # --------------------------------------------------------------
    # Macro handling
    # --------------------------------------------------------------
395

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

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

    def end_macro(self):
415
        if "onecmd" in self.__dict__: del self.onecmd  # remove override
416 417
        self.in_macro = False
        self.prompt = self.promptf()
418
        if self.cur_macro_def != "":
419
            self.macros[self.cur_macro_name] = self.cur_macro_def
420
            macro = self.compile_macro(self.cur_macro_name, self.cur_macro_def)
421 422
            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
423
            if not self.processing_rc:
424
                self.log("Macro '" + self.cur_macro_name + "' defined")
425
                # save it
426
                if not self.processing_args:
427
                    macro_key = "macro " + self.cur_macro_name
428 429 430 431 432 433
                    macro_def = macro_key
                    if "\n" in self.cur_macro_def:
                        macro_def += "\n"
                    else:
                        macro_def += " "
                    macro_def += self.cur_macro_def
434
                    self.save_in_rc(macro_key, macro_def)
435
        else:
436
            self.logError("Empty macro - cancelled")
437
        del self.cur_macro_name, self.cur_macro_def
438

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

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

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

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

485
    def do_macro(self, args):
486 487
        if args.strip() == "":
            self.print_topics("User-defined macros", map(str, self.macros.keys()), 15, 80)
Keegi's avatar
Keegi committed
488
            return
489
        arglist = args.split(None, 1)
Keegi's avatar
Keegi committed
490
        macro_name = arglist[0]
491 492
        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
493
            return
Keegi's avatar
Keegi committed
494 495 496
        if len(arglist) == 2:
            macro_def = arglist[1]
            if macro_def.lower() == "/d":
Keegi's avatar
Keegi committed
497
                self.delete_macro(macro_name)
Keegi's avatar
Keegi committed
498 499 500 501 502 503 504
                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
505
            return
506
        if macro_name in self.macros:
507
            self.start_macro(macro_name, self.macros[macro_name])
508 509
        else:
            self.start_macro(macro_name)
510

Keegi's avatar
Keegi committed
511
    def help_macro(self):
512 513 514 515 516 517 518
        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")
519

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

Guillaume Seguin's avatar
Guillaume Seguin committed
531 532 533
    # --------------------------------------------------------------
    # Configuration handling
    # --------------------------------------------------------------
534

535
    def set(self, var, str):
536
        try:
537 538
            t = type(getattr(self.settings, var))
            value = self.settings._set(var, str)
539
            if not self.processing_rc and not self.processing_args:
540
                self.save_in_rc("set " + var, "set %s %s" % (var, value))
541
        except AttributeError:
542
            logging.debug(_("Unknown variable '%s'") % var)
543
        except ValueError, ve:
544 545 546 547
            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]))
548

549 550
    def do_set(self, argl):
        args = argl.split(None, 1)
551 552
        if len(args) < 1:
            for k in [kk for kk in dir(self.settings) if not kk.startswith("_")]:
553
                self.log("%s = %s" % (k, str(getattr(self.settings, k))))
554 555
            return
        if len(args) < 2:
556 557
            # Try getting the default value of the setting to check whether it
            # actually exists
558
            try:
559
                getattr(self.settings, args[0])
560
            except AttributeError:
561
                logging.warning("Unknown variable '%s'" % args[0])
562
            return
563
        self.set(args[0], args[1])
564

565
    def help_set(self):
566 567 568
        self.log("Set variable:   set <variable> <value>")
        self.log("Show variable:  set <variable>")
        self.log("'set' without arguments displays all variables")
569 570

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

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

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

605
    def save_in_rc(self, key, definition):
606
        """
607
        Saves or updates macro or other definitions in .pronsolerc
608 609 610 611 612 613 614 615 616
        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.
        """
617
        rci, rco = None, None
618 619 620 621 622
        if definition != "" and not definition.endswith("\n"):
            definition += "\n"
        try:
            written = False
            if os.path.exists(self.rc_filename):
623 624
                shutil.copy(self.rc_filename, self.rc_filename + "~bak")
                rci = codecs.open(self.rc_filename + "~bak", "r", "utf-8")
625
            rco = codecs.open(self.rc_filename + "~new", "w", "utf-8")
626
            if rci is not None:
627 628 629 630
                overwriting = False
                for rc_cmd in rci:
                    l = rc_cmd.rstrip()
                    ls = l.lstrip()
631
                    ws = l[:len(l) - len(ls)]  # just leading whitespace
632 633
                    if overwriting and len(ws) == 0:
                        overwriting = False
634
                    if not written and key != "" and rc_cmd.startswith(key) and (rc_cmd + "\n")[len(key)].isspace():
635 636 637 638 639 640 641 642 643 644 645
                        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()
646
            shutil.move(self.rc_filename + "~new", self.rc_filename)
Guillaume Seguin's avatar
Guillaume Seguin committed
647
            # if definition != "":
648
            #    self.log("Saved '"+key+"' to '"+self.rc_filename+"'")
Guillaume Seguin's avatar
Guillaume Seguin committed
649
            # else:
650
            #    self.log("Removed '"+key+"' from '"+self.rc_filename+"'")
651
        except Exception, e:
652
            self.logError("Saving failed for ", key + ":", str(e))
653
        finally:
654
            del rci, rco
655

Guillaume Seguin's avatar
Guillaume Seguin committed
656 657 658
    #  --------------------------------------------------------------
    #  Configuration update callbacks
    #  --------------------------------------------------------------
659 660 661 662 663

    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)

664 665 666
    def update_tcp_streaming_mode(self, param, value):
        self.p.tcp_streaming_mode = self.settings.tcp_streaming_mode

667
    def update_rpc_server(self, param, value):
668 669 670 671 672 673 674
        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
675

Guillaume Seguin's avatar
Guillaume Seguin committed
676 677 678
    #  --------------------------------------------------------------
    #  Command line options handling
    #  --------------------------------------------------------------
679 680

    def add_cmdline_arguments(self, parser):
681
        parser.add_argument('-v', '--verbose', help = _("increase verbosity"), action = "store_true")
682 683 684 685 686
        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):
687 688 689
        if args.verbose:
            logger = logging.getLogger()
            logger.setLevel(logging.DEBUG)
690 691 692 693 694 695 696 697
        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
698
        self.update_rpc_server(None, self.settings.rpc_server)
699 700 701 702 703 704 705 706 707 708 709 710 711
        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)
712
        setup_logging(sys.stdout, self.settings.log_path, True)
713

Guillaume Seguin's avatar
Guillaume Seguin committed
714 715 716
    #  --------------------------------------------------------------
    #  Printer connection handling
    #  --------------------------------------------------------------
717

718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737
    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
738 739 740 741
        self.statuscheck = True
        self.status_thread = threading.Thread(target = self.statuschecker)
        self.status_thread.start()
        return True
742

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

770
    def help_connect(self):
771 772 773
        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")
774
        ports = self.scanserial()
775
        if ports:
776
            self.log("Available ports: ", " ".join(ports))
777
        else:
778
            self.log("No serial ports were automatically found.")
779

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

788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811
    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()

812
    def do_disconnect(self, l):
813
        self.p.disconnect()
814

815
    def help_disconnect(self):
816
        self.log("Disconnects from the printer")
Guillaume Seguin's avatar
Guillaume Seguin committed
817

818 819 820 821 822 823 824 825
    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")

826 827 828 829 830 831 832 833 834 835 836 837
    #  --------------------------------------------------------------
    #  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:
838
                if self.sdprinting and not self.paused:
839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861
                    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
862 863 864
    #  --------------------------------------------------------------
    #  File loading handling
    #  --------------------------------------------------------------
865

866 867
    def do_load(self, filename):
        self._do_load(filename)
868

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

881 882
    def load_gcode(self, filename, layer_callback = None, gcode = None):
        if gcode is None:
883
            self.fgcode = gcoder.LightGCode(deferred = True)
884 885 886 887 888
        else:
            self.fgcode = gcode
        self.fgcode.prepare(open(filename, "rU"),
                            get_home_pos(self.build_dimensions_list),
                            layer_callback = layer_callback)
889
        self.fgcode.estimate_duration()
890
        self.filename = filename
891

892
    def complete_load(self, text, line, begidx, endidx):
893
        s = line.split()
894
        if len(s) > 2:
895
            return []
896 897 898
        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*")]
899
            else:
900
                return glob.glob("*/") + glob.glob("*.g*")
901

902
    def help_load(self):
903
        self.log("Loads a gcode file (with tab-completion)")
904

905
    def do_slice(self, l):
906 907 908
        l = l.split()
        if len(l) == 0:
            self.logError(_("No file name given."))
909
            return
910 911 912 913
        settings = 0
        if l[0] == "set":
            settings = 1
        else:
Guillaume Seguin's avatar
Guillaume Seguin committed
914
            self.log(_("Slicing file: %s") % l[0])
915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935
            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)

936
    def complete_slice(self, text, line, begidx, endidx):
937 938 939 940 941 942 943 944 945
        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")

946
    def help_slice(self):
947
        self.log(_("Creates a gcode file from an stl model using the slicer (with tab-completion)"))
948 949 950
        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"))
951

Guillaume Seguin's avatar
Guillaume Seguin committed
952 953 954
    #  --------------------------------------------------------------
    #  Print/upload handling
    #  --------------------------------------------------------------
955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975

    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()
976
            while self.p.printing:
977
                time.sleep(0.5)
978
                sys.stdout.write("\b\b\b\b\b%04.1f%%" % (100 * float(self.p.queueindex) / len(self.p.mainqueue),))
979
                sys.stdout.flush()
980
            self.p.send_now("M29 " + targetname)
981 982
            time.sleep(0.2)
            self.p.clear = True
983 984 985
            self._do_ls(False)
            self.log("\b\b\b\b\b100%.")
            self.log(_("Upload completed. %s should now be on the card.") % targetname)
986
            return
987 988 989 990
        except (KeyboardInterrupt, Exception) as e:
            if isinstance(e, KeyboardInterrupt):
                self.logError(_("...interrupted!"))
            else:
991 992
                self.logError(_("Something wrong happened while uploading:")
                              + "\n" + traceback.format_exc())
993
            self.p.pause()
994
            self.p.send_now("M29 " + targetname)
995
            time.sleep(0.2)
996
            self.p.cancelprint()
997
            self.logError(_("A partial file named %s may have been written to the sd card.") % targetname)
998

999
    def complete_upload(self, text, line, begidx, endidx):
1000
        s = line.split()
1001
        if len(s) > 2:
1002
            return []
1003 1004 1005
        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*")]
1006
            else:
1007
                return glob.glob("*/") + glob.glob("*.g*")
1008

1009
    def help_upload(self):
1010
        self.log("Uploads a gcode file to the sd card")
1011

1012
    def help_print(self):
1013
        if not self.fgcode:
1014
            self.log(_("Send a loaded gcode file to the printer. Load a file with the load command first."))
1015
        else:
1016
            self.log(_("Send a loaded gcode file to the printer. You have %s loaded right now.") % self.filename)
1017

1018
    def do_print(self, l):
1019
        if not self.fgcode:
1020
            self.logError(_("No file loaded. Please use load first."))
1021 1022
            return
        if not self.p.online:
1023
            self.logError(_("Not connected to printer."))
1024
            return
1025 1026
        self.log(_("Printing %s") % self.filename)
        self.log(_("You can monitor the print with the monitor command."))
1027
        self.sdprinting = False
1028
        self.p.startprint(self.fgcode)
1029

1030
    def do_pause(self, l):
1031 1032 1033
        if self.sdprinting:
            self.p.send_now("M25")
        else:
1034
            if not self.p.printing:
1035
                self.logError(_("Not printing, cannot pause."))
1036 1037
                return
            self.p.pause()
1038
        self.paused = True
1039

1040
    def help_pause(self):
1041
        self.log(_("Pauses a running print"))
1042

1043
    def pause(self, event = None):
1044 1045
        return self.do_pause(None)

1046
    def do_resume(self, l):
1047
        if not self.paused:
1048
            self.logError(_("Not paused, unable to resume. Start a print first."))
1049
            return
1050
        self.paused = False
1051 1052 1053 1054 1055
        if self.sdprinting:
            self.p.send_now("M24")
            return
        else:
            self.p.resume()
1056

1057
    def help_resume(self):
1058
        self.log(_("Resumes a paused print."))
1059

1060
    def listfiles(self, line):
1061
        if "Begin file list" in line:
1062
            self.sdlisting = 1
1063
        elif "End file list" in line:
1064
            self.sdlisting = 0
1065
            self.recvlisteners.remove(self.listfiles)
1066
            if self.sdlisting_echo:
1067 1068
                self.log(_("Files on SD card:"))
                self.log("\n".join(self.sdfiles))
1069
        elif self.sdlisting:
1070 1071 1072 1073
            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
1074 1075
        self.sdlisting = 0
        self.sdlisting_echo = echo
1076
        self.sdfiles = []
1077
        self.recvlisteners.append(self.listfiles)
1078
        self.p.send_now("M20")
1079

1080
    def do_ls(self, l):
1081
        if not self.p.online:
1082
            self.logError(_("Printer is not online. Please connect to it first."))
1083
            return
1084
        self._do_ls(True)
1085

1086
    def help_ls(self):
1087
        self.log(_("Lists files on the SD card"))
1088

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

1116
    def do_reset(self, l):
1117
        self.p.reset()
1118

1119
    def help_reset(self):
1120
        self.log(_("Resets the printer."))
1121

1122
    def do_sdprint(self, l):
1123
        if not self.p.online:
1124
            self.log(_("Printer is not online. Please connect to it first."))
1125
            return
1126 1127 1128 1129 1130
        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."))
1131
            return
1132 1133 1134 1135
        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..."))
1136
        time.sleep(1)
1137

1138
    def help_sdprint(self):
1139 1140
        self.log(_("Print a file from the SD card. Tab completes with available file names."))
        self.log(_("sdprint filename.g"))
1141

1142
    def complete_sdprint(self, text, line, begidx, endidx):
1143 1144 1145 1146
        if not self.sdfiles and self.p.online:
            self._do_ls(False)
            while self.listfiles in self.recvlisteners:
                time.sleep(0.1)
1147
        if (len(line.split()) == 2 and line[-1] != " ") or (len(line.split()) == 1 and line[-1] == " "):
1148
            return [i for i in self.sdfiles if i.startswith(text)]
1149

Guillaume Seguin's avatar
Guillaume Seguin committed
1150 1151 1152
    #  --------------------------------------------------------------
    #  Printcore callbacks
    #  --------------------------------------------------------------
1153

1154 1155 1156
    def startcb(self, resuming = False):
        self.starttime = time.time()
        if resuming:
1157
            self.log(_("Print resumed at: %s") % format_time(self.starttime))
1158
        else:
1159
            self.log(_("Print started at: %s") % format_time(self.starttime))
1160 1161 1162 1163
            if not self.sdprinting:
                self.compute_eta = RemainingTimeEstimator(self.fgcode)
            else:
                self.compute_eta = None
1164 1165 1166 1167 1168 1169 1170 1171

            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())
1172 1173 1174
        try:
            powerset_print_start(reason = "Preventing sleep during print")
        except:
1175 1176
            self.logError(_("Failed to set power settings:")
                          + "\n" + traceback.format_exc())
1177 1178

    def endcb(self):
1179 1180 1181
        try:
            powerset_print_stop()
        except:
1182 1183
            self.logError(_("Failed to set power settings:")
                          + "\n" + traceback.format_exc())
1184 1185
        if self.p.queueindex == 0:
            print_duration = int(time.time() - self.starttime + self.extra_print_time)
1186 1187 1188 1189 1190 1191
            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)
1192

Guillaume Seguin's avatar
Guillaume Seguin committed
1193
            if not self.settings.final_command:
1194
                return
1195 1196 1197 1198 1199 1200
            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())
1201

1202 1203 1204 1205 1206
    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
1207
            isreport = REPORT_POS
1208 1209
            if self.userm114 > 0:
                self.userm114 -= 1
1210
                isreport |= REPORT_MANUAL
1211
        if "ok T:" in l or tempreading_exp.findall(l):
1212
            self.tempreadings = l
1213
            isreport = REPORT_TEMP
1214 1215
            if self.userm105 > 0:
                self.userm105 -= 1
1216
                isreport |= REPORT_MANUAL
1217 1218 1219 1220
            else:
                self.m105_waitcycles = 0
        return isreport

1221 1222 1223
    def recvcb_actions(self, l):
        if l.startswith("!!"):
            self.do_pause(None)
1224 1225
            msg = l.split(" ", 1)
            if len(msg) > 1 and self.silent is False: self.logError(msg[1].ljust(15))
1226 1227
            sys.stdout.write(self.promptf())
            sys.stdout.flush()
1228 1229
            return True
        elif l.startswith("//"):
1230 1231
            command = l.split(" ", 1)
            if len(command) > 1:
1232
                command = command[1]
1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251
                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
1252 1253 1254 1255
        return False

    def recvcb(self, l):
        l = l.rstrip()
1256 1257
        for listener in self.recvlisteners:
            listener(l)
1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268
        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()
1269

1270
    def layer_change_cb(self, newlayer):
1271 1272 1273
        layerz = self.fgcode.all_layers[newlayer].z
        if layerz is not None:
            self.curlayer = layerz
1274 1275 1276 1277
        if self.compute_eta:
            secondselapsed = int(time.time() - self.starttime + self.extra_print_time)
            self.compute_eta.update_layer(newlayer, secondselapsed)

1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288
    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
1289
        elif self.compute_eta is not None:
1290 1291 1292
            secondselapsed = int(time.time() - self.starttime + self.extra_print_time)
            secondsremain, secondsestimate = self.compute_eta(self.p.queueindex, secondselapsed)
            progress = self.p.queueindex
1293 1294
        else:
            secondsremain, secondsestimate, progress = 1, 1, 0
1295 1296
        return secondsremain, secondsestimate, progress

1297 1298 1299 1300
    def do_eta(self, l):
        if not self.p.printing:
            self.logError(_("Printer is not currently printing. No ETA available."))
        else:
1301
            secondsremain, secondsestimate, progress = self.get_eta()
1302 1303 1304 1305 1306 1307 1308
            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
1309 1310 1311
    #  --------------------------------------------------------------
    #  Temperature handling
    #  --------------------------------------------------------------
1312

1313 1314 1315 1316 1317
    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"]))
1318
        else:
1319 1320 1321
            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"]))
1322

1323
    def tempcb(self, l):
1324
        if "T:" in l:
1325
            self.log(l.strip().replace("T", "Hotend").replace("B", "Bed").replace("ok ", ""))
1326

1327
    def do_gettemp(self, l):
1328 1329
        if "dynamic" in l:
            self.dynamic_temp = True
1330 1331
        if self.p.online:
            self.p.send_now("M105")
1332
            time.sleep(0.75)
1333
            if not self.status.bed_enabled:
1334
                self.log(_("Hotend: %s/%s") % (self.status.extruder_temp, self.status.extruder_temp_target))
1335
            else:
1336 1337
                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))
1338

1339
    def help_gettemp(self):
1340
        self.log(_("Read the extruder and bed temperature."))
1341

1342
    def do_settemp(self, l):
1343
        l = l.lower().replace(", ", ".")
1344 1345
        for i in self.temps.keys():
            l = l.replace(i, self.temps[i])
1346
        try:
1347
            f = float(l)
1348
        except:
1349
            self.logError(_("You must enter a temperature."))
1350 1351
            return

1352
        if f >= 0:
1353
            if f > 250:
1354
                self.log(_("%s is a high temperature to set your extruder to. Are you sure you want to do that?") % f)
1355 1356
                if not self.confirm():
                    return
1357
            if self.p.online:
1358
                self.p.send_now("M104 S" + l)
1359 1360 1361 1362 1363
                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."))
1364

1365
    def help_settemp(self):
1366 1367
        self.log(_("Sets the hotend temperature to the value entered."))
        self.log(_("Enter either a temperature in celsius or one of the following keywords"))
1368
        self.log(", ".join([i + "(" + self.temps[i] + ")" for i in self.temps.keys()]))
1369

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

1374
    def do_bedtemp(self, l):
1375 1376
        f = None
        try:
1377
            l = l.lower().replace(", ", ".")
1378 1379 1380 1381 1382 1383 1384 1385 1386
            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)
1387
            else:
1388 1389 1390
                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."))
1391

1392
    def help_bedtemp(self):
1393 1394
        self.log(_("Sets the bed temperature to the value entered."))
        self.log(_("Enter either a temperature in celsius or one of the following keywords"))
1395
        self.log(", ".join([i + "(" + self.bedtemps[i] + ")" for i in self.bedtemps.keys()]))
1396

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

1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427
    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
1428
                    preface = _("SD print progress: ")
1429 1430 1431 1432 1433 1434 1435
                    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:
1436
            if self.silent is False: self.log(_("Done monitoring."))
1437 1438 1439 1440 1441 1442 1443
        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
1444 1445 1446
    #  --------------------------------------------------------------
    #  Manual printer controls
    #  --------------------------------------------------------------
1447

1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465
    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)."))

1466
    def do_move(self, l):
1467
        if len(l.split()) < 2:
1468
            self.logError(_("No move specified."))
1469 1470
            return
        if self.p.printing:
1471
            self.logError(_("Printer is currently printing. Please pause the print before you issue manual commands."))
1472 1473
            return
        if not self.p.online:
1474
            self.logError(_("Printer is not online. Unable to move."))
1475
            return
1476
        l = l.split()
1477
        if l[0].lower() == "x":
1478 1479
            feed = self.settings.xy_feedrate
            axis = "X"
1480
        elif l[0].lower() == "y":
1481 1482
            feed = self.settings.xy_feedrate
            axis = "Y"
1483
        elif l[0].lower() == "z":
1484 1485
            feed = self.settings.z_feedrate
            axis = "Z"
1486
        elif l[0].lower() == "e":
1487 1488
            feed = self.settings.e_feedrate
            axis = "E"
1489
        else:
1490
            self.logError(_("Unknown axis."))
1491 1492
            return
        try:
1493
            float(l[1])  # check if distance can be a float
1494
        except:
1495
            self.logError(_("Invalid distance"))
1496
            return
kliment's avatar
kliment committed
1497
        try:
1498
            feed = int(l[2])
kliment's avatar
kliment committed
1499 1500
        except:
            pass
1501
        self.p.send_now("G91")
1502
        self.p.send_now("G0 " + axis + str(l[1]) + " F" + str(feed))
1503
        self.p.send_now("G90")
1504

1505
    def help_move(self):
1506 1507 1508
        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"))
1509
        self.log(_("move Z -1 will move the Z axis down by 1mm at %s mm/min (default Z speed)") % self.settings.z_feedrate)
1510
        self.log(_("Common amounts are in the tabcomplete list."))
1511

1512
    def complete_move(self, text, line, begidx, endidx):
1513
        if (len(line.split()) == 2 and line[-1] != " ") or (len(line.split()) == 1 and line[-1] == " "):
1514
            return [i for i in ["X ", "Y ", "Z ", "E "] if i.lower().startswith(text)]
1515
        elif len(line.split()) == 3 or (len(line.split()) == 2 and line[-1] == " "):
1516 1517
            base = line.split()[-1]
            rlen = 0
1518
            if base.startswith("-"):
1519
                rlen = 1
1520
            if line[-1] == " ":
1521
                base = ""
1522
            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)]
1523 1524
        else:
            return []
1525

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

    def do_extrude_final(self, length, feed):
1552
        if length > 0:
1553 1554
            self.log(_("Extruding %fmm of filament.") % (length,))
        elif length < 0:
1555
            self.log(_("Reversing %fmm of filament.") % (-length,))
1556
        else:
1557
            self.log(_("Length is 0, not doing anything."))
1558
        self.p.send_now("G91")
1559
        self.p.send_now("G1 E" + str(length) + " F" + str(feed))
1560
        self.p.send_now("G90")
1561

1562
    def help_extrude(self):
1563 1564 1565 1566 1567
        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)"))
1568

1569
    def do_reverse(self, l):
1570
        length = self.settings.default_extrusion  # default extrusion length
1571
        feed = self.settings.e_feedrate  # default speed
1572
        if not self.p.online:
1573
            self.logError(_("Printer is not online. Unable to reverse."))
1574 1575
            return
        if self.p.printing:
1576
            self.logError(_("Printer is currently printing. Please pause the print before you issue manual commands."))
1577
            return
1578
        ls = l.split()
1579 1580
        if len(ls):
            try:
1581
                length = float(ls[0])
1582
            except:
1583
                self.logError(_("Invalid length given."))
1584
        if len(ls) > 1:
1585
            try:
1586
                feed = int(ls[1])
1587
            except:
1588
                self.logError(_("Invalid speed given."))
1589
        self.do_extrude("", -length, feed)
1590

1591
    def help_reverse(self):
1592 1593 1594 1595 1596
        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)"))
1597

1598
    def do_home(self, l):
1599
        if not self.p.online:
1600
            self.logError(_("Printer is not online. Unable to move."))
1601 1602
            return
        if self.p.printing:
1603
            self.logError(_("Printer is currently printing. Please pause the print before you issue manual commands."))
1604 1605
            return
        if "x" in l.lower():
1606
            self.p.send_now("G28 X0")
1607
        if "y" in l.lower():
1608
            self.p.send_now("G28 Y0")
1609
        if "z" in l.lower():
1610
            self.p.send_now("G28 Z0")
kliment's avatar
kliment committed
1611 1612
        if "e" in l.lower():
            self.p.send_now("G92 E0")
1613
        if not len(l):
kliment's avatar
kliment committed
1614
            self.p.send_now("G28")
kliment's avatar
kliment committed
1615
            self.p.send_now("G92 E0")
1616

1617
    def help_home(self):
1618 1619 1620 1621 1622 1623
        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)"))
1624

1625
    def do_off(self, l):
Guillaume Seguin's avatar
Guillaume Seguin committed
1626 1627
        self.off()

Guillaume Seguin's avatar
Guillaume Seguin committed
1628
    def off(self, ignore = None):
1629 1630
        if self.p.online:
            if self.p.printing: self.pause(None)
1631 1632 1633 1634 1635 1636 1637 1638 1639 1640
            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")
1641 1642 1643 1644 1645 1646
        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
1647 1648 1649
    #  --------------------------------------------------------------
    #  Host commands handling
    #  --------------------------------------------------------------
1650

1651 1652 1653 1654 1655 1656 1657
    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)
1658

1659
    def do_run_script(self, l):
1660
        p = run_command(l, {"$s": str(self.filename)}, stdout = subprocess.PIPE)
1661 1662 1663 1664
        for line in p.stdout.readlines():
            self.log("<< " + line.strip())

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

1667
    def do_run_gcode_script(self, l):
1668
        p = run_command(l, {"$s": str(self.filename)}, stdout = subprocess.PIPE)
1669 1670 1671 1672
        for line in p.stdout.readlines():
            self.onecmd(line.strip())

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