# coding=utf-8 from __future__ import absolute_import __author__ = "Gina Häußge based on work by David Braam" __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' __copyright__ = "Copyright (C) 2013 David Braam, Gina Häußge - Released under terms of the AGPLv3 License" import math import os import base64 import zlib import logging from octoprint.settings import settings class AnalysisAborted(Exception): pass class gcode(object): def __init__(self): self._logger = logging.getLogger(__name__) self.layerList = None self.extrusionAmount = [0] self.extrusionVolume = [0] self.totalMoveTimeMinute = 0 self.filename = None self.progressCallback = None self._abort = False self._filamentDiameter = 0 #Parameters for object size check self.minX=None self.minY=None self.minZ=None self.maxX=None self.maxY=None self.maxZ=None def load(self, filename, printer_profile, throttle=None): if os.path.isfile(filename): self.filename = filename self._fileSize = os.stat(filename).st_size import codecs with codecs.open(filename, encoding="utf-8", errors="replace") as f: self._load(f, printer_profile, throttle=throttle) def abort(self): self._abort = True def _load(self, gcodeFile, printer_profile, throttle=None): filePos = 0 readBytes = 0 pos = [0.0, 0.0, 0.0] posOffset = [0.0, 0.0, 0.0] currentE = [0.0] totalExtrusion = [0.0] maxExtrusion = [0.0] currentExtruder = 0 totalMoveTimeMinute = 0.0 absoluteE = True scale = 1.0 posAbs = True fwretractTime = 0 fwretractDist = 0 fwrecoverTime = 0 feedRateXY = min(printer_profile["axes"]["x"]["speed"], printer_profile["axes"]["y"]["speed"]) if feedRateXY == 0: # some somewhat sane default if axes speeds are insane... feedRateXY = 2000 offsets = printer_profile["extruder"]["offsets"] for line in gcodeFile: if self._abort: raise AnalysisAborted() filePos += 1 readBytes += len(line) if isinstance(gcodeFile, (file)): percentage = float(readBytes) / float(self._fileSize) elif isinstance(gcodeFile, (list)): percentage = float(filePos) / float(len(gcodeFile)) else: percentage = None try: if self.progressCallback is not None and (filePos % 1000 == 0) and percentage is not None: self.progressCallback(percentage) except: pass if ';' in line: comment = line[line.find(';')+1:].strip() if comment.startswith("filament_diameter"): filamentValue = comment.split("=", 1)[1].strip() try: self._filamentDiameter = float(filamentValue) except ValueError: try: self._filamentDiameter = float(filamentValue.split(",")[0].strip()) except ValueError: self._filamentDiameter = 0.0 elif comment.startswith("CURA_PROFILE_STRING") or comment.startswith("CURA_OCTO_PROFILE_STRING"): if comment.startswith("CURA_PROFILE_STRING"): prefix = "CURA_PROFILE_STRING:" else: prefix = "CURA_OCTO_PROFILE_STRING:" curaOptions = self._parseCuraProfileString(comment, prefix) if "filament_diameter" in curaOptions: try: self._filamentDiameter = float(curaOptions["filament_diameter"]) except: self._filamentDiameter = 0.0 line = line[0:line.find(';')] G = getCodeInt(line, 'G') M = getCodeInt(line, 'M') T = getCodeInt(line, 'T') if G is not None: if G == 0 or G == 1: #Move x = getCodeFloat(line, 'X') y = getCodeFloat(line, 'Y') z = getCodeFloat(line, 'Z') e = getCodeFloat(line, 'E') f = getCodeFloat(line, 'F') oldPos = pos pos = pos[:] if posAbs: if x is not None: pos[0] = x * scale + posOffset[0] if y is not None: pos[1] = y * scale + posOffset[1] if z is not None: pos[2] = z * scale + posOffset[2] else: if x is not None: pos[0] += x * scale if y is not None: pos[1] += y * scale if z is not None: pos[2] += z * scale if f is not None and f != 0: feedRateXY = f moveType = 'move' if e is not None: if absoluteE: e -= currentE[currentExtruder] # If move includes extrusion, calculate new min/max coordinates of model if e > 0.0: self.minX = pos[0] if self.minX is None or pos[0] < self.minX else self.minX self.maxX = pos[0] if self.maxX is None or pos[0] > self.maxX else self.maxX self.minY = pos[1] if self.minY is None or pos[1] < self.minY else self.minY self.maxY = pos[1] if self.maxY is None or pos[1] > self.maxY else self.maxY self.minZ = pos[2] if self.minZ is None or pos[2] < self.minZ else self.minZ self.maxZ = pos[2] if self.maxZ is None or pos[2] > self.maxZ else self.maxZ moveType = 'extrude' if e < 0.0: moveType = 'retract' totalExtrusion[currentExtruder] += e currentE[currentExtruder] += e if totalExtrusion[currentExtruder] > maxExtrusion[currentExtruder]: maxExtrusion[currentExtruder] = totalExtrusion[currentExtruder] else: e = 0.0 if x is not None or y is not None or z is not None: diffX = oldPos[0] - pos[0] diffY = oldPos[1] - pos[1] totalMoveTimeMinute += math.sqrt(diffX * diffX + diffY * diffY) / feedRateXY elif moveType == "extrude": diffX = oldPos[0] - pos[0] diffY = oldPos[1] - pos[1] time1 = math.sqrt(diffX * diffX + diffY * diffY) / feedRateXY time2 = abs(e / feedRateXY) totalMoveTimeMinute += max(time1, time2) elif moveType == "retract": totalMoveTimeMinute += abs(e / feedRateXY) if moveType == 'move' and oldPos[2] != pos[2]: if oldPos[2] > pos[2] and abs(oldPos[2] - pos[2]) > 5.0 and pos[2] < 1.0: oldPos[2] = 0.0 elif G == 4: #Delay S = getCodeFloat(line, 'S') if S is not None: totalMoveTimeMinute += S / 60.0 P = getCodeFloat(line, 'P') if P is not None: totalMoveTimeMinute += P / 60.0 / 1000.0 elif G == 10: #Firmware retract totalMoveTimeMinute += fwretractTime elif G == 11: #Firmware retract recover totalMoveTimeMinute += fwrecoverTime elif G == 20: #Units are inches scale = 25.4 elif G == 21: #Units are mm scale = 1.0 elif G == 28: #Home x = getCodeFloat(line, 'X') y = getCodeFloat(line, 'Y') z = getCodeFloat(line, 'Z') center = [0.0,0.0,0.0] if x is None and y is None and z is None: pos = center else: pos = pos[:] if x is not None: pos[0] = center[0] if y is not None: pos[1] = center[1] if z is not None: pos[2] = center[2] elif G == 90: #Absolute position posAbs = True elif G == 91: #Relative position posAbs = False elif G == 92: x = getCodeFloat(line, 'X') y = getCodeFloat(line, 'Y') z = getCodeFloat(line, 'Z') e = getCodeFloat(line, 'E') if e is not None: currentE[currentExtruder] = e if x is not None: posOffset[0] = pos[0] - x if y is not None: posOffset[1] = pos[1] - y if z is not None: posOffset[2] = pos[2] - z elif M is not None: if M == 82: #Absolute E absoluteE = True elif M == 83: #Relative E absoluteE = False elif M == 207 or M == 208: #Firmware retract settings s = getCodeFloat(line, 'S') f = getCodeFloat(line, 'F') if s is not None and f is not None: if M == 207: fwretractTime = s / f fwretractDist = s else: fwrecoverTime = (fwretractDist + s) / f elif T is not None: if T > settings().getInt(["gcodeAnalysis", "maxExtruders"]): self._logger.warn("GCODE tried to select tool %d, that looks wrong, ignoring for GCODE analysis" % T) else: posOffset[0] -= offsets[currentExtruder][0] if currentExtruder < len(offsets) else 0 posOffset[1] -= offsets[currentExtruder][1] if currentExtruder < len(offsets) else 0 currentExtruder = T posOffset[0] += offsets[currentExtruder][0] if currentExtruder < len(offsets) else 0 posOffset[1] += offsets[currentExtruder][1] if currentExtruder < len(offsets) else 0 if len(currentE) <= currentExtruder: for i in range(len(currentE), currentExtruder + 1): currentE.append(0.0) if len(maxExtrusion) <= currentExtruder: for i in range(len(maxExtrusion), currentExtruder + 1): maxExtrusion.append(0.0) if len(totalExtrusion) <= currentExtruder: for i in range(len(totalExtrusion), currentExtruder + 1): totalExtrusion.append(0.0) if throttle is not None: throttle() if self.progressCallback is not None: self.progressCallback(100.0) self.extrusionAmount = maxExtrusion self.extrusionVolume = [0] * len(maxExtrusion) for i in range(len(maxExtrusion)): radius = self._filamentDiameter / 2 self.extrusionVolume[i] = (self.extrusionAmount[i] * (math.pi * radius * radius)) / 1000 self.totalMoveTimeMinute = totalMoveTimeMinute def _parseCuraProfileString(self, comment, prefix): return {key: value for (key, value) in map(lambda x: x.split("=", 1), zlib.decompress(base64.b64decode(comment[len(prefix):])).split("\b"))} def getCodeInt(line, code): n = line.find(code) + 1 if n < 1: return None m = line.find(' ', n) try: if m < 0: return int(line[n:]) return int(line[n:m]) except: return None def getCodeFloat(line, code): import math n = line.find(code) + 1 if n < 1: return None m = line.find(' ', n) try: if m < 0: val = float(line[n:]) else: val = float(line[n:m]) return val if not (math.isnan(val) or math.isinf(val)) else None except: return None