linuxcnc word map

This commit is contained in:
Peter Boin 2018-04-22 15:41:41 +10:00
parent 71d3e02261
commit 5577d6560e
11 changed files with 323 additions and 247 deletions

View File

@ -34,6 +34,7 @@ __all__ = [
'Block',
# Comment
'Comment', 'split_line',
# Word
'Word', 'text2words', 'str2word', 'words2dict',

View File

@ -1,11 +1,13 @@
import re
from .words import text2words, WORD_MAP
from .words import text2words
from .gcodes import words2gcodes
from . import dialects
class Block(object):
"""GCode block (effectively any gcode file line that defines any <word><value>)"""
def __init__(self, text=None, verify=True):
def __init__(self, text=None, dialect=None, verify=True):
"""
Block Constructor
:param text: gcode line content (including comments) as string
@ -26,6 +28,12 @@ class Block(object):
self.gcodes = []
self.modal_params = []
if dialect is None:
dialect = dialects.get_default()
self.dialect = dialect
self._word_map = getattr(getattr(dialects, dialect), 'WORD_MAP')
# clean up block string
if text:
self._raw_text = text # unaltered block content (before alteration)
@ -71,7 +79,7 @@ class Block(object):
modal_groups.add(gc.modal_group)
def __getattr__(self, k):
if k in WORD_MAP:
if k in self._word_map:
for w in self.words:
if w.letter == k:
return w

View File

@ -1,14 +0,0 @@
__all__ = [
'DEFAULT',
# registration decorators
'gcode_dialect',
'word_dialect',
]
DEFAULT = 'linuxcnc'
# Registration decorators
from .mapping import gcode_dialect
from .mapping import word_dialect

View File

@ -1,18 +0,0 @@
"""
LinuxCNC
The linuxcnc gcode dialect is typically used for subtractive fabrication, such
as milling.
This dialect is the basis for all other dialects; GCodes and Words in other
dialects either inherit, or directly reference these classes.
**Specification:** http://www.linuxcnc.org
TODO: verify above info before publishing
"""
# ======================== WORDS ========================
# ======================== G-CODES ========================

View File

@ -0,0 +1,55 @@
__all__ = [
'DEFAULT',
# registration decorators
'gcode_dialect',
'word_dialect',
# dialects
'linuxcnc',
'reprap',
]
# Registration decorators
from .mapping import gcode_dialect
from .mapping import word_dialect
# Dialects
from . import linuxcnc
from . import reprap
_DEFAULT = 'linuxcnc'
def get_default():
"""
Get the default gcode interpreter dialect.
(see :meth:`set_default` for details)
"""
return _DEFAULT
def set_default(name):
"""
Set the default gcode interpreter dialect.
This dialect will be used if no other is specified for particular function
calls.
:param name: name of dialect
:type name: :class:`str`
.. doctest::
>>> from pygcode import dialect
>>> dialect.get_default()
'linuxcnc'
>>> dialect.set_default('reprap')
>>> dialect.get_default()
'reprap'
"""
# TODO: verify valid name
_DEFAULT = name

View File

@ -0,0 +1,216 @@
"""
LinuxCNC
The linuxcnc gcode dialect is typically used for subtractive fabrication, such
as milling.
This dialect is the basis for all other dialects; GCodes and Words in other
dialects either inherit, or directly reference these classes.
**Specification:** http://www.linuxcnc.org
TODO: verify above info before publishing
"""
import re
from .utils import WordType
# ======================== WORDS ========================
REGEX_FLOAT = re.compile(r'^\s*-?(\d+\.?\d*|\.\d+)') # testcase: ..tests.test_words.WordValueMatchTests.test_float
REGEX_INT = re.compile(r'^\s*-?\d+')
REGEX_POSITIVEINT = re.compile(r'^\s*\d+')
REGEX_CODE = re.compile(r'^\s*\d+(\.\d)?') # float, but can't be negative
# 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: "{0:g}".format(round(v, 3))
CLEAN_CODE = _clean_codestr
CLEAN_INT = lambda v: "%g" % v
WORD_MAP = {
# Descriptions copied from wikipedia:
# https://en.wikipedia.org/wiki/G-code#Letter_addresses
# Rotational Axes
'A': WordType(
cls=float,
value_regex=REGEX_FLOAT,
description="Absolute or incremental position of A axis (rotational axis around X axis)",
clean_value=CLEAN_FLOAT,
),
'B': WordType(
cls=float,
value_regex=REGEX_FLOAT,
description="Absolute or incremental position of B axis (rotational axis around Y axis)",
clean_value=CLEAN_FLOAT,
),
'C': WordType(
cls=float,
value_regex=REGEX_FLOAT,
description="Absolute or incremental position of C axis (rotational axis around Z axis)",
clean_value=CLEAN_FLOAT,
),
'D': WordType(
cls=float,
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.",
clean_value=CLEAN_FLOAT,
),
# Feed Rates
'E': WordType(
cls=float,
value_regex=REGEX_FLOAT,
description="Precision feedrate for threading on lathes",
clean_value=CLEAN_FLOAT,
),
'F': WordType(
cls=float,
value_regex=REGEX_FLOAT,
description="Feedrate",
clean_value=CLEAN_FLOAT,
),
# G-Codes
'G': WordType(
cls=float,
value_regex=REGEX_CODE,
description="Address for preparatory commands",
clean_value=CLEAN_CODE,
),
# Tool Offsets
'H': WordType(
cls=float,
value_regex=REGEX_FLOAT,
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
'I': WordType(
cls=float,
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.",
clean_value=CLEAN_FLOAT,
),
'J': WordType(
cls=float,
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.",
clean_value=CLEAN_FLOAT,
),
'K': WordType(
cls=float,
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.",
clean_value=CLEAN_FLOAT,
),
# Loop Count
'L': WordType(
cls=int,
value_regex=REGEX_POSITIVEINT,
description="Fixed cycle loop count; Specification of what register to edit using G10",
clean_value=CLEAN_INT,
),
# Miscellaneous Function
'M': WordType(
cls=float,
value_regex=REGEX_CODE,
description="Miscellaneous function",
clean_value=CLEAN_CODE,
),
# Line Number
'N': WordType(
cls=int,
value_regex=REGEX_POSITIVEINT,
description="Line (block) number in program; System parameter number to change using G10",
clean_value=CLEAN_INT,
),
# Program Name
'O': WordType(
cls=str,
value_regex=re.compile(r'^.+$'), # all the way to the end
description="Program name",
clean_value=CLEAN_NONE,
),
# Parameter (arbitrary parameter)
'P': WordType(
cls=float, # parameter is often an integer, but can be a float
value_regex=REGEX_FLOAT,
description="Serves as parameter address for various G and M codes",
clean_value=CLEAN_FLOAT,
),
# Peck increment
'Q': WordType(
cls=float,
value_regex=REGEX_FLOAT,
description="Depth to increase on each peck; Peck increment in canned cycles",
clean_value=CLEAN_FLOAT,
),
# Arc Radius
'R': WordType(
cls=float,
value_regex=REGEX_FLOAT,
description="Defines size of arc radius, or defines retract height in milling canned cycles",
clean_value=CLEAN_FLOAT,
),
# Spindle speed
'S': WordType(
cls=float,
value_regex=REGEX_FLOAT,
description="Defines speed, either spindle speed or surface speed depending on mode",
clean_value=CLEAN_FLOAT,
),
# Tool Selecton
'T': WordType(
cls=str,
value_regex=REGEX_POSITIVEINT, # tool string may have leading '0's, but is effectively an index (integer)
description="Tool selection",
clean_value=CLEAN_NONE,
),
# Incremental axes
'U': WordType(
cls=float,
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').",
clean_value=CLEAN_FLOAT,
),
'V': WordType(
cls=float,
value_regex=REGEX_FLOAT,
description="Incremental axis corresponding to Y axis",
clean_value=CLEAN_FLOAT,
),
'W': WordType(
cls=float,
value_regex=REGEX_FLOAT,
description="Incremental axis corresponding to Z axis (typically only lathe group A controls)",
clean_value=CLEAN_FLOAT,
),
# Linear Axes
'X': WordType(
cls=float,
value_regex=REGEX_FLOAT,
description="Absolute or incremental position of X axis.",
clean_value=CLEAN_FLOAT,
),
'Y': WordType(
cls=float,
value_regex=REGEX_FLOAT,
description="Absolute or incremental position of Y axis.",
clean_value=CLEAN_FLOAT,
),
'Z': WordType(
cls=float,
value_regex=REGEX_FLOAT,
description="Absolute or incremental position of Z axis.",
clean_value=CLEAN_FLOAT,
),
}
# ======================== G-CODES ========================

View File

@ -0,0 +1,9 @@
# Data Classes
class WordType(object):
def __init__(self, cls, value_regex, description, clean_value):
self.cls = cls
self.value_regex = value_regex
self.description = description
self.clean_value = clean_value

View File

@ -2,218 +2,31 @@ import re
import itertools
import six
from . import dialects
from .exceptions import GCodeBlockFormatError, GCodeWordStrError
REGEX_FLOAT = re.compile(r'^\s*-?(\d+\.?\d*|\.\d+)') # testcase: ..tests.test_words.WordValueMatchTests.test_float
REGEX_INT = re.compile(r'^\s*-?\d+')
REGEX_POSITIVEINT = re.compile(r'^\s*\d+')
REGEX_CODE = re.compile(r'^\s*\d+(\.\d)?') # float, but can't be negative
# 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: "{0:g}".format(round(v, 3))
CLEAN_CODE = _clean_codestr
CLEAN_INT = lambda v: "%g" % v
WORD_MAP = {
# Descriptions copied from wikipedia:
# https://en.wikipedia.org/wiki/G-code#Letter_addresses
# Rotational Axes
'A': {
'class': float,
'value_regex': REGEX_FLOAT,
'description': "Absolute or incremental position of A axis (rotational axis around X axis)",
'clean_value': CLEAN_FLOAT,
},
'B': {
'class': float,
'value_regex': REGEX_FLOAT,
'description': "Absolute or incremental position of B axis (rotational axis around Y axis)",
'clean_value': CLEAN_FLOAT,
},
'C': {
'class': float,
'value_regex': REGEX_FLOAT,
'description': "Absolute or incremental position of C axis (rotational axis around Z axis)",
'clean_value': CLEAN_FLOAT,
},
'D': {
'class': float,
'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.",
'clean_value': CLEAN_FLOAT,
},
# Feed Rates
'E': {
'class': float,
'value_regex': REGEX_FLOAT,
'description': "Precision feedrate for threading on lathes",
'clean_value': CLEAN_FLOAT,
},
'F': {
'class': float,
'value_regex': REGEX_FLOAT,
'description': "Feedrate",
'clean_value': CLEAN_FLOAT,
},
# G-Codes
'G': {
'class': float,
'value_regex': REGEX_CODE,
'description': "Address for preparatory commands",
'clean_value': CLEAN_CODE,
},
# Tool Offsets
'H': {
'class': float,
'value_regex': REGEX_FLOAT,
'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
'I': {
'class': float,
'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.",
'clean_value': CLEAN_FLOAT,
},
'J': {
'class': float,
'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.",
'clean_value': CLEAN_FLOAT,
},
'K': {
'class': float,
'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.",
'clean_value': CLEAN_FLOAT,
},
# Loop Count
'L': {
'class': int,
'value_regex': REGEX_POSITIVEINT,
'description': "Fixed cycle loop count; Specification of what register to edit using G10",
'clean_value': CLEAN_INT,
},
# Miscellaneous Function
'M': {
'class': float,
'value_regex': REGEX_CODE,
'description': "Miscellaneous function",
'clean_value': CLEAN_CODE,
},
# Line Number
'N': {
'class': int,
'value_regex': REGEX_POSITIVEINT,
'description': "Line (block) number in program; System parameter number to change using G10",
'clean_value': CLEAN_INT,
},
# Program Name
'O': {
'class': str,
'value_regex': re.compile(r'^.+$'), # all the way to the end
'description': "Program name",
'clean_value': CLEAN_NONE,
},
# Parameter (arbitrary parameter)
'P': {
'class': float, # parameter is often an integer, but can be a float
'value_regex': REGEX_FLOAT,
'description': "Serves as parameter address for various G and M codes",
'clean_value': CLEAN_FLOAT,
},
# Peck increment
'Q': {
'class': float,
'value_regex': REGEX_FLOAT,
'description': "Depth to increase on each peck; Peck increment in canned cycles",
'clean_value': CLEAN_FLOAT,
},
# Arc Radius
'R': {
'class': float,
'value_regex': REGEX_FLOAT,
'description': "Defines size of arc radius, or defines retract height in milling canned cycles",
'clean_value': CLEAN_FLOAT,
},
# Spindle speed
'S': {
'class': float,
'value_regex': REGEX_FLOAT,
'description': "Defines speed, either spindle speed or surface speed depending on mode",
'clean_value': CLEAN_FLOAT,
},
# Tool Selecton
'T': {
'class': str,
'value_regex': REGEX_POSITIVEINT, # tool string may have leading '0's, but is effectively an index (integer)
'description': "Tool selection",
'clean_value': CLEAN_NONE,
},
# Incremental axes
'U': {
'class': float,
'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').",
'clean_value': CLEAN_FLOAT,
},
'V': {
'class': float,
'value_regex': REGEX_FLOAT,
'description': "Incremental axis corresponding to Y axis",
'clean_value': CLEAN_FLOAT,
},
'W': {
'class': float,
'value_regex': REGEX_FLOAT,
'description': "Incremental axis corresponding to Z axis (typically only lathe group A controls)",
'clean_value': CLEAN_FLOAT,
},
# Linear Axes
'X': {
'class': float,
'value_regex': REGEX_FLOAT,
'description': "Absolute or incremental position of X axis.",
'clean_value': CLEAN_FLOAT,
},
'Y': {
'class': float,
'value_regex': REGEX_FLOAT,
'description': "Absolute or incremental position of Y axis.",
'clean_value': CLEAN_FLOAT,
},
'Z': {
'class': float,
'value_regex': REGEX_FLOAT,
'description': "Absolute or incremental position of Z axis.",
'clean_value': CLEAN_FLOAT,
},
}
class Word(object):
def __init__(self, *args):
if len(args) not in (1, 2):
raise AssertionError("input arguments either: (letter, value) or (word_str)")
if len(args) == 2:
# Word('G', 90)
(letter, value) = args
else:
def __init__(self, *args, **kwargs):
# Parameters (listed)
args_count = len(args)
if args_count == 1:
# Word('G90')
letter = args[0][0] # first letter
value = args[0][1:] # rest of string
elif args_count == 2:
# Word('G', 90)
(letter, value) = args
else:
raise AssertionError("input arguments either: (letter, value) or (word_str)")
# Parameters (keyword)
dialect = kwargs.pop('dialect', dialects.get_default())
letter = letter.upper()
self._value_class = WORD_MAP[letter]['class']
self._value_clean = WORD_MAP[letter]['clean_value']
self._word_map = getattr(getattr(dialects, dialect), 'WORD_MAP')
self._value_class = self._word_map[letter].cls
self._value_clean = self._word_map[letter].clean_value
self.letter = letter
self.value = value
@ -272,15 +85,19 @@ class Word(object):
@property
def description(self):
return "%s: %s" % (self.letter, WORD_MAP[self.letter]['description'])
return "%s: %s" % (self.letter, self._word_map[self.letter].description)
def text2words(block_text):
def text2words(block_text, dialect=None):
"""
Iterate through block text yielding Word instances
:param block_text: text for given block with comments removed
"""
next_word = re.compile(r'^.*?(?P<letter>[%s])' % ''.join(WORD_MAP.keys()), re.IGNORECASE)
if dialect is None:
dialect = dialects.get_default()
word_map = getattr(getattr(dialects, dialect), 'WORD_MAP')
next_word = re.compile(r'^.*?(?P<letter>[%s])' % ''.join(word_map.keys()), re.IGNORECASE)
index = 0
while True:
@ -291,7 +108,7 @@ def text2words(block_text):
index += letter_match.end() # propogate index to start of value
# Value
value_regex = WORD_MAP[letter]['value_regex']
value_regex = word_map[letter].value_regex
value_match = value_regex.search(block_text[index:])
if value_match is None:
raise GCodeWordStrError("word '%s' value invalid" % letter)

View File

@ -6,6 +6,7 @@ add_pygcode_to_path()
# Units under test
from pygcode import words
from pygcode import dialects
class WordIterTests(unittest.TestCase):
@ -33,8 +34,7 @@ class WordIterTests(unittest.TestCase):
self.assertEqual([w[5].letter, w[5].value], ['F', 70])
class WordValueMatchTests(unittest.TestCase):
class WordValueMatchTest(unittest.TestCase):
def regex_assertions(self, regex, positive_list, negative_list):
# Assert all elements of positive_list match regex
for (value_str, expected_match) in positive_list:
@ -47,9 +47,11 @@ class WordValueMatchTests(unittest.TestCase):
match = regex.search(value_str)
self.assertIsNone(match, "matched for '%s'" % value_str)
class WordTests_LinuxCNC(WordValueMatchTest):
def test_float(self):
self.regex_assertions(
regex=words.REGEX_FLOAT,
regex=dialects.linuxcnc.REGEX_FLOAT,
positive_list=[
('1.2', '1.2'), ('1', '1'), ('200', '200'), ('0092', '0092'),
('1.', '1.'), ('.2', '.2'), ('-1.234', '-1.234'),
@ -63,7 +65,7 @@ class WordValueMatchTests(unittest.TestCase):
def test_code(self):
self.regex_assertions(
regex=words.REGEX_CODE,
regex=dialects.linuxcnc.REGEX_CODE,
positive_list=[
('1.2', '1.2'), ('1', '1'), ('10', '10'),
('02', '02'), ('02.3', '02.3'),