442 lines
16 KiB
Python
442 lines
16 KiB
Python
"""
|
|
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
|
|
|
|
from fabmetheus_utilities.vector3 import Vector3
|
|
from fabmetheus_utilities import archive
|
|
from fabmetheus_utilities import euclidean
|
|
|
|
import math
|
|
import sys
|
|
|
|
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('(</extruderInitialization>)')
|
|
if extruderInitializationIndex == -1:
|
|
metadataBeginIndex = gcodeText.find('<metadata>')
|
|
metadataEndIndex = gcodeText.find('</metadata>')
|
|
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(object):
|
|
'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 == '(<boundaryPoint>':
|
|
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(object):
|
|
'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('(</boundaryPerimeter>)')
|
|
self.addLine('(</nestedRing>)')
|
|
|
|
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 == '(<alteration>)':
|
|
self.isAlteration = True
|
|
elif firstWord == '(</alteration>)':
|
|
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('(<alteration>)')
|
|
for line in lines:
|
|
splitLine = getSplitLineBeforeBracketSemicolon(line)
|
|
firstWord = getFirstWord(splitLine)
|
|
if firstWord == 'G90':
|
|
absoluteDistanceMode = True
|
|
elif firstWord == 'G91':
|
|
absoluteDistanceMode = False
|
|
self.addLine('(<alterationDeleteThisPrefix/>)' + line)
|
|
if not absoluteDistanceMode:
|
|
self.addLine('G90')
|
|
self.addLine('(</alteration>)')
|
|
|
|
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('(<edge> outer )')
|
|
else:
|
|
self.addLine('(<edge> inner )')
|
|
self.addGcodeFromThreadZ(loop + [loop[0]], z)
|
|
self.addLine('(</edge>)') # 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 '(<boundaryPoint> X%s Y%s Z%s </boundaryPoint>)' % (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 '(<infillPoint> X%s Y%s Z%s </infillPoint>)' % (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 == '(<decimalPlacesCarried>':
|
|
self.decimalPlacesCarried = int(splitLine[1])
|