"""
Gcodec is a collection of utilities to decode and encode gcode.
To run gcodec, install python 2.x on your machine, which is avaliable from http://www.python.org/download/
Then in the folder which gcodec is in, type 'python' in a shell to run the python interpreter. Finally type 'from gcodec import *' to import this program.
Below is an example of gcodec use. This example is run in a terminal in the folder which contains gcodec and Screw Holder Bottom_export.gcode.
>>> from gcodec import *
>>> getFileText('Screw Holder Bottom_export.gcode')
'G90\nG21\nM103\nM105\nM106\nM110 S60.0\nM111 S30.0\nM108 S210.0\nM104 S235.0\nG1 X0.37 Y-4.07 Z1.9 F60.0\nM101\n
..
many lines of text
..
"""
from __future__ import absolute_import
#Init has to be imported first because it has code to workaround the python bug where relative imports don't work if the module is imported as a main module.
import __init__
from fabmetheus_utilities.vector3 import Vector3
from fabmetheus_utilities import archive
from fabmetheus_utilities import euclidean
import math
import os
import sys
import traceback
if sys.version_info[0] < 3:
import cStringIO
else:
import io as cStringIO
__author__ = 'Enrique Perez (perez_enrique@yahoo.com)'
__date__ = '$Date: 2008/21/04 $'
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
def addLineAndNewlineIfNecessary(line, output):
'Add the line and if the line does not end with a newline add a newline.'
output.write(line)
if len(line) < 1:
return
if not line.endswith('\n'):
output.write('\n')
def addLinesToCString(cString, lines):
'Add lines which have something to cStringIO.'
for line in lines:
if line != '':
cString.write(line + '\n')
def getArcDistance(relativeLocation, splitLine):
'Get arc distance.'
halfPlaneLineDistance = 0.5 * abs(relativeLocation.dropAxis())
radius = getDoubleFromCharacterSplitLine('R', splitLine)
if radius == None:
iFloat = getDoubleFromCharacterSplitLine('I', splitLine)
jFloat = getDoubleFromCharacterSplitLine('J', splitLine)
radius = abs(complex(iFloat, jFloat))
angle = 0.0
if radius > 0.0:
halfPlaneLineDistanceOverRadius = halfPlaneLineDistance / radius
if halfPlaneLineDistance < radius:
angle = 2.0 * math.asin(halfPlaneLineDistanceOverRadius)
else:
angle = math.pi * halfPlaneLineDistanceOverRadius
return abs(complex(angle * radius, relativeLocation.z))
def getDoubleAfterFirstLetter(word):
'Get the double value of the word after the first letter.'
return float(word[1 :])
def getDoubleForLetter(letter, splitLine):
'Get the double value of the word after the first occurence of the letter in the split line.'
return getDoubleAfterFirstLetter(splitLine[getIndexOfStartingWithSecond(letter, splitLine)])
def getDoubleFromCharacterSplitLine(character, splitLine):
'Get the double value of the string after the first occurence of the character in the split line.'
indexOfCharacter = getIndexOfStartingWithSecond(character, splitLine)
if indexOfCharacter < 0:
return None
floatString = splitLine[indexOfCharacter][1 :]
try:
return float(floatString)
except ValueError:
return None
def getDoubleFromCharacterSplitLineValue(character, splitLine, value):
'Get the double value of the string after the first occurence of the character in the split line, if it does not exist return the value.'
splitLineFloat = getDoubleFromCharacterSplitLine(character, splitLine)
if splitLineFloat == None:
return value
return splitLineFloat
def getFeedRateMinute(feedRateMinute, splitLine):
'Get the feed rate per minute if the split line has a feed rate.'
indexOfF = getIndexOfStartingWithSecond('F', splitLine)
if indexOfF > 0:
return getDoubleAfterFirstLetter( splitLine[indexOfF] )
return feedRateMinute
def getFirstWord(splitLine):
'Get the first word of a split line.'
if len(splitLine) > 0:
return splitLine[0]
return ''
def getFirstWordFromLine(line):
'Get the first word of a line.'
return getFirstWord(line.split())
def getFirstWordIndexReverse(firstWord, lines, startIndex):
'Parse gcode in reverse order until the first word if there is one, otherwise return -1.'
for lineIndex in xrange(len(lines) - 1, startIndex - 1, -1):
if firstWord == getFirstWord(getSplitLineBeforeBracketSemicolon(lines[lineIndex])):
return lineIndex
return -1
def getGcodeFileText(fileName, gcodeText):
'Get the gcode text from a file if it the gcode text is empty and if the file is a gcode file.'
if gcodeText != '':
return gcodeText
if fileName.endswith('.gcode'):
return archive.getFileText(fileName)
return ''
def getGcodeWithoutDuplication(duplicateWord, gcodeText):
'Get gcode text without duplicate first words.'
lines = archive.getTextLines(gcodeText)
oldWrittenLine = None
output = cStringIO.StringIO()
for line in lines:
firstWord = getFirstWordFromLine(line)
if firstWord == duplicateWord:
if line != oldWrittenLine:
output.write(line + '\n')
oldWrittenLine = line
else:
if len(line) > 0:
output.write(line + '\n')
return output.getvalue()
def getIndexOfStartingWithSecond(letter, splitLine):
'Get index of the first occurence of the given letter in the split line, starting with the second word. Return - 1 if letter is not found'
for wordIndex in xrange( 1, len(splitLine) ):
word = splitLine[ wordIndex ]
firstLetter = word[0]
if firstLetter == letter:
return wordIndex
return - 1
def getLineWithValueString(character, line, splitLine, valueString):
'Get the line with a valueString.'
roundedValueString = character + valueString
indexOfValue = getIndexOfStartingWithSecond(character, splitLine)
if indexOfValue == -1:
return line + ' ' + roundedValueString
word = splitLine[indexOfValue]
return line.replace(word, roundedValueString)
def getLocationFromSplitLine(oldLocation, splitLine):
'Get the location from the split line.'
if oldLocation == None:
oldLocation = Vector3()
return Vector3(
getDoubleFromCharacterSplitLineValue('X', splitLine, oldLocation.x),
getDoubleFromCharacterSplitLineValue('Y', splitLine, oldLocation.y),
getDoubleFromCharacterSplitLineValue('Z', splitLine, oldLocation.z))
def getRotationBySplitLine(splitLine):
'Get the complex rotation from the split gcode line.'
return complex(splitLine[1].replace('(', '').replace(')', ''))
def getSplitLineBeforeBracketSemicolon(line):
'Get the split line before a bracket or semicolon.'
if ';' in line:
line = line[: line.find(';')]
bracketIndex = line.find('(')
if bracketIndex > 0:
return line[: bracketIndex].split()
return line.split()
def getStringFromCharacterSplitLine(character, splitLine):
'Get the string after the first occurence of the character in the split line.'
indexOfCharacter = getIndexOfStartingWithSecond(character, splitLine)
if indexOfCharacter < 0:
return None
return splitLine[indexOfCharacter][1 :]
def getTagBracketedLine(tagName, value):
'Get line with a begin tag, value and end tag.'
return '(<%s> %s %s>)' % (tagName, value, tagName)
def getTagBracketedProcedure(procedure):
'Get line with a begin procedure tag, procedure and end procedure tag.'
return getTagBracketedLine('procedureName', procedure)
def isProcedureDone(gcodeText, procedure):
'Determine if the procedure has been done on the gcode text.'
if gcodeText == '':
return False
extruderInitializationIndex = gcodeText.find('()')
if extruderInitializationIndex == -1:
metadataBeginIndex = gcodeText.find('')
metadataEndIndex = gcodeText.find('')
if metadataBeginIndex != -1 and metadataEndIndex != -1:
attributeString = "procedureName='%s'" % procedure
return gcodeText.find(attributeString, metadataBeginIndex, metadataEndIndex) != -1
return False
return gcodeText.find(getTagBracketedProcedure(procedure), 0, extruderInitializationIndex) != -1
def isProcedureDoneOrFileIsEmpty(gcodeText, procedure):
'Determine if the procedure has been done on the gcode text or the file is empty.'
if gcodeText == '':
return True
return isProcedureDone(gcodeText, procedure)
def isThereAFirstWord(firstWord, lines, startIndex):
'Parse gcode until the first word if there is one.'
for lineIndex in xrange(startIndex, len(lines)):
line = lines[lineIndex]
splitLine = getSplitLineBeforeBracketSemicolon(line)
if firstWord == getFirstWord(splitLine):
return True
return False
class BoundingRectangle:
'A class to get the corners of a gcode text.'
def getFromGcodeLines(self, lines, radius):
'Parse gcode text and get the minimum and maximum corners.'
self.cornerMaximum = complex(-987654321.0, -987654321.0)
self.cornerMinimum = complex(987654321.0, 987654321.0)
self.oldLocation = None
self.cornerRadius = complex(radius, radius)
for line in lines:
self.parseCorner(line)
return self
def isPointInside(self, point):
'Determine if the point is inside the bounding rectangle.'
return point.imag >= self.cornerMinimum.imag and point.imag <= self.cornerMaximum.imag and point.real >= self.cornerMinimum.real and point.real <= self.cornerMaximum.real
def parseCorner(self, line):
'Parse a gcode line and use the location to update the bounding corners.'
splitLine = getSplitLineBeforeBracketSemicolon(line)
firstWord = getFirstWord(splitLine)
if firstWord == '(':
locationComplex = getLocationFromSplitLine(None, splitLine).dropAxis()
self.cornerMaximum = euclidean.getMaximum(self.cornerMaximum, locationComplex)
self.cornerMinimum = euclidean.getMinimum(self.cornerMinimum, locationComplex)
elif firstWord == 'G1':
location = getLocationFromSplitLine(self.oldLocation, splitLine)
locationComplex = location.dropAxis()
self.cornerMaximum = euclidean.getMaximum(self.cornerMaximum, locationComplex + self.cornerRadius)
self.cornerMinimum = euclidean.getMinimum(self.cornerMinimum, locationComplex - self.cornerRadius)
self.oldLocation = location
class DistanceFeedRate:
'A class to limit the z feed rate and round values.'
def __init__(self):
'Initialize.'
self.isAlteration = False
self.decimalPlacesCarried = 3
self.output = cStringIO.StringIO()
def addFlowRateLine(self, flowRate):
'Add a flow rate line.'
self.output.write('M108 S%s\n' % euclidean.getFourSignificantFigures(flowRate))
def addGcodeFromFeedRateThreadZ(self, feedRateMinute, thread, travelFeedRateMinute, z):
'Add a thread to the output.'
if len(thread) > 0:
self.addGcodeMovementZWithFeedRate(travelFeedRateMinute, thread[0], z)
else:
print('zero length vertex positions array which was skipped over, this should never happen.')
if len(thread) < 2:
print('thread of only one point in addGcodeFromFeedRateThreadZ in gcodec, this should never happen.')
print(thread)
return
self.output.write('M101\n') # Turn extruder on.
for point in thread[1 :]:
self.addGcodeMovementZWithFeedRate(feedRateMinute, point, z)
self.output.write('M103\n') # Turn extruder off.
def addGcodeFromLoop(self, loop, z):
'Add the gcode loop.'
euclidean.addNestedRingBeginning(self, loop, z)
self.addPerimeterBlock(loop, z)
self.addLine('()')
self.addLine('()')
def addGcodeFromThreadZ(self, thread, z):
'Add a thread to the output.'
if len(thread) > 0:
self.addGcodeMovementZ(thread[0], z)
else:
print('zero length vertex positions array which was skipped over, this should never happen.')
if len(thread) < 2:
print('thread of only one point in addGcodeFromThreadZ in gcodec, this should never happen.')
print(thread)
return
self.output.write('M101\n') # Turn extruder on.
for point in thread[1 :]:
self.addGcodeMovementZ(point, z)
self.output.write('M103\n') # Turn extruder off.
def addGcodeMovementZ(self, point, z):
'Add a movement to the output.'
self.output.write(self.getLinearGcodeMovement(point, z) + '\n')
def addGcodeMovementZWithFeedRate(self, feedRateMinute, point, z):
'Add a movement to the output.'
self.output.write(self.getLinearGcodeMovementWithFeedRate(feedRateMinute, point, z) + '\n')
def addGcodeMovementZWithFeedRateVector3(self, feedRateMinute, vector3):
'Add a movement to the output by Vector3.'
xRounded = self.getRounded(vector3.x)
yRounded = self.getRounded(vector3.y)
self.output.write('G1 X%s Y%s Z%s F%s\n' % (xRounded, yRounded, self.getRounded(vector3.z), self.getRounded(feedRateMinute)))
def addLine(self, line):
'Add a line of text and a newline to the output.'
if len(line) > 0:
self.output.write(line + '\n')
def addLineCheckAlteration(self, line):
'Add a line of text and a newline to the output and check to see if it is an alteration line.'
firstWord = getFirstWord(getSplitLineBeforeBracketSemicolon(line))
if firstWord == '()':
self.isAlteration = True
elif firstWord == '()':
self.isAlteration = False
if len(line) > 0:
self.output.write(line + '\n')
def addLines(self, lines):
'Add lines of text to the output.'
addLinesToCString(self.output, lines)
def addLinesSetAbsoluteDistanceMode(self, lines):
'Add lines of text to the output and ensure the absolute mode is set.'
if len(lines) < 1:
return
if len(lines[0]) < 1:
return
absoluteDistanceMode = True
self.addLine('()')
for line in lines:
splitLine = getSplitLineBeforeBracketSemicolon(line)
firstWord = getFirstWord(splitLine)
if firstWord == 'G90':
absoluteDistanceMode = True
elif firstWord == 'G91':
absoluteDistanceMode = False
self.addLine('()' + line)
if not absoluteDistanceMode:
self.addLine('G90')
self.addLine('()')
def addParameter(self, firstWord, parameter):
'Add the parameter.'
self.addLine(firstWord + ' S' + euclidean.getRoundedToThreePlaces(parameter))
def addPerimeterBlock(self, loop, z):
'Add the edge gcode block for the loop.'
if len(loop) < 2:
return
if euclidean.isWiddershins(loop): # Indicate that an edge is beginning.
self.addLine('( outer )')
else:
self.addLine('( inner )')
self.addGcodeFromThreadZ(loop + [loop[0]], z)
self.addLine('()') # Indicate that an edge is beginning.
def addTagBracketedLine(self, tagName, value):
'Add a begin tag, value and end tag.'
self.addLine(getTagBracketedLine(tagName, value))
def addTagRoundedLine(self, tagName, value):
'Add a begin tag, rounded value and end tag.'
self.addLine('(<%s> %s %s>)' % (tagName, self.getRounded(value), tagName))
def addTagBracketedProcedure(self, procedure):
'Add a begin procedure tag, procedure and end procedure tag.'
self.addLine(getTagBracketedProcedure(procedure))
def getBoundaryLine(self, location):
'Get boundary gcode line.'
return '( X%s Y%s Z%s )' % (self.getRounded(location.x), self.getRounded(location.y), self.getRounded(location.z))
def getFirstWordMovement(self, firstWord, location):
'Get the start of the arc line.'
return '%s X%s Y%s Z%s' % (firstWord, self.getRounded(location.x), self.getRounded(location.y), self.getRounded(location.z))
def getInfillBoundaryLine(self, location):
'Get infill boundary gcode line.'
return '( X%s Y%s Z%s )' % (self.getRounded(location.x), self.getRounded(location.y), self.getRounded(location.z))
def getIsAlteration(self, line):
'Determine if it is an alteration.'
if self.isAlteration:
self.addLineCheckAlteration(line)
return True
return False
def getLinearGcodeMovement(self, point, z):
'Get a linear gcode movement.'
return 'G1 X%s Y%s Z%s' % (self.getRounded(point.real), self.getRounded(point.imag), self.getRounded(z))
def getLinearGcodeMovementWithFeedRate(self, feedRateMinute, point, z):
'Get a z limited gcode movement.'
linearGcodeMovement = self.getLinearGcodeMovement(point, z)
if feedRateMinute == None:
return linearGcodeMovement
return linearGcodeMovement + ' F' + self.getRounded(feedRateMinute)
def getLineWithFeedRate(self, feedRateMinute, line, splitLine):
'Get the line with a feed rate.'
return getLineWithValueString('F', line, splitLine, self.getRounded(feedRateMinute))
def getLineWithX(self, line, splitLine, x):
'Get the line with an x.'
return getLineWithValueString('X', line, splitLine, self.getRounded(x))
def getLineWithY(self, line, splitLine, y):
'Get the line with a y.'
return getLineWithValueString('Y', line, splitLine, self.getRounded(y))
def getLineWithZ(self, line, splitLine, z):
'Get the line with a z.'
return getLineWithValueString('Z', line, splitLine, self.getRounded(z))
def getRounded(self, number):
'Get number rounded to the number of carried decimal places as a string.'
return euclidean.getRoundedToPlacesString(self.decimalPlacesCarried, number)
def parseSplitLine(self, firstWord, splitLine):
'Parse gcode split line and store the parameters.'
if firstWord == '(':
self.decimalPlacesCarried = int(splitLine[1])