printrun-src/printrun/pronsole.py

changeset 46
cce0af6351f0
parent 39
74801c0f2709
--- a/printrun-src/printrun/pronsole.py	Tue Jan 19 20:45:09 2021 +0100
+++ b/printrun-src/printrun/pronsole.py	Wed Jan 20 10:15:13 2021 +0100
@@ -1,5 +1,3 @@
-#!/usr/bin/env python
-
 # This file is part of the Printrun suite.
 #
 # Printrun is free software: you can redistribute it and/or modify
@@ -18,6 +16,7 @@
 import cmd
 import glob
 import os
+import platform
 import time
 import threading
 import sys
@@ -30,6 +29,7 @@
 import traceback
 import re
 
+from appdirs import user_cache_dir, user_config_dir, user_data_dir
 from serial import SerialException
 
 from . import printcore
@@ -42,10 +42,11 @@
 from .power import powerset_print_start, powerset_print_stop
 from printrun import gcoder
 from .rpc import ProntRPC
+from printrun.spoolmanager import spoolmanager
 
 if os.name == "nt":
     try:
-        import _winreg
+        import winreg
     except:
         pass
 READLINE = True
@@ -64,8 +65,9 @@
 REPORT_POS = 1
 REPORT_TEMP = 2
 REPORT_MANUAL = 4
+DEG = "\N{DEGREE SIGN}"
 
-class Status(object):
+class Status:
 
     def __init__(self):
         self.extruder_temp = 0
@@ -102,6 +104,37 @@
     def extruder_enabled(self):
         return self.extruder_temp != 0
 
+class RGSGCoder():
+    """Bare alternative to gcoder.LightGCode which does not preload all lines in memory,
+but still allows run_gcode_script (hence the RGS) to be processed by do_print (checksum,threading,ok waiting)"""
+    def __init__(self, line):
+        self.lines = True
+        self.filament_length = 0.
+        self.filament_length_multi = [0]
+        self.proc = run_command(line, {"$s": 'str(self.filename)'}, stdout = subprocess.PIPE, universal_newlines = True)
+        lr = gcoder.Layer([])
+        lr.duration = 0.
+        self.all_layers = [lr]
+        self.read() #empty layer causes division by zero during progress calculation
+    def read(self):
+        ln = self.proc.stdout.readline()
+        if not ln:
+            self.proc.stdout.close()
+            return None
+        ln = ln.strip()
+        if not ln:
+            return None
+        pyLn = gcoder.PyLightLine(ln)
+        self.all_layers[0].append(pyLn)
+        return pyLn
+    def has_index(self, i):
+        while i >= len(self.all_layers[0]) and not self.proc.stdout.closed:
+            self.read()
+        return i < len(self.all_layers[0])
+    def __len__(self):
+        return len(self.all_layers[0])
+    def idxs(self, i):
+        return 0, i #layer, line
 
 class pronsole(cmd.Cmd):
     def __init__(self):
@@ -143,28 +176,30 @@
         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.commandprefixes = 'MGTD$'
         self.promptstrs = {"offline": "%(bold)soffline>%(normal)s ",
-                           "fallback": "%(bold)sPC>%(normal)s ",
+                           "fallback": "%(bold)s%(red)s%(port)s%(white)s PC>%(normal)s ",
                            "macro": "%(bold)s..>%(normal)s ",
-                           "online": "%(bold)sT:%(extruder_temp_fancy)s%(progress_fancy)s>%(normal)s "}
+                           "online": "%(bold)s%(green)s%(port)s%(white)s %(extruder_temp_fancy)s%(progress_fancy)s>%(normal)s "}
+        self.spool_manager = spoolmanager.SpoolManager(self)
+        self.current_tool = 0   # Keep track of the extruder being used
+        self.cache_dir = os.path.join(user_cache_dir("Printrun"))
+        self.history_file = os.path.join(self.cache_dir,"history")
+        self.config_dir = os.path.join(user_config_dir("Printrun"))
+        self.data_dir = os.path.join(user_data_dir("Printrun"))
+        self.lineignorepattern=re.compile("ok ?\d*$|.*busy: ?processing|.*busy: ?heating|.*Active Extruder: ?\d*$")
 
     #  --------------------------------------------------------------
     #  General console handling
@@ -196,7 +231,11 @@
                 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)
+                history = (self.history_file)
+                if not os.path.exists(history):
+                    if not os.path.exists(self.cache_dir):
+                        os.makedirs(self.cache_dir)
+                    history = os.path.join(self.cache_dir, "history")
                 if os.path.exists(history):
                     readline.read_history_file(history)
             except ImportError:
@@ -213,7 +252,7 @@
                 else:
                     if self.use_rawinput:
                         try:
-                            line = raw_input(self.prompt)
+                            line = input(self.prompt)
                         except EOFError:
                             self.log("")
                             self.do_exit("")
