words to gcode class instances

This commit is contained in:
Peter Boin 2017-07-05 17:43:45 +10:00
parent da772580e0
commit e84901eccc
5 changed files with 213 additions and 61 deletions

View File

@ -21,7 +21,7 @@ class Block(object):
self.text = text self.text = text
self.words = list(iter_words(self.text)) self.words = list(iter_words(self.text))
#self.gcodes = list(words_to_gcodes(self.words)) self.gcodes = list(words_to_gcodes(self.words))
def __getattr__(self, k): def __getattr__(self, k):
if k in WORD_MAP: if k in WORD_MAP:

View File

@ -1,5 +1,6 @@
from .line import Line from .line import Line
from .machine import AbstractMachine
class GCodeFile(object): class GCodeFile(object):
def __init__(self, filename=None): def __init__(self, filename=None):
@ -13,6 +14,10 @@ class GCodeFile(object):
self.lines.append(line) self.lines.append(line)
class GCodeWriterMachine(AbstractMachine):
def machine_init(self, *args, **kwargs):
pass
def parse(filename): def parse(filename):
# FIXME: should be an iterator, and also not terrible # FIXME: should be an iterator, and also not terrible
file = GCodeFile() file = GCodeFile()

View File

@ -1,3 +1,4 @@
from collections import defaultdict
from .words import Word from .words import Word
@ -7,10 +8,54 @@ class GCode(object):
word_key = None # Word instance to use in lookup word_key = None # Word instance to use in lookup
word_matches = None # function (secondary) word_matches = None # function (secondary)
# Parameters associated to this gcode # Parameters associated to this gcode
param_words = set() param_letters = set()
def __init__(self): def __init__(self, word, *params):
self.params = None # TODO assert isinstance(word, Word), "invalid gcode word %r" % code_word
self.word = word
self.params = {}
# Add Given Parameters
for param in params:
self.add_parameter(param)
def __repr__(self):
return "<{class_name}: {gcode}{{{word_list}}}>".format(
class_name=self.__class__.__name__,
gcode=self.word,
word_list=', '.join([
"{}".format(self.params[k])
for k in sorted(self.params.keys())
]),
)
def __str__(self):
"""String representation of gcode, as it would be seen in a .gcode file"""
return "{gcode} {parameters}".format(
gcode=self.word,
parameters=' '.join([
"{}".format(self.params[k])
for k in sorted(self.params.keys())
]),
)
def add_parameter(self, word):
assert isinstance(word, Word), "invalid parameter class: %r" % word
assert word.letter in self.param_letters, "invalid parameter for %s: %s" % (self.__class__.__name__, str(word))
assert word.letter not in self.params, "parameter defined twice: %s -> %s" % (self.params[word.letter], word)
self.params[word.letter] = word
def __getattr__(self, key):
if key in self.param_letters:
if key in self.params:
return self.params[key].value
else:
return None # parameter is valid for GCode, but undefined
raise AttributeError("'{cls}' object has no attribute '{key}'".format(
cls=self.__class__.__name__,
key=key
))
# ======================= Motion ======================= # ======================= Motion =======================
@ -29,7 +74,7 @@ class GCode(object):
# G80 Cancel Canned Cycle # G80 Cancel Canned Cycle
class GCodeMotion(GCode): class GCodeMotion(GCode):
param_words = set('XYZABCUVW') param_letters = set('XYZABCUVW')
class GCodeRapidMove(GCodeMotion): class GCodeRapidMove(GCodeMotion):
@ -44,7 +89,7 @@ class GCodeLinearMove(GCodeMotion):
class GCodeArcMove(GCodeMotion): class GCodeArcMove(GCodeMotion):
"""Arc Move""" """Arc Move"""
param_words = GCodeMotion.param_words | set('IJKRP') param_letters = GCodeMotion.param_letters | set('IJKRP')
class GCodeArcMoveCW(GCodeArcMove): class GCodeArcMoveCW(GCodeArcMove):
@ -59,25 +104,25 @@ class GCodeArcMoveCCW(GCodeArcMove):
class GCodeDwell(GCodeMotion): class GCodeDwell(GCodeMotion):
"""G4: Dwell""" """G4: Dwell"""
param_words = GCodeMotion.param_words | set('P') param_letters = GCodeMotion.param_letters | set('P')
word_key = Word('G', 4) word_key = Word('G', 4)
class GCodeCublcSpline(GCodeMotion): class GCodeCublcSpline(GCodeMotion):
"""G5: Cubic Spline""" """G5: Cubic Spline"""
param_words = GCodeMotion.param_words | set('IJPQ') param_letters = GCodeMotion.param_letters | set('IJPQ')
word_key = Word('G', 5) word_key = Word('G', 5)
class GCodeQuadraticSpline(GCodeMotion): class GCodeQuadraticSpline(GCodeMotion):
"""G5.1: Quadratic Spline""" """G5.1: Quadratic Spline"""
param_words = GCodeMotion.param_words | set('IJ') param_letters = GCodeMotion.param_letters | set('IJ')
word_key = Word('G', 5.1) word_key = Word('G', 5.1)
class GCodeNURBS(GCodeMotion): class GCodeNURBS(GCodeMotion):
"""G5.2: Non-uniform rational basis spline (NURBS)""" """G5.2: Non-uniform rational basis spline (NURBS)"""
param_words = GCodeMotion.param_words | set('PL') param_letters = GCodeMotion.param_letters | set('PL')
word_key = Word('G', 5.2) word_key = Word('G', 5.2)
@ -95,13 +140,13 @@ class GCodeStraightProbe(GCodeMotion):
class GCodeSpindleSyncMotion(GCodeMotion): class GCodeSpindleSyncMotion(GCodeMotion):
"""G33: Spindle Synchronized Motion""" """G33: Spindle Synchronized Motion"""
param_words = GCodeMotion.param_words | set('K') param_letters = GCodeMotion.param_letters | set('K')
word_key = Word('G', 33) word_key = Word('G', 33)
class GCodeRigidTapping(GCodeMotion): class GCodeRigidTapping(GCodeMotion):
"""G33.1: Rigid Tapping""" """G33.1: Rigid Tapping"""
param_words = GCodeMotion.param_words | set('K') param_letters = GCodeMotion.param_letters | set('K')
word_key = Word('G', 33.1) word_key = Word('G', 33.1)
@ -122,48 +167,48 @@ class GCodeCancelCannedCycle(GCodeMotion):
# G76 P Z I J R K Q H L E Threading Cycle # G76 P Z I J R K Q H L E Threading Cycle
class GCodeCannedCycle(GCode): class GCodeCannedCycle(GCode):
param_words = set('XYZUVW') param_letters = set('XYZUVW')
class GCodeDrillingCycle(GCodeCannedCycle): class GCodeDrillingCycle(GCodeCannedCycle):
"""G81: Drilling Cycle""" """G81: Drilling Cycle"""
param_words = GCodeCannedCycle.param_words | set('RLP') param_letters = GCodeCannedCycle.param_letters | set('RLP')
word_key = Word('G', 81) word_key = Word('G', 81)
class GCodeDrillingCycleDwell(GCodeCannedCycle): class GCodeDrillingCycleDwell(GCodeCannedCycle):
"""G82: Drilling Cycle, Dwell""" """G82: Drilling Cycle, Dwell"""
param_words = GCodeCannedCycle.param_words | set('RLP') param_letters = GCodeCannedCycle.param_letters | set('RLP')
word_key = Word('G', 82) word_key = Word('G', 82)
class GCodeDrillingCyclePeck(GCodeCannedCycle): class GCodeDrillingCyclePeck(GCodeCannedCycle):
"""G83: Drilling Cycle, Peck""" """G83: Drilling Cycle, Peck"""
param_words = GCodeCannedCycle.param_words | set('RLQ') param_letters = GCodeCannedCycle.param_letters | set('RLQ')
word_key = Word('G', 83) word_key = Word('G', 83)
class GCodeDrillingCycleChipBreaking(GCodeCannedCycle): class GCodeDrillingCycleChipBreaking(GCodeCannedCycle):
"""G73: Drilling Cycle, ChipBreaking""" """G73: Drilling Cycle, ChipBreaking"""
param_words = GCodeCannedCycle.param_words | set('RLQ') param_letters = GCodeCannedCycle.param_letters | set('RLQ')
word_key = Word('G', 73) word_key = Word('G', 73)
class GCodeBoringCycleFeedOut(GCodeCannedCycle): class GCodeBoringCycleFeedOut(GCodeCannedCycle):
"""G85: Boring Cycle, Feed Out""" """G85: Boring Cycle, Feed Out"""
param_words = GCodeCannedCycle.param_words | set('RLP') param_letters = GCodeCannedCycle.param_letters | set('RLP')
word_key = Word('G', 85) word_key = Word('G', 85)
class GCodeBoringCycleDwellFeedOut(GCodeCannedCycle): class GCodeBoringCycleDwellFeedOut(GCodeCannedCycle):
"""G89: Boring Cycle, Dwell, Feed Out""" """G89: Boring Cycle, Dwell, Feed Out"""
param_words = GCodeCannedCycle.param_words | set('RLP') param_letters = GCodeCannedCycle.param_letters | set('RLP')
word_key = Word('G', 89) word_key = Word('G', 89)
class GCodeThreadingCycle(GCodeCannedCycle): class GCodeThreadingCycle(GCodeCannedCycle):
"""G76: Threading Cycle""" """G76: Threading Cycle"""
param_words = GCodeCannedCycle.param_words | set('PZIJRKQHLE') param_letters = GCodeCannedCycle.param_letters | set('PZIJRKQHLE')
word_key = Word('G', 76) word_key = Word('G', 76)
@ -243,19 +288,19 @@ class GCodeSpindle(GCode):
class GCodeStartSpindleCW(GCodeSpindle): class GCodeStartSpindleCW(GCodeSpindle):
"""M3: Start Spindle Clockwise""" """M3: Start Spindle Clockwise"""
param_words = set('S') param_letters = set('S')
word_key = Word('M', 3) word_key = Word('M', 3)
class GCodeStartSpindleCCW(GCodeSpindle): class GCodeStartSpindleCCW(GCodeSpindle):
"""M4: Start Spindle Counter-Clockwise""" """M4: Start Spindle Counter-Clockwise"""
param_words = set('S') param_letters = set('S')
word_key = Word('M', 4) word_key = Word('M', 4)
class GCodeStopSpindle(GCodeSpindle): class GCodeStopSpindle(GCodeSpindle):
"""M5: Stop Spindle""" """M5: Stop Spindle"""
param_words = set('S') param_letters = set('S')
word_key = Word('M', 5) word_key = Word('M', 5)
@ -266,13 +311,13 @@ class GCodeOrientSpindle(GCodeSpindle):
class GCodeSpindleConstantSurfaceSpeedMode(GCodeSpindle): class GCodeSpindleConstantSurfaceSpeedMode(GCodeSpindle):
"""G96: Spindle Constant Surface Speed""" """G96: Spindle Constant Surface Speed"""
param_words = set('DS') param_letters = set('DS')
word_key = Word('G', 96) word_key = Word('G', 96)
class GCodeSpindleRPMMode(GCodeSpindle): class GCodeSpindleRPMMode(GCodeSpindle):
"""G97: Spindle RPM Speed""" """G97: Spindle RPM Speed"""
param_words = set('D') param_letters = set('D')
word_key = Word('G', 97) word_key = Word('G', 97)
@ -313,7 +358,7 @@ class GCodeToolLength(GCode):
class GCodeToolLengthOffset(GCodeToolLength): class GCodeToolLengthOffset(GCodeToolLength):
"""G43: Tool Length Offset""" """G43: Tool Length Offset"""
param_words = set('H') param_letters = set('H')
word_key = Word('G', 43) word_key = Word('G', 43)
@ -324,7 +369,7 @@ class GCodeDynamicToolLengthOffset(GCodeToolLength):
class GCodeAddToolLengthOffset(GCodeToolLength): class GCodeAddToolLengthOffset(GCodeToolLength):
"""G43.2: Appkly Additional Tool Length Offset""" """G43.2: Appkly Additional Tool Length Offset"""
param_words = set('H') param_letters = set('H')
word_key = Word('G', 43.2) word_key = Word('G', 43.2)
@ -440,25 +485,25 @@ class GCodeCutterRadiusCompOff(GCodeCutterRadiusComp):
class GCodeCutterCompLeft(GCodeCutterRadiusComp): class GCodeCutterCompLeft(GCodeCutterRadiusComp):
"""G41: Cutter Radius Compensation (left)""" """G41: Cutter Radius Compensation (left)"""
param_words = set('D') param_letters = set('D')
word_key = Word('G', 41) word_key = Word('G', 41)
class GCodeCutterCompRight(GCodeCutterRadiusComp): class GCodeCutterCompRight(GCodeCutterRadiusComp):
"""G42: Cutter Radius Compensation (right)""" """G42: Cutter Radius Compensation (right)"""
param_words = set('D') param_letters = set('D')
word_key = Word('G', 42) word_key = Word('G', 42)
class GCodeDynamicCutterCompLeft(GCodeCutterRadiusComp): class GCodeDynamicCutterCompLeft(GCodeCutterRadiusComp):
"""G41.1: Dynamic Cutter Radius Compensation (left)""" """G41.1: Dynamic Cutter Radius Compensation (left)"""
param_words = set('DL') param_letters = set('DL')
word_key = Word('G', 41.1) word_key = Word('G', 41.1)
class GCodeDynamicCutterCompRight(GCodeCutterRadiusComp): class GCodeDynamicCutterCompRight(GCodeCutterRadiusComp):
"""G42.1: Dynamic Cutter Radius Compensation (right)""" """G42.1: Dynamic Cutter Radius Compensation (right)"""
param_words = set('DL') param_letters = set('DL')
word_key = Word('G', 42.1) word_key = Word('G', 42.1)
@ -483,7 +528,7 @@ class GCodeExactStopMode(GCodePathControlMode):
class GCodePathBlendingMode(GCodePathControlMode): class GCodePathBlendingMode(GCodePathControlMode):
"""G64: Path Blending""" """G64: Path Blending"""
param_words = set('PQ') param_letters = set('PQ')
word_key = Word('G', 64) word_key = Word('G', 64)
@ -548,25 +593,25 @@ class GCodeSpeedAndFeedOverrideOff(GCodeOtherModal):
class GCodeFeedOverride(GCodeOtherModal): class GCodeFeedOverride(GCodeOtherModal):
"""M50: Feed Override Control""" """M50: Feed Override Control"""
param_words = set('P') param_letters = set('P')
word_key = Word('M', 50) word_key = Word('M', 50)
class GCodeSpindleSpeedOverride(GCodeOtherModal): class GCodeSpindleSpeedOverride(GCodeOtherModal):
"""M51: Spindle Speed Override Control""" """M51: Spindle Speed Override Control"""
param_words = set('P') param_letters = set('P')
word_key = Word('M', 51) word_key = Word('M', 51)
class GCodeAdaptiveFeed(GCodeOtherModal): class GCodeAdaptiveFeed(GCodeOtherModal):
"""M52: Adaptive Feed Control""" """M52: Adaptive Feed Control"""
param_words = set('P') param_letters = set('P')
word_key = Word('M', 52) word_key = Word('M', 52)
class GCodeFeedStop(GCodeOtherModal): class GCodeFeedStop(GCodeOtherModal):
"""M53: Feed Stop Control""" """M53: Feed Stop Control"""
param_words = set('P') param_letters = set('P')
word_key = Word('M', 53) word_key = Word('M', 53)
@ -614,7 +659,7 @@ class GCodeIO(GCode):
class GCodeDigitalOutput(GCodeIO): class GCodeDigitalOutput(GCodeIO):
"""Digital Output Control""" """Digital Output Control"""
param_words = set('P') param_letters = set('P')
class GCodeDigitalOutputOnSyncd(GCodeDigitalOutput): class GCodeDigitalOutputOnSyncd(GCodeDigitalOutput):
@ -639,13 +684,13 @@ class GCodeDigitalOutputOff(GCodeDigitalOutput):
class GCodeWaitOnInput(GCodeIO): class GCodeWaitOnInput(GCodeIO):
"""M66: Wait on Input""" """M66: Wait on Input"""
param_words = set('PELQ') param_letters = set('PELQ')
word_key = Word('M', 66) word_key = Word('M', 66)
class GCodeAnalogOutput(GCodeIO): class GCodeAnalogOutput(GCodeIO):
"""Analog Output""" """Analog Output"""
param_words = set('T') param_letters = set('T')
class GCodeAnalogOutputSyncd(GCodeAnalogOutput): class GCodeAnalogOutputSyncd(GCodeAnalogOutput):
@ -681,19 +726,19 @@ class GCodeNonModal(GCode):
class GCodeToolChange(GCodeNonModal): class GCodeToolChange(GCodeNonModal):
"""M6: Tool Change""" """M6: Tool Change"""
param_words = set('T') param_letters = set('T')
word_key = Word('M', 6) word_key = Word('M', 6)
class GCodeToolSetCurrent(GCodeNonModal): class GCodeToolSetCurrent(GCodeNonModal):
"""M61: Set Current Tool""" """M61: Set Current Tool"""
param_words = set('Q') param_letters = set('Q')
word_key = Word('M', 61) word_key = Word('M', 61)
class GCodeSet(GCodeNonModal): class GCodeSet(GCodeNonModal):
"""G10: Set stuff""" """G10: Set stuff"""
param_words = set('LPQR') param_letters = set('LPQR')
word_key = Word('G', 10) word_key = Word('G', 10)
@ -736,7 +781,7 @@ class GCodeRestoreCoordSystemOffset(GCodeNonModal):
class GCodeUserDefined(GCodeNonModal): class GCodeUserDefined(GCodeNonModal):
"""M101-M199: User Defined Commands""" """M101-M199: User Defined Commands"""
# To create user g-codes, inherit from this class # To create user g-codes, inherit from this class
param_words = set('PQ') param_letters = set('PQ')
#@classmethod #@classmethod
#def word_matches(cls, w): #def word_matches(cls, w):
# return (w.letter == 'M') and (101 <= w.value <= 199) # return (w.letter == 'M') and (101 <= w.value <= 199)
@ -778,15 +823,15 @@ def _gcode_class_infostr(base_class=GCode):
) )
return info_str return info_str
# ======================= GCode Word Mapping ======================= # ======================= GCode Word Mapping =======================
_gcode_maps_created = False # only set when the below values are populated _gcode_maps_created = False # only set when the below values are populated
_gcode_word_map = {} # of the form: {Word('G', 0): GCodeRapidMove, ... } _gcode_word_map = {} # of the form: {Word('G', 0): GCodeRapidMove, ... }
_gcode_function_list = [] # of the form: [(lambda w: w.letter == 'F', GCodeFeedRate), ... ] _gcode_function_list = [] # of the form: [(lambda w: w.letter == 'F', GCodeFeedRate), ... ]
def _build_maps():
""" def build_maps():
Populate _gcode_word_map and _gcode_function_list """Populate _gcode_word_map and _gcode_function_list"""
"""
# Ensure lists are clear # Ensure lists are clear
global _gcode_word_map global _gcode_word_map
global _gcode_function_list global _gcode_function_list
@ -806,19 +851,91 @@ def _build_maps():
global _gcode_maps_created global _gcode_maps_created
_gcode_maps_created = True _gcode_maps_created = True
def word_gcode_class(word, exhaustive=False):
"""
Map word to corresponding GCode class
:param word: Word instance
:param exhausitve: if True, all words are tested; not just 'GMFST'
:return: class inheriting GCode
"""
if _gcode_maps_created is False:
build_maps()
# quickly eliminate parameters
if (not exhaustive) and (word.letter not in 'GMFST'):
return None
# by Word Map (faster)
if word in _gcode_word_map:
return _gcode_word_map[word]
# by Function List (slower, so checked last)
for (match_function, gcode_class) in _gcode_function_list:
if match_function(word):
return gcode_class
return None
def words_to_gcodes(words): def words_to_gcodes(words):
""" """
Group words into g-codes (includes both G & M codes) Group words into g-codes (includes both G & M codes)
:param words: list of Word instances :param words: list of Word instances
:return: list containing [<GCode>, <GCode>, ..., list(<words not used in a gcode>)] :return: tuple([<GCode>, <GCode>, ...], list(<unused words>))
""" """
if _gcode_maps_created is False: gcodes = []
_build_maps() # Lines to consider
# Conflicts with non G|M codes (ie: S|F|T)
# Spindle Control:
# - S1000
# - M3 S2000
# Tool Change:
# - T2
# - M6 T1
#
# Conclusion: words are parameters first, gcodes second
# First determine which words are GCodes # First determine which words are GCode candidates
# TODO: next up... word_info_list = [
{
'index': i, # for internal referencing
'word': word,
'gcode_class': word_gcode_class(word), # if not None, word is a candidate
'param_to_index': None,
}
for (i, word) in enumerate(words)
]
unassigned = [] # Link parameters to candidates
#sdrow = list(reversed(words)) # note: gcode candidates may be valid parameters... therefore
#for (i, word) in reversed(enumerate(words)): # Also eliminate candidates that are parameters for earlier gcode candidates
for word_info in word_info_list:
if word_info['gcode_class'] is None:
continue # not a gcode candidate, so cannot have parameters
# which of the words following this gcode candidate are a valid parameter
for param_info in word_info_list[word_info['index'] + 1:]:
if param_info['word'].letter in word_info['gcode_class'].param_letters:
param_info['param_to_index'] = word_info['index']
param_info['gcode_class'] = None # no longer a valid candidate
# Map parameters
parameter_map = defaultdict(list) # {<gcode word index>: [<parameter words>], ... }
for word_info in word_info_list:
if word_info['gcode_class']:
continue # will form a gcode, so must not also be a parameter
parameter_map[word_info['param_to_index']].append(word_info['word'])
# Create gcode instances
for word_info in word_info_list:
if word_info['gcode_class'] is None:
continue # not a gcode candidate
gcode = word_info['gcode_class'](
word_info['word'],
*parameter_map[word_info['index']] # gcode parameters
)
gcodes.append(gcode)
return (gcodes, parameter_map[None])

