diff --git a/GCodeAnalyzer.py b/GCodeAnalyzer.py new file mode 100644 index 0000000..0332701 --- /dev/null +++ b/GCodeAnalyzer.py @@ -0,0 +1,213 @@ +# This file is part of the Printrun suite. +# +# Copyright 2013 Francesco Santini francesco.santini@gmail.com +# +# Printrun is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Printrun is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Printrun. If not, see . +# +# This code is imported from RepetierHost - Original copyright and license: +# Copyright 2011 repetier repetierdev@gmail.com +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import re + +class GCodeAnalyzer(): + def __init__(self): + self.x = 0 + self.y = 0 + self.z = 0 + self.e = 0 + self.emax = 0 + self.f = 1000 + self.lastX = 0 + self.lastY = 0 + self.lastZ = 0 + self.lastE = 0 + self.xOffset = 0 + self.yOffset = 0 + self.zOffset = 0 + self.eOffset = 0 + self.lastZPrint = 0 + self.layerZ = 0 + self.relative = False + self.eRelative = False + self.homeX = 0 + self.homeY = 0 + self.homeZ = 0 + self.maxX = 150 + self.maxY = 150 + self.maxZ = 150 + self.minX = 0 + self.minY = 0 + self.minZ = 0 + self.hasHomeX = False + self.hasHomeY = False + self.hasHomeZ = False + + + # find a code in a gstring line + def findCode(self, gcode, codeStr): + pattern = re.compile(codeStr + "\\s*(-?[\d.]*)",re.I) + m=re.search(pattern, gcode) + if m == None: + return None + else: + return m.group(1) + + def Analyze(self, gcode): + gcode = gcode[:gcode.find(";")].lstrip() # remove comments + if gcode.startswith("@"): return # code is a host command + code_g = self.findCode(gcode, "G") + code_m = self.findCode(gcode, "M") + # we have a g_code + if code_g != None: + code_g = int(code_g) + + #get movement codes + if code_g == 0 or code_g == 1 or code_g == 2 or code_g == 3: + self.lastX = self.x + self.lastY = self.y + self.lastZ = self.z + self.lastE = self.e + eChanged = False; + code_f = self.findCode(gcode, "F") + if code_f != None: + self.f=float(code_f) + + code_x = self.findCode(gcode, "X") + code_y = self.findCode(gcode, "Y") + code_z = self.findCode(gcode, "Z") + code_e = self.findCode(gcode, "E") + + if self.relative: + if code_x != None: self.x += float(code_x) + if code_y != None: self.y += float(code_y) + if code_z != None: self.z += float(code_z) + if code_e != None: + e = float(code_e) + if e != 0: + eChanged = True + self.e += e + else: + #absolute coordinates + if code_x != None: self.x = self.xOffset + float(code_x) + if code_y != None: self.y = self.yOffset + float(code_y) + if code_z != None: self.z = self.zOffset + float(code_z) + if code_e != None: + e = float(code_e) + if self.eRelative: + if e != 0: + eChanged = True + self.e += e + else: + # e is absolute. Is it changed? + if self.e != self.eOffset + e: + eChanged = True + self.e = self.eOffset + e + #limit checking + if self.x < self.minX: self.x = self.minX + if self.y < self.minY: self.y = self.minY + if self.z < self.minZ: self.z = self.minZ + + if self.x > self.maxX: self.x = self.maxX + if self.y > self.maxY: self.y = self.maxY + if self.z > self.maxZ: self.z = self.maxZ + #Repetier has a bunch of limit-checking code here and time calculations: we are leaving them for now + elif code_g == 28 or code_g == 161: + self.lastX = self.x + self.lastY = self.y + self.lastZ = self.z + self.lastE = self.e + code_x = self.findCode(gcode, "X") + code_y = self.findCode(gcode, "Y") + code_z = self.findCode(gcode, "Z") + code_e = self.findCode(gcode, "E") + homeAll = False + if code_x == None and code_y == None and code_z == None: homeAll = True + if code_x != None or homeAll: + self.hasHomeX = True + self.xOffset = 0 + self.x = self.homeX + if code_y != None or homeAll: + self.hasHomeY = True + self.yOffset = 0 + self.y = self.homeY + if code_z != None or homeAll: + self.hasHomeZ = True + self.zOffset = 0 + self.z = self.homeZ + if code_e != None: + self.eOffset = 0 + self.e = 0 + elif code_g == 162: + self.lastX = self.x + self.lastY = self.y + self.lastZ = self.z + self.lastE = self.e + code_x = self.findCode(gcode, "X") + code_y = self.findCode(gcode, "Y") + code_z = self.findCode(gcode, "Z") + homeAll = False + if code_x == None and code_y == None and code_z == None: homeAll = True + if code_x != None or homeAll: + self.hasHomeX = True + self.xOffset = 0 + self.x = self.maxX + if code_y != None or homeAll: + self.hasHomeY = True + self.yOffset = 0 + self.y = self.maxY + if code_z != None or homeAll: + self.hasHomeZ = True + self.zOffset = 0 + self.z = self.maxZ + elif code_g == 90: self.relative = False + elif code_g == 91: self.relative = True + elif code_g == 92: + code_x = self.findCode(gcode, "X") + code_y = self.findCode(gcode, "Y") + code_z = self.findCode(gcode, "Z") + code_e = self.findCode(gcode, "E") + if code_x != None: + self.xOffset = self.x - float(code_x) + self.x = self.xOffset + if code_y != None: + self.yOffset = self.y - float(code_y) + self.y = self.yOffset + if code_z != None: + self.zOffset = self.z - float(code_z) + self.z = self.zOffset + if code_e != None: + self.xOffset = self.e - float(code_e) + self.e = self.eOffset + #End code_g != None + if code_m != None: + code_m = int(code_m) + if code_m == 82: self.eRelative = False + elif code_m == 83: self.eRelative = True + + def print_status(self): + attrs = vars(self) + print '\n'.join("%s: %s" % item for item in attrs.items()) + \ No newline at end of file diff --git a/printcore.py b/printcore.py index 3350761..acc981d 100755 --- a/printcore.py +++ b/printcore.py @@ -20,6 +20,7 @@ from threading import Thread from select import error as SelectError import time, getopt, sys import platform, os +from GCodeAnalyzer import GCodeAnalyzer def control_ttyhup(port, disable_hup): """Controls the HUPCL""" @@ -69,7 +70,11 @@ class printcore(): self.print_thread = None if port is not None and baud is not None: self.connect(port, baud) - + self.analyzer = GCodeAnalyzer() + self.xy_feedrate = None + self.z_feedrate = None + self.pronterface = None + def disconnect(self): """Disconnects from printer and pauses the print """ @@ -219,19 +224,69 @@ class printcore(): self.print_thread.start() return True + # run a simple script if it exists, no multithreading + def runSmallScript(self, filename): + if filename == None: return + f = None + try: + f = open(filename) + except: + pass + + if f != None: + for i in f: + l = i.replace("\n", "") + l = l[:l.find(";")] #remove comment + self.send_now(l) + f.close() + def pause(self): """Pauses the print, saving the current position. """ if not self.printing: return False self.paused = True self.printing = False - self.print_thread.join() + + # try joining the print thread: enclose it in try/except because we might be calling it from the thread itself + + try: + self.print_thread.join() + except: + pass + self.print_thread = None + + # saves the status + self.pauseX = self.analyzer.x-self.analyzer.xOffset; + self.pauseY = self.analyzer.y-self.analyzer.yOffset; + self.pauseZ = self.analyzer.z-self.analyzer.zOffset; + self.pauseE = self.analyzer.e-self.analyzer.eOffset; + self.pauseF = self.analyzer.f; + self.pauseRelative = self.analyzer.relative; + + def resume(self): """Resumes a paused print. """ if not self.paused: return False + if self.paused: + #restores the status + self.send_now("G90") # go to absolute coordinates + + xyFeedString = "" + zFeedString = "" + if self.xy_feedrate != None: xyFeedString = " F" + str(self.xy_feedrate) + if self.z_feedrate != None: zFeedString = " F" + str(self.z_feedrate) + + self.send_now("G1 X" + str(self.pauseX) + " Y" + str(self.pauseY) + xyFeedString) + self.send_now("G1 Z" + str(self.pauseZ) + zFeedString) + self.send_now("G92 E" + str(self.pauseE)) + + if self.pauseRelative: self.send_now("G91") # go back to relative if needed + #reset old feed rate + self.send_now("G1 F" + str(self.pauseF)) + self.paused = False self.printing = True self.print_thread = Thread(target = self._print) @@ -289,13 +344,24 @@ class printcore(): self.sentlines = {} self.log = [] self.sent = [] - self.print_thread.join() + try: + self.print_thread.join() + except: pass self.print_thread = None if self.endcb: #callback for printing done try: self.endcb() except: pass + #now only "pause" is implemented as host command + def processHostCommand(self, command): + command = command.lstrip() + if command.startswith(";@pause"): + if self.pronterface != None: + self.pronterface.pause(None) + else: + self.pause() + def _sendnext(self): if not self.printer: return @@ -316,6 +382,13 @@ class printcore(): return if self.printing and self.queueindex < len(self.mainqueue): tline = self.mainqueue[self.queueindex] + #check for host command + if tline.lstrip().startswith(";@"): + #it is a host command: pop it from the list + self.mainqueue.pop(self.queueindex) + self.processHostCommand(tline) + return + tline = tline.split(";")[0] if len(tline) > 0: self._send(tline, self.lineno, True) @@ -339,6 +412,7 @@ class printcore(): self.sentlines[lineno] = command if self.printer: self.sent.append(command) + self.analyzer.Analyze(command) # run the command through the analyzer if self.loud: print "SENT: ", command if self.sendcb: diff --git a/pronterface.py b/pronterface.py index 9554bb7..ceb87dc 100755 --- a/pronterface.py +++ b/pronterface.py @@ -85,7 +85,7 @@ class Tee(object): class PronterWindow(MainWindow, pronsole.pronsole): def __init__(self, filename = None, size = winsize): pronsole.pronsole.__init__(self) - self.settings.build_dimensions = '200x200x100+0+0+0' #default build dimensions are 200x200x100 with 0, 0, 0 in the corner of the bed + self.settings.build_dimensions = '200x200x100+0+0+0+0+0+0' #default build dimensions are 200x200x100 with 0, 0, 0 in the corner of the bed self.settings.last_bed_temperature = 0.0 self.settings.last_file_path = "" self.settings.last_temperature = 0.0 @@ -93,7 +93,11 @@ class PronterWindow(MainWindow, pronsole.pronsole): self.settings.preview_grid_step1 = 10. self.settings.preview_grid_step2 = 50. self.settings.bgcolor = "#FFFFFF" - self.helpdict["build_dimensions"] = _("Dimensions of Build Platform\n & optional offset of origin\n\nExamples:\n XXXxYYY\n XXX,YYY,ZZZ\n XXXxYYYxZZZ+OffX+OffY+OffZ") + + self.pauseScript = "pause.gcode" + self.endScript = "end.gcode" + + self.helpdict["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") self.helpdict["last_bed_temperature"] = _("Last Set Temperature for the Heated Print Bed") self.helpdict["last_file_path"] = _("Folder of last opened file") self.helpdict["last_temperature"] = _("Last Temperature of the Hot End") @@ -128,6 +132,30 @@ class PronterWindow(MainWindow, pronsole.pronsole): self.btndict = {} self.parse_cmdline(sys.argv[1:]) self.build_dimensions_list = self.get_build_dimensions(self.settings.build_dimensions) + + #initialize the code analyzer with the correct sizes. There must be a more general way to do so + + # minimum = offset + self.p.analyzer.minX = self.build_dimensions_list[3] + self.p.analyzer.minY = self.build_dimensions_list[4] + self.p.analyzer.minZ = self.build_dimensions_list[5] + + #max = offset + bedsize + self.p.analyzer.maxX = self.build_dimensions_list[3] + self.build_dimensions_list[0] + self.p.analyzer.maxY = self.build_dimensions_list[4] + self.build_dimensions_list[1] + self.p.analyzer.maxZ = self.build_dimensions_list[5] + self.build_dimensions_list[2] + + self.p.analyzer.homeX = self.build_dimensions_list[6] + self.p.analyzer.homeY = self.build_dimensions_list[7] + self.p.analyzer.homeZ = self.build_dimensions_list[8] + + #set feedrates in printcore for pause/resume + self.p.xy_feedrate = self.settings.xy_feedrate + self.p.z_feedrate = self.settings.z_feedrate + + #make printcore aware of me + self.p.pronterface = self + self.panel.SetBackgroundColour(self.settings.bgcolor) customdict = {} try: @@ -198,6 +226,8 @@ class PronterWindow(MainWindow, pronsole.pronsole): wx.CallAfter(self.pausebtn.Disable) wx.CallAfter(self.printbtn.SetLabel, _("Print")) + self.p.runSmallScript(self.endScript) + param = self.settings.final_command if not param: return @@ -1388,7 +1418,9 @@ class PronterWindow(MainWindow, pronsole.pronsole): #print "Not printing, cannot pause." return self.p.pause() + self.p.runSmallScript(self.pauseScript) self.paused = True + #self.p.runSmallScript(self.pauseScript) self.extra_print_time += int(time.time() - self.starttime) wx.CallAfter(self.pausebtn.SetLabel, _("Resume")) else: @@ -1527,9 +1559,12 @@ class PronterWindow(MainWindow, pronsole.pronsole): "[^\d+-]*(\d+)?" + # Z build size "[^\d+-]*([+-]\d+)?" + # X corner coordinate "[^\d+-]*([+-]\d+)?" + # Y corner coordinate - "[^\d+-]*([+-]\d+)?" # Z corner coordinate + "[^\d+-]*([+-]\d+)?" + # Z corner coordinate + "[^\d+-]*([+-]\d+)?" + # X endstop + "[^\d+-]*([+-]\d+)?" + # Y endstop + "[^\d+-]*([+-]\d+)?" # Z endstop ,bdim).groups() - defaults = [200, 200, 100, 0, 0, 0] + defaults = [200, 200, 100, 0, 0, 0, 0, 0, 0] bdl_float = [float(value) if value else defaults[i] for i, value in enumerate(bdl)] return bdl_float