named modal groups, clean words to str

This commit is contained in:
Peter Boin 2017-07-06 22:11:20 +10:00
parent cf5f9a17cd
commit c7578de0d7
8 changed files with 194 additions and 91 deletions

View File

@ -1,6 +1,6 @@
import re import re
from .words import iter_words, WORD_MAP from .words import iter_words, WORD_MAP
from .gcodes import words_to_gcodes from .gcodes import words2gcodes
class Block(object): class Block(object):
"""GCode block (effectively any gcode file line that defines any <word><value>)""" """GCode block (effectively any gcode file line that defines any <word><value>)"""
@ -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, self.modal_params) = words_to_gcodes(self.words) (self.gcodes, self.modal_params) = words2gcodes(self.words)
self._assert_gcodes() self._assert_gcodes()
@ -31,7 +31,9 @@ class Block(object):
def _assert_gcodes(self): def _assert_gcodes(self):
modal_groups = set() modal_groups = set()
code_words = set() code_words = set()
for gc in self.gcodes: for gc in self.gcodes:
# Assert all gcodes are not repeated in the same block # Assert all gcodes are not repeated in the same block
if gc.word in code_words: if gc.word in code_words:
raise AssertionError("%s cannot be in the same block" % ([ raise AssertionError("%s cannot be in the same block" % ([
@ -39,6 +41,7 @@ class Block(object):
if x.modal_group == gc.modal_group if x.modal_group == gc.modal_group
])) ]))
code_words.add(gc.word) code_words.add(gc.word)
# Assert all gcodes are from different modal groups # Assert all gcodes are from different modal groups
if gc.modal_group is not None: if gc.modal_group is not None:
if gc.modal_group in modal_groups: if gc.modal_group in modal_groups:
@ -61,3 +64,6 @@ class Block(object):
cls=self.__class__.__name__, cls=self.__class__.__name__,
key=k key=k
)) ))
def __str__(self):
return ' '.join(str(x) for x in (self.gcodes + [p.clean_str for p in self.modal_params]))

View File

@ -24,5 +24,6 @@ def parse(filename):
with open(filename, 'r') as fh: with open(filename, 'r') as fh:
for line in fh.readlines(): for line in fh.readlines():
line_obj = Line(line) line_obj = Line(line)
# FIXME: don't dump entire file into RAM; change to generator model
file.append(line_obj) file.append(line_obj)
return file return file

View File