View File

@ -11,9 +11,11 @@ class MachineState(object):
self.time = 0 self.time = 0
class Machine(object):
""""""
def __init__(self, **kwargs): class AbstractMachine(object):
"""Basis for a real / virtualized machine to process gcode"""
def __init__(self, *args, **kwargs):
self.axes = kwargs.get('axes', ('x', 'y', 'z')) self.axes = kwargs.get('axes', ('x', 'y', 'z'))
self.max_rate = kwargs.get('max_rate', { self.max_rate = kwargs.get('max_rate', {
'x': 500, # mm/min 'x': 500, # mm/min
@ -31,6 +33,14 @@ class Machine(object):
# initialize # initialize
self.state = MachineState(self.axes) self.state = MachineState(self.axes)
# machine-specific initialization
self.machine_init(*args, **kwargs)
def machine_init(self, *args, **kwargs):
# Executed last in instances' __init__ call.
# Parameters are identical to that of __init__
pass
def process_line(self, line): def process_line(self, line):
"""Change machine's state based on the given gcode line""" """Change machine's state based on the given gcode line"""
pass # TODO pass # TODO

View File

@ -8,11 +8,11 @@ import unittest
_this_path = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) _this_path = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
sys.path.insert(0, os.path.join(_this_path, '..')) sys.path.insert(0, os.path.join(_this_path, '..'))
from pygcode import gcodes from pygcode import gcodes
from pygcode import words
class TestGCodeWordMapping(unittest.TestCase): class TestGCodeWordMapping(unittest.TestCase):
def test_word_map_integrity(self): def test_word_map_integrity(self):
gcodes._build_maps()
gcodes.build_maps()
for (word_maches, fn_class) in gcodes._gcode_function_list: for (word_maches, fn_class) in gcodes._gcode_function_list:
for (word, key_class) in gcodes._gcode_word_map.items(): for (word, key_class) in gcodes._gcode_word_map.items():
# Verify that no mapped word will yield a True result # Verify that no mapped word will yield a True result
@ -21,3 +21,23 @@ class TestGCodeWordMapping(unittest.TestCase):
word_maches(word), word_maches(word),
"conflict with %s and %s" % (fn_class, key_class) "conflict with %s and %s" % (fn_class, key_class)
) )
class TestWordsToGCodes(unittest.TestCase):
def test_stuff(self): # FIXME: function name
line = 'G1 X82.6892 Y-38.6339 F1500'
word_list = list(words.iter_words(line))
result = gcodes.words_to_gcodes(word_list)
# result form
self.assertIsInstance(result, tuple)
self.assertEqual(len(result), 2)
# result content
(gcode_list, unused_words) = result
self.assertEqual(len(gcode_list), 2)
self.assertEqual(unused_words, [])
# Parsed GCodes
# G1
self.assertEqual(gcode_list[0].word, words.Word('G', 1))
self.assertEqual(gcode_list[0].X, 82.6892)
self.assertEqual(gcode_list[0].Y, -38.6339)
# F1500
self.assertEqual(gcode_list[1].word, words.Word('F', 1500))