@@ -237,12 +276,12 @@
                 try:
                     import readline
                     readline.set_completer(self.old_completer)
-                    readline.write_history_file(history)
+                    readline.write_history_file(self.history_file)
                 except ImportError:
                     pass
 
     def confirm(self):
-        y_or_n = raw_input("y/n: ")
+        y_or_n = input("y/n: ")
         if y_or_n == "y":
             return True
         elif y_or_n != "n":
@@ -250,11 +289,11 @@
         return False
 
     def log(self, *msg):
-        msg = u"".join(unicode(i) for i in msg)
+        msg = "".join(str(i) for i in msg)
         logging.info(msg)
 
     def logError(self, *msg):
-        msg = u"".join(unicode(i) for i in msg)
+        msg = "".join(str(i) for i in msg)
         logging.error(msg)
         if not self.settings.error_command:
             return
@@ -279,10 +318,12 @@
             specials = {}
             specials["extruder_temp"] = str(int(self.status.extruder_temp))
             specials["extruder_temp_target"] = str(int(self.status.extruder_temp_target))
+            # port: /dev/tty* | netaddress:port
+            specials["port"] = self.settings.port.replace('/dev/', '')
             if self.status.extruder_temp_target == 0:
-                specials["extruder_temp_fancy"] = str(int(self.status.extruder_temp))
+                specials["extruder_temp_fancy"] = str(int(self.status.extruder_temp)) + DEG
             else:
-                specials["extruder_temp_fancy"] = "%s/%s" % (str(int(self.status.extruder_temp)), str(int(self.status.extruder_temp_target)))
+                specials["extruder_temp_fancy"] = "%s%s/%s%s" % (str(int(self.status.extruder_temp)), DEG, str(int(self.status.extruder_temp_target)), DEG)
             if self.p.printing:
                 progress = int(1000 * float(self.p.queueindex) / len(self.p.mainqueue)) / 10
             elif self.sdprinting:
@@ -294,6 +335,9 @@
                 specials["progress_fancy"] = " " + str(progress) + "%"
             else:
                 specials["progress_fancy"] = ""
+            specials["red"] = "\033[31m"
+            specials["green"] = "\033[32m"
+            specials["white"] = "\033[37m"
             specials["bold"] = "\033[01m"
             specials["normal"] = "\033[00m"
             return promptstr % specials
@@ -376,7 +420,7 @@
                 self.log("Setting bed temp to 0")
             self.p.send_now("M140 S0.0")
         self.log("Disconnecting from printer...")
-        if self.p.printing:
+        if self.p.printing and l != "force":
             self.log(_("Are you sure you want to exit while printing?\n\
 (this will terminate the print)."))
             if not self.confirm():
@@ -453,6 +497,7 @@
             self.logError("Empty macro - cancelled")
             return
         macro = None
+        namespace={}
         pycode = "def macro(self,*arg):\n"
         if "\n" not in macro_def.strip():
             pycode += self.compile_macro_line("  " + macro_def.strip())
@@ -460,7 +505,11 @@
             lines = macro_def.split("\n")
             for l in lines:
                 pycode += self.compile_macro_line(l)
-        exec pycode
+        exec(pycode,namespace)
+        try:
+            macro=namespace['macro']
+        except:
+            pass
         return macro
 
     def start_macro(self, macro_name, prev_definition = "", suppress_instructions = False):
@@ -484,7 +533,7 @@
 
     def do_macro(self, args):
         if args.strip() == "":
-            self.print_topics("User-defined macros", map(str, self.macros.keys()), 15, 80)
+            self.print_topics("User-defined macros", [str(k) for k in self.macros.keys()], 15, 80)
             return
         arglist = args.split(None, 1)
         macro_name = arglist[0]
@@ -540,7 +589,7 @@
                 self.save_in_rc("set " + var, "set %s %s" % (var, value))
         except AttributeError:
             logging.debug(_("Unknown variable '%s'") % var)
-        except ValueError, ve:
+        except ValueError as ve:
             if hasattr(ve, "from_validator"):
                 self.logError(_("Bad value %s for variable '%s': %s") % (str, var, ve.args[0]))
             else:
@@ -582,6 +631,7 @@
             self.rc_filename = os.path.abspath(rc_filename)
             for rc_cmd in rc:
                 if not rc_cmd.lstrip().startswith("#"):
+                    logging.debug(rc_cmd.rstrip())
                     self.onecmd(rc_cmd)
             rc.close()
             if hasattr(self, "cur_macro_def"):
@@ -590,17 +640,33 @@
         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"