@ -64,6 +64,44 @@ from .words import Word
# Override Switches (Group 9) M48, M49 # Override Switches (Group 9) M48, M49
# User Defined (Group 10) M100-M199 # User Defined (Group 10) M100-M199
# #
MODAL_GROUP_NUM = {
# "G" codes
'motion': 1
'plane_selection': 2,
'distance': 3,
'arc_ijk_distance': 4,
'feed_rate_mode': 5,
'units': 6,
'cutter_diameter_comp': 7,
'tool_length_offset': 8,
'canned_cycles_return': 10,
'coordinate_system': 12,
'control_mode': 13,
'spindle_speed': 14,
'lathe_diameter': 15,
# "M" codes
'stopping': 104,
'spindle': 107,
'coolant': 108,
'override_switches': 109,
'user_defined': 110,
# Traditionally Non-grouped:
# Although these GCodes set the machine's mode, there are no other GCodes to
# group with them. So although they're modal, they doesn't have a defined
# modal group.
# However, having a modal group assists with:
# - validating gcode blocks for conflicting commands
# - remembering machine's state with consistency across gcodes
# Therefore, I've added F, S, and T GCodes to custom group numbers (> 200)
'feed_rate': 201,
'spindle_speed': 202,
'tool': 203,
}
# Execution Order # Execution Order
# Order taken http://linuxcnc.org/docs/html/gcode/overview.html#_g_code_order_of_execution # Order taken http://linuxcnc.org/docs/html/gcode/overview.html#_g_code_order_of_execution
# (as of 2017-07-03) # (as of 2017-07-03)
@ -132,12 +170,15 @@ class GCode(object):
def __str__(self): def __str__(self):
"""String representation of gcode, as it would be seen in a .gcode file""" """String representation of gcode, as it would be seen in a .gcode file"""
return "{gcode} {parameters}".format( param_str = ''
gcode=self.word, if self.params:
parameters=' '.join([ param_str += ' ' + ' '.join([
"{}".format(self.params[k]) "{}".format(self.params[k].clean_str)
for k in sorted(self.params.keys()) for k in sorted(self.params.keys())
]), ])
return "{gcode}{parameters}".format(
gcode=self.word.clean_str,
parameters=param_str,
) )
# Sort by exec_order # Sort by exec_order
@ -167,6 +208,16 @@ class GCode(object):
def description(self): def description(self):
return self.__doc__ return self.__doc__
@property
def mode(self):
"""
Mode word for modal GCodes
:return: Word to save machine's state, or None if GCode is not modal
"""
if self.modal_group is None:
return None
return self.word
# ======================= Motion ======================= # ======================= Motion =======================
# (X Y Z A B C U V W apply to all motions) # (X Y Z A B C U V W apply to all motions)
@ -185,7 +236,7 @@ class GCode(object):
class GCodeMotion(GCode): class GCodeMotion(GCode):
param_letters = set('XYZABCUVW') param_letters = set('XYZABCUVW')
modal_group = 1 modal_group = MODAL_GROUP_NUM['motion']
exec_order = 241 exec_order = 241
class GCodeRapidMove(GCodeMotion): class GCodeRapidMove(GCodeMotion):
@ -281,7 +332,7 @@ class GCodeCancelCannedCycle(GCodeMotion):
class GCodeCannedCycle(GCode): class GCodeCannedCycle(GCode):
param_letters = set('XYZUVW') param_letters = set('XYZUVW')
modal_group = 1 modal_group = MODAL_GROUP_NUM['motion']
exec_order = 241 exec_order = 241
class GCodeDrillingCycle(GCodeCannedCycle): class GCodeDrillingCycle(GCodeCannedCycle):
@ -340,36 +391,36 @@ class GCodeDistanceMode(GCode):
class GCodeAbsoluteDistanceMode(GCodeDistanceMode): class GCodeAbsoluteDistanceMode(GCodeDistanceMode):
"""G90: Absolute Distance Mode""" """G90: Absolute Distance Mode"""
word_key = Word('G', 90) word_key = Word('G', 90)
modal_group = 3 modal_group = MODAL_GROUP_NUM['distance']
class GCodeIncrementalDistanceMode(GCodeDistanceMode): class GCodeIncrementalDistanceMode(GCodeDistanceMode):
"""G91: Incremental Distance Mode""" """G91: Incremental Distance Mode"""
word_key = Word('G', 91) word_key = Word('G', 91)
modal_group = 3 modal_group = MODAL_GROUP_NUM['distance']
class GCodeAbsoluteArcDistanceMode(GCodeDistanceMode): class GCodeAbsoluteArcDistanceMode(GCodeDistanceMode):
"""G90.1: Absolute Distance Mode for Arc IJK Parameters""" """G90.1: Absolute Distance Mode for Arc IJK Parameters"""
word_key = Word('G', 90.1) word_key = Word('G', 90.1)
modal_group = 4 modal_group = MODAL_GROUP_NUM['arc_ijk_distance']
class GCodeIncrementalArcDistanceMode(GCodeDistanceMode): class GCodeIncrementalArcDistanceMode(GCodeDistanceMode):
"""G91.1: Incremental Distance Mode for Arc IJK Parameters""" """G91.1: Incremental Distance Mode for Arc IJK Parameters"""
word_key = Word('G', 91.1) word_key = Word('G', 91.1)
modal_group = 4 modal_group = MODAL_GROUP_NUM['arc_ijk_distance']
class GCodeLatheDiameterMode(GCodeDistanceMode): class GCodeLatheDiameterMode(GCodeDistanceMode):
"""G7: Lathe Diameter Mode""" """G7: Lathe Diameter Mode"""
word_key = Word('G', 7) word_key = Word('G', 7)
modal_group = 15 modal_group = MODAL_GROUP_NUM['lathe_diameter']
class GCodeLatheRadiusMode(GCodeDistanceMode): class GCodeLatheRadiusMode(GCodeDistanceMode):
"""G8: Lathe Radius Mode""" """G8: Lathe Radius Mode"""
word_key = Word('G', 8) word_key = Word('G', 8)
modal_group = 15 modal_group = MODAL_GROUP_NUM['lathe_diameter']
# ======================= Feed Rate Mode ======================= # ======================= Feed Rate Mode =======================
@ -377,7 +428,7 @@ class GCodeLatheRadiusMode(GCodeDistanceMode):
# G93, G94, G95 Feed Rate Mode # G93, G94, G95 Feed Rate Mode
class GCodeFeedRateMode(GCode): class GCodeFeedRateMode(GCode):
modal_group = 5 modal_group = MODAL_GROUP_NUM['feed_rate_mode']
exec_order = 30 exec_order = 30
class GCodeInverseTimeMode(GCodeFeedRateMode): class GCodeInverseTimeMode(GCodeFeedRateMode):
@ -409,20 +460,20 @@ class GCodeStartSpindleCW(GCodeSpindle):
"""M3: Start Spindle Clockwise""" """M3: Start Spindle Clockwise"""
#param_letters = set('S') # S is it's own gcode, makes no sense to be here #param_letters = set('S') # S is it's own gcode, makes no sense to be here
word_key = Word('M', 3) word_key = Word('M', 3)
modal_group = 7 modal_group = MODAL_GROUP_NUM['spindle']
class GCodeStartSpindleCCW(GCodeSpindle): class GCodeStartSpindleCCW(GCodeSpindle):
"""M4: Start Spindle Counter-Clockwise""" """M4: Start Spindle Counter-Clockwise"""
#param_letters = set('S') # S is it's own gcode, makes no sense to be here #param_letters = set('S') # S is it's own gcode, makes no sense to be here
word_key = Word('M', 4) word_key = Word('M', 4)
modal_group = 7 modal_group = MODAL_GROUP_NUM['spindle']
class GCodeStopSpindle(GCodeSpindle): class GCodeStopSpindle(GCodeSpindle):
"""M5: Stop Spindle""" """M5: Stop Spindle"""
#param_letters = set('S') # S is it's own gcode, makes no sense to be here #param_letters = set('S') # S is it's own gcode, makes no sense to be here
word_key = Word('M', 5) word_key = Word('M', 5)
modal_group = 7 modal_group = MODAL_GROUP_NUM['spindle']
class GCodeOrientSpindle(GCodeSpindle): class GCodeOrientSpindle(GCodeSpindle):
@ -434,14 +485,14 @@ class GCodeSpindleConstantSurfaceSpeedMode(GCodeSpindle):
"""G96: Spindle Constant Surface Speed""" """G96: Spindle Constant Surface Speed"""
param_letters = set('DS') param_letters = set('DS')
word_key = Word('G', 96) word_key = Word('G', 96)
modal_group = 14 modal_group = MODAL_GROUP_NUM['spindle_speed']
class GCodeSpindleRPMMode(GCodeSpindle): class GCodeSpindleRPMMode(GCodeSpindle):
"""G97: Spindle RPM Speed""" """G97: Spindle RPM Speed"""
param_letters = set('D') param_letters = set('D')
word_key = Word('G', 97) word_key = Word('G', 97)
modal_group = 14 modal_group = MODAL_GROUP_NUM['spindle_speed']
@ -450,7 +501,7 @@ class GCodeSpindleRPMMode(GCodeSpindle):
# M7, M8, M9 Coolant Control # M7, M8, M9 Coolant Control
class GCodeCoolant(GCode): class GCodeCoolant(GCode):
modal_group = 8 modal_group = MODAL_GROUP_NUM['coolant']
exec_order = 110 exec_order = 110
@ -477,7 +528,7 @@ class GCodeCoolantOff(GCodeCoolant):
# G49 Cancel Tool Length Compensation # G49 Cancel Tool Length Compensation
class GCodeToolLength(GCode): class GCodeToolLength(GCode):
modal_group = 8 modal_group = MODAL_GROUP_NUM['tool_length_offset']
exec_order = 180 exec_order = 180
@ -510,7 +561,7 @@ class GCodeCancelToolLengthOffset(GCodeToolLength):
# M60 Pallet Change Pause # M60 Pallet Change Pause
class GCodeProgramControl(GCode): class GCodeProgramControl(GCode):
modal_group = 4 modal_group = MODAL_GROUP_NUM['stopping']
exec_order = 250 exec_order = 250
class GCodePauseProgram(GCodeProgramControl): class GCodePauseProgram(GCodeProgramControl):
@ -543,7 +594,7 @@ class GCodePalletChangePause(GCodeProgramControl):
# G20, G21 Units # G20, G21 Units
class GCodeUnit(GCode): class GCodeUnit(GCode):
modal_group = 6 modal_group = MODAL_GROUP_NUM['units']
exec_order = 160 exec_order = 160
@ -563,7 +614,7 @@ class GCodeUseMillimeters(GCodeUnit):
# G17 - G19.1 Plane Select # G17 - G19.1 Plane Select
class GCodePlaneSelect(GCode): class GCodePlaneSelect(GCode):
modal_group = 2 modal_group = MODAL_GROUP_NUM['plane_selection']
exec_order = 150 exec_order = 150
@ -604,7 +655,7 @@ class GCodeSelectVWPlane(GCodePlaneSelect):
# G41.1, G42.1 D L Dynamic Cutter Compensation # G41.1, G42.1 D L Dynamic Cutter Compensation
class GCodeCutterRadiusComp(GCode): class GCodeCutterRadiusComp(GCode):
modal_group = 7 modal_group = MODAL_GROUP_NUM['cutter_diameter_comp']
exec_order = 170 exec_order = 170
@ -643,7 +694,7 @@ class GCodeDynamicCutterCompRight(GCodeCutterRadiusComp):
# G64 P Q Path Blending # G64 P Q Path Blending
class GCodePathControlMode(GCode): class GCodePathControlMode(GCode):
modal_group = 13 modal_group = MODAL_GROUP_NUM['control_mode']
exec_order = 200 exec_order = 200
@ -668,7 +719,7 @@ class GCodePathBlendingMode(GCodePathControlMode):
# G98 Canned Cycle Return Level # G98 Canned Cycle Return Level
class GCodeCannedReturnMode(GCode): class GCodeCannedReturnMode(GCode):
modal_group = 10 modal_group = MODAL_GROUP_NUM['canned_cycles_return']
exec_order = 220 exec_order = 220
@ -698,11 +749,7 @@ class GCodeFeedRate(GCodeOtherModal):
@classmethod @classmethod
def word_matches(cls, w): def word_matches(cls, w):
return w.letter == 'F' return w.letter == 'F'
# Modal Group : modal_group = MODAL_GROUP_NUM['feed_rate']
# although this sets the machine's state, there are no other codes to
# group with this one, so although it's modal, it usually has no group.
# However,
modal_group = 101
exec_order = 40 exec_order = 40
@ -712,7 +759,7 @@ class GCodeSpindleSpeed(GCodeOtherModal):
def word_matches(cls, w): def word_matches(cls, w):
return w.letter == 'S' return w.letter == 'S'
# Modal Group: (see description in GCodeFeedRate) # Modal Group: (see description in GCodeFeedRate)
modal_group = 102 modal_group = MODAL_GROUP_NUM['spindle_speed']
exec_order = 50 exec_order = 50
@ -722,21 +769,21 @@ class GCodeSelectTool(GCodeOtherModal):
def word_matches(cls, w): def word_matches(cls, w):
return w.letter == 'T' return w.letter == 'T'
# Modal Group: (see description in GCodeFeedRate) # Modal Group: (see description in GCodeFeedRate)
modal_group = 103 modal_group = MODAL_GROUP_NUM['tool']
exec_order = 60 exec_order = 60
class GCodeSpeedAndFeedOverrideOn(GCodeOtherModal): class GCodeSpeedAndFeedOverrideOn(GCodeOtherModal):
"""M48: Speed and Feed Override Control On""" """M48: Speed and Feed Override Control On"""
word_key = Word('M', 48) word_key = Word('M', 48)
modal_group = 9 modal_group = MODAL_GROUP_NUM['override_switches']
exec_order = 120 exec_order = 120
class GCodeSpeedAndFeedOverrideOff(GCodeOtherModal): class GCodeSpeedAndFeedOverrideOff(GCodeOtherModal):
"""M49: Speed and Feed Override Control Off""" """M49: Speed and Feed Override Control Off"""
word_key = Word('M', 49) word_key = Word('M', 49)
modal_group = 9 modal_group = MODAL_GROUP_NUM['override_switches']
exec_order = 120 exec_order = 120
@ -783,7 +830,7 @@ class GCodeSelectCoordinateSystem(GCodeOtherModal):
@classmethod @classmethod
def word_matches(cls, w): def word_matches(cls, w):
return (w.letter == 'G') and (w.value in [54, 55, 56, 57, 58, 59, 59.1, 59.2, 59.3]) return (w.letter == 'G') and (w.value in [54, 55, 56, 57, 58, 59, 59.1, 59.2, 59.3])
modal_group = 12 modal_group = MODAL_GROUP_NUM['coordinate_system']
exec_order = 190 exec_order = 190
@ -950,6 +997,7 @@ class GCodeUserDefined(GCodeNonModal):
#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)
exec_order = 130 exec_order = 130
modal_group = MODAL_GROUP_NUM['user_defined']
# ======================= Utilities ======================= # ======================= Utilities =======================
@ -997,7 +1045,7 @@ _gcode_function_list = [] # of the form: [(lambda w: w.letter == 'F', GCodeFeedR
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 Word maps / lists are clear
global _gcode_word_map global _gcode_word_map
global _gcode_function_list global _gcode_function_list
_gcode_word_map = {} _gcode_word_map = {}
@ -1043,8 +1091,7 @@ def word_gcode_class(word, exhaustive=False):
return None return None
def words2gcodes(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

View File

@ -20,9 +20,8 @@ class Line(object):
@property @property
def text(self): def text(self):
if self._text is None: if self._text is None:
return self.build_line_text() return str(self)
return self._text return self._text
def __str__(self):
def build_line_text(self): return ' '.join([str(x) for x in [self.block, self.comment] if x])
return ' '.join([str(x) for x in [self.block, self.comment] if x]) + '\n'

View File

@ -5,10 +5,21 @@ import six
from .exceptions import GCodeBlockFormatError from .exceptions import GCodeBlockFormatError
FLOAT_REGEX = re.compile(r'^-?(\d+\.?\d*|\.\d+)') # testcase: ..tests.test_words.WordValueMatchTests.test_float REGEX_FLOAT = re.compile(r'^-?(\d+\.?\d*|\.\d+)') # testcase: ..tests.test_words.WordValueMatchTests.test_float
INT_REGEX = re.compile(r'^-?\d+') REGEX_INT = re.compile(r'^-?\d+')
POSITIVEINT_REGEX = re.compile(r'^\d+') REGEX_POSITIVEINT = re.compile(r'^\d+')
CODE_REGEX = re.compile(r'^\d+(\.\d)?') # similar REGEX_CODE = re.compile(r'^\d+(\.\d)?') # similar
# Value cleaning functions
def _clean_codestr(value):
if value < 10:
return "0%g" % value
return "%g" % value
CLEAN_NONE = lambda v: v
CLEAN_FLOAT = lambda v: "%g" % v
CLEAN_CODE = _clean_codestr
CLEAN_INT = lambda v: "%g" % v
WORD_MAP = { WORD_MAP = {
# Descriptions copied from wikipedia: # Descriptions copied from wikipedia:
@ -17,148 +28,174 @@ WORD_MAP = {
# Rotational Axes # Rotational Axes
'A': { 'A': {
'class': float, 'class': float,
'value_regex': FLOAT_REGEX, 'value_regex': REGEX_FLOAT,
'description': "Absolute or incremental position of A axis (rotational axis around X axis)", 'description': "Absolute or incremental position of A axis (rotational axis around X axis)",
'clean_value': CLEAN_FLOAT,
}, },
'B': { 'B': {
'class': float, 'class': float,
'value_regex': FLOAT_REGEX, 'value_regex': REGEX_FLOAT,
'description': "Absolute or incremental position of B axis (rotational axis around Y axis)", 'description': "Absolute or incremental position of B axis (rotational axis around Y axis)",
'clean_value': CLEAN_FLOAT,
}, },
'C': { 'C': {
'class': float, 'class': float,
'value_regex': FLOAT_REGEX, 'value_regex': REGEX_FLOAT,
'description': "Absolute or incremental position of C axis (rotational axis around Z axis)", 'description': "Absolute or incremental position of C axis (rotational axis around Z axis)",
'clean_value': CLEAN_FLOAT,
}, },
'D': { 'D': {
'class': float, 'class': float,
'value_regex': FLOAT_REGEX, 'value_regex': REGEX_FLOAT,
'description': "Defines diameter or radial offset used for cutter compensation. D is used for depth of cut on lathes. It is used for aperture selection and commands on photoplotters.", 'description': "Defines diameter or radial offset used for cutter compensation. D is used for depth of cut on lathes. It is used for aperture selection and commands on photoplotters.",
'clean_value': CLEAN_FLOAT,
}, },
# Feed Rates # Feed Rates
'E': { 'E': {
'class': float, 'class': float,
'value_regex': FLOAT_REGEX, 'value_regex': REGEX_FLOAT,
'description': "Precision feedrate for threading on lathes", 'description': "Precision feedrate for threading on lathes",
'clean_value': CLEAN_FLOAT,
}, },
'F': { 'F': {
'class': float, 'class': float,
'value_regex': FLOAT_REGEX, 'value_regex': REGEX_FLOAT,
'description': "Feedrate", 'description': "Feedrate",
'clean_value': CLEAN_FLOAT,
}, },
# G-Codes # G-Codes
'G': { 'G': {
'class': float, 'class': float,
'value_regex': CODE_REGEX, 'value_regex': REGEX_CODE,
'description': "Address for preparatory commands", 'description': "Address for preparatory commands",
'clean_value': CLEAN_CODE,
}, },
# Tool Offsets # Tool Offsets
'H': { 'H': {
'class': float, 'class': float,
'value_regex': FLOAT_REGEX, 'value_regex': REGEX_FLOAT,
'description': "Defines tool length offset; Incremental axis corresponding to C axis (e.g., on a turn-mill)", 'description': "Defines tool length offset; Incremental axis corresponding to C axis (e.g., on a turn-mill)",
'clean_value': CLEAN_FLOAT,
}, },
# Arc radius center coords # Arc radius center coords
'I': { 'I': {
'class': float, 'class': float,
'value_regex': FLOAT_REGEX, 'value_regex': REGEX_FLOAT,
'description': "Defines arc center in X axis for G02 or G03 arc commands. Also used as a parameter within some fixed cycles.", 'description': "Defines arc center in X axis for G02 or G03 arc commands. Also used as a parameter within some fixed cycles.",
'clean_value': CLEAN_FLOAT,
}, },
'J': { 'J': {
'class': float, 'class': float,
'value_regex': FLOAT_REGEX, 'value_regex': REGEX_FLOAT,
'description': "Defines arc center in Y axis for G02 or G03 arc commands. Also used as a parameter within some fixed cycles.", 'description': "Defines arc center in Y axis for G02 or G03 arc commands. Also used as a parameter within some fixed cycles.",
'clean_value': CLEAN_FLOAT,
}, },
'K': { 'K': {
'class': float, 'class': float,
'value_regex': FLOAT_REGEX, 'value_regex': REGEX_FLOAT,
'description': "Defines arc center in Z axis for G02 or G03 arc commands. Also used as a parameter within some fixed cycles, equal to L address.", 'description': "Defines arc center in Z axis for G02 or G03 arc commands. Also used as a parameter within some fixed cycles, equal to L address.",
'clean_value': CLEAN_FLOAT,
}, },
# Loop Count # Loop Count
'L': { 'L': {
'class': int, 'class': int,
'value_regex': POSITIVEINT_REGEX, 'value_regex': REGEX_POSITIVEINT,
'description': "Fixed cycle loop count; Specification of what register to edit using G10", 'description': "Fixed cycle loop count; Specification of what register to edit using G10",
'clean_value': CLEAN_INT,
}, },
# Miscellaneous Function # Miscellaneous Function
'M': { 'M': {
'class': float, 'class': float,
'value_regex': CODE_REGEX, 'value_regex': REGEX_CODE,
'description': "Miscellaneous function", 'description': "Miscellaneous function",
'clean_value': CLEAN_CODE,
}, },
# Line Number # Line Number
'N': { 'N': {
'class': int, 'class': int,
'value_regex': POSITIVEINT_REGEX, 'value_regex': REGEX_POSITIVEINT,
'description': "Line (block) number in program; System parameter number to change using G10", 'description': "Line (block) number in program; System parameter number to change using G10",
'clean_value': CLEAN_INT,
}, },
# Program Name # Program Name
'O': { 'O': {
'class': str, 'class': str,
'value_regex': re.compile(r'^.+$'), # all the way to the end 'value_regex': re.compile(r'^.+$'), # all the way to the end
'description': "Program name", 'description': "Program name",
'clean_value': CLEAN_NONE,
}, },
# Parameter (arbitrary parameter) # Parameter (arbitrary parameter)
'P': { 'P': {
'class': float, # parameter is often an integer, but can be a float 'class': float, # parameter is often an integer, but can be a float
'value_regex': FLOAT_REGEX, 'value_regex': REGEX_FLOAT,
'description': "Serves as parameter address for various G and M codes", 'description': "Serves as parameter address for various G and M codes",
'clean_value': CLEAN_FLOAT,
}, },
# Peck increment # Peck increment
'Q': { 'Q': {
'class': float, 'class': float,
'value_regex': FLOAT_REGEX, 'value_regex': REGEX_FLOAT,
'description': "Depth to increase on each peck; Peck increment in canned cycles", 'description': "Depth to increase on each peck; Peck increment in canned cycles",
'clean_value': CLEAN_FLOAT,
}, },
# Arc Radius # Arc Radius
'R': { 'R': {
'class': float, 'class': float,
'value_regex': FLOAT_REGEX, 'value_regex': REGEX_FLOAT,
'description': "Defines size of arc radius, or defines retract height in milling canned cycles", 'description': "Defines size of arc radius, or defines retract height in milling canned cycles",
'clean_value': CLEAN_FLOAT,
}, },
# Spindle speed # Spindle speed
'S': { 'S': {
'class': float, 'class': float,
'value_regex': FLOAT_REGEX, 'value_regex': REGEX_FLOAT,
'description': "Defines speed, either spindle speed or surface speed depending on mode", 'description': "Defines speed, either spindle speed or surface speed depending on mode",
'clean_value': CLEAN_FLOAT,
}, },
# Tool Selecton # Tool Selecton
'T': { 'T': {
'class': str, 'class': str,
'value_regex': POSITIVEINT_REGEX, # tool string may have leading '0's, but is effectively an index (integer) 'value_regex': REGEX_POSITIVEINT, # tool string may have leading '0's, but is effectively an index (integer)
'description': "Tool selection", 'description': "Tool selection",
'clean_value': CLEAN_NONE,
}, },
# Incremental axes # Incremental axes
'U': { 'U': {
'class': float, 'class': float,
'value_regex': FLOAT_REGEX, 'value_regex': REGEX_FLOAT,
'description': "Incremental axis corresponding to X axis (typically only lathe group A controls) Also defines dwell time on some machines (instead of 'P' or 'X').", 'description': "Incremental axis corresponding to X axis (typically only lathe group A controls) Also defines dwell time on some machines (instead of 'P' or 'X').",
'clean_value': CLEAN_FLOAT,
}, },
'V': { 'V': {
'class': float, 'class': float,
'value_regex': FLOAT_REGEX, 'value_regex': REGEX_FLOAT,
'description': "Incremental axis corresponding to Y axis", 'description': "Incremental axis corresponding to Y axis",
'clean_value': CLEAN_FLOAT,
}, },
'W': { 'W': {
'class': float, 'class': float,
'value_regex': FLOAT_REGEX, 'value_regex': REGEX_FLOAT,
'description': "Incremental axis corresponding to Z axis (typically only lathe group A controls)", 'description': "Incremental axis corresponding to Z axis (typically only lathe group A controls)",
'clean_value': CLEAN_FLOAT,
}, },
# Linear Axes # Linear Axes
'X': { 'X': {
'class': float, 'class': float,
'value_regex': FLOAT_REGEX, 'value_regex': REGEX_FLOAT,
'description': "Absolute or incremental position of X axis.", 'description': "Absolute or incremental position of X axis.",
'clean_value': CLEAN_FLOAT,
}, },
'Y': { 'Y': {
'class': float, 'class': float,
'value_regex': FLOAT_REGEX, 'value_regex': REGEX_FLOAT,
'description': "Absolute or incremental position of Y axis.", 'description': "Absolute or incremental position of Y axis.",
'clean_value': CLEAN_FLOAT,
}, },
'Z': { 'Z': {
'class': float, 'class': float,
'value_regex': FLOAT_REGEX, 'value_regex': REGEX_FLOAT,
'description': "Absolute or incremental position of Z axis.", 'description': "Absolute or incremental position of Z axis.",
'clean_value': CLEAN_FLOAT,
}, },
} }
@ -174,8 +211,8 @@ class Word(object):
else: else:
self._value = value self._value = value
# Sorting Order self._value_class = WORD_MAP[self.letter]['class']
self._order_linuxcnc = None self._value_clean = WORD_MAP[self.letter]['clean_value']
def __str__(self): def __str__(self):
return "{letter}{value}".format( return "{letter}{value}".format(
@ -183,6 +220,14 @@ class Word(object):
value=self.value_str, value=self.value_str,
) )
@property
def clean_str(self):
"""same as __str__ but with a cleaned value (eg: X.4 is X0.4)"""
return "{letter}{value}".format(
letter=self.letter,
value=self.value_cleanstr,
)
def __repr__(self): def __repr__(self):
return "<{class_name}: {string}>".format( return "<{class_name}: {string}>".format(
class_name=self.__class__.__name__, class_name=self.__class__.__name__,
@ -190,6 +235,8 @@ class Word(object):
) )
def __eq__(self, other): def __eq__(self, other):
if isinstance(other, six.string_types):
other = str2word(other)
return (self.letter == other.letter) and (self.value == other.value) return (self.letter == other.letter) and (self.value == other.value)
def __ne__(self, other): def __ne__(self, other):
@ -198,42 +245,45 @@ class Word(object):
def __hash__(self): def __hash__(self):
return hash((self.letter, self.value)) return hash((self.letter, self.value))
# Value Properties # Value Properties
@property @property
def value_str(self): def value_str(self):
"""Value string, or """ """Value string, or string representation of value"""
if self._value_str is None: if self._value_str is None:
return str(self._value) return str(self._value)
return self._value_str return self._value_str
@property
def value_cleanstr(self):
"""Clean string representation, for consistent file output"""
return self._value_clean(self.value)
@property @property
def value(self): def value(self):
if self._value is None: if self._value is None:
return WORD_MAP[self.letter]['class'](self._value_str) return self._value_class(self._value_str)
return self._value return self._value
# Order # Order
@property def __lt__(self, other):
def orderval_linuxcnc(self): return self.letter < other.letter
if self._order_linuxcnc is None:
self._order_linuxcnc = _word_order_linuxcnc(self)
return self._order_linuxcnc
@property @property
def description(self): def description(self):
return "%s: %s" % (self.letter, WORD_MAP[self.letter]['description']) return "%s: %s" % (self.letter, WORD_MAP[self.letter]['description'])
NEXT_WORD = re.compile(r'^.*?(?P<letter>[%s])' % ''.join(WORD_MAP.keys()), re.IGNORECASE)
def iter_words(block_text): def iter_words(block_text):
""" """
Iterate through block text yielding Word instances Iterate through block text yielding Word instances
:param block_text: text for given block with comments removed :param block_text: text for given block with comments removed
""" """
index = 0 next_word = re.compile(r'^.*?(?P<letter>[%s])' % ''.join(WORD_MAP.keys()), re.IGNORECASE)
index = 0
while True: while True:
letter_match = NEXT_WORD.search(block_text[index:]) letter_match = next_word.search(block_text[index:])
if letter_match: if letter_match:
# Letter # Letter
letter = letter_match.group('letter').upper() letter = letter_match.group('letter').upper()

View File

@ -17,4 +17,4 @@ class FileParseTest(unittest.TestCase):
self.assertEqual(len(file.lines), 26) self.assertEqual(len(file.lines), 26)
# FIXME: just verifying content visually # FIXME: just verifying content visually
#for line in file.lines: #for line in file.lines:
# print(' '.join(["%s%s" % (w.letter, w.value_str) for w in line.block.words])) # print(line)

View File

@ -82,7 +82,7 @@ class TestWordsToGCodes(unittest.TestCase):
def test_stuff(self): # FIXME: function name def test_stuff(self): # FIXME: function name
line = 'G1 X82.6892 Y-38.6339 F1500' line = 'G1 X82.6892 Y-38.6339 F1500'
word_list = list(words.iter_words(line)) word_list = list(words.iter_words(line))
result = gcodes.words_to_gcodes(word_list) result = gcodes.words2gcodes(word_list)
# result form # result form
self.assertIsInstance(result, tuple) self.assertIsInstance(result, tuple)
self.assertEqual(len(result), 2) self.assertEqual(len(result), 2)

View File

@ -58,7 +58,7 @@ class WordValueMatchTests(unittest.TestCase):
def test_float(self): def test_float(self):
self.regex_assertions( self.regex_assertions(
regex=words.FLOAT_REGEX, regex=words.REGEX_FLOAT,
positive_list=[ positive_list=[
('1.2', '1.2'), ('1', '1'), ('200', '200'), ('0092', '0092'), ('1.2', '1.2'), ('1', '1'), ('200', '200'), ('0092', '0092'),
('1.', '1.'), ('.2', '.2'), ('-1.234', '-1.234'), ('1.', '1.'), ('.2', '.2'), ('-1.234', '-1.234'),
@ -71,7 +71,7 @@ class WordValueMatchTests(unittest.TestCase):
def test_code(self): def test_code(self):
self.regex_assertions( self.regex_assertions(
regex=words.CODE_REGEX, regex=words.REGEX_CODE,
positive_list=[ positive_list=[
('1.2', '1.2'), ('1', '1'), ('10', '10'), ('1.2', '1.2'), ('1', '1'), ('10', '10'),
('02', '02'), ('02.3', '02.3'), ('02', '02'), ('02.3', '02.3'),