+    def load_default_rc(self):
+        # Check if a configuration file exists in an "old" location,
+        # if not, use the "new" location provided by appdirs
+        for f in '~/.pronsolerc', '~/printrunconf.ini':
+            expanded = os.path.expanduser(f)
+            if os.path.exists(expanded):
+                config = expanded
+                break
+        else:
+            if not os.path.exists(self.config_dir):
+                os.makedirs(self.config_dir)
+
+            config_name = ('printrunconf.ini'
+                            if platform.system() == 'Windows'
+                            else 'pronsolerc')
+
+            config = os.path.join(self.config_dir, config_name)
+        logging.info('Loading config file ' + config)
+
+        # Load the default configuration file
         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))
+            self.load_rc(config)
+        except FileNotFoundError:
+            # Make sure the filename is initialized,
+            # and create the file if it doesn't exist
+            self.rc_filename = config
+            open(self.rc_filename, 'a').close()
 
     def save_in_rc(self, key, definition):
         """
@@ -620,9 +686,14 @@
         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 not os.path.exists(self.cache_dir):
+                    os.makedirs(self.cache_dir)
+                configcache = os.path.join(self.cache_dir, os.path.basename(self.rc_filename))
+                configcachebak = configcache + "~bak"
+                configcachenew = configcache + "~new"
+                shutil.copy(self.rc_filename, configcachebak)
+                rci = codecs.open(configcachebak, "r", "utf-8")
+            rco = codecs.open(configcachenew, "w", "utf-8")
             if rci is not None:
                 overwriting = False
                 for rc_cmd in rci:
@@ -643,12 +714,12 @@
             if rci is not None:
                 rci.close()
             rco.close()
-            shutil.move(self.rc_filename + "~new", self.rc_filename)
+            shutil.move(configcachenew, 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:
+        except Exception as e:
             self.logError("Saving failed for ", key + ":", str(e))
         finally:
             del rci, rco
@@ -688,7 +759,12 @@
             logger = logging.getLogger()
             logger.setLevel(logging.DEBUG)
         for config in args.conf:
-            self.load_rc(config)
+            try:
+                self.load_rc(config)
+            except EnvironmentError as err:
+                print(("ERROR: Unable to load configuration file: %s" %
+                       str(err)[10:]))
+                sys.exit(1)
         if not self.rc_loaded:
             self.load_default_rc()
         self.processing_args = True
@@ -697,8 +773,7 @@
         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)
+            self.cmdline_filename_callback(args.filename)
 
     def cmdline_filename_callback(self, filename):
         self.do_load(filename)
@@ -736,7 +811,8 @@
                 self.logError(traceback.format_exc())
             return False
         self.statuscheck = True
-        self.status_thread = threading.Thread(target = self.statuschecker)
+        self.status_thread = threading.Thread(target = self.statuschecker,
+                                              name = 'status thread')
         self.status_thread.start()
         return True
 
@@ -790,17 +866,19 @@
         baselist = []
         if os.name == "nt":
             try:
-                key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, "HARDWARE\\DEVICEMAP\\SERIALCOMM")
+                key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, "HARDWARE\\DEVICEMAP\\SERIALCOMM")
                 i = 0
                 while(1):
-                    baselist += [_winreg.EnumValue(key, i)[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)
+        if(sys.platform!="win32" and self.settings.devicepath):
+            baselist += glob.glob(self.settings.devicepath)
+        return [p for p in baselist if self._bluetoothSerialFilter(p)]
 
     def _bluetoothSerialFilter(self, serial):
         return not ("Bluetooth" in serial or "FireFly" in serial)
@@ -832,7 +910,7 @@
             if self.p.writefailures >= 4:
                 self.logError(_("Disconnecting after 4 failed writes."))
                 self.status_thread = None
-                self.disconnect()
+                self.p.disconnect()
                 return
             if do_monitoring:
                 if self.sdprinting and not self.paused:
@@ -883,7 +961,7 @@
             self.fgcode = gcoder.LightGCode(deferred = True)
         else:
             self.fgcode = gcode
-        self.fgcode.prepare(open(filename, "rU"),
+        self.fgcode.prepare(open(filename, "r", encoding="utf-8"),
                             get_home_pos(self.build_dimensions_list),
                             layer_callback = layer_callback)
         self.fgcode.estimate_duration()
@@ -917,11 +995,11 @@
                 return
         try:
             if settings:
-                command = self.settings.sliceoptscommand
+                command = self.settings.slicecommandpath+self.settings.sliceoptscommand
                 self.log(_("Entering slicer settings: %s") % command)
                 run_command(command, blocking = True)
             else:
-                command = self.settings.slicecommand
+                command = self.settings.slicecommandpath+self.settings.slicecommand
                 stl_name = l[0]
                 gcode_name = stl_name.replace(".stl", "_export.gcode").replace(".STL", "_export.gcode")
                 run_command(command,
@@ -930,7 +1008,7 @@
                             blocking = True)
                 self.log(_("Loading sliced file."))
                 self.do_load(l[0].replace(".stl", "_export.gcode"))
-        except Exception, e:
+        except Exception as e:
             self.logError(_("Slicing failed: %s") % e)
 
     def complete_slice(self, text, line, begidx, endidx):
@@ -1067,7 +1145,7 @@
                 self.log(_("Files on SD card:"))
                 self.log("\n".join(self.sdfiles))
         elif self.sdlisting:
-            self.sdfiles.append(line.strip().lower())
+            self.sdfiles.append(re.sub(" \d+$","",line.strip().lower()))
 
     def _do_ls(self, echo):
         # FIXME: this was 2, but I think it should rather be 0 as in do_upload
@@ -1191,6 +1269,18 @@
                 new_total = self.settings.total_filament_used + self.fgcode.filament_length
                 self.set("total_filament_used", new_total)
 
+                # Update the length of filament in the spools
+                self.spool_manager.refresh()
+                if(len(self.fgcode.filament_length_multi)>1):
+                    for i in enumerate(self.fgcode.filament_length_multi):
+                        if self.spool_manager.getSpoolName(i[0]) != None:
+                            self.spool_manager.editLength(
+                                -i[1], extruder = i[0])
+                else:
+                    if self.spool_manager.getSpoolName(0) != None:
+                            self.spool_manager.editLength(
+                                -self.fgcode.filament_length, extruder = 0)
+
             if not self.settings.final_command:
                 return
             output = get_command_output(self.settings.final_command,
@@ -1202,7 +1292,7 @@
 
     def recvcb_report(self, l):
         isreport = REPORT_NONE
-        if "ok C:" in l or "Count" in l \
+        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
@@ -1260,7 +1350,7 @@
             report_type = self.recvcb_report(l)
             if report_type & REPORT_TEMP:
                 self.status.update_tempreading(l)
-            if l != "ok" and not self.sdlisting \
+            if not self.lineignorepattern.match(l) and l[:4] != "wait" 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()
@@ -1332,10 +1422,10 @@
             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))
+                self.log(_("Hotend: %s%s/%s%s") % (self.status.extruder_temp, DEG, self.status.extruder_temp_target, DEG))
             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))
+                self.log(_("Hotend: %s%s/%s%s") % (self.status.extruder_temp, DEG, self.status.extruder_temp_target, DEG))
+                self.log(_("Bed:    %s%s/%s%s") % (self.status.bed_temp, DEG, self.status.bed_temp_target, DEG))
 
     def help_gettemp(self):
         self.log(_("Read the extruder and bed temperature."))
@@ -1366,7 +1456,7 @@
     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()]))
+        self.log(', '.join('%s (%s)'%kv for kv in self.temps.items()))
 
     def complete_settemp(self, text, line, begidx, endidx):
         if (len(line.split()) == 2 and line[-1] != " ") or (len(line.split()) == 1 and line[-1] == " "):
@@ -1456,6 +1546,7 @@
             if self.p.online:
                 self.p.send_now("T%d" % tool)
                 self.log(_("Using tool %d.") % tool)
+                self.current_tool = tool
             else:
                 self.logError(_("Printer is not online."))
         else:
@@ -1560,6 +1651,12 @@
         self.p.send_now("G1 E" + str(length) + " F" + str(feed))
         self.p.send_now("G90")
 
+        # Update the length of filament in the current spool
+        self.spool_manager.refresh()
+        if self.spool_manager.getSpoolName(self.current_tool) != None:
+            self.spool_manager.editLength(-length,
+                extruder = self.current_tool)
+
     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)"))
@@ -1658,7 +1755,7 @@
             self.onecmd(command)
 
     def do_run_script(self, l):
-        p = run_command(l, {"$s": str(self.filename)}, stdout = subprocess.PIPE)
+        p = run_command(l, {"$s": str(self.filename)}, stdout = subprocess.PIPE, universal_newlines = True)
         for line in p.stdout.readlines():
             self.log("<< " + line.strip())
 
@@ -1666,9 +1763,24 @@
         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())
+        try:
+            self.fgcode = RGSGCoder(l)
+            self.do_print(None)
+        except BaseException as e:
+            self.logError(traceback.format_exc())
 
     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."))
+
+    def complete_run_gcode_script(self, text, line, begidx, endidx):
+        words = line.split()
+        sep = os.path.sep
+        if len(words) < 2:
+            return ['.' + sep , sep]
+        corrected_text = words[-1] # text arg skips leading '/', include it
+        if corrected_text == '.':
+            return ['./'] # guide user that in linux, PATH does not include . and relative executed scripts must start with ./
+        prefix_len = len(corrected_text) - len(text)
+        res = [((f + sep) if os.path.isdir(f) else f)[prefix_len:] #skip unskipped prefix_len
+                for f in glob.glob(corrected_text + '*')]
+        return res

mercurial