From 2d125028b135b743158bcef557d06605610381da Mon Sep 17 00:00:00 2001 From: Peter Boin Date: Mon, 3 Jul 2017 22:28:26 +1000 Subject: [PATCH 01/32] initial content (24hrs later) --- .gitignore | 6 + README.md | 27 ++- pygcode/__init__.py | 0 pygcode/block.py | 36 ++++ pygcode/comment.py | 61 ++++++ pygcode/exceptions.py | 7 + pygcode/file.py | 23 +++ pygcode/line.py | 28 +++ pygcode/machine.py | 36 ++++ pygcode/words.py | 321 +++++++++++++++++++++++++++++ tests/runtests.sh | 3 + tests/test-files/vertical-slot.ngc | 26 +++ tests/test_file.py | 20 ++ tests/test_line.py | 27 +++ tests/test_words.py | 42 ++++ 15 files changed, 652 insertions(+), 11 deletions(-) create mode 100644 .gitignore create mode 100644 pygcode/__init__.py create mode 100644 pygcode/block.py create mode 100644 pygcode/comment.py create mode 100644 pygcode/exceptions.py create mode 100644 pygcode/file.py create mode 100644 pygcode/line.py create mode 100644 pygcode/machine.py create mode 100644 pygcode/words.py create mode 100755 tests/runtests.sh create mode 100644 tests/test-files/vertical-slot.ngc create mode 100644 tests/test_file.py create mode 100644 tests/test_line.py create mode 100644 tests/test_words.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d2015f --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# python cache / compilations +*__pycache__ +*.pyc + +# editor backups +*.swp diff --git a/README.md b/README.md index 4a8a505..cc50c33 100644 --- a/README.md +++ b/README.md @@ -16,16 +16,19 @@ Just brainstorming here... import math import euclid - gfile_in = pygcode.Parser('part.gcode') - gfile_out = pygcode.Writer('part2.gcode') + gfile_in = pygcode.parse('part1.gcode') # + gfile_out = pygcode.GCodeFile('part2.gcode') total_travel = 0 total_time = 0 - for (state, block) in gfile_in.iterstate(): - # where: - # state = CNC's state before the block is executed - # block = the gcode to be executed next + machine = pygcode.Machine() + + for line in gfile_in.iterlines(): + + block = line.block + if block is None: + continue # validation if isinstance(block, pygcode.GCodeArc): @@ -36,13 +39,13 @@ Just brainstorming here... block.set_precision(0.0005, method=pygcode.GCodeArc.EFFECT_RADIUS) # random metrics - travel_vector = block.position - state.position # euclid.Vector3 instance + travel_vector = block.position - machine.state.position # euclid.Vector3 instance distance = travel_vector.magnitude() - travel = block.travel_distance(position=state.position) # eg: distance != travel for G02 & G03 + travel = block.travel_distance(position=machine.state.position) # eg: distance != travel for G02 & G03 total_travel += travel - #total_time += block.time(feed_rate=state.feed_rate) # doesn't consider the feedrate being changed in this block - total_time += block.time(state=state) + #total_time += block.time(feed_rate=machine.state.feed_rate) # doesn't consider the feedrate being changed in this block + total_time += block.time(state=machine.state) # rotate : entire file 90deg CCW block.rotate(euclid.Quaternion.new_rotate_axis( @@ -51,6 +54,8 @@ Just brainstorming here... # translate : entire file x += 1, y += 2 mm (after rotation) block.translate(euclid.Vector3(1, 2, 0), unit=pygcode.UNIT_MM) + + # TODO: then do something like write it to another file gfile_out.write(block) @@ -59,7 +64,7 @@ Just brainstorming here... ## Supported G-Codes -GCode support is planned to follow that of [GRBL](https://github.com/gnea/grbl). +GCode support is planned to follow that of [GRBL](https://github.com/gnea/grbl) which follows [LinuxCNC](http://linuxcnc.org) (list of gcodes documented [here](http://linuxcnc.org/docs/html/gcode.html)). But anything pre v1.0 will be a sub-set, focusing on the issues I'm having... I'm selfish that way. diff --git a/pygcode/__init__.py b/pygcode/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pygcode/block.py b/pygcode/block.py new file mode 100644 index 0000000..6018fd3 --- /dev/null +++ b/pygcode/block.py @@ -0,0 +1,36 @@ +import re +from .words import iter_words, WORD_MAP + +class Block(object): + """GCode block (effectively any gcode file line that defines any )""" + + def __init__(self, text): + """ + Block Constructor + :param A-Z: gcode parameter values + :param comment: comment text + """ + + self._raw_text = text # unaltered block content (before alteration) + + # clean up block string + text = re.sub(r'(^\s+|\s+$)', '', text) # remove whitespace padding + text = re.sub(r'\s+', ' ', text) # remove duplicate whitespace with ' ' + + self.text = text + + self.words = list(iter_words(self.text)) + + def __getattr__(self, k): + if k in WORD_MAP: + for w in self.words: + if w.letter == k: + return w + # if word is not in this block: + return None + + else: + raise AttributeError("'{cls}' object has no attribute '{key}'".format( + cls=self.__class__.__name__, + key=k + )) diff --git a/pygcode/comment.py b/pygcode/comment.py new file mode 100644 index 0000000..dfae3ed --- /dev/null +++ b/pygcode/comment.py @@ -0,0 +1,61 @@ +import re + + +class CommentBase(object): + ORDER = 0 + MULTICOMMENT_JOINER = ". " # joiner if multiple comments are found on the same line + def __init__(self, text): + self.text = text + + +class CommentSemicolon(CommentBase): + "Comments of the format: 'G00 X1 Y2 ; something profound'" + ORDER = 1 + AUTO_REGEX = re.compile(r'\s*;\s*(?P.*)$') + + def __str__(self): + return "; {text}".format(text=self.text) + + +class CommentBrackets(CommentBase): + "Comments of the format: 'G00 X1 Y2 (something profound)" + ORDER = 2 + AUTO_REGEX = re.compile(r'\((?P[^\)]*)\)') + + def __str__(self): + return "({text})".format(text=self.text) + + +Comment = CommentBrackets # default comment type + + +def split_line(line_text): + """ + Split functional block content from comments + :param line_text: line from gcode file + :return: tuple of (str(), CommentBase()) + """ + comments_class = None + + # Auto-detect comment type if I can + comments = [] + block_str = line_text.rstrip("\n") # to remove potential return carriage from comment body + + for cls in sorted(CommentBase.__subclasses__(), key=lambda c: c.ORDER): + matches = list(cls.AUTO_REGEX.finditer(block_str)) + if matches: + for match in reversed(matches): + # Build list of comment text + comments.insert(0, match.group('text')) # prepend + # Remove comments from given block_str + block_str = block_str[:match.start()] + block_str[match.end():] + comments_class = cls + break + + # Create comment instance if content was found + comment_obj = None + if comments_class: + comment_text = comments_class.MULTICOMMENT_JOINER.join(comments) + comment_obj = comments_class(comment_text) + + return (block_str, comment_obj) diff --git a/pygcode/exceptions.py b/pygcode/exceptions.py new file mode 100644 index 0000000..0249ff7 --- /dev/null +++ b/pygcode/exceptions.py @@ -0,0 +1,7 @@ + +# ===================== Parsing Exceptions ===================== +class GCodeBlockFormatError(Exception): + """Raised when errors encountered while parsing block text""" + pass + +# ===================== Parsing Exceptions ===================== diff --git a/pygcode/file.py b/pygcode/file.py new file mode 100644 index 0000000..db99d99 --- /dev/null +++ b/pygcode/file.py @@ -0,0 +1,23 @@ +from .line import Line + + +class GCodeFile(object): + def __init__(self, filename=None): + self.filename = filename + + # Initialize + self.lines = [] + + def append(self, line): + assert isinstance(line, Line), "invalid line type" + self.lines.append(line) + + +def parse(filename): + # FIXME: should be an iterator, and also not terrible + file = GCodeFile() + with open(filename, 'r') as fh: + for line in fh.readlines(): + line_obj = Line(line) + file.append(line_obj) + return file diff --git a/pygcode/line.py b/pygcode/line.py new file mode 100644 index 0000000..f12ebd6 --- /dev/null +++ b/pygcode/line.py @@ -0,0 +1,28 @@ +from .comment import split_line +from .block import Block + +class Line(object): + def __init__(self, text=None): + self._text = text + + # Initialize + self.block = None + self.comment = None + + # Split line into block text, and comments + if text is not None: + (block_str, comment) = split_line(text) + if block_str: + self.block = Block(block_str) + if comment: + self.comment = comment + + @property + def text(self): + if self._text is None: + return self.build_line_text() + return self._text + + + def build_line_text(self): + return ' '.join([str(x) for x in [self.block, self.comment] if x]) + '\n' diff --git a/pygcode/machine.py b/pygcode/machine.py new file mode 100644 index 0000000..39e49bc --- /dev/null +++ b/pygcode/machine.py @@ -0,0 +1,36 @@ + + +class MachineState(object): + def __init__(self, axes=('x', 'y', 'z')): + self.axes = axes + + # initialize + self.position = {} + for axis in self.axes: + self.position[axis] = 0 + + self.time = 0 + +class Machine(object): + """""" + def __init__(self, **kwargs): + self.axes = kwargs.get('axes', ('x', 'y', 'z')) + self.max_rate = kwargs.get('max_rate', { + 'x': 500, # mm/min + 'y': 500, # mm/min + 'z': 500, # mm/min + }) + self.max_travel = kwargs.get('max_travel', { + 'x': 200, # mm + 'y': 200, # mm + 'z': 200, # mm + }) + self.max_spindle_speed = kwargs.get('max_spindle_speed', 1000) # rpm + self.min_spindle_speed = kwargs.get('max_spindle_speed', 0) # rpm + + # initialize + self.state = MachineState(self.axes) + + def process_line(self, line): + """Change machine's state based on the given gcode line""" + pass # TODO diff --git a/pygcode/words.py b/pygcode/words.py new file mode 100644 index 0000000..cce53eb --- /dev/null +++ b/pygcode/words.py @@ -0,0 +1,321 @@ +import re +import itertools +import six + +from .exceptions import GCodeBlockFormatError + +FLOAT_REGEX = re.compile(r'^-?\d+(\.\d+)?') +INT_REGEX = re.compile(r'^-?\d+') +POSITIVEINT_REGEX = re.compile(r'^\d+') + +WORD_MAP = { + # Descriptions copied from wikipedia: + # https://en.wikipedia.org/wiki/G-code#Letter_addresses + + # Rotational Axes + 'A': { + 'class': float, + 'value_regex': FLOAT_REGEX, + 'description': "Absolute or incremental position of A axis (rotational axis around X axis)", + }, + 'B': { + 'class': float, + 'value_regex': FLOAT_REGEX, + 'description': "Absolute or incremental position of B axis (rotational axis around Y axis)", + }, + 'C': { + 'class': float, + 'value_regex': FLOAT_REGEX, + 'description': "Absolute or incremental position of C axis (rotational axis around Z axis)", + }, + 'D': { + 'class': float, + 'value_regex': FLOAT_REGEX, + '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.", + }, + # Feed Rates + 'E': { + 'class': float, + 'value_regex': FLOAT_REGEX, + 'description': "Precision feedrate for threading on lathes", + }, + 'F': { + 'class': float, + 'value_regex': FLOAT_REGEX, + 'description': "Feedrate", + }, + # G-Codes + 'G': { + 'class': str, + 'value_regex': re.compile(r'^\d+(\.\d+)?'), + 'description': "Address for preparatory commands", + }, + # Tool Offsets + 'H': { + 'class': float, + 'value_regex': FLOAT_REGEX, + 'description': "Defines tool length offset; Incremental axis corresponding to C axis (e.g., on a turn-mill)", + }, + # Arc radius center coords + 'I': { + 'class': float, + 'value_regex': FLOAT_REGEX, + 'description': "Defines arc center in X axis for G02 or G03 arc commands. Also used as a parameter within some fixed cycles.", + }, + 'J': { + 'class': float, + 'value_regex': FLOAT_REGEX, + 'description': "Defines arc center in Y axis for G02 or G03 arc commands. Also used as a parameter within some fixed cycles.", + }, + 'K': { + 'class': float, + 'value_regex': FLOAT_REGEX, + '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.", + }, + # Loop Count + 'L': { + 'class': int, + 'value_regex': POSITIVEINT_REGEX, + 'description': "Fixed cycle loop count; Specification of what register to edit using G10", + }, + # Miscellaneous Function + 'M': { + 'class': str, + 'value_regex': re.compile(r'^\d+(\.\d+)?'), + 'description': "Miscellaneous function", + }, + # Line Number + 'N': { + 'class': int, + 'value_regex': POSITIVEINT_REGEX, + 'description': "Line (block) number in program; System parameter number to change using G10", + }, + # Program Name + 'O': { + 'class': str, + 'value_regex': re.compile(r'^.+$'), # all the way to the end + 'description': "Program name", + }, + # Parameter (arbitrary parameter) + 'P': { + 'class': float, # parameter is often an integer, but can be a float + 'value_regex': FLOAT_REGEX, + 'description': "Serves as parameter address for various G and M codes", + }, + # Peck increment + 'Q': { + 'class': float, + 'value_regex': FLOAT_REGEX, + 'description': "Depth to increase on each peck; Peck increment in canned cycles", + }, + # Arc Radius + 'R': { + 'class': float, + 'value_regex': FLOAT_REGEX, + 'description': "Defines size of arc radius, or defines retract height in milling canned cycles", + }, + # Spindle speed + 'S': { + 'class': float, + 'value_regex': FLOAT_REGEX, + 'description': "Defines speed, either spindle speed or surface speed depending on mode", + }, + # Tool Selecton + 'T': { + 'class': str, + 'value_regex': POSITIVEINT_REGEX, # tool string may have leading '0's, but is effectively an index (integer) + 'description': "Tool selection", + }, + # Incremental axes + 'U': { + 'class': float, + 'value_regex': FLOAT_REGEX, + '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').", + }, + 'V': { + 'class': float, + 'value_regex': FLOAT_REGEX, + 'description': "Incremental axis corresponding to Y axis", + }, + 'W': { + 'class': float, + 'value_regex': FLOAT_REGEX, + 'description': "Incremental axis corresponding to Z axis (typically only lathe group A controls)", + }, + # Linear Axes + 'X': { + 'class': float, + 'value_regex': FLOAT_REGEX, + 'description': "Absolute or incremental position of X axis.", + }, + 'Y': { + 'class': float, + 'value_regex': FLOAT_REGEX, + 'description': "Absolute or incremental position of Y axis.", + }, + 'Z': { + 'class': float, + 'value_regex': FLOAT_REGEX, + 'description': "Absolute or incremental position of Z axis.", + }, +} + + +ORDER_LINUXCNC_LETTER_MAP = { + 'O': 10, + 'F': 40, + 'S': 50, + 'T': 60, +} + +_v_csv = lambda v, ks: [(k, v) for k in ks.split(',')] + +ORDER_LINUXCNC_LETTERVALUE_MAP = dict(itertools.chain.from_iterable([ + _v_csv(30, 'G93,G94'), + _v_csv(70, 'M62,M63,M64,M65,M66,M67,M68'), + _v_csv(80, 'M6,M61'), + _v_csv(90, 'M3,M4,M5'), + _v_csv(100, 'M71,M73,M72,M71'), + _v_csv(110, 'M7,M8,M9'), + _v_csv(120, 'M48,M49,M50,M51,M52,M53'), + [('G4', 140)], + _v_csv(150, 'G17,G18,G19'), + _v_csv(160, 'G20,G21'), + _v_csv(170, 'G40,G41,G42'), + _v_csv(180, 'G43,G49'), + _v_csv(190, 'G54,G55,G56,G57,G58,G59,G59.1,G59.2,G59.3'), + _v_csv(200, 'G61,G61.1,G64'), + _v_csv(210, 'G90,G91'), + _v_csv(220, 'G98,G99'), + _v_csv(230, 'G28,G30,G10,G92,G92.1,G92.2,G94'), + _v_csv(240, 'G0,G1,G2,G3,G33,G73,G76,G80,G81,G82,G83,G84,G85,G86,G87,G88,G89'), + _v_csv(250, 'M0,M1,M2,M30,M60'), +])) + +def _word_order_linuxcnc(word): + ''' + Order taken http://linuxcnc.org/docs/html/gcode/overview.html#_g_code_order_of_execution + (as of 2017-07-03) + 010: O-word commands (optionally followed by a comment but no other words allowed on the same line) + N/A: Comment (including message) + 030: Set feed rate mode (G93, G94). + 040: Set feed rate (F). + 050: Set spindle speed (S). + 060: Select tool (T). + 070: HAL pin I/O (M62-M68). + 080: Change tool (M6) and Set Tool Number (M61). + 090: Spindle on or off (M3, M4, M5). + 100: Save State (M70, M73), Restore State (M72), Invalidate State (M71). + 110: Coolant on or off (M7, M8, M9). + 120: Enable or disable overrides (M48, M49,M50,M51,M52,M53). + 130: User-defined Commands (M100-M199). + 140: Dwell (G4). + 150: Set active plane (G17, G18, G19). + 160: Set length units (G20, G21). + 170: Cutter radius compensation on or off (G40, G41, G42) + 180: Cutter length compensation on or off (G43, G49) + 190: Coordinate system selection (G54, G55, G56, G57, G58, G59, G59.1, G59.2, G59.3). + 200: Set path control mode (G61, G61.1, G64) + 210: Set distance mode (G90, G91). + 220: Set retract mode (G98, G99). + 230: Go to reference location (G28, G30) or change coordinate system data (G10) or set axis offsets (G92, G92.1, G92.2, G94). + 240: Perform motion (G0 to G3, G33, G38.x, G73, G76, G80 to G89), as modified (possibly) by G53. + 250: Stop (M0, M1, M2, M30, M60). + 900 + letter val: (else) + ''' + if word.letter in ORDER_LINUXCNC_LETTER_MAP: + return ORDER_LINUXCNC_LETTER_MAP[word.letter] + letter_value = str(word) + if letter_value in ORDER_LINUXCNC_LETTERVALUE_MAP: + return ORDER_LINUXCNC_LETTERVALUE_MAP[letter_value] + + # special cases + if (word.letter == 'M') and (100 <= int(word.value) <= 199): + return 130 + if (word.letter == 'G') and (38 < float(word.value) < 39): + return 240 + + # otherwise, sort last, in alphabetic order + return (900 + (ord(word.letter) - ord('A'))) + +def by_linuxcnc_order(word): + return word.orderval_linuxcnc + + +class Word(object): + def __init__(self, letter, value): + self.letter = letter.upper() + + self._value_str = None + self._value = None + if isinstance(value, six.string_types): + self._value_str = value + else: + self._value = value + + # Sorting Order + self._order_linuxcnc = None + + def __str__(self): + return "{letter}{value}".format( + letter=self.letter, + value=self.value_str, + ) + + # Value Properties + @property + def value_str(self): + """Value string, or """ + if self._value_str is None: + return str(self._value) + return self._value_str + + @property + def value(self): + if self._value is None: + return WORD_MAP[self.letter]['class'](self._value_str) + return self._value + + # Order + @property + def orderval_linuxcnc(self): + if self._order_linuxcnc is None: + self._order_linuxcnc = _word_order_linuxcnc(self) + return self._order_linuxcnc + + @property + def description(self): + return WORD_MAP[self.letter]['description'] + +NEXT_WORD = re.compile(r'^.*?(?P[%s])' % ''.join(WORD_MAP.keys()), re.IGNORECASE) + +def iter_words(block_text): + """ + Iterate through block text yielding Word instances + :param block_text: text for given block with comments removed + """ + index = 0 + + while True: + letter_match = NEXT_WORD.search(block_text[index:]) + if letter_match: + # Letter + letter = letter_match.group('letter').upper() + index += letter_match.end() # propogate index to start of value + + # Value + value_regex = WORD_MAP[letter]['value_regex'] + value_match = value_regex.search(block_text[index:]) + if value_match is None: + raise GCodeBlockFormatError("word '%s' value invalid" % letter) + value = value_match.group() # matched text + + yield Word(letter, value) + + index += value_match.end() # propogate index to end of value + else: + break + + remainder = block_text[index:] + if remainder and re.search(r'\S', remainder): + raise GCodeBlockFormatError("block code remaining '%s'" % remainder) diff --git a/tests/runtests.sh b/tests/runtests.sh new file mode 100755 index 0000000..06ad089 --- /dev/null +++ b/tests/runtests.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +python -m unittest discover -s . -p 'test_*.py' --verbose diff --git a/tests/test-files/vertical-slot.ngc b/tests/test-files/vertical-slot.ngc new file mode 100644 index 0000000..5f8d4b8 --- /dev/null +++ b/tests/test-files/vertical-slot.ngc @@ -0,0 +1,26 @@ +T5 M06 G43 +G17 G90 G21 +G00 Z5 S7000 M03 +X9.60 Y48.74 +Z2 +G01 Z-0.5 F100 +G02 X10.75 Y47.44 I-0.11 J-1.26 F70 +G01 X10.75 Y-47.44 +G02 X9.51 Y-48.69 I-1.25 J0 +G02 X8.25 Y-47.44 I0 J1.25 +G01 X8.25 Y-47.44 +X8.25 Y47.44 +G02 X9.6 Y48.74 I1.25 J0.05 +G00 Z5 +Z1.5 +G01 Z-1 F100 +G02 X10.75 Y47.44 I-0.11 J-1.26 F70 +G01 X10.75 Y-47.44 +G02 X9.51 Y-48.69 I-1.25 J0 +G02 X8.25 Y-47.44 I0 J1.25 +G01 X8.25 Y-47.44 +X8.25 Y47.44 +G02 X9.60 Y48.74 I1.25 J0.05 +G00 Z5 +T0 M06 M02 + diff --git a/tests/test_file.py b/tests/test_file.py new file mode 100644 index 0000000..30f131e --- /dev/null +++ b/tests/test_file.py @@ -0,0 +1,20 @@ +import sys +import os +import inspect + +import unittest + +# Units Under Test +_this_path = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) +sys.path.insert(0, os.path.join(_this_path, '..')) +from pygcode.file import parse, GCodeFile + +class FileParseTest(unittest.TestCase): + FILENAME = 'test-files/vertical-slot.ngc' + + def test_parser(self): + file = parse(self.FILENAME) + self.assertEqual(len(file.lines), 26) + # FIXME: just verifying content visually + for line in file.lines: + print(' '.join(["%s%s" % (w.letter, w.value_str) for w in line.block.words])) diff --git a/tests/test_line.py b/tests/test_line.py new file mode 100644 index 0000000..2ad74a5 --- /dev/null +++ b/tests/test_line.py @@ -0,0 +1,27 @@ +import sys +import os +import inspect + +import unittest + +# Units Under Test +_this_path = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) +sys.path.insert(0, os.path.join(_this_path, '..')) +from pygcode.line import Line + + +class LineCommentTests(unittest.TestCase): + def test_line_comment_semicolon(self): + line = Line('G02 X10.75 Y47.44 I-0.11 J-1.26 F70 ; blah blah') + self.assertEqual(line.comment.text, 'blah blah') + self.assertEqual(len(line.block.words), 6) + + def test_line_comment_brackets(self): + line = Line('G02 X10.75 Y47.44 I-0.11 J-1.26 F70 (blah blah)') + self.assertEqual(line.comment.text, 'blah blah') + self.assertEqual(len(line.block.words), 6) + + def test_line_comment_brackets_multi(self): + line = Line('G02 X10.75 (x coord) Y47.44 (y coord) I-0.11 J-1.26 F70 (eol)') + self.assertEqual(line.comment.text, 'x coord. y coord. eol') + self.assertEqual(len(line.block.words), 6) diff --git a/tests/test_words.py b/tests/test_words.py new file mode 100644 index 0000000..b60ad23 --- /dev/null +++ b/tests/test_words.py @@ -0,0 +1,42 @@ +import sys +import os +import inspect + +import unittest + +# Units Under Test +_this_path = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) +sys.path.insert(0, os.path.join(_this_path, '..')) +import pygcode.words as gcode_words + +#words.iter_words + +class WordTests(unittest.TestCase): + def test_O(self): + pass # TODO + + + +class WordIterTests(unittest.TestCase): + def test_iter1(self): + block_str = 'G01 Z-0.5 F100' + w = list(gcode_words.iter_words(block_str)) + # word length + self.assertEqual(len(w), 3) + # word values + self.assertEqual([w[0].letter, w[0].value], ['G', '01']) + self.assertEqual([w[1].letter, w[1].value], ['Z', -0.5]) + self.assertEqual([w[2].letter, w[2].value], ['F', 100]) + + def test_iter2(self): + block_str = 'G02 X10.75 Y47.44 I-0.11 J-1.26 F70' + w = list(gcode_words.iter_words(block_str)) + # word length + self.assertEqual(len(w), 6) + # word values + self.assertEqual([w[0].letter, w[0].value], ['G', '02']) + self.assertEqual([w[1].letter, w[1].value], ['X', 10.75]) + self.assertEqual([w[2].letter, w[2].value], ['Y', 47.44]) + self.assertEqual([w[3].letter, w[3].value], ['I', -0.11]) + self.assertEqual([w[4].letter, w[4].value], ['J', -1.26]) + self.assertEqual([w[5].letter, w[5].value], ['F', 70]) From dcac4fcec5462567f541fa6a9c1a1b901972cdc7 Mon Sep 17 00:00:00 2001 From: Peter Boin Date: Wed, 5 Jul 2017 02:06:38 +1000 Subject: [PATCH 02/32] added gcodes classes as a concept --- pygcode/block.py | 2 + pygcode/gcodes.py | 824 +++++++++++++++++++++++++++++++++++++++++++ pygcode/words.py | 23 +- tests/test_file.py | 4 +- tests/test_gcodes.py | 23 ++ tests/test_words.py | 14 +- 6 files changed, 877 insertions(+), 13 deletions(-) create mode 100644 pygcode/gcodes.py create mode 100644 tests/test_gcodes.py diff --git a/pygcode/block.py b/pygcode/block.py index 6018fd3..49436f2 100644 --- a/pygcode/block.py +++ b/pygcode/block.py @@ -1,5 +1,6 @@ import re from .words import iter_words, WORD_MAP +from .gcodes import words_to_gcodes class Block(object): """GCode block (effectively any gcode file line that defines any )""" @@ -20,6 +21,7 @@ class Block(object): self.text = text self.words = list(iter_words(self.text)) + #self.gcodes = list(words_to_gcodes(self.words)) def __getattr__(self, k): if k in WORD_MAP: diff --git a/pygcode/gcodes.py b/pygcode/gcodes.py new file mode 100644 index 0000000..8c7e2b2 --- /dev/null +++ b/pygcode/gcodes.py @@ -0,0 +1,824 @@ + +from .words import Word + + +class GCode(object): + # Defining Word + word_key = None # Word instance to use in lookup + word_matches = None # function (secondary) + # Parameters associated to this gcode + param_words = set() + + def __init__(self): + self.params = None # TODO + + +# ======================= Motion ======================= +# (X Y Z A B C U V W apply to all motions) +# CODE PARAMETERS DESCRIPTION +# G0 Rapid Move +# G1 Linear Move +# G2, G3 I J K or R, P Arc Move +# G4 P Dwell +# G5 I J P Q Cubic Spline +# G5.1 I J Quadratic Spline +# G5.2 P L NURBS +# G38.2 - G38.5 Straight Probe +# G33 K Spindle Synchronized Motion +# G33.1 K Rigid Tapping +# G80 Cancel Canned Cycle + +class GCodeMotion(GCode): + param_words = set('XYZABCUVW') + + +class GCodeRapidMove(GCodeMotion): + """G0: Rapid Move""" + word_key = Word('G', 0) + + +class GCodeLinearMove(GCodeMotion): + """G1: Linear Move""" + word_key = Word('G', 1) + + +class GCodeArcMove(GCodeMotion): + """Arc Move""" + param_words = GCodeMotion.param_words | set('IJKRP') + + +class GCodeArcMoveCW(GCodeArcMove): + """G2: Arc Move (clockwise)""" + word_key = Word('G', 2) + + +class GCodeArcMoveCCW(GCodeArcMove): + """G3: Arc Move (counter-clockwise)""" + word_key = Word('G', 3) + + +class GCodeDwell(GCodeMotion): + """G4: Dwell""" + param_words = GCodeMotion.param_words | set('P') + word_key = Word('G', 4) + + +class GCodeCublcSpline(GCodeMotion): + """G5: Cubic Spline""" + param_words = GCodeMotion.param_words | set('IJPQ') + word_key = Word('G', 5) + + +class GCodeQuadraticSpline(GCodeMotion): + """G5.1: Quadratic Spline""" + param_words = GCodeMotion.param_words | set('IJ') + word_key = Word('G', 5.1) + + +class GCodeNURBS(GCodeMotion): + """G5.2: Non-uniform rational basis spline (NURBS)""" + param_words = GCodeMotion.param_words | set('PL') + word_key = Word('G', 5.2) + + +class GCodeNURBSEnd(GCodeNURBS): + """G5.3: end NURBS mode""" + word_key = Word('G', 5.3) + + +class GCodeStraightProbe(GCodeMotion): + """G38.2-G38.5: Straight Probe""" + @classmethod + def word_matches(cls, w): + return (w.letter == 'G') and (38.2 <= w.value <= 38.5) + + +class GCodeSpindleSyncMotion(GCodeMotion): + """G33: Spindle Synchronized Motion""" + param_words = GCodeMotion.param_words | set('K') + word_key = Word('G', 33) + + +class GCodeRigidTapping(GCodeMotion): + """G33.1: Rigid Tapping""" + param_words = GCodeMotion.param_words | set('K') + word_key = Word('G', 33.1) + + +class GCodeCancelCannedCycle(GCodeMotion): + """G80: Cancel Canned Cycle""" + word_key = Word('G', 80) + + +# ======================= Canned Cycles ======================= +# (X Y Z or U V W apply to canned cycles, depending on active plane) +# CODE PARAMETERS DESCRIPTION +# G81 R L (P) Drilling Cycle +# G82 R L (P) Drilling Cycle, Dwell +# G83 R L Q Drilling Cycle, Peck +# G73 R L Q Drilling Cycle, Chip Breaking +# G85 R L (P) Boring Cycle, Feed Out +# G89 R L (P) Boring Cycle, Dwell, Feed Out +# G76 P Z I J R K Q H L E Threading Cycle + +class GCodeCannedCycle(GCode): + param_words = set('XYZUVW') + + +class GCodeDrillingCycle(GCodeCannedCycle): + """G81: Drilling Cycle""" + param_words = GCodeCannedCycle.param_words | set('RLP') + word_key = Word('G', 81) + + +class GCodeDrillingCycleDwell(GCodeCannedCycle): + """G82: Drilling Cycle, Dwell""" + param_words = GCodeCannedCycle.param_words | set('RLP') + word_key = Word('G', 82) + + +class GCodeDrillingCyclePeck(GCodeCannedCycle): + """G83: Drilling Cycle, Peck""" + param_words = GCodeCannedCycle.param_words | set('RLQ') + word_key = Word('G', 83) + + +class GCodeDrillingCycleChipBreaking(GCodeCannedCycle): + """G73: Drilling Cycle, ChipBreaking""" + param_words = GCodeCannedCycle.param_words | set('RLQ') + word_key = Word('G', 73) + + +class GCodeBoringCycleFeedOut(GCodeCannedCycle): + """G85: Boring Cycle, Feed Out""" + param_words = GCodeCannedCycle.param_words | set('RLP') + word_key = Word('G', 85) + + +class GCodeBoringCycleDwellFeedOut(GCodeCannedCycle): + """G89: Boring Cycle, Dwell, Feed Out""" + param_words = GCodeCannedCycle.param_words | set('RLP') + word_key = Word('G', 89) + + +class GCodeThreadingCycle(GCodeCannedCycle): + """G76: Threading Cycle""" + param_words = GCodeCannedCycle.param_words | set('PZIJRKQHLE') + word_key = Word('G', 76) + + +# ======================= Distance Mode ======================= +# CODE PARAMETERS DESCRIPTION +# G90, G91 Distance Mode +# G90.1, G91.1 Arc Distance Mode +# G7 Lathe Diameter Mode +# G8 Lathe Radius Mode + +class GCodeDistanceMode(GCode): + pass + + +class GCodeAbsoluteDistanceMode(GCodeDistanceMode): + """G90: Absolute Distance Mode""" + word_key = Word('G', 90) + + +class GCodeIncrementalDistanceMode(GCodeDistanceMode): + """G91: Incremental Distance Mode""" + word_key = Word('G', 91) + + +class GCodeAbsoluteArcDistanceMode(GCodeDistanceMode): + """G90.1: Absolute Distance Mode for Arc IJK Parameters""" + word_key = Word('G', 90.1) + + +class GCodeIncrementalArcDistanceMode(GCodeDistanceMode): + """G91.1: Incremental Distance Mode for Arc IJK Parameters""" + word_key = Word('G', 91.1) + + +class GCodeLatheDiameterMode(GCodeDistanceMode): + """G7: Lathe Diameter Mode""" + word_key = Word('G', 7) + + +class GCodeLatheRadiusMode(GCodeDistanceMode): + """G8: Lathe Radius Mode""" + word_key = Word('G', 8) + + +# ======================= Feed Rate Mode ======================= +# CODE PARAMETERS DESCRIPTION +# G93, G94, G95 Feed Rate Mode + +class GCodeFeedRateMode(GCode): + pass + + +class GCodeInverseTimeMode(GCodeFeedRateMode): + """G93: Inverse Time Mode""" + word_key = Word('G', 93) + + +class GCodeUnitsPerMinuteMode(GCodeFeedRateMode): + """G94: Units Per MinuteMode""" + word_key = Word('G', 94) + + +class GCodeUnitsPerRevolution(GCodeFeedRateMode): + """G95: Units Per Revolution""" + word_key = Word('G', 95) + + +# ======================= Spindle Control ======================= +# CODE PARAMETERS DESCRIPTION +# M3, M4, M5 S Spindle Control +# M19 Orient Spindle +# G96, G97 S D Spindle Control Mode + +class GCodeSpindle(GCode): + pass + + +class GCodeStartSpindleCW(GCodeSpindle): + """M3: Start Spindle Clockwise""" + param_words = set('S') + word_key = Word('M', 3) + + +class GCodeStartSpindleCCW(GCodeSpindle): + """M4: Start Spindle Counter-Clockwise""" + param_words = set('S') + word_key = Word('M', 4) + + +class GCodeStopSpindle(GCodeSpindle): + """M5: Stop Spindle""" + param_words = set('S') + word_key = Word('M', 5) + + +class GCodeOrientSpindle(GCodeSpindle): + """M19: Orient Spindle""" + word_key = Word('M', 19) + + +class GCodeSpindleConstantSurfaceSpeedMode(GCodeSpindle): + """G96: Spindle Constant Surface Speed""" + param_words = set('DS') + word_key = Word('G', 96) + + +class GCodeSpindleRPMMode(GCodeSpindle): + """G97: Spindle RPM Speed""" + param_words = set('D') + word_key = Word('G', 97) + + + +# ======================= Coolant ======================= +# CODE PARAMETERS DESCRIPTION +# M7, M8, M9 Coolant Control + +class GCodeCoolant(GCode): + pass + + +class GCodeCoolantMistOn(GCodeCoolant): + """M7: turn mist coolant on""" + word_key = Word('M', 7) + + +class GCodeCoolantFloodOn(GCodeCoolant): + """M8: turn flood coolant on""" + word_key = Word('M', 8) + + +class GCodeCoolantOff(GCodeCoolant): + """M9: turn all coolant off""" + word_key = Word('M', 9) + + +# ======================= Tool Length ======================= +# CODE PARAMETERS DESCRIPTION +# G43 H Tool Length Offset +# G43.1 Dynamic Tool Length Offset +# G43.2 H Apply additional Tool Length Offset +# G49 Cancel Tool Length Compensation + +class GCodeToolLength(GCode): + pass + + +class GCodeToolLengthOffset(GCodeToolLength): + """G43: Tool Length Offset""" + param_words = set('H') + word_key = Word('G', 43) + + +class GCodeDynamicToolLengthOffset(GCodeToolLength): + """G43.1: Dynamic Tool Length Offset""" + word_key = Word('G', 43.1) + + +class GCodeAddToolLengthOffset(GCodeToolLength): + """G43.2: Appkly Additional Tool Length Offset""" + param_words = set('H') + word_key = Word('G', 43.2) + + +class GCodeCancelToolLengthOffset(GCodeToolLength): + """G49: Cancel Tool Length Compensation""" + word_key = Word('G', 49) + + +# ======================= Stopping (Program Control) ======================= +# CODE PARAMETERS DESCRIPTION +# M0, M1 Program Pause +# M2, M30 Program End +# M60 Pallet Change Pause + +class GCodeProgramControl(GCode): + pass + + +class GCodePauseProgram(GCodeProgramControl): + """M0: Program Pause""" + word_key = Word('M', 0) + + +class GCodePauseProgramOptional(GCodeProgramControl): + """M1: Program Pause (optional)""" + word_key = Word('M', 1) + + +class GCodeEndProgram(GCodeProgramControl): + """M2: Program End""" + word_key = Word('M', 2) + + +class GCodeEndProgramPalletShuttle(GCodeProgramControl): + """M30: exchange pallet shuttles and end the program""" + word_key = Word('M', 30) + + +class GCodePalletChangePause(GCodeProgramControl): + """M60: Pallet Change Pause""" + word_key = Word('M', 60) + + +# ======================= Units ======================= +# CODE PARAMETERS DESCRIPTION +# G20, G21 Units + +class GCodeUnit(GCode): + pass + +class GCodeUseInches(GCodeUnit): + """G20: use inches for length units""" + word_key = Word('G', 20) + +class GCodeUseMillimeters(GCodeUnit): + """G21: use millimeters for length units""" + word_key = Word('G', 21) + + +# ======================= Plane Selection ======================= +# (affects G2, G3, G81-G89, G40-G42) +# CODE PARAMETERS DESCRIPTION +# G17 - G19.1 Plane Select + +class GCodePlaneSelect(GCode): + pass + + +class GCodeSelectZYPlane(GCodePlaneSelect): + """G17: select XY plane (default)""" + word_key = Word('G', 17) + + +class GCodeSelectZXPlane(GCodePlaneSelect): + """G18: select ZX plane""" + word_key = Word('G', 18) + + +class GCodeSelectYZPlane(GCodePlaneSelect): + """G19: select YZ plane""" + word_key = Word('G', 19) + + +class GCodeSelectUVPlane(GCodePlaneSelect): + """G17.1: select UV plane""" + word_key = Word('G', 17.1) + + +class GCodeSelectWUPlane(GCodePlaneSelect): + """G18.1: select WU plane""" + word_key = Word('G', 18.1) + + +class GCodeSelectVWPlane(GCodePlaneSelect): + """G19.1: select VW plane""" + word_key = Word('G', 19.1) + + +# ======================= Cutter Radius Compensation ======================= +# CODE PARAMETERS DESCRIPTION +# G40 Compensation Off +# G41,G42 D Cutter Compensation +# G41.1, G42.1 D L Dynamic Cutter Compensation + +class GCodeCutterRadiusComp(GCode): + pass + + +class GCodeCutterRadiusCompOff(GCodeCutterRadiusComp): + """G40: Cutter Radius Compensation Off""" + word_key = Word('G', 40) + + +class GCodeCutterCompLeft(GCodeCutterRadiusComp): + """G41: Cutter Radius Compensation (left)""" + param_words = set('D') + word_key = Word('G', 41) + + +class GCodeCutterCompRight(GCodeCutterRadiusComp): + """G42: Cutter Radius Compensation (right)""" + param_words = set('D') + word_key = Word('G', 42) + + +class GCodeDynamicCutterCompLeft(GCodeCutterRadiusComp): + """G41.1: Dynamic Cutter Radius Compensation (left)""" + param_words = set('DL') + word_key = Word('G', 41.1) + + +class GCodeDynamicCutterCompRight(GCodeCutterRadiusComp): + """G42.1: Dynamic Cutter Radius Compensation (right)""" + param_words = set('DL') + word_key = Word('G', 42.1) + + +# ======================= Path Control Mode ======================= +# CODE PARAMETERS DESCRIPTION +# G61 G61.1 Exact Path Mode +# G64 P Q Path Blending + +class GCodePathControlMode(GCode): + pass + + +class GCodeExactPathMode(GCodePathControlMode): + """G61: Exact path mode""" + word_key = Word('G', 61) + + +class GCodeExactStopMode(GCodePathControlMode): + """G61.1: Exact stop mode""" + word_key = Word('G', 61.1) + + +class GCodePathBlendingMode(GCodePathControlMode): + """G64: Path Blending""" + param_words = set('PQ') + word_key = Word('G', 64) + + +# ======================= Return Mode in Canned Cycles ======================= +# CODE PARAMETERS DESCRIPTION +# G98 Canned Cycle Return Level + +class GCodeCannedReturnMode(GCode): + pass + + +class GCodeCannedCycleReturnLevel(GCodeCannedReturnMode): + """G98: Canned Cycle Return Level""" + word_key = Word('G', 98) + + +# ======================= Other Modal Codes ======================= +# CODE PARAMETERS DESCRIPTION +# F Set Feed Rate +# S Set Spindle Speed +# T Select Tool +# M48, M49 Speed and Feed Override Control +# M50 P0 (off) or P1 (on) Feed Override Control +# M51 P0 (off) or P1 (on) Spindle Speed Override Control +# M52 P0 (off) or P1 (on) Adaptive Feed Control +# M53 P0 (off) or P1 (on) Feed Stop Control +# G54-G59.3 Select Coordinate System + +class GCodeOtherModal(GCode): + pass + + +class GCodeFeedRate(GCodeOtherModal): + """F: Set Feed Rate""" + @classmethod + def word_matches(cls, w): + return w.letter == 'F' + +class GCodeSpindleSpeed(GCodeOtherModal): + """S: Set Spindle Speed""" + @classmethod + def word_matches(cls, w): + return w.letter == 'S' + + +class GCodeSelectTool(GCodeOtherModal): + """T: Select Tool""" + @classmethod + def word_matches(cls, w): + return w.letter == 'T' + + +class GCodeSpeedAndFeedOverrideOn(GCodeOtherModal): + """M48: Speed and Feed Override Control On""" + word_key = Word('M', 48) + + +class GCodeSpeedAndFeedOverrideOff(GCodeOtherModal): + """M49: Speed and Feed Override Control Off""" + word_key = Word('M', 49) + + +class GCodeFeedOverride(GCodeOtherModal): + """M50: Feed Override Control""" + param_words = set('P') + word_key = Word('M', 50) + + +class GCodeSpindleSpeedOverride(GCodeOtherModal): + """M51: Spindle Speed Override Control""" + param_words = set('P') + word_key = Word('M', 51) + + +class GCodeAdaptiveFeed(GCodeOtherModal): + """M52: Adaptive Feed Control""" + param_words = set('P') + word_key = Word('M', 52) + + +class GCodeFeedStop(GCodeOtherModal): + """M53: Feed Stop Control""" + param_words = set('P') + word_key = Word('M', 53) + + +class GCodeSelectCoordinateSystem(GCodeOtherModal): + """ + G54 - select coordinate system 1 + G55 - select coordinate system 2 + G56 - select coordinate system 3 + G57 - select coordinate system 4 + G58 - select coordinate system 5 + G59 - select coordinate system 6 + G59.1 - select coordinate system 7 + G59.2 - select coordinate system 8 + G59.3 - select coordinate system 9 + """ + @classmethod + 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]) + + +# ======================= Flow-control Codes ======================= +# CODE PARAMETERS DESCRIPTION +# o sub Subroutines, sub/endsub call [unsupported] +# o while Looping, while/endwhile do/while [unsupported] +# o if Conditional, if/else/endif [unsupported] +# o repeat Repeat a loop of code [unsupported] +# [] Indirection [unsupported] +# o call Call named file [unsupported] +# M70 Save modal state [unsupported] +# M71 Invalidate stored state [unsupported] +# M72 Restore modal state [unsupported] +# M73 Save and Auto-restore modal state [unsupported] + + +# ======================= Input/Output Codes ======================= +# CODE PARAMETERS DESCRIPTION +# M62 - M65 P Digital Output Control +# M66 P E L Q Wait on Input +# M67 T Analog Output, Synchronized +# M68 T Analog Output, Immediate + +class GCodeIO(GCode): + pass + + +class GCodeDigitalOutput(GCodeIO): + """Digital Output Control""" + param_words = set('P') + + +class GCodeDigitalOutputOnSyncd(GCodeDigitalOutput): + """M62: turn on digital output synchronized with motion""" + word_key = Word('M', 62) + + +class GCodeDigitalOutputOffSyncd(GCodeDigitalOutput): + """M63: turn off digital output synchronized with motion""" + word_key = Word('M', 63) + + +class GCodeDigitalOutputOn(GCodeDigitalOutput): + """M64: turn on digital output immediately""" + word_key = Word('M', 64) + + +class GCodeDigitalOutputOff(GCodeDigitalOutput): + """M65: turn off digital output immediately""" + word_key = Word('M', 65) + + +class GCodeWaitOnInput(GCodeIO): + """M66: Wait on Input""" + param_words = set('PELQ') + word_key = Word('M', 66) + + +class GCodeAnalogOutput(GCodeIO): + """Analog Output""" + param_words = set('T') + + +class GCodeAnalogOutputSyncd(GCodeAnalogOutput): + """M67: Analog Output, Synchronized""" + word_key = Word('M', 67) + + +class GCodeAnalogOutputImmediate(GCodeAnalogOutput): + """M68: Analog Output, Immediate""" + word_key = Word('M', 68) + + +# ======================= Non-modal Codes ======================= +# CODE PARAMETERS DESCRIPTION +# M6 T Tool Change +# M61 Q Set Current Tool +# G10 L1 P Q R Set Tool Table +# G10 L10 P Set Tool Table +# G10 L11 P Set Tool Table +# G10 L2 P R Set Coordinate System +# G10 L20 P Set Coordinate System +# G28, G28.1 Go/Set Predefined Position +# G30, G30.1 Go/Set Predefined Position +# G53 Move in Machine Coordinates +# G92 Coordinate System Offset +# G92.1, G92.2 Reset G92 Offsets +# G92.3 Restore G92 Offsets +# M101 - M199 P Q User Defined Commands + +class GCodeNonModal(GCode): + pass + + +class GCodeToolChange(GCodeNonModal): + """M6: Tool Change""" + param_words = set('T') + word_key = Word('M', 6) + + +class GCodeToolSetCurrent(GCodeNonModal): + """M61: Set Current Tool""" + param_words = set('Q') + word_key = Word('M', 61) + + +class GCodeSet(GCodeNonModal): + """G10: Set stuff""" + param_words = set('LPQR') + word_key = Word('G', 10) + + +class GCodeGotoPredefinedPosition(GCodeNonModal): + """G28,G30: Goto Predefined Position (rapid movement)""" + @classmethod + def word_matches(cls, w): + return (w.letter == 'G') and (w.value in [28, 30]) + + +class GCodeSetPredefinedPosition(GCodeNonModal): + """G28.1,G30.1: Set Predefined Position""" # redundancy in language there, but I'll let it slide + @classmethod + def word_matches(cls, w): + return (w.letter == 'G') and (w.value in [28.1, 30.1]) + + +class GCodeMoveInMachineCoords(GCodeNonModal): + """G53: Move in Machine Coordinates""" + word_key = Word('G', 53) + + +class GCodeCoordSystemOffset(GCodeNonModal): + """G92: Coordinate System Offset""" + word_key = Word('G', 92) + + +class GCodeResetCoordSystemOffset(GCodeNonModal): + """G92.1,G92.2: Reset Coordinate System Offset""" + @classmethod + def word_matches(cls, w): + return (w.letter == 'G') and (w.value in [92.1, 92.2]) + + +class GCodeRestoreCoordSystemOffset(GCodeNonModal): + """G92.3: Restore Coordinate System Offset""" + word_key = Word('G', 92.3) + + +class GCodeUserDefined(GCodeNonModal): + """M101-M199: User Defined Commands""" + # To create user g-codes, inherit from this class + param_words = set('PQ') + #@classmethod + #def word_matches(cls, w): + # return (w.letter == 'M') and (101 <= w.value <= 199) + + +# ======================= Utilities ======================= + +def _subclasses_level(root_class, recursion_level=0): + """ + Hierarcical list of all classes inheriting from the given root class (recursive) + :param root_class: class used as trunk of hierarchy (included inoutput) + :param recursion_level: should always be 0 (unless recursively called) + :param + """ + yield (root_class, recursion_level) + for cls in root_class.__subclasses__(): + for (sub, level) in _subclasses_level(cls, recursion_level+1): + yield (sub, level) + + +def _subclasses(root_class): + """Flat list of all classes inheriting from the given root class (recursive)""" + for (cls, level) in _subclasses_level(root_class): + yield cls + + +def _gcode_class_infostr(base_class=GCode): + """ + List all ineheriting classes for the given gcode class + :param base_class: root of hierarcy + :return: str listing all gcode classes + """ + info_str = "" + for (cls, level) in _subclasses_level(base_class): + info_str += "{indent}- {name}: {description}\n".format( + indent=(level * " "), + name=cls.__name__, + description=cls.__doc__ or "", + ) + return info_str + +# ======================= GCode Word Mapping ======================= +_gcode_maps_created = False # only set when the below values are populated +_gcode_word_map = {} # of the form: {Word('G', 0): GCodeRapidMove, ... } +_gcode_function_list = [] # of the form: [(lambda w: w.letter == 'F', GCodeFeedRate), ... ] + +def _build_maps(): + """ + Populate _gcode_word_map and _gcode_function_list + """ + # Ensure lists are clear + global _gcode_word_map + global _gcode_function_list + _gcode_word_map = {} + _gcode_function_list = [] + + for cls in _subclasses(GCode): + if cls.word_key is not None: + # Map Word instance to g-code class + if cls.word_key in _gcode_word_map: + raise RuntimeError("Multiple GCode classes map to '%s'" % str(cls.word_key)) + _gcode_word_map[cls.word_key] = cls + elif cls.word_matches is not None: + # Add to list of functions + _gcode_function_list.append((cls.word_matches, cls)) + + global _gcode_maps_created + _gcode_maps_created = True + +def words_to_gcodes(words): + """ + Group words into g-codes (includes both G & M codes) + :param words: list of Word instances + :return: list containing [, , ..., list()] + """ + + if _gcode_maps_created is False: + _build_maps() + + # First determine which words are GCodes + # TODO: next up... + + unassigned = [] + #sdrow = list(reversed(words)) + #for (i, word) in reversed(enumerate(words)): diff --git a/pygcode/words.py b/pygcode/words.py index cce53eb..e8729fb 100644 --- a/pygcode/words.py +++ b/pygcode/words.py @@ -46,8 +46,8 @@ WORD_MAP = { }, # G-Codes 'G': { - 'class': str, - 'value_regex': re.compile(r'^\d+(\.\d+)?'), + 'class': float, + 'value_regex': re.compile(r'^\d+(\.\d)?'), 'description': "Address for preparatory commands", }, # Tool Offsets @@ -80,8 +80,8 @@ WORD_MAP = { }, # Miscellaneous Function 'M': { - 'class': str, - 'value_regex': re.compile(r'^\d+(\.\d+)?'), + 'class': float, + 'value_regex': re.compile(r'^\d+(\.\d)?'), 'description': "Miscellaneous function", }, # Line Number @@ -262,6 +262,21 @@ class Word(object): value=self.value_str, ) + def __repr__(self): + return "<{class_name}: {string}>".format( + class_name=self.__class__.__name__, + string=str(self), + ) + + def __eq__(self, other): + return (self.letter == other.letter) and (self.value == other.value) + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash((self.letter, self.value)) + # Value Properties @property def value_str(self): diff --git a/tests/test_file.py b/tests/test_file.py index 30f131e..b09bae3 100644 --- a/tests/test_file.py +++ b/tests/test_file.py @@ -16,5 +16,5 @@ class FileParseTest(unittest.TestCase): file = parse(self.FILENAME) self.assertEqual(len(file.lines), 26) # FIXME: just verifying content visually - for line in file.lines: - print(' '.join(["%s%s" % (w.letter, w.value_str) for w in line.block.words])) + #for line in file.lines: + # print(' '.join(["%s%s" % (w.letter, w.value_str) for w in line.block.words])) diff --git a/tests/test_gcodes.py b/tests/test_gcodes.py new file mode 100644 index 0000000..b15f564 --- /dev/null +++ b/tests/test_gcodes.py @@ -0,0 +1,23 @@ +import sys +import os +import inspect + +import unittest + +# Units Under Test +_this_path = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) +sys.path.insert(0, os.path.join(_this_path, '..')) +from pygcode import gcodes + + +class TestGCodeWordMapping(unittest.TestCase): + def test_word_map_integrity(self): + gcodes._build_maps() + for (word_maches, fn_class) in gcodes._gcode_function_list: + for (word, key_class) in gcodes._gcode_word_map.items(): + # Verify that no mapped word will yield a True result + # from any of the 'word_maches' functions + self.assertFalse( + word_maches(word), + "conflict with %s and %s" % (fn_class, key_class) + ) diff --git a/tests/test_words.py b/tests/test_words.py index b60ad23..a5ffe95 100644 --- a/tests/test_words.py +++ b/tests/test_words.py @@ -7,7 +7,7 @@ import unittest # Units Under Test _this_path = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) sys.path.insert(0, os.path.join(_this_path, '..')) -import pygcode.words as gcode_words +from pygcode import words #words.iter_words @@ -20,21 +20,21 @@ class WordTests(unittest.TestCase): class WordIterTests(unittest.TestCase): def test_iter1(self): block_str = 'G01 Z-0.5 F100' - w = list(gcode_words.iter_words(block_str)) + w = list(words.iter_words(block_str)) # word length self.assertEqual(len(w), 3) # word values - self.assertEqual([w[0].letter, w[0].value], ['G', '01']) - self.assertEqual([w[1].letter, w[1].value], ['Z', -0.5]) - self.assertEqual([w[2].letter, w[2].value], ['F', 100]) + self.assertEqual(w[0], words.Word('G', 1)) + self.assertEqual(w[1], words.Word('Z', -0.5)) + self.assertEqual(w[2], words.Word('F', 100)) def test_iter2(self): block_str = 'G02 X10.75 Y47.44 I-0.11 J-1.26 F70' - w = list(gcode_words.iter_words(block_str)) + w = list(words.iter_words(block_str)) # word length self.assertEqual(len(w), 6) # word values - self.assertEqual([w[0].letter, w[0].value], ['G', '02']) + self.assertEqual([w[0].letter, w[0].value], ['G', 2]) self.assertEqual([w[1].letter, w[1].value], ['X', 10.75]) self.assertEqual([w[2].letter, w[2].value], ['Y', 47.44]) self.assertEqual([w[3].letter, w[3].value], ['I', -0.11]) From da772580e046728be66ebb1f0d1624c6ec90b3eb Mon Sep 17 00:00:00 2001 From: Peter Boin Date: Wed, 5 Jul 2017 13:12:58 +1000 Subject: [PATCH 03/32] gcode and float regex improvement --- pygcode/words.py | 8 +++++--- tests/test_words.py | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/pygcode/words.py b/pygcode/words.py index e8729fb..7f38c5e 100644 --- a/pygcode/words.py +++ b/pygcode/words.py @@ -4,9 +4,11 @@ import six from .exceptions import GCodeBlockFormatError -FLOAT_REGEX = re.compile(r'^-?\d+(\.\d+)?') +FLOAT_REGEX = re.compile(r'^-?(\d+\.?\d*|\.\d+)') # testcase: ..tests.test_words.WordValueMatchTests.test_float INT_REGEX = re.compile(r'^-?\d+') POSITIVEINT_REGEX = re.compile(r'^\d+') +CODE_REGEX = re.compile(r'^\d+(\.\d)?') # similar + WORD_MAP = { # Descriptions copied from wikipedia: @@ -47,7 +49,7 @@ WORD_MAP = { # G-Codes 'G': { 'class': float, - 'value_regex': re.compile(r'^\d+(\.\d)?'), + 'value_regex': CODE_REGEX, 'description': "Address for preparatory commands", }, # Tool Offsets @@ -81,7 +83,7 @@ WORD_MAP = { # Miscellaneous Function 'M': { 'class': float, - 'value_regex': re.compile(r'^\d+(\.\d)?'), + 'value_regex': CODE_REGEX, 'description': "Miscellaneous function", }, # Line Number diff --git a/tests/test_words.py b/tests/test_words.py index a5ffe95..e42e8ef 100644 --- a/tests/test_words.py +++ b/tests/test_words.py @@ -40,3 +40,44 @@ class WordIterTests(unittest.TestCase): self.assertEqual([w[3].letter, w[3].value], ['I', -0.11]) self.assertEqual([w[4].letter, w[4].value], ['J', -1.26]) self.assertEqual([w[5].letter, w[5].value], ['F', 70]) + + +class WordValueMatchTests(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: + match = regex.search(value_str) + self.assertIsNotNone(match, "failed to match '%s'" % value_str) + self.assertEqual(match.group(), expected_match) + + # Asesrt all elements of negative_list do not match regex + for value_str in negative_list: + match = regex.search(value_str) + self.assertIsNone(match, "matched for '%s'" % value_str) + + def test_float(self): + self.regex_assertions( + regex=words.FLOAT_REGEX, + positive_list=[ + ('1.2', '1.2'), ('1', '1'), ('200', '200'), ('0092', '0092'), + ('1.', '1.'), ('.2', '.2'), ('-1.234', '-1.234'), + ('-1.', '-1.'), ('-.289', '-.289'), + # error cases (only detectable in gcode context) + ('1.2e3', '1.2'), + ], + negative_list=['.', ' 1.2'] + ) + + def test_code(self): + self.regex_assertions( + regex=words.CODE_REGEX, + positive_list=[ + ('1.2', '1.2'), ('1', '1'), ('10', '10'), + ('02', '02'), ('02.3', '02.3'), + ('1.', '1'), ('03 ', '03'), + # error cases (only detectable in gcode context) + ('30.12', '30.1'), + ], + negative_list=['.2', '.', ' 2'] + ) From e84901eccc8f75733598f570e0a8df6b231ce420 Mon Sep 17 00:00:00 2001 From: Peter Boin Date: Wed, 5 Jul 2017 17:43:45 +1000 Subject: [PATCH 04/32] words to gcode class instances --- pygcode/block.py | 2 +- pygcode/file.py | 5 + pygcode/gcodes.py | 225 ++++++++++++++++++++++++++++++++----------- pygcode/machine.py | 16 ++- tests/test_gcodes.py | 26 ++++- 5 files changed, 213 insertions(+), 61 deletions(-) diff --git a/pygcode/block.py b/pygcode/block.py index 49436f2..d7a6041 100644 --- a/pygcode/block.py +++ b/pygcode/block.py @@ -21,7 +21,7 @@ class Block(object): self.text = 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): if k in WORD_MAP: diff --git a/pygcode/file.py b/pygcode/file.py index db99d99..9172e19 100644 --- a/pygcode/file.py +++ b/pygcode/file.py @@ -1,5 +1,6 @@ from .line import Line +from .machine import AbstractMachine class GCodeFile(object): def __init__(self, filename=None): @@ -13,6 +14,10 @@ class GCodeFile(object): self.lines.append(line) +class GCodeWriterMachine(AbstractMachine): + def machine_init(self, *args, **kwargs): + pass + def parse(filename): # FIXME: should be an iterator, and also not terrible file = GCodeFile() diff --git a/pygcode/gcodes.py b/pygcode/gcodes.py index 8c7e2b2..db38797 100644 --- a/pygcode/gcodes.py +++ b/pygcode/gcodes.py @@ -1,3 +1,4 @@ +from collections import defaultdict from .words import Word @@ -7,10 +8,54 @@ class GCode(object): word_key = None # Word instance to use in lookup word_matches = None # function (secondary) # Parameters associated to this gcode - param_words = set() + param_letters = set() - def __init__(self): - self.params = None # TODO + def __init__(self, word, *params): + 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 ======================= @@ -29,7 +74,7 @@ class GCode(object): # G80 Cancel Canned Cycle class GCodeMotion(GCode): - param_words = set('XYZABCUVW') + param_letters = set('XYZABCUVW') class GCodeRapidMove(GCodeMotion): @@ -44,7 +89,7 @@ class GCodeLinearMove(GCodeMotion): class GCodeArcMove(GCodeMotion): """Arc Move""" - param_words = GCodeMotion.param_words | set('IJKRP') + param_letters = GCodeMotion.param_letters | set('IJKRP') class GCodeArcMoveCW(GCodeArcMove): @@ -59,25 +104,25 @@ class GCodeArcMoveCCW(GCodeArcMove): class GCodeDwell(GCodeMotion): """G4: Dwell""" - param_words = GCodeMotion.param_words | set('P') + param_letters = GCodeMotion.param_letters | set('P') word_key = Word('G', 4) class GCodeCublcSpline(GCodeMotion): """G5: Cubic Spline""" - param_words = GCodeMotion.param_words | set('IJPQ') + param_letters = GCodeMotion.param_letters | set('IJPQ') word_key = Word('G', 5) class GCodeQuadraticSpline(GCodeMotion): """G5.1: Quadratic Spline""" - param_words = GCodeMotion.param_words | set('IJ') + param_letters = GCodeMotion.param_letters | set('IJ') word_key = Word('G', 5.1) class GCodeNURBS(GCodeMotion): """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) @@ -95,13 +140,13 @@ class GCodeStraightProbe(GCodeMotion): class GCodeSpindleSyncMotion(GCodeMotion): """G33: Spindle Synchronized Motion""" - param_words = GCodeMotion.param_words | set('K') + param_letters = GCodeMotion.param_letters | set('K') word_key = Word('G', 33) class GCodeRigidTapping(GCodeMotion): """G33.1: Rigid Tapping""" - param_words = GCodeMotion.param_words | set('K') + param_letters = GCodeMotion.param_letters | set('K') 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 class GCodeCannedCycle(GCode): - param_words = set('XYZUVW') + param_letters = set('XYZUVW') class GCodeDrillingCycle(GCodeCannedCycle): """G81: Drilling Cycle""" - param_words = GCodeCannedCycle.param_words | set('RLP') + param_letters = GCodeCannedCycle.param_letters | set('RLP') word_key = Word('G', 81) class GCodeDrillingCycleDwell(GCodeCannedCycle): """G82: Drilling Cycle, Dwell""" - param_words = GCodeCannedCycle.param_words | set('RLP') + param_letters = GCodeCannedCycle.param_letters | set('RLP') word_key = Word('G', 82) class GCodeDrillingCyclePeck(GCodeCannedCycle): """G83: Drilling Cycle, Peck""" - param_words = GCodeCannedCycle.param_words | set('RLQ') + param_letters = GCodeCannedCycle.param_letters | set('RLQ') word_key = Word('G', 83) class GCodeDrillingCycleChipBreaking(GCodeCannedCycle): """G73: Drilling Cycle, ChipBreaking""" - param_words = GCodeCannedCycle.param_words | set('RLQ') + param_letters = GCodeCannedCycle.param_letters | set('RLQ') word_key = Word('G', 73) class GCodeBoringCycleFeedOut(GCodeCannedCycle): """G85: Boring Cycle, Feed Out""" - param_words = GCodeCannedCycle.param_words | set('RLP') + param_letters = GCodeCannedCycle.param_letters | set('RLP') word_key = Word('G', 85) class GCodeBoringCycleDwellFeedOut(GCodeCannedCycle): """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) class GCodeThreadingCycle(GCodeCannedCycle): """G76: Threading Cycle""" - param_words = GCodeCannedCycle.param_words | set('PZIJRKQHLE') + param_letters = GCodeCannedCycle.param_letters | set('PZIJRKQHLE') word_key = Word('G', 76) @@ -243,19 +288,19 @@ class GCodeSpindle(GCode): class GCodeStartSpindleCW(GCodeSpindle): """M3: Start Spindle Clockwise""" - param_words = set('S') + param_letters = set('S') word_key = Word('M', 3) class GCodeStartSpindleCCW(GCodeSpindle): """M4: Start Spindle Counter-Clockwise""" - param_words = set('S') + param_letters = set('S') word_key = Word('M', 4) class GCodeStopSpindle(GCodeSpindle): """M5: Stop Spindle""" - param_words = set('S') + param_letters = set('S') word_key = Word('M', 5) @@ -266,13 +311,13 @@ class GCodeOrientSpindle(GCodeSpindle): class GCodeSpindleConstantSurfaceSpeedMode(GCodeSpindle): """G96: Spindle Constant Surface Speed""" - param_words = set('DS') + param_letters = set('DS') word_key = Word('G', 96) class GCodeSpindleRPMMode(GCodeSpindle): """G97: Spindle RPM Speed""" - param_words = set('D') + param_letters = set('D') word_key = Word('G', 97) @@ -313,7 +358,7 @@ class GCodeToolLength(GCode): class GCodeToolLengthOffset(GCodeToolLength): """G43: Tool Length Offset""" - param_words = set('H') + param_letters = set('H') word_key = Word('G', 43) @@ -324,7 +369,7 @@ class GCodeDynamicToolLengthOffset(GCodeToolLength): class GCodeAddToolLengthOffset(GCodeToolLength): """G43.2: Appkly Additional Tool Length Offset""" - param_words = set('H') + param_letters = set('H') word_key = Word('G', 43.2) @@ -440,25 +485,25 @@ class GCodeCutterRadiusCompOff(GCodeCutterRadiusComp): class GCodeCutterCompLeft(GCodeCutterRadiusComp): """G41: Cutter Radius Compensation (left)""" - param_words = set('D') + param_letters = set('D') word_key = Word('G', 41) class GCodeCutterCompRight(GCodeCutterRadiusComp): """G42: Cutter Radius Compensation (right)""" - param_words = set('D') + param_letters = set('D') word_key = Word('G', 42) class GCodeDynamicCutterCompLeft(GCodeCutterRadiusComp): """G41.1: Dynamic Cutter Radius Compensation (left)""" - param_words = set('DL') + param_letters = set('DL') word_key = Word('G', 41.1) class GCodeDynamicCutterCompRight(GCodeCutterRadiusComp): """G42.1: Dynamic Cutter Radius Compensation (right)""" - param_words = set('DL') + param_letters = set('DL') word_key = Word('G', 42.1) @@ -483,7 +528,7 @@ class GCodeExactStopMode(GCodePathControlMode): class GCodePathBlendingMode(GCodePathControlMode): """G64: Path Blending""" - param_words = set('PQ') + param_letters = set('PQ') word_key = Word('G', 64) @@ -548,25 +593,25 @@ class GCodeSpeedAndFeedOverrideOff(GCodeOtherModal): class GCodeFeedOverride(GCodeOtherModal): """M50: Feed Override Control""" - param_words = set('P') + param_letters = set('P') word_key = Word('M', 50) class GCodeSpindleSpeedOverride(GCodeOtherModal): """M51: Spindle Speed Override Control""" - param_words = set('P') + param_letters = set('P') word_key = Word('M', 51) class GCodeAdaptiveFeed(GCodeOtherModal): """M52: Adaptive Feed Control""" - param_words = set('P') + param_letters = set('P') word_key = Word('M', 52) class GCodeFeedStop(GCodeOtherModal): """M53: Feed Stop Control""" - param_words = set('P') + param_letters = set('P') word_key = Word('M', 53) @@ -614,7 +659,7 @@ class GCodeIO(GCode): class GCodeDigitalOutput(GCodeIO): """Digital Output Control""" - param_words = set('P') + param_letters = set('P') class GCodeDigitalOutputOnSyncd(GCodeDigitalOutput): @@ -639,13 +684,13 @@ class GCodeDigitalOutputOff(GCodeDigitalOutput): class GCodeWaitOnInput(GCodeIO): """M66: Wait on Input""" - param_words = set('PELQ') + param_letters = set('PELQ') word_key = Word('M', 66) class GCodeAnalogOutput(GCodeIO): """Analog Output""" - param_words = set('T') + param_letters = set('T') class GCodeAnalogOutputSyncd(GCodeAnalogOutput): @@ -681,19 +726,19 @@ class GCodeNonModal(GCode): class GCodeToolChange(GCodeNonModal): """M6: Tool Change""" - param_words = set('T') + param_letters = set('T') word_key = Word('M', 6) class GCodeToolSetCurrent(GCodeNonModal): """M61: Set Current Tool""" - param_words = set('Q') + param_letters = set('Q') word_key = Word('M', 61) class GCodeSet(GCodeNonModal): """G10: Set stuff""" - param_words = set('LPQR') + param_letters = set('LPQR') word_key = Word('G', 10) @@ -736,7 +781,7 @@ class GCodeRestoreCoordSystemOffset(GCodeNonModal): class GCodeUserDefined(GCodeNonModal): """M101-M199: User Defined Commands""" # To create user g-codes, inherit from this class - param_words = set('PQ') + param_letters = set('PQ') #@classmethod #def word_matches(cls, w): # return (w.letter == 'M') and (101 <= w.value <= 199) @@ -778,15 +823,15 @@ def _gcode_class_infostr(base_class=GCode): ) return info_str + # ======================= GCode Word Mapping ======================= _gcode_maps_created = False # only set when the below values are populated _gcode_word_map = {} # of the form: {Word('G', 0): GCodeRapidMove, ... } _gcode_function_list = [] # of the form: [(lambda w: w.letter == 'F', GCodeFeedRate), ... ] -def _build_maps(): - """ - Populate _gcode_word_map and _gcode_function_list - """ + +def build_maps(): + """Populate _gcode_word_map and _gcode_function_list""" # Ensure lists are clear global _gcode_word_map global _gcode_function_list @@ -806,19 +851,91 @@ def _build_maps(): global _gcode_maps_created _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): """ Group words into g-codes (includes both G & M codes) :param words: list of Word instances - :return: list containing [, , ..., list()] + :return: tuple([, , ...], list()) """ - if _gcode_maps_created is False: - _build_maps() + gcodes = [] + # 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 - # TODO: next up... + # First determine which words are GCode candidates + 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 = [] - #sdrow = list(reversed(words)) - #for (i, word) in reversed(enumerate(words)): + # Link parameters to candidates + # note: gcode candidates may be valid parameters... therefore + # 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) # {: [], ... } + 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]) diff --git a/pygcode/machine.py b/pygcode/machine.py index 39e49bc..770127e 100644 --- a/pygcode/machine.py +++ b/pygcode/machine.py @@ -11,9 +11,11 @@ class MachineState(object): 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.max_rate = kwargs.get('max_rate', { 'x': 500, # mm/min @@ -31,6 +33,14 @@ class Machine(object): # initialize 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): """Change machine's state based on the given gcode line""" pass # TODO diff --git a/tests/test_gcodes.py b/tests/test_gcodes.py index b15f564..72138ce 100644 --- a/tests/test_gcodes.py +++ b/tests/test_gcodes.py @@ -8,11 +8,11 @@ import unittest _this_path = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) sys.path.insert(0, os.path.join(_this_path, '..')) from pygcode import gcodes - - +from pygcode import words class TestGCodeWordMapping(unittest.TestCase): def test_word_map_integrity(self): - gcodes._build_maps() + + gcodes.build_maps() for (word_maches, fn_class) in gcodes._gcode_function_list: for (word, key_class) in gcodes._gcode_word_map.items(): # Verify that no mapped word will yield a True result @@ -21,3 +21,23 @@ class TestGCodeWordMapping(unittest.TestCase): word_maches(word), "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)) From 3a5dec14f0882e76e11d880242bd1f55bcf3de50 Mon Sep 17 00:00:00 2001 From: Peter Boin Date: Thu, 6 Jul 2017 01:04:37 +1000 Subject: [PATCH 05/32] modal groups --- pygcode/gcodes.py | 120 ++++++++++++++++++++++++++++++++++++++----- tests/test_gcodes.py | 58 ++++++++++++++++++++- 2 files changed, 165 insertions(+), 13 deletions(-) diff --git a/pygcode/gcodes.py b/pygcode/gcodes.py index db38797..4af5578 100644 --- a/pygcode/gcodes.py +++ b/pygcode/gcodes.py @@ -2,14 +2,81 @@ from collections import defaultdict from .words import Word +# Terminology of a "G-Code" +# For the purposes of this library, so-called "G" codes do not necessarily +# use the letter "G" in their word; other letters include M, F, S, and T +# Why? +# I've seen documentation thrown around using the term "gcode" interchangably +# for any word that triggers an action, or mode change. The fact that the +# entire language is called "gcode" obfuscates any other convention. +# Considering it's much easier for me to call everything GCode, I'm taking +# the lazy route, so sue me (but seriously, please don't sue me). +# +# Modality groups +# Modal GCodes: +# A machine's "mode" can be changed by any gcode with a modal_group. +# This state is retained by the machine in the blocks to follow until +# the "mode" is revoked, or changed. +# A typical example of this is units: +# G20 +# this will change the machine's "mode" to treat all positions as +# millimeters; G20 does not have to be on every line, thus the machine's +# "mode" is in millimeters for that, and every block after it until G21 +# is specified (changing units to inches). +# +# Modal Groups: +# Only one mode of each modal group can be active. That is to say, a +# modal g-code can only change the sate of a previously set mode if +# they're in the same group. +# For example: +# G20 (mm), and G21 (inches) are in group 6 +# G1 (linear movement), and G2 (arc movement) are in group 1 +# A machine can't use mm and inches at the same time, just as it can't +# move in a straight line, and move in an arc simultaneously. +# +# There are 15 groups: +# ref: http://linuxcnc.org/docs/html/gcode/overview.html#_modal_groups +# +# Table 5. G-Code Modal Groups +# MODAL GROUP MEANING MEMBER WORDS +# Non-modal codes (Group 0) G4, G10 G28, G30, G53 G92, G92.1, G92.2, G92.3, +# Motion (Group 1) G0, G1, G2, G3, G33, G38.x, G73, G76, G80, G81 +# G82, G83, G84, G85, G86, G87, G88,G89 +# Plane selection (Group 2) G17, G18, G19, G17.1, G18.1, G19.1 +# Distance Mode (Group 3) G90, G91 +# Arc IJK Distance Mode (Group 4) G90.1, G91.1 +# Feed Rate Mode (Group 5) G93, G94, G95 +# Units (Group 6) G20, G21 +# Cutter Diameter Compensation (Group 7) G40, G41, G42, G41.1, G42.1 +# Tool Length Offset (Group 8) G43, G43.1, G49 +# Canned Cycles Return Mode (Group 10) G98, G99 +# Coordinate System (Group 12) G54, G55, G56, G57, G58, G59, +# G59.1, G59.2, G59.3 +# Control Mode (Group 13) G61, G61.1, G64 +# Spindle Speed Mode (Group 14) G96, G97 +# Lathe Diameter Mode (Group 15) G7, G8 +# +# Table 6. M-Code Modal Groups +# MODAL GROUP MEANING MEMBER WORDS +# Stopping (Group 4) M0, M1, M2, M30, M60 +# Spindle (Group 7) M3, M4, M5 +# Coolant (Group 8) (M7 M8 can both be on), M9 +# Override Switches (Group 9) M48, M49 +# User Defined (Group 10) M100-M199 +# + class GCode(object): # Defining Word word_key = None # Word instance to use in lookup word_matches = None # function (secondary) + # Parameters associated to this gcode param_letters = set() + # Modal Group + modal_group = None + def __init__(self, word, *params): assert isinstance(word, Word), "invalid gcode word %r" % code_word self.word = word @@ -75,6 +142,7 @@ class GCode(object): class GCodeMotion(GCode): param_letters = set('XYZABCUVW') + modal_group = 1 class GCodeRapidMove(GCodeMotion): @@ -106,6 +174,7 @@ class GCodeDwell(GCodeMotion): """G4: Dwell""" param_letters = GCodeMotion.param_letters | set('P') word_key = Word('G', 4) + modal_group = None # one of the few motion commands that isn't modal class GCodeCublcSpline(GCodeMotion): @@ -168,7 +237,7 @@ class GCodeCancelCannedCycle(GCodeMotion): class GCodeCannedCycle(GCode): param_letters = set('XYZUVW') - + modal_group = 1 class GCodeDrillingCycle(GCodeCannedCycle): """G81: Drilling Cycle""" @@ -226,31 +295,36 @@ class GCodeDistanceMode(GCode): class GCodeAbsoluteDistanceMode(GCodeDistanceMode): """G90: Absolute Distance Mode""" word_key = Word('G', 90) - + modal_group = 3 class GCodeIncrementalDistanceMode(GCodeDistanceMode): """G91: Incremental Distance Mode""" word_key = Word('G', 91) + modal_group = 3 class GCodeAbsoluteArcDistanceMode(GCodeDistanceMode): """G90.1: Absolute Distance Mode for Arc IJK Parameters""" word_key = Word('G', 90.1) + modal_group = 4 class GCodeIncrementalArcDistanceMode(GCodeDistanceMode): """G91.1: Incremental Distance Mode for Arc IJK Parameters""" word_key = Word('G', 91.1) + modal_group = 4 class GCodeLatheDiameterMode(GCodeDistanceMode): """G7: Lathe Diameter Mode""" word_key = Word('G', 7) + modal_group = 15 class GCodeLatheRadiusMode(GCodeDistanceMode): """G8: Lathe Radius Mode""" word_key = Word('G', 8) + modal_group = 15 # ======================= Feed Rate Mode ======================= @@ -258,7 +332,7 @@ class GCodeLatheRadiusMode(GCodeDistanceMode): # G93, G94, G95 Feed Rate Mode class GCodeFeedRateMode(GCode): - pass + modal_group = 5 class GCodeInverseTimeMode(GCodeFeedRateMode): @@ -290,18 +364,20 @@ class GCodeStartSpindleCW(GCodeSpindle): """M3: Start Spindle Clockwise""" param_letters = set('S') word_key = Word('M', 3) - + modal_group = 7 class GCodeStartSpindleCCW(GCodeSpindle): """M4: Start Spindle Counter-Clockwise""" param_letters = set('S') word_key = Word('M', 4) + modal_group = 7 class GCodeStopSpindle(GCodeSpindle): """M5: Stop Spindle""" param_letters = set('S') word_key = Word('M', 5) + modal_group = 7 class GCodeOrientSpindle(GCodeSpindle): @@ -313,12 +389,14 @@ class GCodeSpindleConstantSurfaceSpeedMode(GCodeSpindle): """G96: Spindle Constant Surface Speed""" param_letters = set('DS') word_key = Word('G', 96) + modal_group = 14 class GCodeSpindleRPMMode(GCodeSpindle): """G97: Spindle RPM Speed""" param_letters = set('D') word_key = Word('G', 97) + modal_group = 14 @@ -327,7 +405,7 @@ class GCodeSpindleRPMMode(GCodeSpindle): # M7, M8, M9 Coolant Control class GCodeCoolant(GCode): - pass + modal_group = 8 class GCodeCoolantMistOn(GCodeCoolant): @@ -353,7 +431,7 @@ class GCodeCoolantOff(GCodeCoolant): # G49 Cancel Tool Length Compensation class GCodeToolLength(GCode): - pass + modal_group = 8 class GCodeToolLengthOffset(GCodeToolLength): @@ -385,7 +463,7 @@ class GCodeCancelToolLengthOffset(GCodeToolLength): # M60 Pallet Change Pause class GCodeProgramControl(GCode): - pass + modal_group = 4 class GCodePauseProgram(GCodeProgramControl): @@ -418,12 +496,14 @@ class GCodePalletChangePause(GCodeProgramControl): # G20, G21 Units class GCodeUnit(GCode): - pass + modal_group = 6 + class GCodeUseInches(GCodeUnit): """G20: use inches for length units""" word_key = Word('G', 20) + class GCodeUseMillimeters(GCodeUnit): """G21: use millimeters for length units""" word_key = Word('G', 21) @@ -435,7 +515,7 @@ class GCodeUseMillimeters(GCodeUnit): # G17 - G19.1 Plane Select class GCodePlaneSelect(GCode): - pass + modal_group = 2 class GCodeSelectZYPlane(GCodePlaneSelect): @@ -475,7 +555,7 @@ class GCodeSelectVWPlane(GCodePlaneSelect): # G41.1, G42.1 D L Dynamic Cutter Compensation class GCodeCutterRadiusComp(GCode): - pass + modal_group = 7 class GCodeCutterRadiusCompOff(GCodeCutterRadiusComp): @@ -513,7 +593,7 @@ class GCodeDynamicCutterCompRight(GCodeCutterRadiusComp): # G64 P Q Path Blending class GCodePathControlMode(GCode): - pass + modal_group = 13 class GCodeExactPathMode(GCodePathControlMode): @@ -537,7 +617,7 @@ class GCodePathBlendingMode(GCodePathControlMode): # G98 Canned Cycle Return Level class GCodeCannedReturnMode(GCode): - pass + modal_group = 10 class GCodeCannedCycleReturnLevel(GCodeCannedReturnMode): @@ -566,12 +646,21 @@ class GCodeFeedRate(GCodeOtherModal): @classmethod def word_matches(cls, w): return w.letter == 'F' + # Modal Group n/a: + # although this sets the machine's state, there are no other codes to + # group with this one, so although it's modal, it has no group. + #modal_group = n/a + class GCodeSpindleSpeed(GCodeOtherModal): """S: Set Spindle Speed""" @classmethod def word_matches(cls, w): return w.letter == 'S' + # Modal Group n/a: + # although this sets the machine's state, there are no other codes to + # group with this one, so although it's modal, it has no group. + #modal_group = n/a class GCodeSelectTool(GCodeOtherModal): @@ -579,16 +668,22 @@ class GCodeSelectTool(GCodeOtherModal): @classmethod def word_matches(cls, w): return w.letter == 'T' + # Modal Group n/a: + # although this sets the machine's state, there are no other codes to + # group with this one, so although it's modal, it has no group. + #modal_group = n/a class GCodeSpeedAndFeedOverrideOn(GCodeOtherModal): """M48: Speed and Feed Override Control On""" word_key = Word('M', 48) + modal_group = 9 class GCodeSpeedAndFeedOverrideOff(GCodeOtherModal): """M49: Speed and Feed Override Control Off""" word_key = Word('M', 49) + modal_group = 9 class GCodeFeedOverride(GCodeOtherModal): @@ -630,6 +725,7 @@ class GCodeSelectCoordinateSystem(GCodeOtherModal): @classmethod 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]) + modal_group = 12 # ======================= Flow-control Codes ======================= diff --git a/tests/test_gcodes.py b/tests/test_gcodes.py index 72138ce..8462928 100644 --- a/tests/test_gcodes.py +++ b/tests/test_gcodes.py @@ -1,7 +1,7 @@ import sys import os import inspect - +import re import unittest # Units Under Test @@ -22,6 +22,62 @@ class TestGCodeWordMapping(unittest.TestCase): "conflict with %s and %s" % (fn_class, key_class) ) +class TestGCodeModalGroups(unittest.TestCase): + def test_modal_groups(self): + # Modal groups taken (and slightly modified) from LinuxCNC documentation: + # link: http://linuxcnc.org/docs/html/gcode/overview.html#_modal_groups + table_rows = '' + # Table 5. G-Code Modal Groups + # MODAL GROUP MEANING MEMBER WORDS + table_rows += ''' + Non-modal codes (Group 0) G4,G10,G28,G30,G53,G92,G92.1,G92.2,G92.3 + Motion (Group 1) G0,G1,G2,G3,G33,G38.2,G38.3,G38.4 + Motion (Group 1) G38.5,G73,G76,G80,G81,G82,G83,G85,G89 + Plane selection (Group 2) G17, G18, G19, G17.1, G18.1, G19.1 + Distance Mode (Group 3) G90, G91 + Arc IJK Distance Mode (Group 4) G90.1, G91.1 + Feed Rate Mode (Group 5) G93, G94, G95 + Units (Group 6) G20, G21 + Cutter Diameter Compensation (Group 7) G40, G41, G42, G41.1, G42.1 + Tool Length Offset (Group 8) G43, G43.1, G49 + Canned Cycles Return Mode (Group 10) G98 + Coordinate System (Group 12) G54,G55,G56,G57,G58,G59,G59.1,G59.2,G59.3 + Control Mode (Group 13) G61, G61.1, G64 + Spindle Speed Mode (Group 14) G96, G97 + Lathe Diameter Mode (Group 15) G7,G8 + ''' + + # Table 6. M-Code Modal Groups + # MODAL GROUP MEANING MEMBER WORDS + table_rows += ''' + Stopping (Group 4) M0, M1, M2, M30, M60 + Spindle (Group 7) M3, M4, M5 + Coolant (Group 8) M7, M8, M9 + Override Switches (Group 9) M48, M49 + ''' + + for row in table_rows.split('\n'): + match = re.search(r'^\s*(?P.*)\s*\(Group (?P<group>\d+)\)\s*(?P<words>.*)$', row, re.I) + if match: + for word_str in re.split(r'\s*,\s*', match.group('words')): + word = list(words.iter_words(word_str))[0] + gcode_class = gcodes.word_gcode_class(word) + # GCode class found for each word in the table + self.assertIsNotNone(gcode_class) + # GCode's modal group equals that defined in the table + expected_group = int(match.group('group')) + if expected_group == 0: + self.assertIsNone( + gcode_class.modal_group, + "%s modal_group: %s is not None" % (gcode_class, gcode_class.modal_group) + ) + else: + self.assertEqual( + gcode_class.modal_group, expected_group, + "%s != %s (%r)" % (gcode_class.modal_group, expected_group, word) + ) + + class TestWordsToGCodes(unittest.TestCase): def test_stuff(self): # FIXME: function name line = 'G1 X82.6892 Y-38.6339 F1500' From cf5f9a17cd2b041ae28304933a8dc674e04e13e9 Mon Sep 17 00:00:00 2001 From: Peter Boin <nymphii@gmail.com> Date: Thu, 6 Jul 2017 03:14:09 +1000 Subject: [PATCH 06/32] gcodes sortable by linuxcnc execution order --- pygcode/block.py | 27 +++++++++- pygcode/gcodes.py | 119 +++++++++++++++++++++++++++++++++++---------- pygcode/machine.py | 24 +++++++++ pygcode/words.py | 93 ++++------------------------------- 4 files changed, 154 insertions(+), 109 deletions(-) diff --git a/pygcode/block.py b/pygcode/block.py index d7a6041..9e4b178 100644 --- a/pygcode/block.py +++ b/pygcode/block.py @@ -21,7 +21,32 @@ class Block(object): self.text = text self.words = list(iter_words(self.text)) - self.gcodes = list(words_to_gcodes(self.words)) + (self.gcodes, self.modal_params) = words_to_gcodes(self.words) + + self._assert_gcodes() + + # TODO: gcode verification + # - gcodes in the same modal_group raises exception + + def _assert_gcodes(self): + modal_groups = set() + code_words = set() + for gc in self.gcodes: + # Assert all gcodes are not repeated in the same block + if gc.word in code_words: + raise AssertionError("%s cannot be in the same block" % ([ + x for x in self.gcodes + if x.modal_group == gc.modal_group + ])) + code_words.add(gc.word) + # Assert all gcodes are from different modal groups + if gc.modal_group is not None: + if gc.modal_group in modal_groups: + raise AssertionError("%s cannot be in the same block" % ([ + x for x in self.gcodes + if x.modal_group == gc.modal_group + ])) + modal_groups.add(gc.modal_group) def __getattr__(self, k): if k in WORD_MAP: diff --git a/pygcode/gcodes.py b/pygcode/gcodes.py index 4af5578..5caf0ca 100644 --- a/pygcode/gcodes.py +++ b/pygcode/gcodes.py @@ -64,6 +64,34 @@ from .words import Word # Override Switches (Group 9) M48, M49 # User Defined (Group 10) M100-M199 # +# Execution Order +# Order taken http://linuxcnc.org/docs/html/gcode/overview.html#_g_code_order_of_execution +# (as of 2017-07-03) +# 010: O-word commands (optionally followed by a comment but no other words allowed on the same line) +# 020: Comment (including message) +# 030: Set feed rate mode (G93, G94). +# 040: Set feed rate (F). +# 050: Set spindle speed (S). +# 060: Select tool (T). +# 070: HAL pin I/O (M62-M68). +# 080: Change tool (M6) and Set Tool Number (M61). +# 090: Spindle on or off (M3, M4, M5). +# 100: Save State (M70, M73), Restore State (M72), Invalidate State (M71). +# 110: Coolant on or off (M7, M8, M9). +# 120: Enable or disable overrides (M48, M49,M50,M51,M52,M53). +# 130: User-defined Commands (M100-M199). +# 140: Dwell (G4). +# 150: Set active plane (G17, G18, G19). +# 160: Set length units (G20, G21). +# 170: Cutter radius compensation on or off (G40, G41, G42) +# 180: Cutter length compensation on or off (G43, G49) +# 190: Coordinate system selection (G54, G55, G56, G57, G58, G59, G59.1, G59.2, G59.3). +# 200: Set path control mode (G61, G61.1, G64) +# 210: Set distance mode (G90, G91). +# 220: Set retract mode (G98, G99). +# 230: Go to reference location (G28, G30) or change coordinate system data (G10) or set axis offsets (G92, G92.1, G92.2, G94). +# 240: Perform motion (G0 to G3, G33, G38.x, G73, G76, G80 to G89), as modified (possibly) by G53. +# 250: Stop (M0, M1, M2, M30, M60). class GCode(object): @@ -77,6 +105,9 @@ class GCode(object): # Modal Group modal_group = None + # Execution Order + exec_order = 999 # if not otherwise specified, run last + def __init__(self, word, *params): assert isinstance(word, Word), "invalid gcode word %r" % code_word self.word = word @@ -87,13 +118,16 @@ class GCode(object): 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([ + param_str = '' + if self.params: + param_str = "{%s}" % (', '.join([ "{}".format(self.params[k]) for k in sorted(self.params.keys()) - ]), + ])) + return "<{class_name}: {gcode}{params}>".format( + class_name=self.__class__.__name__, + gcode=self.word, + params=param_str, ) def __str__(self): @@ -106,6 +140,10 @@ class GCode(object): ]), ) + # Sort by exec_order + def __lt__(self, other): + return self.exec_order < other.exec_order + 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)) @@ -113,6 +151,7 @@ class GCode(object): self.params[word.letter] = word def __getattr__(self, key): + # Return parameter values (if valid parameter for gcode) if key in self.param_letters: if key in self.params: return self.params[key].value @@ -124,6 +163,10 @@ class GCode(object): key=key )) + @property + def description(self): + return self.__doc__ + # ======================= Motion ======================= # (X Y Z A B C U V W apply to all motions) @@ -143,7 +186,7 @@ class GCode(object): class GCodeMotion(GCode): param_letters = set('XYZABCUVW') modal_group = 1 - + exec_order = 241 class GCodeRapidMove(GCodeMotion): """G0: Rapid Move""" @@ -175,6 +218,7 @@ class GCodeDwell(GCodeMotion): param_letters = GCodeMotion.param_letters | set('P') word_key = Word('G', 4) modal_group = None # one of the few motion commands that isn't modal + exec_order = 140 class GCodeCublcSpline(GCodeMotion): @@ -238,6 +282,7 @@ class GCodeCancelCannedCycle(GCodeMotion): class GCodeCannedCycle(GCode): param_letters = set('XYZUVW') modal_group = 1 + exec_order = 241 class GCodeDrillingCycle(GCodeCannedCycle): """G81: Drilling Cycle""" @@ -289,7 +334,7 @@ class GCodeThreadingCycle(GCodeCannedCycle): # G8 Lathe Radius Mode class GCodeDistanceMode(GCode): - pass + exec_order = 210 class GCodeAbsoluteDistanceMode(GCodeDistanceMode): @@ -333,7 +378,7 @@ class GCodeLatheRadiusMode(GCodeDistanceMode): class GCodeFeedRateMode(GCode): modal_group = 5 - + exec_order = 30 class GCodeInverseTimeMode(GCodeFeedRateMode): """G93: Inverse Time Mode""" @@ -357,25 +402,25 @@ class GCodeUnitsPerRevolution(GCodeFeedRateMode): # G96, G97 S D Spindle Control Mode class GCodeSpindle(GCode): - pass + exec_order = 90 class GCodeStartSpindleCW(GCodeSpindle): """M3: Start Spindle Clockwise""" - param_letters = set('S') + #param_letters = set('S') # S is it's own gcode, makes no sense to be here word_key = Word('M', 3) modal_group = 7 class GCodeStartSpindleCCW(GCodeSpindle): """M4: Start Spindle Counter-Clockwise""" - param_letters = set('S') + #param_letters = set('S') # S is it's own gcode, makes no sense to be here word_key = Word('M', 4) modal_group = 7 class GCodeStopSpindle(GCodeSpindle): """M5: Stop Spindle""" - param_letters = set('S') + #param_letters = set('S') # S is it's own gcode, makes no sense to be here word_key = Word('M', 5) modal_group = 7 @@ -406,6 +451,7 @@ class GCodeSpindleRPMMode(GCodeSpindle): class GCodeCoolant(GCode): modal_group = 8 + exec_order = 110 class GCodeCoolantMistOn(GCodeCoolant): @@ -432,6 +478,7 @@ class GCodeCoolantOff(GCodeCoolant): class GCodeToolLength(GCode): modal_group = 8 + exec_order = 180 class GCodeToolLengthOffset(GCodeToolLength): @@ -464,7 +511,7 @@ class GCodeCancelToolLengthOffset(GCodeToolLength): class GCodeProgramControl(GCode): modal_group = 4 - + exec_order = 250 class GCodePauseProgram(GCodeProgramControl): """M0: Program Pause""" @@ -497,6 +544,7 @@ class GCodePalletChangePause(GCodeProgramControl): class GCodeUnit(GCode): modal_group = 6 + exec_order = 160 class GCodeUseInches(GCodeUnit): @@ -516,6 +564,7 @@ class GCodeUseMillimeters(GCodeUnit): class GCodePlaneSelect(GCode): modal_group = 2 + exec_order = 150 class GCodeSelectZYPlane(GCodePlaneSelect): @@ -556,6 +605,7 @@ class GCodeSelectVWPlane(GCodePlaneSelect): class GCodeCutterRadiusComp(GCode): modal_group = 7 + exec_order = 170 class GCodeCutterRadiusCompOff(GCodeCutterRadiusComp): @@ -594,6 +644,7 @@ class GCodeDynamicCutterCompRight(GCodeCutterRadiusComp): class GCodePathControlMode(GCode): modal_group = 13 + exec_order = 200 class GCodeExactPathMode(GCodePathControlMode): @@ -618,6 +669,7 @@ class GCodePathBlendingMode(GCodePathControlMode): class GCodeCannedReturnMode(GCode): modal_group = 10 + exec_order = 220 class GCodeCannedCycleReturnLevel(GCodeCannedReturnMode): @@ -646,10 +698,12 @@ class GCodeFeedRate(GCodeOtherModal): @classmethod def word_matches(cls, w): return w.letter == 'F' - # Modal Group n/a: + # Modal Group : # although this sets the machine's state, there are no other codes to - # group with this one, so although it's modal, it has no group. - #modal_group = n/a + # group with this one, so although it's modal, it usually has no group. + # However, + modal_group = 101 + exec_order = 40 class GCodeSpindleSpeed(GCodeOtherModal): @@ -657,10 +711,9 @@ class GCodeSpindleSpeed(GCodeOtherModal): @classmethod def word_matches(cls, w): return w.letter == 'S' - # Modal Group n/a: - # although this sets the machine's state, there are no other codes to - # group with this one, so although it's modal, it has no group. - #modal_group = n/a + # Modal Group: (see description in GCodeFeedRate) + modal_group = 102 + exec_order = 50 class GCodeSelectTool(GCodeOtherModal): @@ -668,46 +721,51 @@ class GCodeSelectTool(GCodeOtherModal): @classmethod def word_matches(cls, w): return w.letter == 'T' - # Modal Group n/a: - # although this sets the machine's state, there are no other codes to - # group with this one, so although it's modal, it has no group. - #modal_group = n/a + # Modal Group: (see description in GCodeFeedRate) + modal_group = 103 + exec_order = 60 class GCodeSpeedAndFeedOverrideOn(GCodeOtherModal): """M48: Speed and Feed Override Control On""" word_key = Word('M', 48) modal_group = 9 + exec_order = 120 class GCodeSpeedAndFeedOverrideOff(GCodeOtherModal): """M49: Speed and Feed Override Control Off""" word_key = Word('M', 49) modal_group = 9 + exec_order = 120 class GCodeFeedOverride(GCodeOtherModal): """M50: Feed Override Control""" param_letters = set('P') word_key = Word('M', 50) + exec_order = 120 class GCodeSpindleSpeedOverride(GCodeOtherModal): """M51: Spindle Speed Override Control""" param_letters = set('P') word_key = Word('M', 51) + exec_order = 120 class GCodeAdaptiveFeed(GCodeOtherModal): """M52: Adaptive Feed Control""" param_letters = set('P') word_key = Word('M', 52) + exec_order = 120 class GCodeFeedStop(GCodeOtherModal): """M53: Feed Stop Control""" param_letters = set('P') word_key = Word('M', 53) + exec_order = 120 class GCodeSelectCoordinateSystem(GCodeOtherModal): @@ -726,6 +784,7 @@ class GCodeSelectCoordinateSystem(GCodeOtherModal): 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]) modal_group = 12 + exec_order = 190 # ======================= Flow-control Codes ======================= @@ -750,7 +809,7 @@ class GCodeSelectCoordinateSystem(GCodeOtherModal): # M68 T Analog Output, Immediate class GCodeIO(GCode): - pass + exec_order = 70 class GCodeDigitalOutput(GCodeIO): @@ -824,18 +883,21 @@ class GCodeToolChange(GCodeNonModal): """M6: Tool Change""" param_letters = set('T') word_key = Word('M', 6) + exec_order = 80 class GCodeToolSetCurrent(GCodeNonModal): """M61: Set Current Tool""" param_letters = set('Q') word_key = Word('M', 61) + exec_order = 80 class GCodeSet(GCodeNonModal): """G10: Set stuff""" param_letters = set('LPQR') word_key = Word('G', 10) + exec_order = 230 class GCodeGotoPredefinedPosition(GCodeNonModal): @@ -843,6 +905,7 @@ class GCodeGotoPredefinedPosition(GCodeNonModal): @classmethod def word_matches(cls, w): return (w.letter == 'G') and (w.value in [28, 30]) + exec_order = 230 class GCodeSetPredefinedPosition(GCodeNonModal): @@ -850,16 +913,19 @@ class GCodeSetPredefinedPosition(GCodeNonModal): @classmethod def word_matches(cls, w): return (w.letter == 'G') and (w.value in [28.1, 30.1]) + exec_order = 230 class GCodeMoveInMachineCoords(GCodeNonModal): """G53: Move in Machine Coordinates""" word_key = Word('G', 53) + exec_order = 240 class GCodeCoordSystemOffset(GCodeNonModal): """G92: Coordinate System Offset""" word_key = Word('G', 92) + exec_order = 230 class GCodeResetCoordSystemOffset(GCodeNonModal): @@ -867,11 +933,13 @@ class GCodeResetCoordSystemOffset(GCodeNonModal): @classmethod def word_matches(cls, w): return (w.letter == 'G') and (w.value in [92.1, 92.2]) + exec_order = 230 class GCodeRestoreCoordSystemOffset(GCodeNonModal): """G92.3: Restore Coordinate System Offset""" word_key = Word('G', 92.3) + exec_order = 230 class GCodeUserDefined(GCodeNonModal): @@ -881,6 +949,7 @@ class GCodeUserDefined(GCodeNonModal): #@classmethod #def word_matches(cls, w): # return (w.letter == 'M') and (101 <= w.value <= 199) + exec_order = 130 # ======================= Utilities ======================= diff --git a/pygcode/machine.py b/pygcode/machine.py index 770127e..91c82a0 100644 --- a/pygcode/machine.py +++ b/pygcode/machine.py @@ -44,3 +44,27 @@ class AbstractMachine(object): def process_line(self, line): """Change machine's state based on the given gcode line""" pass # TODO + + + + +""" +class Axes(object): + pass + +class MyMachineState(MachineState): + axes_state_class = AxesState + pass + +class MyMachine(AbstractMachine): + available_axes = set('xyz') + state_class = MyMachineState + + +m = MyMachine( + state=MyMachineState( + absolute_position= + ), +) + +""" diff --git a/pygcode/words.py b/pygcode/words.py index 7f38c5e..9765b65 100644 --- a/pygcode/words.py +++ b/pygcode/words.py @@ -4,12 +4,12 @@ import six from .exceptions import GCodeBlockFormatError + FLOAT_REGEX = re.compile(r'^-?(\d+\.?\d*|\.\d+)') # testcase: ..tests.test_words.WordValueMatchTests.test_float INT_REGEX = re.compile(r'^-?\d+') POSITIVEINT_REGEX = re.compile(r'^\d+') CODE_REGEX = re.compile(r'^\d+(\.\d)?') # similar - WORD_MAP = { # Descriptions copied from wikipedia: # https://en.wikipedia.org/wiki/G-code#Letter_addresses @@ -163,87 +163,6 @@ WORD_MAP = { } -ORDER_LINUXCNC_LETTER_MAP = { - 'O': 10, - 'F': 40, - 'S': 50, - 'T': 60, -} - -_v_csv = lambda v, ks: [(k, v) for k in ks.split(',')] - -ORDER_LINUXCNC_LETTERVALUE_MAP = dict(itertools.chain.from_iterable([ - _v_csv(30, 'G93,G94'), - _v_csv(70, 'M62,M63,M64,M65,M66,M67,M68'), - _v_csv(80, 'M6,M61'), - _v_csv(90, 'M3,M4,M5'), - _v_csv(100, 'M71,M73,M72,M71'), - _v_csv(110, 'M7,M8,M9'), - _v_csv(120, 'M48,M49,M50,M51,M52,M53'), - [('G4', 140)], - _v_csv(150, 'G17,G18,G19'), - _v_csv(160, 'G20,G21'), - _v_csv(170, 'G40,G41,G42'), - _v_csv(180, 'G43,G49'), - _v_csv(190, 'G54,G55,G56,G57,G58,G59,G59.1,G59.2,G59.3'), - _v_csv(200, 'G61,G61.1,G64'), - _v_csv(210, 'G90,G91'), - _v_csv(220, 'G98,G99'), - _v_csv(230, 'G28,G30,G10,G92,G92.1,G92.2,G94'), - _v_csv(240, 'G0,G1,G2,G3,G33,G73,G76,G80,G81,G82,G83,G84,G85,G86,G87,G88,G89'), - _v_csv(250, 'M0,M1,M2,M30,M60'), -])) - -def _word_order_linuxcnc(word): - ''' - Order taken http://linuxcnc.org/docs/html/gcode/overview.html#_g_code_order_of_execution - (as of 2017-07-03) - 010: O-word commands (optionally followed by a comment but no other words allowed on the same line) - N/A: Comment (including message) - 030: Set feed rate mode (G93, G94). - 040: Set feed rate (F). - 050: Set spindle speed (S). - 060: Select tool (T). - 070: HAL pin I/O (M62-M68). - 080: Change tool (M6) and Set Tool Number (M61). - 090: Spindle on or off (M3, M4, M5). - 100: Save State (M70, M73), Restore State (M72), Invalidate State (M71). - 110: Coolant on or off (M7, M8, M9). - 120: Enable or disable overrides (M48, M49,M50,M51,M52,M53). - 130: User-defined Commands (M100-M199). - 140: Dwell (G4). - 150: Set active plane (G17, G18, G19). - 160: Set length units (G20, G21). - 170: Cutter radius compensation on or off (G40, G41, G42) - 180: Cutter length compensation on or off (G43, G49) - 190: Coordinate system selection (G54, G55, G56, G57, G58, G59, G59.1, G59.2, G59.3). - 200: Set path control mode (G61, G61.1, G64) - 210: Set distance mode (G90, G91). - 220: Set retract mode (G98, G99). - 230: Go to reference location (G28, G30) or change coordinate system data (G10) or set axis offsets (G92, G92.1, G92.2, G94). - 240: Perform motion (G0 to G3, G33, G38.x, G73, G76, G80 to G89), as modified (possibly) by G53. - 250: Stop (M0, M1, M2, M30, M60). - 900 + letter val: (else) - ''' - if word.letter in ORDER_LINUXCNC_LETTER_MAP: - return ORDER_LINUXCNC_LETTER_MAP[word.letter] - letter_value = str(word) - if letter_value in ORDER_LINUXCNC_LETTERVALUE_MAP: - return ORDER_LINUXCNC_LETTERVALUE_MAP[letter_value] - - # special cases - if (word.letter == 'M') and (100 <= int(word.value) <= 199): - return 130 - if (word.letter == 'G') and (38 < float(word.value) < 39): - return 240 - - # otherwise, sort last, in alphabetic order - return (900 + (ord(word.letter) - ord('A'))) - -def by_linuxcnc_order(word): - return word.orderval_linuxcnc - - class Word(object): def __init__(self, letter, value): self.letter = letter.upper() @@ -302,7 +221,7 @@ class Word(object): @property def description(self): - return 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) @@ -336,3 +255,11 @@ def iter_words(block_text): remainder = block_text[index:] if remainder and re.search(r'\S', remainder): raise GCodeBlockFormatError("block code remaining '%s'" % remainder) + + +def str2word(word_str): + words = list(iter_words(word_str)) + if words: + assert len(words) <= 1, "more than one word given" + return words[0] + return None From c7578de0d74ee0748f713360083e1f3d4726927b Mon Sep 17 00:00:00 2001 From: Peter Boin <nymphii@gmail.com> Date: Thu, 6 Jul 2017 22:11:20 +1000 Subject: [PATCH 07/32] named modal groups, clean words to str --- pygcode/block.py | 10 +++- pygcode/file.py | 1 + pygcode/gcodes.py | 127 ++++++++++++++++++++++++++++------------- pygcode/line.py | 7 +-- pygcode/words.py | 132 +++++++++++++++++++++++++++++-------------- tests/test_file.py | 2 +- tests/test_gcodes.py | 2 +- tests/test_words.py | 4 +- 8 files changed, 194 insertions(+), 91 deletions(-) diff --git a/pygcode/block.py b/pygcode/block.py index 9e4b178..2f456ba 100644 --- a/pygcode/block.py +++ b/pygcode/block.py @@ -1,6 +1,6 @@ import re from .words import iter_words, WORD_MAP -from .gcodes import words_to_gcodes +from .gcodes import words2gcodes class Block(object): """GCode block (effectively any gcode file line that defines any <word><value>)""" @@ -21,7 +21,7 @@ class Block(object): self.text = 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() @@ -31,7 +31,9 @@ class Block(object): def _assert_gcodes(self): modal_groups = set() code_words = set() + for gc in self.gcodes: + # Assert all gcodes are not repeated in the same block if gc.word in code_words: raise AssertionError("%s cannot be in the same block" % ([ @@ -39,6 +41,7 @@ class Block(object): if x.modal_group == gc.modal_group ])) code_words.add(gc.word) + # Assert all gcodes are from different modal groups if gc.modal_group is not None: if gc.modal_group in modal_groups: @@ -61,3 +64,6 @@ class Block(object): cls=self.__class__.__name__, key=k )) + + def __str__(self): + return ' '.join(str(x) for x in (self.gcodes + [p.clean_str for p in self.modal_params])) diff --git a/pygcode/file.py b/pygcode/file.py index 9172e19..ed8d780 100644 --- a/pygcode/file.py +++ b/pygcode/file.py @@ -24,5 +24,6 @@ def parse(filename): with open(filename, 'r') as fh: for line in fh.readlines(): line_obj = Line(line) + # FIXME: don't dump entire file into RAM; change to generator model file.append(line_obj) return file diff --git a/pygcode/gcodes.py b/pygcode/gcodes.py index 5caf0ca..3d1ccff 100644 --- a/pygcode/gcodes.py +++ b/pygcode/gcodes.py @@ -64,6 +64,44 @@ from .words import Word # Override Switches (Group 9) M48, M49 # 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 # Order taken http://linuxcnc.org/docs/html/gcode/overview.html#_g_code_order_of_execution # (as of 2017-07-03) @@ -132,12 +170,15 @@ class GCode(object): 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]) + param_str = '' + if self.params: + param_str += ' ' + ' '.join([ + "{}".format(self.params[k].clean_str) for k in sorted(self.params.keys()) - ]), + ]) + return "{gcode}{parameters}".format( + gcode=self.word.clean_str, + parameters=param_str, ) # Sort by exec_order @@ -167,6 +208,16 @@ class GCode(object): def description(self): 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 ======================= # (X Y Z A B C U V W apply to all motions) @@ -185,7 +236,7 @@ class GCode(object): class GCodeMotion(GCode): param_letters = set('XYZABCUVW') - modal_group = 1 + modal_group = MODAL_GROUP_NUM['motion'] exec_order = 241 class GCodeRapidMove(GCodeMotion): @@ -281,7 +332,7 @@ class GCodeCancelCannedCycle(GCodeMotion): class GCodeCannedCycle(GCode): param_letters = set('XYZUVW') - modal_group = 1 + modal_group = MODAL_GROUP_NUM['motion'] exec_order = 241 class GCodeDrillingCycle(GCodeCannedCycle): @@ -340,36 +391,36 @@ class GCodeDistanceMode(GCode): class GCodeAbsoluteDistanceMode(GCodeDistanceMode): """G90: Absolute Distance Mode""" word_key = Word('G', 90) - modal_group = 3 + modal_group = MODAL_GROUP_NUM['distance'] class GCodeIncrementalDistanceMode(GCodeDistanceMode): """G91: Incremental Distance Mode""" word_key = Word('G', 91) - modal_group = 3 + modal_group = MODAL_GROUP_NUM['distance'] class GCodeAbsoluteArcDistanceMode(GCodeDistanceMode): """G90.1: Absolute Distance Mode for Arc IJK Parameters""" word_key = Word('G', 90.1) - modal_group = 4 + modal_group = MODAL_GROUP_NUM['arc_ijk_distance'] class GCodeIncrementalArcDistanceMode(GCodeDistanceMode): """G91.1: Incremental Distance Mode for Arc IJK Parameters""" word_key = Word('G', 91.1) - modal_group = 4 + modal_group = MODAL_GROUP_NUM['arc_ijk_distance'] class GCodeLatheDiameterMode(GCodeDistanceMode): """G7: Lathe Diameter Mode""" word_key = Word('G', 7) - modal_group = 15 + modal_group = MODAL_GROUP_NUM['lathe_diameter'] class GCodeLatheRadiusMode(GCodeDistanceMode): """G8: Lathe Radius Mode""" word_key = Word('G', 8) - modal_group = 15 + modal_group = MODAL_GROUP_NUM['lathe_diameter'] # ======================= Feed Rate Mode ======================= @@ -377,7 +428,7 @@ class GCodeLatheRadiusMode(GCodeDistanceMode): # G93, G94, G95 Feed Rate Mode class GCodeFeedRateMode(GCode): - modal_group = 5 + modal_group = MODAL_GROUP_NUM['feed_rate_mode'] exec_order = 30 class GCodeInverseTimeMode(GCodeFeedRateMode): @@ -409,20 +460,20 @@ class GCodeStartSpindleCW(GCodeSpindle): """M3: Start Spindle Clockwise""" #param_letters = set('S') # S is it's own gcode, makes no sense to be here word_key = Word('M', 3) - modal_group = 7 + modal_group = MODAL_GROUP_NUM['spindle'] class GCodeStartSpindleCCW(GCodeSpindle): """M4: Start Spindle Counter-Clockwise""" #param_letters = set('S') # S is it's own gcode, makes no sense to be here word_key = Word('M', 4) - modal_group = 7 + modal_group = MODAL_GROUP_NUM['spindle'] class GCodeStopSpindle(GCodeSpindle): """M5: Stop Spindle""" #param_letters = set('S') # S is it's own gcode, makes no sense to be here word_key = Word('M', 5) - modal_group = 7 + modal_group = MODAL_GROUP_NUM['spindle'] class GCodeOrientSpindle(GCodeSpindle): @@ -434,14 +485,14 @@ class GCodeSpindleConstantSurfaceSpeedMode(GCodeSpindle): """G96: Spindle Constant Surface Speed""" param_letters = set('DS') word_key = Word('G', 96) - modal_group = 14 + modal_group = MODAL_GROUP_NUM['spindle_speed'] class GCodeSpindleRPMMode(GCodeSpindle): """G97: Spindle RPM Speed""" param_letters = set('D') 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 class GCodeCoolant(GCode): - modal_group = 8 + modal_group = MODAL_GROUP_NUM['coolant'] exec_order = 110 @@ -477,7 +528,7 @@ class GCodeCoolantOff(GCodeCoolant): # G49 Cancel Tool Length Compensation class GCodeToolLength(GCode): - modal_group = 8 + modal_group = MODAL_GROUP_NUM['tool_length_offset'] exec_order = 180 @@ -510,7 +561,7 @@ class GCodeCancelToolLengthOffset(GCodeToolLength): # M60 Pallet Change Pause class GCodeProgramControl(GCode): - modal_group = 4 + modal_group = MODAL_GROUP_NUM['stopping'] exec_order = 250 class GCodePauseProgram(GCodeProgramControl): @@ -543,7 +594,7 @@ class GCodePalletChangePause(GCodeProgramControl): # G20, G21 Units class GCodeUnit(GCode): - modal_group = 6 + modal_group = MODAL_GROUP_NUM['units'] exec_order = 160 @@ -563,7 +614,7 @@ class GCodeUseMillimeters(GCodeUnit): # G17 - G19.1 Plane Select class GCodePlaneSelect(GCode): - modal_group = 2 + modal_group = MODAL_GROUP_NUM['plane_selection'] exec_order = 150 @@ -604,7 +655,7 @@ class GCodeSelectVWPlane(GCodePlaneSelect): # G41.1, G42.1 D L Dynamic Cutter Compensation class GCodeCutterRadiusComp(GCode): - modal_group = 7 + modal_group = MODAL_GROUP_NUM['cutter_diameter_comp'] exec_order = 170 @@ -643,7 +694,7 @@ class GCodeDynamicCutterCompRight(GCodeCutterRadiusComp): # G64 P Q Path Blending class GCodePathControlMode(GCode): - modal_group = 13 + modal_group = MODAL_GROUP_NUM['control_mode'] exec_order = 200 @@ -668,7 +719,7 @@ class GCodePathBlendingMode(GCodePathControlMode): # G98 Canned Cycle Return Level class GCodeCannedReturnMode(GCode): - modal_group = 10 + modal_group = MODAL_GROUP_NUM['canned_cycles_return'] exec_order = 220 @@ -698,11 +749,7 @@ class GCodeFeedRate(GCodeOtherModal): @classmethod def word_matches(cls, w): return w.letter == 'F' - # Modal Group : - # 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 + modal_group = MODAL_GROUP_NUM['feed_rate'] exec_order = 40 @@ -712,7 +759,7 @@ class GCodeSpindleSpeed(GCodeOtherModal): def word_matches(cls, w): return w.letter == 'S' # Modal Group: (see description in GCodeFeedRate) - modal_group = 102 + modal_group = MODAL_GROUP_NUM['spindle_speed'] exec_order = 50 @@ -722,21 +769,21 @@ class GCodeSelectTool(GCodeOtherModal): def word_matches(cls, w): return w.letter == 'T' # Modal Group: (see description in GCodeFeedRate) - modal_group = 103 + modal_group = MODAL_GROUP_NUM['tool'] exec_order = 60 class GCodeSpeedAndFeedOverrideOn(GCodeOtherModal): """M48: Speed and Feed Override Control On""" word_key = Word('M', 48) - modal_group = 9 + modal_group = MODAL_GROUP_NUM['override_switches'] exec_order = 120 class GCodeSpeedAndFeedOverrideOff(GCodeOtherModal): """M49: Speed and Feed Override Control Off""" word_key = Word('M', 49) - modal_group = 9 + modal_group = MODAL_GROUP_NUM['override_switches'] exec_order = 120 @@ -783,7 +830,7 @@ class GCodeSelectCoordinateSystem(GCodeOtherModal): @classmethod 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]) - modal_group = 12 + modal_group = MODAL_GROUP_NUM['coordinate_system'] exec_order = 190 @@ -950,6 +997,7 @@ class GCodeUserDefined(GCodeNonModal): #def word_matches(cls, w): # return (w.letter == 'M') and (101 <= w.value <= 199) exec_order = 130 + modal_group = MODAL_GROUP_NUM['user_defined'] # ======================= Utilities ======================= @@ -997,7 +1045,7 @@ _gcode_function_list = [] # of the form: [(lambda w: w.letter == 'F', GCodeFeedR def build_maps(): """Populate _gcode_word_map and _gcode_function_list""" - # Ensure lists are clear + # Ensure Word maps / lists are clear global _gcode_word_map global _gcode_function_list _gcode_word_map = {} @@ -1043,8 +1091,7 @@ def word_gcode_class(word, exhaustive=False): return None - -def words_to_gcodes(words): +def words2gcodes(words): """ Group words into g-codes (includes both G & M codes) :param words: list of Word instances diff --git a/pygcode/line.py b/pygcode/line.py index f12ebd6..a1bd290 100644 --- a/pygcode/line.py +++ b/pygcode/line.py @@ -20,9 +20,8 @@ class Line(object): @property def text(self): if self._text is None: - return self.build_line_text() + return str(self) return self._text - - def build_line_text(self): - return ' '.join([str(x) for x in [self.block, self.comment] if x]) + '\n' + def __str__(self): + return ' '.join([str(x) for x in [self.block, self.comment] if x]) diff --git a/pygcode/words.py b/pygcode/words.py index 9765b65..47a6bbe 100644 --- a/pygcode/words.py +++ b/pygcode/words.py @@ -5,10 +5,21 @@ import six from .exceptions import GCodeBlockFormatError -FLOAT_REGEX = re.compile(r'^-?(\d+\.?\d*|\.\d+)') # testcase: ..tests.test_words.WordValueMatchTests.test_float -INT_REGEX = re.compile(r'^-?\d+') -POSITIVEINT_REGEX = re.compile(r'^\d+') -CODE_REGEX = re.compile(r'^\d+(\.\d)?') # similar +REGEX_FLOAT = re.compile(r'^-?(\d+\.?\d*|\.\d+)') # testcase: ..tests.test_words.WordValueMatchTests.test_float +REGEX_INT = re.compile(r'^-?\d+') +REGEX_POSITIVEINT = re.compile(r'^\d+') +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 = { # Descriptions copied from wikipedia: @@ -17,148 +28,174 @@ WORD_MAP = { # Rotational Axes 'A': { 'class': float, - 'value_regex': FLOAT_REGEX, + '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': FLOAT_REGEX, + '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': FLOAT_REGEX, + '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': 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.", + 'clean_value': CLEAN_FLOAT, }, # Feed Rates 'E': { 'class': float, - 'value_regex': FLOAT_REGEX, + 'value_regex': REGEX_FLOAT, 'description': "Precision feedrate for threading on lathes", + 'clean_value': CLEAN_FLOAT, }, 'F': { 'class': float, - 'value_regex': FLOAT_REGEX, + 'value_regex': REGEX_FLOAT, 'description': "Feedrate", + 'clean_value': CLEAN_FLOAT, }, # G-Codes 'G': { 'class': float, - 'value_regex': CODE_REGEX, + 'value_regex': REGEX_CODE, 'description': "Address for preparatory commands", + 'clean_value': CLEAN_CODE, }, # Tool Offsets 'H': { '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)", + 'clean_value': CLEAN_FLOAT, }, # Arc radius center coords 'I': { '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.", + 'clean_value': CLEAN_FLOAT, }, 'J': { '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.", + 'clean_value': CLEAN_FLOAT, }, 'K': { '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.", + 'clean_value': CLEAN_FLOAT, }, # Loop Count 'L': { 'class': int, - 'value_regex': POSITIVEINT_REGEX, + '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': CODE_REGEX, + 'value_regex': REGEX_CODE, 'description': "Miscellaneous function", + 'clean_value': CLEAN_CODE, }, # Line Number 'N': { 'class': int, - 'value_regex': POSITIVEINT_REGEX, + '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': FLOAT_REGEX, + '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': FLOAT_REGEX, + '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': FLOAT_REGEX, + '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': FLOAT_REGEX, + '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': 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", + 'clean_value': CLEAN_NONE, }, # Incremental axes 'U': { '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').", + 'clean_value': CLEAN_FLOAT, }, 'V': { 'class': float, - 'value_regex': FLOAT_REGEX, + 'value_regex': REGEX_FLOAT, 'description': "Incremental axis corresponding to Y axis", + 'clean_value': CLEAN_FLOAT, }, 'W': { 'class': float, - 'value_regex': FLOAT_REGEX, + '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': FLOAT_REGEX, + 'value_regex': REGEX_FLOAT, 'description': "Absolute or incremental position of X axis.", + 'clean_value': CLEAN_FLOAT, }, 'Y': { 'class': float, - 'value_regex': FLOAT_REGEX, + 'value_regex': REGEX_FLOAT, 'description': "Absolute or incremental position of Y axis.", + 'clean_value': CLEAN_FLOAT, }, 'Z': { 'class': float, - 'value_regex': FLOAT_REGEX, + 'value_regex': REGEX_FLOAT, 'description': "Absolute or incremental position of Z axis.", + 'clean_value': CLEAN_FLOAT, }, } @@ -174,8 +211,8 @@ class Word(object): else: self._value = value - # Sorting Order - self._order_linuxcnc = None + self._value_class = WORD_MAP[self.letter]['class'] + self._value_clean = WORD_MAP[self.letter]['clean_value'] def __str__(self): return "{letter}{value}".format( @@ -183,6 +220,14 @@ class Word(object): 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): return "<{class_name}: {string}>".format( class_name=self.__class__.__name__, @@ -190,6 +235,8 @@ class Word(object): ) def __eq__(self, other): + if isinstance(other, six.string_types): + other = str2word(other) return (self.letter == other.letter) and (self.value == other.value) def __ne__(self, other): @@ -198,42 +245,45 @@ class Word(object): def __hash__(self): return hash((self.letter, self.value)) + # Value Properties @property def value_str(self): - """Value string, or """ + """Value string, or string representation of value""" if self._value_str is None: return str(self._value) return self._value_str + @property + def value_cleanstr(self): + """Clean string representation, for consistent file output""" + return self._value_clean(self.value) + @property def value(self): if self._value is None: - return WORD_MAP[self.letter]['class'](self._value_str) + return self._value_class(self._value_str) return self._value # Order - @property - def orderval_linuxcnc(self): - if self._order_linuxcnc is None: - self._order_linuxcnc = _word_order_linuxcnc(self) - return self._order_linuxcnc + def __lt__(self, other): + return self.letter < other.letter @property def description(self): 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): """ Iterate through block text yielding Word instances :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: - letter_match = NEXT_WORD.search(block_text[index:]) + letter_match = next_word.search(block_text[index:]) if letter_match: # Letter letter = letter_match.group('letter').upper() diff --git a/tests/test_file.py b/tests/test_file.py index b09bae3..82a20ee 100644 --- a/tests/test_file.py +++ b/tests/test_file.py @@ -17,4 +17,4 @@ class FileParseTest(unittest.TestCase): self.assertEqual(len(file.lines), 26) # FIXME: just verifying content visually #for line in file.lines: - # print(' '.join(["%s%s" % (w.letter, w.value_str) for w in line.block.words])) + # print(line) diff --git a/tests/test_gcodes.py b/tests/test_gcodes.py index 8462928..828107d 100644 --- a/tests/test_gcodes.py +++ b/tests/test_gcodes.py @@ -82,7 +82,7 @@ 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 = gcodes.words2gcodes(word_list) # result form self.assertIsInstance(result, tuple) self.assertEqual(len(result), 2) diff --git a/tests/test_words.py b/tests/test_words.py index e42e8ef..6dfa959 100644 --- a/tests/test_words.py +++ b/tests/test_words.py @@ -58,7 +58,7 @@ class WordValueMatchTests(unittest.TestCase): def test_float(self): self.regex_assertions( - regex=words.FLOAT_REGEX, + regex=words.REGEX_FLOAT, positive_list=[ ('1.2', '1.2'), ('1', '1'), ('200', '200'), ('0092', '0092'), ('1.', '1.'), ('.2', '.2'), ('-1.234', '-1.234'), @@ -71,7 +71,7 @@ class WordValueMatchTests(unittest.TestCase): def test_code(self): self.regex_assertions( - regex=words.CODE_REGEX, + regex=words.REGEX_CODE, positive_list=[ ('1.2', '1.2'), ('1', '1'), ('10', '10'), ('02', '02'), ('02.3', '02.3'), From e913c5a4ba8e3c98bf48a425b0661f6275642c89 Mon Sep 17 00:00:00 2001 From: Peter Boin <nymphii@gmail.com> Date: Thu, 6 Jul 2017 22:20:04 +1000 Subject: [PATCH 08/32] -derps MODAL_GROUP constant refactor --- pygcode/gcodes.py | 64 ++++++++++++++++++++++---------------------- tests/test_gcodes.py | 4 +-- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/pygcode/gcodes.py b/pygcode/gcodes.py index 3d1ccff..16460f6 100644 --- a/pygcode/gcodes.py +++ b/pygcode/gcodes.py @@ -65,9 +65,9 @@ from .words import Word # User Defined (Group 10) M100-M199 # -MODAL_GROUP_NUM = { +MODAL_GROUP_MAP = { # "G" codes - 'motion': 1 + 'motion': 1, 'plane_selection': 2, 'distance': 3, 'arc_ijk_distance': 4, @@ -78,7 +78,7 @@ MODAL_GROUP_NUM = { 'canned_cycles_return': 10, 'coordinate_system': 12, 'control_mode': 13, - 'spindle_speed': 14, + 'spindle_speed_mode': 14, 'lathe_diameter': 15, # "M" codes @@ -236,7 +236,7 @@ class GCode(object): class GCodeMotion(GCode): param_letters = set('XYZABCUVW') - modal_group = MODAL_GROUP_NUM['motion'] + modal_group = MODAL_GROUP_MAP['motion'] exec_order = 241 class GCodeRapidMove(GCodeMotion): @@ -332,7 +332,7 @@ class GCodeCancelCannedCycle(GCodeMotion): class GCodeCannedCycle(GCode): param_letters = set('XYZUVW') - modal_group = MODAL_GROUP_NUM['motion'] + modal_group = MODAL_GROUP_MAP['motion'] exec_order = 241 class GCodeDrillingCycle(GCodeCannedCycle): @@ -391,36 +391,36 @@ class GCodeDistanceMode(GCode): class GCodeAbsoluteDistanceMode(GCodeDistanceMode): """G90: Absolute Distance Mode""" word_key = Word('G', 90) - modal_group = MODAL_GROUP_NUM['distance'] + modal_group = MODAL_GROUP_MAP['distance'] class GCodeIncrementalDistanceMode(GCodeDistanceMode): """G91: Incremental Distance Mode""" word_key = Word('G', 91) - modal_group = MODAL_GROUP_NUM['distance'] + modal_group = MODAL_GROUP_MAP['distance'] class GCodeAbsoluteArcDistanceMode(GCodeDistanceMode): """G90.1: Absolute Distance Mode for Arc IJK Parameters""" word_key = Word('G', 90.1) - modal_group = MODAL_GROUP_NUM['arc_ijk_distance'] + modal_group = MODAL_GROUP_MAP['arc_ijk_distance'] class GCodeIncrementalArcDistanceMode(GCodeDistanceMode): """G91.1: Incremental Distance Mode for Arc IJK Parameters""" word_key = Word('G', 91.1) - modal_group = MODAL_GROUP_NUM['arc_ijk_distance'] + modal_group = MODAL_GROUP_MAP['arc_ijk_distance'] class GCodeLatheDiameterMode(GCodeDistanceMode): """G7: Lathe Diameter Mode""" word_key = Word('G', 7) - modal_group = MODAL_GROUP_NUM['lathe_diameter'] + modal_group = MODAL_GROUP_MAP['lathe_diameter'] class GCodeLatheRadiusMode(GCodeDistanceMode): """G8: Lathe Radius Mode""" word_key = Word('G', 8) - modal_group = MODAL_GROUP_NUM['lathe_diameter'] + modal_group = MODAL_GROUP_MAP['lathe_diameter'] # ======================= Feed Rate Mode ======================= @@ -428,7 +428,7 @@ class GCodeLatheRadiusMode(GCodeDistanceMode): # G93, G94, G95 Feed Rate Mode class GCodeFeedRateMode(GCode): - modal_group = MODAL_GROUP_NUM['feed_rate_mode'] + modal_group = MODAL_GROUP_MAP['feed_rate_mode'] exec_order = 30 class GCodeInverseTimeMode(GCodeFeedRateMode): @@ -460,20 +460,20 @@ class GCodeStartSpindleCW(GCodeSpindle): """M3: Start Spindle Clockwise""" #param_letters = set('S') # S is it's own gcode, makes no sense to be here word_key = Word('M', 3) - modal_group = MODAL_GROUP_NUM['spindle'] + modal_group = MODAL_GROUP_MAP['spindle'] class GCodeStartSpindleCCW(GCodeSpindle): """M4: Start Spindle Counter-Clockwise""" #param_letters = set('S') # S is it's own gcode, makes no sense to be here word_key = Word('M', 4) - modal_group = MODAL_GROUP_NUM['spindle'] + modal_group = MODAL_GROUP_MAP['spindle'] class GCodeStopSpindle(GCodeSpindle): """M5: Stop Spindle""" #param_letters = set('S') # S is it's own gcode, makes no sense to be here word_key = Word('M', 5) - modal_group = MODAL_GROUP_NUM['spindle'] + modal_group = MODAL_GROUP_MAP['spindle'] class GCodeOrientSpindle(GCodeSpindle): @@ -485,14 +485,14 @@ class GCodeSpindleConstantSurfaceSpeedMode(GCodeSpindle): """G96: Spindle Constant Surface Speed""" param_letters = set('DS') word_key = Word('G', 96) - modal_group = MODAL_GROUP_NUM['spindle_speed'] + modal_group = MODAL_GROUP_MAP['spindle_speed_mode'] class GCodeSpindleRPMMode(GCodeSpindle): """G97: Spindle RPM Speed""" param_letters = set('D') word_key = Word('G', 97) - modal_group = MODAL_GROUP_NUM['spindle_speed'] + modal_group = MODAL_GROUP_MAP['spindle_speed_mode'] @@ -501,7 +501,7 @@ class GCodeSpindleRPMMode(GCodeSpindle): # M7, M8, M9 Coolant Control class GCodeCoolant(GCode): - modal_group = MODAL_GROUP_NUM['coolant'] + modal_group = MODAL_GROUP_MAP['coolant'] exec_order = 110 @@ -528,7 +528,7 @@ class GCodeCoolantOff(GCodeCoolant): # G49 Cancel Tool Length Compensation class GCodeToolLength(GCode): - modal_group = MODAL_GROUP_NUM['tool_length_offset'] + modal_group = MODAL_GROUP_MAP['tool_length_offset'] exec_order = 180 @@ -561,7 +561,7 @@ class GCodeCancelToolLengthOffset(GCodeToolLength): # M60 Pallet Change Pause class GCodeProgramControl(GCode): - modal_group = MODAL_GROUP_NUM['stopping'] + modal_group = MODAL_GROUP_MAP['stopping'] exec_order = 250 class GCodePauseProgram(GCodeProgramControl): @@ -594,7 +594,7 @@ class GCodePalletChangePause(GCodeProgramControl): # G20, G21 Units class GCodeUnit(GCode): - modal_group = MODAL_GROUP_NUM['units'] + modal_group = MODAL_GROUP_MAP['units'] exec_order = 160 @@ -614,7 +614,7 @@ class GCodeUseMillimeters(GCodeUnit): # G17 - G19.1 Plane Select class GCodePlaneSelect(GCode): - modal_group = MODAL_GROUP_NUM['plane_selection'] + modal_group = MODAL_GROUP_MAP['plane_selection'] exec_order = 150 @@ -655,7 +655,7 @@ class GCodeSelectVWPlane(GCodePlaneSelect): # G41.1, G42.1 D L Dynamic Cutter Compensation class GCodeCutterRadiusComp(GCode): - modal_group = MODAL_GROUP_NUM['cutter_diameter_comp'] + modal_group = MODAL_GROUP_MAP['cutter_diameter_comp'] exec_order = 170 @@ -694,7 +694,7 @@ class GCodeDynamicCutterCompRight(GCodeCutterRadiusComp): # G64 P Q Path Blending class GCodePathControlMode(GCode): - modal_group = MODAL_GROUP_NUM['control_mode'] + modal_group = MODAL_GROUP_MAP['control_mode'] exec_order = 200 @@ -719,7 +719,7 @@ class GCodePathBlendingMode(GCodePathControlMode): # G98 Canned Cycle Return Level class GCodeCannedReturnMode(GCode): - modal_group = MODAL_GROUP_NUM['canned_cycles_return'] + modal_group = MODAL_GROUP_MAP['canned_cycles_return'] exec_order = 220 @@ -749,7 +749,7 @@ class GCodeFeedRate(GCodeOtherModal): @classmethod def word_matches(cls, w): return w.letter == 'F' - modal_group = MODAL_GROUP_NUM['feed_rate'] + modal_group = MODAL_GROUP_MAP['feed_rate'] exec_order = 40 @@ -759,7 +759,7 @@ class GCodeSpindleSpeed(GCodeOtherModal): def word_matches(cls, w): return w.letter == 'S' # Modal Group: (see description in GCodeFeedRate) - modal_group = MODAL_GROUP_NUM['spindle_speed'] + modal_group = MODAL_GROUP_MAP['spindle_speed'] exec_order = 50 @@ -769,21 +769,21 @@ class GCodeSelectTool(GCodeOtherModal): def word_matches(cls, w): return w.letter == 'T' # Modal Group: (see description in GCodeFeedRate) - modal_group = MODAL_GROUP_NUM['tool'] + modal_group = MODAL_GROUP_MAP['tool'] exec_order = 60 class GCodeSpeedAndFeedOverrideOn(GCodeOtherModal): """M48: Speed and Feed Override Control On""" word_key = Word('M', 48) - modal_group = MODAL_GROUP_NUM['override_switches'] + modal_group = MODAL_GROUP_MAP['override_switches'] exec_order = 120 class GCodeSpeedAndFeedOverrideOff(GCodeOtherModal): """M49: Speed and Feed Override Control Off""" word_key = Word('M', 49) - modal_group = MODAL_GROUP_NUM['override_switches'] + modal_group = MODAL_GROUP_MAP['override_switches'] exec_order = 120 @@ -830,7 +830,7 @@ class GCodeSelectCoordinateSystem(GCodeOtherModal): @classmethod 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]) - modal_group = MODAL_GROUP_NUM['coordinate_system'] + modal_group = MODAL_GROUP_MAP['coordinate_system'] exec_order = 190 @@ -997,7 +997,7 @@ class GCodeUserDefined(GCodeNonModal): #def word_matches(cls, w): # return (w.letter == 'M') and (101 <= w.value <= 199) exec_order = 130 - modal_group = MODAL_GROUP_NUM['user_defined'] + modal_group = MODAL_GROUP_MAP['user_defined'] # ======================= Utilities ======================= diff --git a/tests/test_gcodes.py b/tests/test_gcodes.py index 828107d..1ae0490 100644 --- a/tests/test_gcodes.py +++ b/tests/test_gcodes.py @@ -49,12 +49,12 @@ class TestGCodeModalGroups(unittest.TestCase): # Table 6. M-Code Modal Groups # MODAL GROUP MEANING MEMBER WORDS - table_rows += ''' + table_rows += re.sub(r'\(Group (\d+)\)', r'(Group 10\1)', ''' Stopping (Group 4) M0, M1, M2, M30, M60 Spindle (Group 7) M3, M4, M5 Coolant (Group 8) M7, M8, M9 Override Switches (Group 9) M48, M49 - ''' + ''') # groups += 100 (to distinguish "M" GCodes from "G" GCodes) for row in table_rows.split('\n'): match = re.search(r'^\s*(?P<title>.*)\s*\(Group (?P<group>\d+)\)\s*(?P<words>.*)$', row, re.I) From 3d849667f53f232b9228aeaab153f88d216a9a51 Mon Sep 17 00:00:00 2001 From: Peter Boin <nymphii@gmail.com> Date: Fri, 7 Jul 2017 02:41:39 +1000 Subject: [PATCH 09/32] derp --- pygcode/gcodes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygcode/gcodes.py b/pygcode/gcodes.py index 16460f6..499500e 100644 --- a/pygcode/gcodes.py +++ b/pygcode/gcodes.py @@ -618,7 +618,7 @@ class GCodePlaneSelect(GCode): exec_order = 150 -class GCodeSelectZYPlane(GCodePlaneSelect): +class GCodeSelectXYPlane(GCodePlaneSelect): """G17: select XY plane (default)""" word_key = Word('G', 17) From c62ee74b78d4ee5b9fb13298a0dff7e6cafa0605 Mon Sep 17 00:00:00 2001 From: Peter Boin <nymphii@gmail.com> Date: Fri, 7 Jul 2017 02:42:43 +1000 Subject: [PATCH 10/32] simplified machine class separated machine state to avoid circular dependency between gcodes.py and machine.py --- pygcode/file.py | 6 +-- pygcode/machine.py | 86 +++++++++--------------------------- pygcode/machinestate.py | 97 +++++++++++++++++++++++++++++++++++++++++ pygcode/words.py | 11 ++++- 4 files changed, 127 insertions(+), 73 deletions(-) create mode 100644 pygcode/machinestate.py diff --git a/pygcode/file.py b/pygcode/file.py index ed8d780..44fab3f 100644 --- a/pygcode/file.py +++ b/pygcode/file.py @@ -1,6 +1,6 @@ from .line import Line -from .machine import AbstractMachine +#from .machine import AbstractMachine class GCodeFile(object): def __init__(self, filename=None): @@ -14,10 +14,6 @@ class GCodeFile(object): self.lines.append(line) -class GCodeWriterMachine(AbstractMachine): - def machine_init(self, *args, **kwargs): - pass - def parse(filename): # FIXME: should be an iterator, and also not terrible file = GCodeFile() diff --git a/pygcode/machine.py b/pygcode/machine.py index 91c82a0..646aa0d 100644 --- a/pygcode/machine.py +++ b/pygcode/machine.py @@ -1,70 +1,24 @@ +from collections import defaultdict + +from .gcodes import MODAL_GROUP_MAP, GCode +from .line import Line + +from .machinestate import MachineState + -class MachineState(object): - def __init__(self, axes=('x', 'y', 'z')): - self.axes = axes +class Machine(object): + def __init__(self): + self.state = MachineState() - # initialize - self.position = {} - for axis in self.axes: - self.position[axis] = 0 + def process(self, *gcode_list, **kwargs): + """ + Process gcodes + :param gcode_list: list of GCode instances + :param modal_params: list of Word instances to be applied to current movement mode + """ + modal_params = kwargs.get('modal_params', []) + for gcode in sorted(gcode_list): + self.state.set_mode(gcode) # if gcode is not modal, it's ignored - self.time = 0 - - - -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.max_rate = kwargs.get('max_rate', { - 'x': 500, # mm/min - 'y': 500, # mm/min - 'z': 500, # mm/min - }) - self.max_travel = kwargs.get('max_travel', { - 'x': 200, # mm - 'y': 200, # mm - 'z': 200, # mm - }) - self.max_spindle_speed = kwargs.get('max_spindle_speed', 1000) # rpm - self.min_spindle_speed = kwargs.get('max_spindle_speed', 0) # rpm - - # initialize - 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): - """Change machine's state based on the given gcode line""" - pass # TODO - - - - -""" -class Axes(object): - pass - -class MyMachineState(MachineState): - axes_state_class = AxesState - pass - -class MyMachine(AbstractMachine): - available_axes = set('xyz') - state_class = MyMachineState - - -m = MyMachine( - state=MyMachineState( - absolute_position= - ), -) - -""" + # TODO: gcode instance to change machine's state diff --git a/pygcode/machinestate.py b/pygcode/machinestate.py new file mode 100644 index 0000000..1e550bf --- /dev/null +++ b/pygcode/machinestate.py @@ -0,0 +1,97 @@ +from .gcodes import GCode +from .line import Line + + +class State(object): + """State of a Machine""" + # State is very forgiving: + # Anything possible in a machine's state may be changed & fetched. + # For example: x, y, z, a, b, c may all be set & requested. + # However, the machine for which this state is stored probably doesn't + # have all possible 6 axes. + # It is also possible to set an axis to an impossibly large distance. + # It is the responsibility of the Machine using this class to be + # discerning in these respects. + def __init__(self): + self.axes = defaultdict(lambda: 0.0) # aka: "machine coordinates" + + self.work_offset = defaultdict(lambda: 0.0) + # TODO: how to manage work offsets? (probs not like the above) + # read up on: + # - G92: coordinate system offset + # - G54-G59: select coordinate system (offsets from machine coordinates set by G10 L2) + + # TODO: Move this class into MachineState + + +class MachineState(object): + """Machine's state, and mode""" + # Mode is defined by gcodes set by processed blocks: + # see modal_group in gcode.py module for details + def __init__(self): + self.modal_groups = defaultdict(lambda: None) + # populate with all groups + for modal_group in MODAL_GROUP_MAP.values(): + self.modal_groups[modal_group] = None + + # Default mode: + self.set_mode(*Line(''' + G17 (plane_selection: X/Y plane) + G90 (distance: absolute position. ie: not "turtle" mode) + G91.1 (arc_ijk_distance: IJK sets arc center vertex relative to current position) + G94 (feed_rate_mode: feed-rate defined in units/min) + G21 (units: mm) + G40 (cutter_diameter_comp: no compensation) + G49 (tool_length_offset: no offset) + G61 (control_mode: exact path mode) + G97 (spindle_speed_mode: RPM Mode) + M0 (stopping: program paused) + M5 (spindle: off) + M9 (coolant: off) + F100 (feed_rate: 100 mm/min) + S1000 (tool: 1000 rpm, when it's switched on) + ''').block.gcodes) # note: although this is not a single line + # '\n' is just treated like any other whitespace, + # so it behaves like a single line. + + def set_mode(self, *gcode_list): + for g in sorted(gcode_list): # sorted by execution order + if g.modal_group is not None: + self.modal_groups[g.modal_group] = g + + def __getattr__(self, key): + if key in MODAL_GROUP_MAP: + return self.modal_groups[MODAL_GROUP_MAP[key]] + + raise AttributeError("'{cls}' object has no attribute '{key}'".format( + cls=self.__class__.__name__, + key=key + )) + + def __setattr__(self, key, value): + if key in MODAL_GROUP_MAP: + # Set/Clear modal group gcode + if value is None: + # clear mode group + self.modal_groups[MODAL_GROUP_MAP[key]] = None + else: + # set mode group explicitly + # (recommended to use self.set_mode(value) instead) + assert isinstance(value, GCode), "invalid value type: %r" % value + assert value.modal_group == MODAL_GROUP_MAP[key], \ + "cannot set '%s' mode as %r, wrong group" % (key, value) + self.modal_groups[MODAL_GROUP_MAP[key]] = value + else: + self.__dict__[key] = value + + def __str__(self): + gcode_list = [] + for modal_group in sorted(MODAL_GROUP_MAP.values()): + if self.modal_groups[modal_group]: + gcode_list.append(self.modal_groups[modal_group]) + return ' '.join(str(g) for g in gcode_list) + + def __repr__(self): + return "<{class_name}: {gcodes}>".format( + class_name=self.__class__.__name__, gcodes=str(self) + ) diff --git a/pygcode/words.py b/pygcode/words.py index 47a6bbe..5ced857 100644 --- a/pygcode/words.py +++ b/pygcode/words.py @@ -4,7 +4,6 @@ import six from .exceptions import GCodeBlockFormatError - REGEX_FLOAT = re.compile(r'^-?(\d+\.?\d*|\.\d+)') # testcase: ..tests.test_words.WordValueMatchTests.test_float REGEX_INT = re.compile(r'^-?\d+') REGEX_POSITIVEINT = re.compile(r'^\d+') @@ -201,7 +200,15 @@ WORD_MAP = { class Word(object): - def __init__(self, letter, value): + def __init__(self, *args): + assert len(args) in [1, 2], "input arguments either: (letter, value) or (word_str)" + if len(args) == 2: + (letter, value) = args + else: + word_str = args[0] + letter = word_str[0] # first letter + value = word_str[1:] # rest of string + self.letter = letter.upper() self._value_str = None From 3674f9d6aad3ef9643301e25dac2d34ff9d5ad50 Mon Sep 17 00:00:00 2001 From: Peter Boin <nymphii@gmail.com> Date: Fri, 7 Jul 2017 14:38:08 +1000 Subject: [PATCH 11/32] moved machinestate contents back to machine.py --- pygcode/gcodes.py | 19 ++++---- pygcode/machine.py | 101 +++++++++++++++++++++++++++++++++++++++- pygcode/machinestate.py | 97 -------------------------------------- 3 files changed, 109 insertions(+), 108 deletions(-) delete mode 100644 pygcode/machinestate.py diff --git a/pygcode/gcodes.py b/pygcode/gcodes.py index 499500e..cfe49e4 100644 --- a/pygcode/gcodes.py +++ b/pygcode/gcodes.py @@ -1,4 +1,5 @@ from collections import defaultdict +from copy import copy from .words import Word @@ -140,8 +141,9 @@ class GCode(object): # Parameters associated to this gcode param_letters = set() - # Modal Group + # Modal stuff modal_group = None + modal_param_letters = set() # by default: no parameters are retained in modal state # Execution Order exec_order = 999 # if not otherwise specified, run last @@ -208,15 +210,12 @@ class GCode(object): def description(self): 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 + def modal_copy(self): + """Copy of GCode instance containing only parameters listed in modal_param_letters""" + return self.__class__(self.word, *[ + w for (l, w) in self.params.items() + if l in self.modal_param_letters + ]) # ======================= Motion ======================= diff --git a/pygcode/machine.py b/pygcode/machine.py index 646aa0d..53afa0a 100644 --- a/pygcode/machine.py +++ b/pygcode/machine.py @@ -4,7 +4,98 @@ from collections import defaultdict from .gcodes import MODAL_GROUP_MAP, GCode from .line import Line -from .machinestate import MachineState + +class State(object): + """State of a Machine""" + # State is very forgiving: + # Anything possible in a machine's state may be changed & fetched. + # For example: x, y, z, a, b, c may all be set & requested. + # However, the machine for which this state is stored probably doesn't + # have all possible 6 axes. + # It is also possible to set an axis to an impossibly large distance. + # It is the responsibility of the Machine using this class to be + # discerning in these respects. + def __init__(self): + self.axes = defaultdict(lambda: 0.0) # aka: "machine coordinates" + + self.work_offset = defaultdict(lambda: 0.0) + # TODO: how to manage work offsets? (probs not like the above) + # read up on: + # - G92: coordinate system offset + # - G54-G59: select coordinate system (offsets from machine coordinates set by G10 L2) + + # TODO: Move this class into MachineState + + +class MachineState(object): + """Machine's state, and mode""" + # Default Mode + default_mode = ''' + G17 (plane_selection: X/Y plane) + G90 (distance: absolute position. ie: not "turtle" mode) + G91.1 (arc_ijk_distance: IJK sets arc center vertex relative to current position) + G94 (feed_rate_mode: feed-rate defined in units/min) + G21 (units: mm) + G40 (cutter_diameter_comp: no compensation) + G49 (tool_length_offset: no offset) + G61 (control_mode: exact path mode) + G97 (spindle_speed_mode: RPM Mode) + M0 (stopping: program paused) + M5 (spindle: off) + M9 (coolant: off) + F100 (feed_rate: 100 mm/min) + S1000 (tool: 1000 rpm, when it's switched on) + ''' + + # Mode is defined by gcodes set by processed blocks: + # see modal_group in gcode.py module for details + def __init__(self): + self.modal_groups = defaultdict(lambda: None) + + # Initialize + self.set_mode(*Line(self.default_mode).block.gcodes) + + def set_mode(self, *gcode_list): + for g in sorted(gcode_list): # sorted by execution order + if g.modal_group is not None: + self.modal_groups[g.modal_group] = g.modal_copy() + + def __getattr__(self, key): + if key in MODAL_GROUP_MAP: + return self.modal_groups[MODAL_GROUP_MAP[key]] + + raise AttributeError("'{cls}' object has no attribute '{key}'".format( + cls=self.__class__.__name__, + key=key + )) + + def __setattr__(self, key, value): + if key in MODAL_GROUP_MAP: + # Set/Clear modal group gcode + if value is None: + # clear mode group + self.modal_groups[MODAL_GROUP_MAP[key]] = None + else: + # set mode group explicitly + # (recommended to use self.set_mode(value) instead) + assert isinstance(value, GCode), "invalid value type: %r" % value + assert value.modal_group == MODAL_GROUP_MAP[key], \ + "cannot set '%s' mode as %r, wrong group" % (key, value) + self.modal_groups[MODAL_GROUP_MAP[key]] = value.modal_copy() + else: + self.__dict__[key] = value + + def __str__(self): + gcode_list = [] + for modal_group in sorted(MODAL_GROUP_MAP.values()): + if self.modal_groups[modal_group]: + gcode_list.append(self.modal_groups[modal_group]) + return ' '.join(str(g) for g in gcode_list) + + def __repr__(self): + return "<{class_name}: {gcodes}>".format( + class_name=self.__class__.__name__, gcodes=str(self) + ) class Machine(object): @@ -18,7 +109,15 @@ class Machine(object): :param modal_params: list of Word instances to be applied to current movement mode """ modal_params = kwargs.get('modal_params', []) + + #process_gcodes = + for gcode in sorted(gcode_list): self.state.set_mode(gcode) # if gcode is not modal, it's ignored + gcode.process(self.state) # TODO: gcode instance to change machine's state + # Questions to drive design: + # - how much time did the command take? + # - what was the tool's distance / displacement + # - did the tool travel outside machine boundaries? diff --git a/pygcode/machinestate.py b/pygcode/machinestate.py deleted file mode 100644 index 1e550bf..0000000 --- a/pygcode/machinestate.py +++ /dev/null @@ -1,97 +0,0 @@ -from .gcodes import GCode -from .line import Line - - -class State(object): - """State of a Machine""" - # State is very forgiving: - # Anything possible in a machine's state may be changed & fetched. - # For example: x, y, z, a, b, c may all be set & requested. - # However, the machine for which this state is stored probably doesn't - # have all possible 6 axes. - # It is also possible to set an axis to an impossibly large distance. - # It is the responsibility of the Machine using this class to be - # discerning in these respects. - def __init__(self): - self.axes = defaultdict(lambda: 0.0) # aka: "machine coordinates" - - self.work_offset = defaultdict(lambda: 0.0) - # TODO: how to manage work offsets? (probs not like the above) - # read up on: - # - G92: coordinate system offset - # - G54-G59: select coordinate system (offsets from machine coordinates set by G10 L2) - - # TODO: Move this class into MachineState - - -class MachineState(object): - """Machine's state, and mode""" - # Mode is defined by gcodes set by processed blocks: - # see modal_group in gcode.py module for details - def __init__(self): - self.modal_groups = defaultdict(lambda: None) - # populate with all groups - for modal_group in MODAL_GROUP_MAP.values(): - self.modal_groups[modal_group] = None - - # Default mode: - self.set_mode(*Line(''' - G17 (plane_selection: X/Y plane) - G90 (distance: absolute position. ie: not "turtle" mode) - G91.1 (arc_ijk_distance: IJK sets arc center vertex relative to current position) - G94 (feed_rate_mode: feed-rate defined in units/min) - G21 (units: mm) - G40 (cutter_diameter_comp: no compensation) - G49 (tool_length_offset: no offset) - G61 (control_mode: exact path mode) - G97 (spindle_speed_mode: RPM Mode) - M0 (stopping: program paused) - M5 (spindle: off) - M9 (coolant: off) - F100 (feed_rate: 100 mm/min) - S1000 (tool: 1000 rpm, when it's switched on) - ''').block.gcodes) # note: although this is not a single line - # '\n' is just treated like any other whitespace, - # so it behaves like a single line. - - def set_mode(self, *gcode_list): - for g in sorted(gcode_list): # sorted by execution order - if g.modal_group is not None: - self.modal_groups[g.modal_group] = g - - def __getattr__(self, key): - if key in MODAL_GROUP_MAP: - return self.modal_groups[MODAL_GROUP_MAP[key]] - - raise AttributeError("'{cls}' object has no attribute '{key}'".format( - cls=self.__class__.__name__, - key=key - )) - - def __setattr__(self, key, value): - if key in MODAL_GROUP_MAP: - # Set/Clear modal group gcode - if value is None: - # clear mode group - self.modal_groups[MODAL_GROUP_MAP[key]] = None - else: - # set mode group explicitly - # (recommended to use self.set_mode(value) instead) - assert isinstance(value, GCode), "invalid value type: %r" % value - assert value.modal_group == MODAL_GROUP_MAP[key], \ - "cannot set '%s' mode as %r, wrong group" % (key, value) - self.modal_groups[MODAL_GROUP_MAP[key]] = value - else: - self.__dict__[key] = value - - def __str__(self): - gcode_list = [] - for modal_group in sorted(MODAL_GROUP_MAP.values()): - if self.modal_groups[modal_group]: - gcode_list.append(self.modal_groups[modal_group]) - return ' '.join(str(g) for g in gcode_list) - - def __repr__(self): - return "<{class_name}: {gcodes}>".format( - class_name=self.__class__.__name__, gcodes=str(self) - ) From ec6ebb2870c30bf5c2b126196f6bcf23a2d6b733 Mon Sep 17 00:00:00 2001 From: Peter Boin <nymphii@gmail.com> Date: Sat, 8 Jul 2017 22:18:58 +1000 Subject: [PATCH 12/32] machine processing underway --- pygcode/block.py | 37 +++-- pygcode/comment.py | 6 + pygcode/file.py | 21 +-- pygcode/gcodes.py | 144 +++++++++++++++--- pygcode/machine.py | 331 +++++++++++++++++++++++++++++++++++++----- pygcode/words.py | 17 ++- tests/test_file.py | 24 +-- tests/test_gcodes.py | 12 +- tests/test_line.py | 8 +- tests/test_machine.py | 122 ++++++++++++++++ tests/test_words.py | 19 +-- tests/testutils.py | 25 ++++ 12 files changed, 661 insertions(+), 105 deletions(-) create mode 100644 tests/test_machine.py create mode 100644 tests/testutils.py diff --git a/pygcode/block.py b/pygcode/block.py index 2f456ba..afc267d 100644 --- a/pygcode/block.py +++ b/pygcode/block.py @@ -1,39 +1,50 @@ import re -from .words import iter_words, WORD_MAP +from .words import text2words, WORD_MAP from .gcodes import words2gcodes class Block(object): """GCode block (effectively any gcode file line that defines any <word><value>)""" - def __init__(self, text): + def __init__(self, text=None, verify=True): """ Block Constructor :param A-Z: gcode parameter values :param comment: comment text """ - self._raw_text = text # unaltered block content (before alteration) + self._raw_text = None + self._text = None + self.words = [] + self.gcodes = [] + self.modal_params = [] # clean up block string - text = re.sub(r'(^\s+|\s+$)', '', text) # remove whitespace padding - text = re.sub(r'\s+', ' ', text) # remove duplicate whitespace with ' ' + if text: + self._raw_text = text # unaltered block content (before alteration) + text = re.sub(r'(^\s+|\s+$)', '', text) # remove whitespace padding + text = re.sub(r'\s+', ' ', text) # remove duplicate whitespace with ' ' + self._text = text # cleaned up block content - self.text = text + # Get words from text, and group into gcodes + self.words = list(text2words(self._text)) + (self.gcodes, self.modal_params) = words2gcodes(self.words) - self.words = list(iter_words(self.text)) - (self.gcodes, self.modal_params) = words2gcodes(self.words) + # Verification + if verify: + self._assert_gcodes() - self._assert_gcodes() - - # TODO: gcode verification - # - gcodes in the same modal_group raises exception + @property + def text(self): + if self._text: + return self._text + return str(self) def _assert_gcodes(self): modal_groups = set() code_words = set() for gc in self.gcodes: - + # Assert all gcodes are not repeated in the same block if gc.word in code_words: raise AssertionError("%s cannot be in the same block" % ([ diff --git a/pygcode/comment.py b/pygcode/comment.py index dfae3ed..5e9b170 100644 --- a/pygcode/comment.py +++ b/pygcode/comment.py @@ -7,6 +7,12 @@ class CommentBase(object): def __init__(self, text): self.text = text + def __repr__(self): + return "<{class_name}: '{comment}'>".format( + class_name=self.__class__.__name__, + comment=str(self), + ) + class CommentSemicolon(CommentBase): "Comments of the format: 'G00 X1 Y2 ; something profound'" diff --git a/pygcode/file.py b/pygcode/file.py index 44fab3f..fa9f395 100644 --- a/pygcode/file.py +++ b/pygcode/file.py @@ -14,12 +14,15 @@ class GCodeFile(object): self.lines.append(line) -def parse(filename): - # FIXME: should be an iterator, and also not terrible - file = GCodeFile() - with open(filename, 'r') as fh: - for line in fh.readlines(): - line_obj = Line(line) - # FIXME: don't dump entire file into RAM; change to generator model - file.append(line_obj) - return file +class GCodeParser(object): + """Parse a gocde file""" + def __init__(self, filename): + self.filename = filename + self._fh = open(filename, 'r') + + def iterlines(self): + for line in self._fh.readlines(): + yield Line(line) + + def close(self): + self._fh.close() diff --git a/pygcode/gcodes.py b/pygcode/gcodes.py index cfe49e4..4e8b116 100644 --- a/pygcode/gcodes.py +++ b/pygcode/gcodes.py @@ -40,7 +40,7 @@ from .words import Word # # Table 5. G-Code Modal Groups # MODAL GROUP MEANING MEMBER WORDS -# Non-modal codes (Group 0) G4, G10 G28, G30, G53 G92, G92.1, G92.2, G92.3, +# Non-modal codes (Group 0) G4, G10 G28, G30, G53, G92, G92.1, G92.2, G92.3, # Motion (Group 1) G0, G1, G2, G3, G33, G38.x, G73, G76, G80, G81 # G82, G83, G84, G85, G86, G87, G88,G89 # Plane selection (Group 2) G17, G18, G19, G17.1, G18.1, G19.1 @@ -132,6 +132,12 @@ MODAL_GROUP_MAP = { # 240: Perform motion (G0 to G3, G33, G38.x, G73, G76, G80 to G89), as modified (possibly) by G53. # 250: Stop (M0, M1, M2, M30, M60). +class GCodeEffect(object): + """Effect a gcode has on a machine upon processing""" + def __init__(self, **kwargs): + self.mode = {} + self.dt = 0.0 + class GCode(object): # Defining Word @@ -149,6 +155,10 @@ class GCode(object): exec_order = 999 # if not otherwise specified, run last def __init__(self, word, *params): + """ + :param word: Word instance defining gcode (eg: Word('G0') for rapid movement) + :param params: list of Word instances (eg: Word('X-1.2') as x-coordinate) + """ assert isinstance(word, Word), "invalid gcode word %r" % code_word self.word = word self.params = {} @@ -188,6 +198,10 @@ class GCode(object): return self.exec_order < other.exec_order def add_parameter(self, word): + """ + Add given word as a parameter for this gcode + :param word: Word instance + """ 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) @@ -217,6 +231,44 @@ class GCode(object): if l in self.modal_param_letters ]) + def get_param_dict(self, letters=None): + """ + Get gcode parameters as a dict + gcode parameter like "X3.1, Y-2" would return {'X': 3.1, 'Y': -2} + :param letters: iterable whitelist of letters to include as dict keys + :return: dict of gcode parameters' (letter, value) pairs + """ + return dict( + (w.letter, w.value) for w in self.params.values() + if (letters is None) or (w.letter in letters) + ) + + # Process GCode + def process(self, machine): + """ + Process a GCode on the given Machine + :param machine: Machine instance, to change state + :return: GCodeEffect instance; effect the gcode just had on machine + """ + from .machine import Machine # importing high-level state + assert isinstance(machine, Machine), "invalid parameter" + effect = GCodeEffect() + + # Set mode + self._process_mode(machine) + + # GCode specific + self._process(machine) + + def _process_mode(self, machine): + """Set machine's state""" + machine.set_mode(self) + + def _process(self, machine): + """Process this GCode (to be overridden)""" + pass + + # ======================= Motion ======================= # (X Y Z A B C U V W apply to all motions) @@ -238,10 +290,17 @@ class GCodeMotion(GCode): modal_group = MODAL_GROUP_MAP['motion'] exec_order = 241 + def _process(self, machine): + machine.move_to(**self.get_param_dict(letters=machine.axes)) + + class GCodeRapidMove(GCodeMotion): """G0: Rapid Move""" word_key = Word('G', 0) + def _process(self, machine): + machine.move_to(rapid=True, **self.get_param_dict(letters=machine.axes)) + class GCodeLinearMove(GCodeMotion): """G1: Linear Move""" @@ -334,6 +393,7 @@ class GCodeCannedCycle(GCode): modal_group = MODAL_GROUP_MAP['motion'] exec_order = 241 + class GCodeDrillingCycle(GCodeCannedCycle): """G81: Drilling Cycle""" param_letters = GCodeCannedCycle.param_letters | set('RLP') @@ -600,11 +660,13 @@ class GCodeUnit(GCode): class GCodeUseInches(GCodeUnit): """G20: use inches for length units""" word_key = Word('G', 20) + unit_id = 0 class GCodeUseMillimeters(GCodeUnit): """G21: use millimeters for length units""" word_key = Word('G', 21) + unit_id = 1 # ======================= Plane Selection ======================= @@ -815,22 +877,64 @@ class GCodeFeedStop(GCodeOtherModal): class GCodeSelectCoordinateSystem(GCodeOtherModal): - """ - G54 - select coordinate system 1 - G55 - select coordinate system 2 - G56 - select coordinate system 3 - G57 - select coordinate system 4 - G58 - select coordinate system 5 - G59 - select coordinate system 6 - G59.1 - select coordinate system 7 - G59.2 - select coordinate system 8 - G59.3 - select coordinate system 9 - """ - @classmethod - 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]) + """Select Coordinate System""" modal_group = MODAL_GROUP_MAP['coordinate_system'] exec_order = 190 + coord_system_id = None # overridden in inheriting classes + + +class GCodeSelectCoordinateSystem1(GCodeSelectCoordinateSystem): + """Select Coordinate System 1""" + word_key = Word('G', 54) + coord_system_id = 1 + + +class GCodeSelectCoordinateSystem2(GCodeSelectCoordinateSystem): + """Select Coordinate System 2""" + word_key = Word('G', 55) + coord_system_id = 2 + + +class GCodeSelectCoordinateSystem3(GCodeSelectCoordinateSystem): + """Select Coordinate System 3""" + word_key = Word('G', 56) + coord_system_id = 3 + + +class GCodeSelectCoordinateSystem4(GCodeSelectCoordinateSystem): + """Select Coordinate System 4""" + word_key = Word('G', 57) + coord_system_id = 4 + + +class GCodeSelectCoordinateSystem5(GCodeSelectCoordinateSystem): + """Select Coordinate System 5""" + word_key = Word('G', 58) + coord_system_id = 5 + + +class GCodeSelectCoordinateSystem6(GCodeSelectCoordinateSystem): + """Select Coordinate System 6""" + word_key = Word('G', 59) + coord_system_id = 6 + + +class GCodeSelectCoordinateSystem7(GCodeSelectCoordinateSystem): + """Select Coordinate System 7""" + coord_system_id = 7 + word_key = Word('G', 59.1) + + +class GCodeSelectCoordinateSystem8(GCodeSelectCoordinateSystem): + """Select Coordinate System 8""" + word_key = Word('G', 59.2) + coord_system_id = 8 + + +class GCodeSelectCoordinateSystem9(GCodeSelectCoordinateSystem): + """Select Coordinate System 9""" + word_key = Word('G', 59.3) + coord_system_id = 9 # ======================= Flow-control Codes ======================= @@ -981,6 +1085,8 @@ class GCodeResetCoordSystemOffset(GCodeNonModal): return (w.letter == 'G') and (w.value in [92.1, 92.2]) exec_order = 230 + # TODO: machine.state.offset *= 0 + class GCodeRestoreCoordSystemOffset(GCodeNonModal): """G92.3: Restore Coordinate System Offset""" @@ -1009,7 +1115,7 @@ def _subclasses_level(root_class, recursion_level=0): :param """ yield (root_class, recursion_level) - for cls in root_class.__subclasses__(): + for cls in sorted(root_class.__subclasses__(), key=lambda c: c.__name__): for (sub, level) in _subclasses_level(cls, recursion_level+1): yield (sub, level) @@ -1028,7 +1134,11 @@ def _gcode_class_infostr(base_class=GCode): """ info_str = "" for (cls, level) in _subclasses_level(base_class): - info_str += "{indent}- {name}: {description}\n".format( + word_str = '' + if cls.word_key: + word_str = cls.word_key.clean_str + info_str += "{word} {indent}- {name}: {description}\n".format( + word="%-5s" % word_str, indent=(level * " "), name=cls.__name__, description=cls.__doc__ or "", diff --git a/pygcode/machine.py b/pygcode/machine.py index 53afa0a..61a1b5c 100644 --- a/pygcode/machine.py +++ b/pygcode/machine.py @@ -1,12 +1,211 @@ - +from copy import copy, deepcopy from collections import defaultdict -from .gcodes import MODAL_GROUP_MAP, GCode +from .gcodes import ( + MODAL_GROUP_MAP, GCode, + # Modal GCodes + GCodeIncrementalDistanceMode, + GCodeUseInches, GCodeUseMillimeters, +) +from .block import Block from .line import Line +from .words import Word + + +UNIT_IMPERIAL = GCodeUseInches.unit_id # G20 +UNIT_METRIC = GCodeUseMillimeters.unit_id # G21 +UNIT_MAP = { + UNIT_IMPERIAL: { + 'name': 'inches', + 'conversion_factor': { UNIT_METRIC: 25.4 }, + }, + UNIT_METRIC: { + 'name': 'millimeters', + 'conversion_factor': { UNIT_IMPERIAL: 1. / 25.4 }, + }, +} + + +class Position(object): + default_axes = 'XYZABCUVW' + default_unit = UNIT_METRIC + POSSIBLE_AXES = set('XYZABCUVW') + + def __init__(self, axes=None, **kwargs): + # Set axes (note: usage in __getattr__ and __setattr__) + if axes is None: + axes = self.__class__.default_axes + else: + invalid_axes = set(axes) - self.POSSIBLE_AXES + assert not invalid_axes, "invalid axes proposed %s" % invalid_axes + self.__dict__['axes'] = set(axes) & self.POSSIBLE_AXES + + # Unit + self._unit = kwargs.pop('unit', self.default_unit) + + # Initial Values + self._value = defaultdict(lambda: 0.0, dict((k, 0.0) for k in self.axes)) + self._value.update(kwargs) + + # Attributes Get/Set + def __getattr__(self, key): + if key in self.axes: + return self._value[key] + + raise AttributeError("'{cls}' object has no attribute '{key}'".format( + cls=self.__class__.__name__, + key=key + )) + + def __setattr__(self, key, value): + if key in self.axes: + self._value[key] = value + elif key in self.POSSIBLE_AXES: + raise AssertionError("'%s' axis is not defined to be set" % key) + else: + self.__dict__[key] = value + + # Copy + def __copy__(self): + return self.__class__(axes=copy(self.axes), unit=self._unit, **self.values) + + # Equality + def __eq__(self, other): + if self.axes ^ other.axes: + return False + else: + if self._unit == other._unit: + return self._value == other._value + else: + x = copy(other) + x.set_unit(self._unit) + return self._value == x._value + + def __ne__(self, other): + return not self.__eq__(other) + + # Arithmetic + def __add__(self, other): + assert not (self.axes ^ other.axes), "axes: %r != %r" % (self.axes, other.axes) + new_obj = copy(self) + for k in new_obj._value: + new_obj._value[k] += other._value[k] + return new_obj + + def __sub__(self, other): + assert not (other.axes - self.axes), "for a - b: axes in b, that are not in a: %r" % (other.axes - self.axes) + new_obj = copy(self) + for k in other._value: + new_obj._value[k] -= other._value[k] + return new_obj + + def __mul__(self, scalar): + new_obj = copy(self) + for k in self._value: + new_obj._value[k] = self._value[k] * scalar + return new_obj + + def __div__(self, scalar): + new_obj = copy(self) + for k in self._value: + new_obj._value[k] = self._value[k] / scalar + return new_obj + + __truediv__ = __div__ # Python 3 division + + # Conversion + def set_unit(self, unit): + if unit == self._unit: + return + factor = UNIT_MAP[self._unit]['conversion_factor'][unit] + for k in [k for (k, v) in self._value.items() if v is not None]: + self._value[k] *= factor + self._unit = unit + + @property + def words(self): + return sorted(Word(k, self._value[k]) for k in self.axes) + + @property + def values(self): + return dict(self._value) + + def __repr__(self): + return "<{class_name}: {coordinates}>".format( + class_name=self.__class__.__name__, + coordinates=' '.join(str(w) for w in self.words) + ) + + +class CoordinateSystem(object): + def __init__(self, axes): + self.offset = Position(axes) + + def __add__(self, other): + if isinstance(other, CoordinateSystem): + pass + + def __repr__(self): + return "<{class_name}: offset={offset}>".format( + class_name=self.__class__.__name__, + offset=repr(self.offset), + ) class State(object): """State of a Machine""" + # LinuxCNC documentation lists parameters for a machine's state: + # http://linuxcnc.org/docs/html/gcode/overview.html#sub:numbered-parameters + # AFAIK: this is everything needed to remember a machine's state that isn't + # handled by modal gcodes. + def __init__(self, axes=None): + # Coordinate Systems + self.coord_systems = {} + for i in range(1, 10): # G54-G59.3 + self.coord_systems[i] = CoordinateSystem(axes) + + self.cur_coord_sys = 1 # default to coord system 1 (G54) + + # Temporary Offset + self.offset = Position(axes) # G92 offset (reset by G92.x) + + # Missing from state (according to LinuxCNC's state variables): + # - G38.2 probe result (Position()) + # - G38 probe result (bool) + # - M66: result (bool) + # - Tool offsets (Position()) + # - Tool info (number, diameter, front angle, back angle, orientation) + + #self.work_offset = defaultdict(lambda: 0.0) + + # TODO: how to manage work offsets? (probs not like the above) + # read up on: + # Coordinate System config: + # - G92: set machine coordinate system value (no movement, effects all coordinate systems) + # - G10 L2: offsets the origin of the axes in the coordinate system specified to the value of the axis word + # - G10 L20: makes the current machine coordinates the coordinate system's offset + # Coordinate System selection: + # - G54-G59: select coordinate system (offsets from machine coordinates set by G10 L2) + + # TODO: Move this class into MachineState + + @property + def coord_sys(self): + """Current equivalent coordinate system, including all """ + if self.cur_coord_sys in self.coord_systems: + return self.coord_systems[self.cur_coord_sys] + return None + + def __repr__(self): + return "<{class_name}: coord_sys[{coord_index}]; offset={offset}>".format( + class_name=self.__class__.__name__, + coord_index=self.cur_coord_sys, + offset=repr(self.offset), + ) + + +class Mode(object): + """Machine's mode""" # State is very forgiving: # Anything possible in a machine's state may be changed & fetched. # For example: x, y, z, a, b, c may all be set & requested. @@ -15,36 +214,29 @@ class State(object): # It is also possible to set an axis to an impossibly large distance. # It is the responsibility of the Machine using this class to be # discerning in these respects. - def __init__(self): - self.axes = defaultdict(lambda: 0.0) # aka: "machine coordinates" - self.work_offset = defaultdict(lambda: 0.0) - # TODO: how to manage work offsets? (probs not like the above) - # read up on: - # - G92: coordinate system offset - # - G54-G59: select coordinate system (offsets from machine coordinates set by G10 L2) - - # TODO: Move this class into MachineState - - -class MachineState(object): - """Machine's state, and mode""" # Default Mode + # for a Grbl controller this can be obtained with the `$G` command, eg: + # > $G + # > [GC:G0 G54 G17 G21 G90 G94 M5 M9 T0 F0 S0] + # ref: https://github.com/gnea/grbl/wiki/Grbl-v1.1-Commands#g---view-gcode-parser-state default_mode = ''' - G17 (plane_selection: X/Y plane) - G90 (distance: absolute position. ie: not "turtle" mode) - G91.1 (arc_ijk_distance: IJK sets arc center vertex relative to current position) - G94 (feed_rate_mode: feed-rate defined in units/min) - G21 (units: mm) - G40 (cutter_diameter_comp: no compensation) - G49 (tool_length_offset: no offset) - G61 (control_mode: exact path mode) - G97 (spindle_speed_mode: RPM Mode) - M0 (stopping: program paused) - M5 (spindle: off) - M9 (coolant: off) - F100 (feed_rate: 100 mm/min) - S1000 (tool: 1000 rpm, when it's switched on) + G0 (movement: rapid) + G17 (plane_selection: X/Y plane) + G90 (distance: absolute position. ie: not "turtle" mode) + G91.1 (arc_ijk_distance: IJK sets arc center vertex relative to current position) + G94 (feed_rate_mode: feed-rate defined in units/min) + G21 (units: mm) + G40 (cutter_diameter_comp: no compensation) + G49 (tool_length_offset: no offset) + G54 (coordinate_system: 1) + G61 (control_mode: exact path mode) + G97 (spindle_speed_mode: RPM Mode) + M5 (spindle: off) + M9 (coolant: off) + F0 (feed_rate: 0) + S0 (spindle_speed: 0) + T0 (tool: 0) ''' # Mode is defined by gcodes set by processed blocks: @@ -56,9 +248,18 @@ class MachineState(object): self.set_mode(*Line(self.default_mode).block.gcodes) def set_mode(self, *gcode_list): + """ + Set machine mode from given gcodes + :param gcode_list: list of GCode instances (given as individual parameters) + :return: dict of form: {<modal group>: <new mode GCode>, ...} + """ + modal_gcodes = {} for g in sorted(gcode_list): # sorted by execution order if g.modal_group is not None: self.modal_groups[g.modal_group] = g.modal_copy() + modal_gcodes[g.modal_group] = self.modal_groups[g.modal_group] + # assumption: no 2 gcodes are in the same modal_group + return modal_gcodes def __getattr__(self, key): if key in MODAL_GROUP_MAP: @@ -99,8 +300,34 @@ class MachineState(object): class Machine(object): + """Machine to process gcodes, enforce axis limits, keep track of time, etc""" + + # Class types + MODE_CLASS = Mode + STATE_CLASS = State + + axes = set('XYZ') + def __init__(self): - self.state = MachineState() + self.mode = self.MODE_CLASS() + self.state = self.STATE_CLASS(axes=self.axes) + + # Position type (with default axes the same as this machine) + self.Position = type('Position', (Position,), { + 'default_axes': self.axes, + 'default_unit': self.mode.units.unit_id, + }) + + # Absolute machine position + self.abs_pos = self.Position() + + def set_mode(self, *gcode_list): + self.mode.set_mode(*gcode_list) # passthrough + + # Act on mode changes + coord_sys_mode = self.mode.coordinate_system + if coord_sys_mode: + self.state.cur_coord_sys = coord_sys_mode.coord_system_id def process(self, *gcode_list, **kwargs): """ @@ -108,16 +335,52 @@ class Machine(object): :param gcode_list: list of GCode instances :param modal_params: list of Word instances to be applied to current movement mode """ + # Add modal gcode to list of given gcodes modal_params = kwargs.get('modal_params', []) - - #process_gcodes = + if modal_params: + assert self.mode.motion is not None, "unable to assign modal parameters when no motion mode is set" + (modal_gcodes, unasigned_words) = words2gcodes([self.mode.motion.word] + modal_params) + assert unasigned_words == [], "modal parameters '%s' unknown when in mode: %r" % ( + ' '.join(str(x) for x in unasigned_words), self.mode + ) + gcode_list += modal_gcodes for gcode in sorted(gcode_list): - self.state.set_mode(gcode) # if gcode is not modal, it's ignored + gcode.process(self) # shifts ownership of what happens now to GCode class - gcode.process(self.state) # TODO: gcode instance to change machine's state # Questions to drive design: # - how much time did the command take? # - what was the tool's distance / displacement # - did the tool travel outside machine boundaries? + # Use-cases + # - Transform / rotate coordinate system in given gcode + # - Convert arcs to linear segments (visa versa?) + # - Correct precision errors + # - Crop a file (eg: resume half way through) + + def process_block(self, block): + self.process(*block.gcodes, modal_params=block.modal_params) + + @property + def pos(self): + """Return current position in current coordinate system""" + coord_sys_offset = getattr(self.state.coord_sys, 'offset', Position(axes=self.axes)) + temp_offset = self.state.offset + return (self.abs_pos - coord_sys_offset) - temp_offset + + @pos.setter + def pos(self, value): + """Set absolute position given current position and coordinate system""" + coord_sys_offset = getattr(self.state.coord_sys, 'offset', Position(axes=self.axes)) + temp_offset = self.state.offset + self.abs_pos = (value + temp_offset) + coord_sys_offset + + # =================== Machine Actions =================== + def move_to(self, rapid=False, **coords): + """Move machine to given position""" + given_position = Position(axes=self.axes, **coords) + if isinstance(self.mode.distance, GCodeIncrementalDistanceMode): + self.pos += given_position + else: # assumed: GCodeAbsoluteDistanceMode + self.pos = given_position diff --git a/pygcode/words.py b/pygcode/words.py index 5ced857..f4ead70 100644 --- a/pygcode/words.py +++ b/pygcode/words.py @@ -281,7 +281,7 @@ class Word(object): return "%s: %s" % (self.letter, WORD_MAP[self.letter]['description']) -def iter_words(block_text): +def text2words(block_text): """ Iterate through block text yielding Word instances :param block_text: text for given block with comments removed @@ -315,8 +315,21 @@ def iter_words(block_text): def str2word(word_str): - words = list(iter_words(word_str)) + words = list(text2words(word_str)) if words: assert len(words) <= 1, "more than one word given" return words[0] return None + + +def words2dict(word_list, limit_word_letters=None): + """ + Represent a list of words as a dict + :param limit_word_letters: iterable containing a white-list of word letters (None allows all) + :return: dict of the form: {<letter>: <value>, ... } + """ + # Remember: duplicate word letters cannot be represented as a dict + return dict( + (w.letter, w.value) for w in word_list + if (limit_word_letters is None) or (w.letter in limit_word_letters) + ) diff --git a/tests/test_file.py b/tests/test_file.py index 82a20ee..e678b74 100644 --- a/tests/test_file.py +++ b/tests/test_file.py @@ -4,17 +4,21 @@ import inspect import unittest -# Units Under Test -_this_path = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) -sys.path.insert(0, os.path.join(_this_path, '..')) -from pygcode.file import parse, GCodeFile +# Add relative pygcode to path +from testutils import add_pygcode_to_path, str_lines +add_pygcode_to_path() -class FileParseTest(unittest.TestCase): +# Units under test +from pygcode.file import GCodeFile, GCodeParser + +class GCodeParserTest(unittest.TestCase): FILENAME = 'test-files/vertical-slot.ngc' def test_parser(self): - file = parse(self.FILENAME) - self.assertEqual(len(file.lines), 26) - # FIXME: just verifying content visually - #for line in file.lines: - # print(line) + parser = GCodeParser(self.FILENAME) + # count lines + line_count = 0 + for line in parser.iterlines(): + line_count += 1 + self.assertEqual(line_count, 26) + parser.close() diff --git a/tests/test_gcodes.py b/tests/test_gcodes.py index 1ae0490..ef3374a 100644 --- a/tests/test_gcodes.py +++ b/tests/test_gcodes.py @@ -4,9 +4,11 @@ import inspect import re import unittest -# Units Under Test -_this_path = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) -sys.path.insert(0, os.path.join(_this_path, '..')) +# Add relative pygcode to path +from testutils import add_pygcode_to_path, str_lines +add_pygcode_to_path() + +# Units under test from pygcode import gcodes from pygcode import words class TestGCodeWordMapping(unittest.TestCase): @@ -60,7 +62,7 @@ class TestGCodeModalGroups(unittest.TestCase): match = re.search(r'^\s*(?P<title>.*)\s*\(Group (?P<group>\d+)\)\s*(?P<words>.*)$', row, re.I) if match: for word_str in re.split(r'\s*,\s*', match.group('words')): - word = list(words.iter_words(word_str))[0] + word = list(words.text2words(word_str))[0] gcode_class = gcodes.word_gcode_class(word) # GCode class found for each word in the table self.assertIsNotNone(gcode_class) @@ -81,7 +83,7 @@ class TestGCodeModalGroups(unittest.TestCase): 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)) + word_list = list(words.text2words(line)) result = gcodes.words2gcodes(word_list) # result form self.assertIsInstance(result, tuple) diff --git a/tests/test_line.py b/tests/test_line.py index 2ad74a5..24337ca 100644 --- a/tests/test_line.py +++ b/tests/test_line.py @@ -4,9 +4,11 @@ import inspect import unittest -# Units Under Test -_this_path = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) -sys.path.insert(0, os.path.join(_this_path, '..')) +# Add relative pygcode to path +from testutils import add_pygcode_to_path, str_lines +add_pygcode_to_path() + +# Units under test from pygcode.line import Line diff --git a/tests/test_machine.py b/tests/test_machine.py new file mode 100644 index 0000000..c77a3f8 --- /dev/null +++ b/tests/test_machine.py @@ -0,0 +1,122 @@ +import unittest + +# Add relative pygcode to path +from testutils import add_pygcode_to_path, str_lines +add_pygcode_to_path() + +# Units under test +from pygcode.machine import Position, Machine +from pygcode.line import Line + + +class PositionTests(unittest.TestCase): + def test_basics(self): + p = Position() + # + + def test_default_axes(self): + p = Position() # no instantiation parameters + # all initialized to zero + for axis in 'XYZABCUVW': + self.assertEqual(getattr(p, axis), 0) + + for axis in 'XYZABCUVW': + # set to 100 + setattr(p, axis, 100) + self.assertEqual(getattr(p, axis), 100) + for inner_axis in set('XYZABCUVW') - {axis}: # no other axis has changed + self.assertEqual(getattr(p, inner_axis), 0), "axis '%s'" % inner_axis + # revert back to zero + setattr(p, axis, 0) + self.assertEqual(getattr(p, axis), 0) + + # Equality + def test_equality(self): + p1 = Position(axes='XYZ', X=1, Y=2) + p2 = Position(axes='XYZ', X=1, Y=2, Z=0) + p3 = Position(axes='XYZ', X=1, Y=2, Z=1000) + p4 = Position(axes='XYZA', X=1, Y=2, Z=0) + + # p1 <--> p2 + self.assertTrue(p1 == p2) + self.assertFalse(p1 != p2) # negative case + + # p2 <--> p3 + self.assertTrue(p2 != p3) + self.assertFalse(p2 == p3) # negative case + + # p2 <--> p4 + self.assertTrue(p2 != p4) + self.assertFalse(p2 == p4) # negative case + + # Arithmetic + def test_arithmetic_add(self): + p1 = Position(axes='XYZ', X=1, Y=2) + p2 = Position(axes='XYZ', Y=10, Z=-20) + self.assertEqual(p1 + p2, Position(axes='XYZ', X=1, Y=12, Z=-20)) + + p3 = Position(axes='XYZA') + with self.assertRaises(AssertionError): + p1 + p3 # mismatched axes + with self.assertRaises(AssertionError): + p3 + p1 # mismatched axes + + def test_arithmetic_sub(self): + p1 = Position(axes='XYZ', X=1, Y=2) + p2 = Position(axes='XYZ', Y=10, Z=-20) + self.assertEqual(p1 - p2, Position(axes='XYZ', X=1, Y=-8, Z=20)) + + p3 = Position(axes='XYZA') + p3 - p1 # fine + with self.assertRaises(AssertionError): + p1 - p3 # mismatched axes + + def test_arithmetic_multiply(self): + p = Position(axes='XYZ', X=2, Y=10) + self.assertEqual(p * 2, Position(axes='XYZ', X=4, Y=20)) + + def test_arithmetic_divide(self): + p = Position(axes='XYZ', X=2, Y=10) + self.assertEqual(p / 2, Position(axes='XYZ', X=1, Y=5)) + + + +class MachineGCodeProcessingTests(unittest.TestCase): + def test_linear_movement(self): + m = Machine() + test_str = '''; move in a 10mm square + F100 M3 S1000 ; 0 + g1 x0 y10 ; 1 + g1 x10 y10 ; 2 + g1 x10 y0 ; 3 + g1 x0 y0 ; 4 + ''' + expected_pos = { + '0': m.Position(), + '1': m.Position(X=0, Y=10), + '2': m.Position(X=10, Y=10), + '3': m.Position(X=10, Y=0), + '4': m.Position(X=0, Y=0), + } + #print("\n%r\n%r" % (m.mode, m.state)) + for line_text in str_lines(test_str): + line = Line(line_text) + if line.block: + #print("\n%s" % line.block) + m.process_block(line.block) + # Assert possition change correct + comment = line.comment.text + if comment in expected_pos: + self.assertEqual(m.pos, expected_pos[comment]) + #print("%r\n%r\npos=%r" % (m.mode, m.state, m.pos)) + + +#m = Machine() +# +#file = GCodeParser('part1.gcode') +#for line in file.iterlines(): +# for (i, gcode) in enumerate(line.block.gcode): +# if isinstance(gcode, GCodeArcMove): +# arc = gcode +# line_params = arc.line_segments(precision=0.0005) +# for diff --git a/tests/test_words.py b/tests/test_words.py index 6dfa959..5152f5d 100644 --- a/tests/test_words.py +++ b/tests/test_words.py @@ -4,23 +4,18 @@ import inspect import unittest -# Units Under Test -_this_path = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) -sys.path.insert(0, os.path.join(_this_path, '..')) +# Add relative pygcode to path +from testutils import add_pygcode_to_path, str_lines +add_pygcode_to_path() + +# Units under test from pygcode import words -#words.iter_words - -class WordTests(unittest.TestCase): - def test_O(self): - pass # TODO - - class WordIterTests(unittest.TestCase): def test_iter1(self): block_str = 'G01 Z-0.5 F100' - w = list(words.iter_words(block_str)) + w = list(words.text2words(block_str)) # word length self.assertEqual(len(w), 3) # word values @@ -30,7 +25,7 @@ class WordIterTests(unittest.TestCase): def test_iter2(self): block_str = 'G02 X10.75 Y47.44 I-0.11 J-1.26 F70' - w = list(words.iter_words(block_str)) + w = list(words.text2words(block_str)) # word length self.assertEqual(len(w), 6) # word values diff --git a/tests/testutils.py b/tests/testutils.py new file mode 100644 index 0000000..f6124aa --- /dev/null +++ b/tests/testutils.py @@ -0,0 +1,25 @@ +import sys +import os +import inspect + +import re + +# Units Under Test +_pygcode_in_path = False +def add_pygcode_to_path(): + global _pygcode_in_path + if not _pygcode_in_path: + # Add pygcode (relative to this test-path) to the system path + _this_path = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) + sys.path.insert(0, os.path.join(_this_path, '..')) + + _pygcode_in_path = True + +add_pygcode_to_path() + + +# String Utilities +def str_lines(text): + """Split given string into lines (ignore blank lines, and automagically strip)""" + for match in re.finditer(r'\s*(?P<content>.*?)\s*\n', text): + yield match.group('content') From 0ae4e08b81c3180eaa022c329179ee445b62b4c6 Mon Sep 17 00:00:00 2001 From: Peter Boin <nymphii@gmail.com> Date: Sun, 9 Jul 2017 01:25:30 +1000 Subject: [PATCH 13/32] __init__ --- pygcode/__init__.py | 367 ++++++++++++++++++++++++++++++++++++++++++++ pygcode/gcodes.py | 8 - 2 files changed, 367 insertions(+), 8 deletions(-) diff --git a/pygcode/__init__.py b/pygcode/__init__.py index e69de29..bfffe04 100644 --- a/pygcode/__init__.py +++ b/pygcode/__init__.py @@ -0,0 +1,367 @@ +__all__ = [ + # Machine + 'Machine', 'Position', 'CoordinateSystem', 'State', 'Mode', + # Line + 'Line', + # Block + 'Block', + # Comment + 'Comment', 'split_line', + # Word + 'Word', 'text2words', 'str2word', 'words2dict', + + # GCodes + 'GCode', 'words2gcodes', + 'GCodeBoringCycleDwellFeedOut', + 'GCodeBoringCycleFeedOut', + 'GCodeDrillingCycle', + 'GCodeDrillingCycleChipBreaking', + 'GCodeDrillingCycleDwell', + 'GCodeDrillingCyclePeck', + 'GCodeThreadingCycle', + 'GCodeCannedCycleReturnLevel', + 'GCodeCoolantFloodOn', + 'GCodeCoolantMistOn', + 'GCodeCoolantOff', + 'GCodeCutterCompLeft', + 'GCodeCutterCompRight', + 'GCodeCutterRadiusCompOff', + 'GCodeDynamicCutterCompLeft', + 'GCodeDynamicCutterCompRight', + 'GCodeAbsoluteArcDistanceMode', + 'GCodeAbsoluteDistanceMode', + 'GCodeIncrementalArcDistanceMode', + 'GCodeIncrementalDistanceMode', + 'GCodeLatheDiameterMode', + 'GCodeLatheRadiusMode', + 'GCodeInverseTimeMode', + 'GCodeUnitsPerMinuteMode', + 'GCodeUnitsPerRevolution', + 'GCodeAnalogOutputImmediate', + 'GCodeAnalogOutputSyncd', + 'GCodeDigitalOutputOff', + 'GCodeDigitalOutputOffSyncd', + 'GCodeDigitalOutputOn', + 'GCodeDigitalOutputOnSyncd', + 'GCodeWaitOnInput', + 'GCodeArcMove', + 'GCodeArcMoveCCW', + 'GCodeArcMoveCW', + 'GCodeCancelCannedCycle', + 'GCodeCublcSpline', + 'GCodeDwell', + 'GCodeLinearMove', + 'GCodeNURBS', + 'GCodeNURBSEnd', + 'GCodeQuadraticSpline', + 'GCodeRapidMove', + 'GCodeRigidTapping', + 'GCodeSpindleSyncMotion', + 'GCodeStraightProbe', + 'GCodeCoordSystemOffset', + 'GCodeGotoPredefinedPosition', + 'GCodeMoveInMachineCoords', + 'GCodeResetCoordSystemOffset', + 'GCodeRestoreCoordSystemOffset', + 'GCodeSet', + 'GCodeSetPredefinedPosition', + 'GCodeToolChange', + 'GCodeToolSetCurrent', + 'GCodeUserDefined', + 'GCodeAdaptiveFeed', + 'GCodeFeedOverride', + 'GCodeFeedRate', + 'GCodeFeedStop', + 'GCodeSelectCoordinateSystem', + 'GCodeSelectCoordinateSystem1', + 'GCodeSelectCoordinateSystem2', + 'GCodeSelectCoordinateSystem3', + 'GCodeSelectCoordinateSystem4', + 'GCodeSelectCoordinateSystem5', + 'GCodeSelectCoordinateSystem6', + 'GCodeSelectCoordinateSystem7', + 'GCodeSelectCoordinateSystem8', + 'GCodeSelectCoordinateSystem9', + 'GCodeSelectTool', + 'GCodeSpeedAndFeedOverrideOff', + 'GCodeSpeedAndFeedOverrideOn', + 'GCodeSpindleSpeed', + 'GCodeSpindleSpeedOverride', + 'GCodeExactPathMode', + 'GCodeExactStopMode', + 'GCodePathBlendingMode', + 'GCodeSelectUVPlane', + 'GCodeSelectVWPlane', + 'GCodeSelectWUPlane', + 'GCodeSelectXYPlane', + 'GCodeSelectYZPlane', + 'GCodeSelectZXPlane', + 'GCodeEndProgram', + 'GCodeEndProgramPalletShuttle', + 'GCodePalletChangePause', + 'GCodePauseProgram', + 'GCodePauseProgramOptional', + 'GCodeOrientSpindle', + 'GCodeSpindleConstantSurfaceSpeedMode', + 'GCodeSpindleRPMMode', + 'GCodeStartSpindleCCW', + 'GCodeStartSpindleCW', + 'GCodeStopSpindle', + 'GCodeAddToolLengthOffset', + 'GCodeCancelToolLengthOffset', + 'GCodeDynamicToolLengthOffset', + 'GCodeToolLengthOffset', + 'GCodeUseInches', + 'GCodeUseMillimeters', +] + +# Machine +from .machine import ( + Position, CoordinateSystem, + State, Mode, + Machine, +) + +# Line +from .line import Line + +# Block +from .block import Block + +# Comment +from .comment import Comment, split_line + +# Word +from .words import ( + Word, + text2words, str2word, words2dict, +) + +# GCode +from .gcodes import ( + GCode, words2gcodes, + + # $ python -c "from pygcode.gcodes import _gcode_class_infostr; print(_gcode_class_infostr())" + # - GCode: + # - GCodeCannedCycle: + # G89 - GCodeBoringCycleDwellFeedOut: G89: Boring Cycle, Dwell, Feed Out + # G85 - GCodeBoringCycleFeedOut: G85: Boring Cycle, Feed Out + # G81 - GCodeDrillingCycle: G81: Drilling Cycle + # G73 - GCodeDrillingCycleChipBreaking: G73: Drilling Cycle, ChipBreaking + # G82 - GCodeDrillingCycleDwell: G82: Drilling Cycle, Dwell + # G83 - GCodeDrillingCyclePeck: G83: Drilling Cycle, Peck + # G76 - GCodeThreadingCycle: G76: Threading Cycle + # - GCodeCannedReturnMode: + # G98 - GCodeCannedCycleReturnLevel: G98: Canned Cycle Return Level + # - GCodeCoolant: + # M08 - GCodeCoolantFloodOn: M8: turn flood coolant on + # M07 - GCodeCoolantMistOn: M7: turn mist coolant on + # M09 - GCodeCoolantOff: M9: turn all coolant off + # - GCodeCutterRadiusComp: + # G41 - GCodeCutterCompLeft: G41: Cutter Radius Compensation (left) + # G42 - GCodeCutterCompRight: G42: Cutter Radius Compensation (right) + # G40 - GCodeCutterRadiusCompOff: G40: Cutter Radius Compensation Off + # G41.1 - GCodeDynamicCutterCompLeft: G41.1: Dynamic Cutter Radius Compensation (left) + # G42.1 - GCodeDynamicCutterCompRight: G42.1: Dynamic Cutter Radius Compensation (right) + # - GCodeDistanceMode: + # G90.1 - GCodeAbsoluteArcDistanceMode: G90.1: Absolute Distance Mode for Arc IJK Parameters + # G90 - GCodeAbsoluteDistanceMode: G90: Absolute Distance Mode + # G91.1 - GCodeIncrementalArcDistanceMode: G91.1: Incremental Distance Mode for Arc IJK Parameters + # G91 - GCodeIncrementalDistanceMode: G91: Incremental Distance Mode + # G07 - GCodeLatheDiameterMode: G7: Lathe Diameter Mode + # G08 - GCodeLatheRadiusMode: G8: Lathe Radius Mode + # - GCodeFeedRateMode: + # G93 - GCodeInverseTimeMode: G93: Inverse Time Mode + # G94 - GCodeUnitsPerMinuteMode: G94: Units Per MinuteMode + # G95 - GCodeUnitsPerRevolution: G95: Units Per Revolution + # - GCodeIO: + # - GCodeAnalogOutput: Analog Output + # M68 - GCodeAnalogOutputImmediate: M68: Analog Output, Immediate + # M67 - GCodeAnalogOutputSyncd: M67: Analog Output, Synchronized + # - GCodeDigitalOutput: Digital Output Control + # M65 - GCodeDigitalOutputOff: M65: turn off digital output immediately + # M63 - GCodeDigitalOutputOffSyncd: M63: turn off digital output synchronized with motion + # M64 - GCodeDigitalOutputOn: M64: turn on digital output immediately + # M62 - GCodeDigitalOutputOnSyncd: M62: turn on digital output synchronized with motion + # M66 - GCodeWaitOnInput: M66: Wait on Input + # - GCodeMotion: + # - GCodeArcMove: Arc Move + # G03 - GCodeArcMoveCCW: G3: Arc Move (counter-clockwise) + # G02 - GCodeArcMoveCW: G2: Arc Move (clockwise) + # G80 - GCodeCancelCannedCycle: G80: Cancel Canned Cycle + # G05 - GCodeCublcSpline: G5: Cubic Spline + # G04 - GCodeDwell: G4: Dwell + # G01 - GCodeLinearMove: G1: Linear Move + # G05.2 - GCodeNURBS: G5.2: Non-uniform rational basis spline (NURBS) + # G05.3 - GCodeNURBSEnd: G5.3: end NURBS mode + # G05.1 - GCodeQuadraticSpline: G5.1: Quadratic Spline + # G00 - GCodeRapidMove: G0: Rapid Move + # G33.1 - GCodeRigidTapping: G33.1: Rigid Tapping + # G33 - GCodeSpindleSyncMotion: G33: Spindle Synchronized Motion + # - GCodeStraightProbe: G38.2-G38.5: Straight Probe + # - GCodeNonModal: + # G92 - GCodeCoordSystemOffset: G92: Coordinate System Offset + # - GCodeGotoPredefinedPosition: G28,G30: Goto Predefined Position (rapid movement) + # G53 - GCodeMoveInMachineCoords: G53: Move in Machine Coordinates + # - GCodeResetCoordSystemOffset: G92.1,G92.2: Reset Coordinate System Offset + # G92.3 - GCodeRestoreCoordSystemOffset: G92.3: Restore Coordinate System Offset + # G10 - GCodeSet: G10: Set stuff + # - GCodeSetPredefinedPosition: G28.1,G30.1: Set Predefined Position + # M06 - GCodeToolChange: M6: Tool Change + # M61 - GCodeToolSetCurrent: M61: Set Current Tool + # - GCodeUserDefined: M101-M199: User Defined Commands + # - GCodeOtherModal: + # M52 - GCodeAdaptiveFeed: M52: Adaptive Feed Control + # M50 - GCodeFeedOverride: M50: Feed Override Control + # - GCodeFeedRate: F: Set Feed Rate + # M53 - GCodeFeedStop: M53: Feed Stop Control + # - GCodeSelectCoordinateSystem: Select Coordinate System + # G54 - GCodeSelectCoordinateSystem1: Select Coordinate System 1 + # G55 - GCodeSelectCoordinateSystem2: Select Coordinate System 2 + # G56 - GCodeSelectCoordinateSystem3: Select Coordinate System 3 + # G57 - GCodeSelectCoordinateSystem4: Select Coordinate System 4 + # G58 - GCodeSelectCoordinateSystem5: Select Coordinate System 5 + # G59 - GCodeSelectCoordinateSystem6: Select Coordinate System 6 + # G59.1 - GCodeSelectCoordinateSystem7: Select Coordinate System 7 + # G59.2 - GCodeSelectCoordinateSystem8: Select Coordinate System 8 + # G59.3 - GCodeSelectCoordinateSystem9: Select Coordinate System 9 + # - GCodeSelectTool: T: Select Tool + # M49 - GCodeSpeedAndFeedOverrideOff: M49: Speed and Feed Override Control Off + # M48 - GCodeSpeedAndFeedOverrideOn: M48: Speed and Feed Override Control On + # - GCodeSpindleSpeed: S: Set Spindle Speed + # M51 - GCodeSpindleSpeedOverride: M51: Spindle Speed Override Control + # - GCodePathControlMode: + # G61 - GCodeExactPathMode: G61: Exact path mode + # G61.1 - GCodeExactStopMode: G61.1: Exact stop mode + # G64 - GCodePathBlendingMode: G64: Path Blending + # - GCodePlaneSelect: + # G17.1 - GCodeSelectUVPlane: G17.1: select UV plane + # G19.1 - GCodeSelectVWPlane: G19.1: select VW plane + # G18.1 - GCodeSelectWUPlane: G18.1: select WU plane + # G17 - GCodeSelectXYPlane: G17: select XY plane (default) + # G19 - GCodeSelectYZPlane: G19: select YZ plane + # G18 - GCodeSelectZXPlane: G18: select ZX plane + # - GCodeProgramControl: + # M02 - GCodeEndProgram: M2: Program End + # M30 - GCodeEndProgramPalletShuttle: M30: exchange pallet shuttles and end the program + # M60 - GCodePalletChangePause: M60: Pallet Change Pause + # M00 - GCodePauseProgram: M0: Program Pause + # M01 - GCodePauseProgramOptional: M1: Program Pause (optional) + # - GCodeSpindle: + # M19 - GCodeOrientSpindle: M19: Orient Spindle + # G96 - GCodeSpindleConstantSurfaceSpeedMode: G96: Spindle Constant Surface Speed + # G97 - GCodeSpindleRPMMode: G97: Spindle RPM Speed + # M04 - GCodeStartSpindleCCW: M4: Start Spindle Counter-Clockwise + # M03 - GCodeStartSpindleCW: M3: Start Spindle Clockwise + # M05 - GCodeStopSpindle: M5: Stop Spindle + # - GCodeToolLength: + # G43.2 - GCodeAddToolLengthOffset: G43.2: Appkly Additional Tool Length Offset + # G49 - GCodeCancelToolLengthOffset: G49: Cancel Tool Length Compensation + # G43.1 - GCodeDynamicToolLengthOffset: G43.1: Dynamic Tool Length Offset + # G43 - GCodeToolLengthOffset: G43: Tool Length Offset + # - GCodeUnit: + # G20 - GCodeUseInches: G20: use inches for length units + # G21 - GCodeUseMillimeters: G21: use millimeters for length units + + GCodeBoringCycleDwellFeedOut, + GCodeBoringCycleFeedOut, + GCodeDrillingCycle, + GCodeDrillingCycleChipBreaking, + GCodeDrillingCycleDwell, + GCodeDrillingCyclePeck, + GCodeThreadingCycle, + GCodeCannedCycleReturnLevel, + GCodeCoolantFloodOn, + GCodeCoolantMistOn, + GCodeCoolantOff, + GCodeCutterCompLeft, + GCodeCutterCompRight, + GCodeCutterRadiusCompOff, + GCodeDynamicCutterCompLeft, + GCodeDynamicCutterCompRight, + GCodeAbsoluteArcDistanceMode, + GCodeAbsoluteDistanceMode, + GCodeIncrementalArcDistanceMode, + GCodeIncrementalDistanceMode, + GCodeLatheDiameterMode, + GCodeLatheRadiusMode, + GCodeInverseTimeMode, + GCodeUnitsPerMinuteMode, + GCodeUnitsPerRevolution, + GCodeAnalogOutputImmediate, + GCodeAnalogOutputSyncd, + GCodeDigitalOutputOff, + GCodeDigitalOutputOffSyncd, + GCodeDigitalOutputOn, + GCodeDigitalOutputOnSyncd, + GCodeWaitOnInput, + GCodeArcMove, + GCodeArcMoveCCW, + GCodeArcMoveCW, + GCodeCancelCannedCycle, + GCodeCublcSpline, + GCodeDwell, + GCodeLinearMove, + GCodeNURBS, + GCodeNURBSEnd, + GCodeQuadraticSpline, + GCodeRapidMove, + GCodeRigidTapping, + GCodeSpindleSyncMotion, + GCodeStraightProbe, + GCodeCoordSystemOffset, + GCodeGotoPredefinedPosition, + GCodeMoveInMachineCoords, + GCodeResetCoordSystemOffset, + GCodeRestoreCoordSystemOffset, + GCodeSet, + GCodeSetPredefinedPosition, + GCodeToolChange, + GCodeToolSetCurrent, + GCodeUserDefined, + GCodeAdaptiveFeed, + GCodeFeedOverride, + GCodeFeedRate, + GCodeFeedStop, + GCodeSelectCoordinateSystem, + GCodeSelectCoordinateSystem1, + GCodeSelectCoordinateSystem2, + GCodeSelectCoordinateSystem3, + GCodeSelectCoordinateSystem4, + GCodeSelectCoordinateSystem5, + GCodeSelectCoordinateSystem6, + GCodeSelectCoordinateSystem7, + GCodeSelectCoordinateSystem8, + GCodeSelectCoordinateSystem9, + GCodeSelectTool, + GCodeSpeedAndFeedOverrideOff, + GCodeSpeedAndFeedOverrideOn, + GCodeSpindleSpeed, + GCodeSpindleSpeedOverride, + GCodeExactPathMode, + GCodeExactStopMode, + GCodePathBlendingMode, + GCodeSelectUVPlane, + GCodeSelectVWPlane, + GCodeSelectWUPlane, + GCodeSelectXYPlane, + GCodeSelectYZPlane, + GCodeSelectZXPlane, + GCodeEndProgram, + GCodeEndProgramPalletShuttle, + GCodePalletChangePause, + GCodePauseProgram, + GCodePauseProgramOptional, + GCodeOrientSpindle, + GCodeSpindleConstantSurfaceSpeedMode, + GCodeSpindleRPMMode, + GCodeStartSpindleCCW, + GCodeStartSpindleCW, + GCodeStopSpindle, + GCodeAddToolLengthOffset, + GCodeCancelToolLengthOffset, + GCodeDynamicToolLengthOffset, + GCodeToolLengthOffset, + GCodeUseInches, + GCodeUseMillimeters, +) diff --git a/pygcode/gcodes.py b/pygcode/gcodes.py index 4e8b116..5f70312 100644 --- a/pygcode/gcodes.py +++ b/pygcode/gcodes.py @@ -102,7 +102,6 @@ MODAL_GROUP_MAP = { 'tool': 203, } - # Execution Order # Order taken http://linuxcnc.org/docs/html/gcode/overview.html#_g_code_order_of_execution # (as of 2017-07-03) @@ -132,12 +131,6 @@ MODAL_GROUP_MAP = { # 240: Perform motion (G0 to G3, G33, G38.x, G73, G76, G80 to G89), as modified (possibly) by G53. # 250: Stop (M0, M1, M2, M30, M60). -class GCodeEffect(object): - """Effect a gcode has on a machine upon processing""" - def __init__(self, **kwargs): - self.mode = {} - self.dt = 0.0 - class GCode(object): # Defining Word @@ -252,7 +245,6 @@ class GCode(object): """ from .machine import Machine # importing high-level state assert isinstance(machine, Machine), "invalid parameter" - effect = GCodeEffect() # Set mode self._process_mode(machine) From 988ee9db94dcadda6b7cc21eaaecf834c1686dc1 Mon Sep 17 00:00:00 2001 From: Peter Boin <nymphii@gmail.com> Date: Sun, 9 Jul 2017 11:32:37 +1000 Subject: [PATCH 14/32] fixed over-usage of assert --- pygcode/exceptions.py | 15 +++++++++++++-- pygcode/gcodes.py | 11 ++++++++--- pygcode/machine.py | 45 +++++++++++++++++++++++++++++-------------- pygcode/words.py | 14 +++++++++----- tests/test_machine.py | 7 ++++--- 5 files changed, 65 insertions(+), 27 deletions(-) diff --git a/pygcode/exceptions.py b/pygcode/exceptions.py index 0249ff7..d445e6b 100644 --- a/pygcode/exceptions.py +++ b/pygcode/exceptions.py @@ -2,6 +2,17 @@ # ===================== Parsing Exceptions ===================== class GCodeBlockFormatError(Exception): """Raised when errors encountered while parsing block text""" - pass -# ===================== Parsing Exceptions ===================== +class GCodeParameterError(Exception): + """Raised for conflicting / invalid / badly formed parameters""" + +class GCodeWordStrError(Exception): + """Raised when issues found while parsing a word string""" + +# ===================== Machine Exceptions ===================== +class MachineInvalidAxis(Exception): + """Raised if an axis is invalid""" + # For example: for axes X/Y/Z, set the value of "Q"; wtf? + +class MachineInvalidState(Exception): + """Raised if a machine state is set incorrectly, or in conflict""" diff --git a/pygcode/gcodes.py b/pygcode/gcodes.py index 5f70312..6b20d9e 100644 --- a/pygcode/gcodes.py +++ b/pygcode/gcodes.py @@ -3,6 +3,8 @@ from copy import copy from .words import Word +from .exceptions import GCodeParameterError + # Terminology of a "G-Code" # For the purposes of this library, so-called "G" codes do not necessarily # use the letter "G" in their word; other letters include M, F, S, and T @@ -196,8 +198,11 @@ class GCode(object): :param word: Word instance """ 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) + if word.letter not in self.param_letters: + raise GCodeParameterError("invalid parameter for %s: %s" % (self.__class__.__name__, str(word))) + if word.letter in self.params: + raise GCodeParameterError("parameter defined twice: %s -> %s" % (self.params[word.letter], word)) + self.params[word.letter] = word def __getattr__(self, key): @@ -243,7 +248,7 @@ class GCode(object): :param machine: Machine instance, to change state :return: GCodeEffect instance; effect the gcode just had on machine """ - from .machine import Machine # importing high-level state + from .machine import Machine # importing up (done live to avoid dependency loop) assert isinstance(machine, Machine), "invalid parameter" # Set mode diff --git a/pygcode/machine.py b/pygcode/machine.py index 61a1b5c..8dc1db8 100644 --- a/pygcode/machine.py +++ b/pygcode/machine.py @@ -11,6 +11,7 @@ from .block import Block from .line import Line from .words import Word +from .exceptions import MachineInvalidAxis, MachineInvalidState UNIT_IMPERIAL = GCodeUseInches.unit_id # G20 UNIT_METRIC = GCodeUseMillimeters.unit_id # G21 @@ -37,7 +38,8 @@ class Position(object): axes = self.__class__.default_axes else: invalid_axes = set(axes) - self.POSSIBLE_AXES - assert not invalid_axes, "invalid axes proposed %s" % invalid_axes + if invalid_axes: + raise MachineInvalidAxis("invalid axes proposed %s" % invalid_axes) self.__dict__['axes'] = set(axes) & self.POSSIBLE_AXES # Unit @@ -61,7 +63,7 @@ class Position(object): if key in self.axes: self._value[key] = value elif key in self.POSSIBLE_AXES: - raise AssertionError("'%s' axis is not defined to be set" % key) + raise MachineInvalidAxis("'%s' axis is not defined to be set" % key) else: self.__dict__[key] = value @@ -86,14 +88,16 @@ class Position(object): # Arithmetic def __add__(self, other): - assert not (self.axes ^ other.axes), "axes: %r != %r" % (self.axes, other.axes) + if self.axes ^ other.axes: + raise MachineInvalidAxis("axes: %r != %r" % (self.axes, other.axes)) new_obj = copy(self) for k in new_obj._value: new_obj._value[k] += other._value[k] return new_obj def __sub__(self, other): - assert not (other.axes - self.axes), "for a - b: axes in b, that are not in a: %r" % (other.axes - self.axes) + if other.axes - self.axes: + raise MachineInvalidAxis("for a - b: axes in b, that are not in a: %r" % (other.axes - self.axes)) new_obj = copy(self) for k in other._value: new_obj._value[k] -= other._value[k] @@ -277,11 +281,12 @@ class Mode(object): # clear mode group self.modal_groups[MODAL_GROUP_MAP[key]] = None else: - # set mode group explicitly + # set mode group explicitly, not advisable # (recommended to use self.set_mode(value) instead) - assert isinstance(value, GCode), "invalid value type: %r" % value - assert value.modal_group == MODAL_GROUP_MAP[key], \ - "cannot set '%s' mode as %r, wrong group" % (key, value) + if not isinstance(value, GCode): + raise MachineInvalidState("invalid mode value: %r" % value) + if value.modal_group != MODAL_GROUP_MAP[key]: + raise MachineInvalidState("cannot set '%s' mode as %r, wrong group" % (key, value)) self.modal_groups[MODAL_GROUP_MAP[key]] = value.modal_copy() else: self.__dict__[key] = value @@ -329,6 +334,21 @@ class Machine(object): if coord_sys_mode: self.state.cur_coord_sys = coord_sys_mode.coord_system_id + def modal_gcode(self, modal_params): + if not modal_params: + return None + if self.mode.motion is None: + raise MachineInvalidState("unable to assign modal parameters when no motion mode is set") + (modal_gcodes, unasigned_words) = words2gcodes([self.mode.motion.word] + modal_params) + if unasigned_words: + raise MachineInvalidState("modal parameters '%s' cannot be assigned when in mode: %r" % ( + ' '.join(str(x) for x in unasigned_words), self.mode + )) + if modal_gcodes: + assert len(modal_gcodes) == 1, "more than 1 modal code found" + return modal_gcodes[0] + return None + def process(self, *gcode_list, **kwargs): """ Process gcodes @@ -338,12 +358,9 @@ class Machine(object): # Add modal gcode to list of given gcodes modal_params = kwargs.get('modal_params', []) if modal_params: - assert self.mode.motion is not None, "unable to assign modal parameters when no motion mode is set" - (modal_gcodes, unasigned_words) = words2gcodes([self.mode.motion.word] + modal_params) - assert unasigned_words == [], "modal parameters '%s' unknown when in mode: %r" % ( - ' '.join(str(x) for x in unasigned_words), self.mode - ) - gcode_list += modal_gcodes + modal_gcode = self.modal_gcode(modal_params) + if modal_gcode: + gcode_list.append(modal_gcode) for gcode in sorted(gcode_list): gcode.process(self) # shifts ownership of what happens now to GCode class diff --git a/pygcode/words.py b/pygcode/words.py index f4ead70..059dc42 100644 --- a/pygcode/words.py +++ b/pygcode/words.py @@ -2,7 +2,7 @@ import re import itertools import six -from .exceptions import GCodeBlockFormatError +from .exceptions import GCodeBlockFormatError, GCodeWordStrError REGEX_FLOAT = re.compile(r'^-?(\d+\.?\d*|\.\d+)') # testcase: ..tests.test_words.WordValueMatchTests.test_float REGEX_INT = re.compile(r'^-?\d+') @@ -201,10 +201,13 @@ WORD_MAP = { class Word(object): def __init__(self, *args): - assert len(args) in [1, 2], "input arguments either: (letter, value) or (word_str)" + 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: + # Word('G90') word_str = args[0] letter = word_str[0] # first letter value = word_str[1:] # rest of string @@ -300,7 +303,7 @@ def text2words(block_text): value_regex = WORD_MAP[letter]['value_regex'] value_match = value_regex.search(block_text[index:]) if value_match is None: - raise GCodeBlockFormatError("word '%s' value invalid" % letter) + raise GCodeWordStrError("word '%s' value invalid" % letter) value = value_match.group() # matched text yield Word(letter, value) @@ -311,13 +314,14 @@ def text2words(block_text): remainder = block_text[index:] if remainder and re.search(r'\S', remainder): - raise GCodeBlockFormatError("block code remaining '%s'" % remainder) + raise GCodeWordStrError("block code remaining '%s'" % remainder) def str2word(word_str): words = list(text2words(word_str)) if words: - assert len(words) <= 1, "more than one word given" + if len(words) > 1: + raise GCodeWordStrError("more than one word given") return words[0] return None diff --git a/tests/test_machine.py b/tests/test_machine.py index c77a3f8..5f52aa6 100644 --- a/tests/test_machine.py +++ b/tests/test_machine.py @@ -7,6 +7,7 @@ add_pygcode_to_path() # Units under test from pygcode.machine import Position, Machine from pygcode.line import Line +from pygcode.exceptions import MachineInvalidAxis class PositionTests(unittest.TestCase): @@ -56,9 +57,9 @@ class PositionTests(unittest.TestCase): self.assertEqual(p1 + p2, Position(axes='XYZ', X=1, Y=12, Z=-20)) p3 = Position(axes='XYZA') - with self.assertRaises(AssertionError): + with self.assertRaises(MachineInvalidAxis): p1 + p3 # mismatched axes - with self.assertRaises(AssertionError): + with self.assertRaises(MachineInvalidAxis): p3 + p1 # mismatched axes def test_arithmetic_sub(self): @@ -68,7 +69,7 @@ class PositionTests(unittest.TestCase): p3 = Position(axes='XYZA') p3 - p1 # fine - with self.assertRaises(AssertionError): + with self.assertRaises(MachineInvalidAxis): p1 - p3 # mismatched axes def test_arithmetic_multiply(self): From 2cae923587f0b6612c0f9dfeb45891299b9af4ae Mon Sep 17 00:00:00 2001 From: Peter Boin <nymphii@gmail.com> Date: Sun, 9 Jul 2017 18:03:22 +1000 Subject: [PATCH 15/32] added split_gcode --- pygcode/__init__.py | 525 +++++++++++++++++++++++-------------------- pygcode/gcodes.py | 117 ++++++++-- pygcode/line.py | 8 +- pygcode/machine.py | 23 +- tests/test_gcodes.py | 46 +++- 5 files changed, 451 insertions(+), 268 deletions(-) diff --git a/pygcode/__init__.py b/pygcode/__init__.py index bfffe04..5a3d921 100644 --- a/pygcode/__init__.py +++ b/pygcode/__init__.py @@ -11,67 +11,84 @@ __all__ = [ 'Word', 'text2words', 'str2word', 'words2dict', # GCodes - 'GCode', 'words2gcodes', + 'words2gcodes', 'text2gcodes', 'split_gcodes', + 'GCode', + 'GCodeAbsoluteArcDistanceMode', + 'GCodeAbsoluteDistanceMode', + 'GCodeAdaptiveFeed', + 'GCodeAddToolLengthOffset', + 'GCodeAnalogOutput', + 'GCodeAnalogOutputImmediate', + 'GCodeAnalogOutputSyncd', + 'GCodeArcMove', + 'GCodeArcMoveCCW', + 'GCodeArcMoveCW', 'GCodeBoringCycleDwellFeedOut', 'GCodeBoringCycleFeedOut', - 'GCodeDrillingCycle', - 'GCodeDrillingCycleChipBreaking', - 'GCodeDrillingCycleDwell', - 'GCodeDrillingCyclePeck', - 'GCodeThreadingCycle', + 'GCodeCancelCannedCycle', + 'GCodeCancelToolLengthOffset', + 'GCodeCannedCycle', 'GCodeCannedCycleReturnLevel', + 'GCodeCannedReturnMode', + 'GCodeCoolant', 'GCodeCoolantFloodOn', 'GCodeCoolantMistOn', 'GCodeCoolantOff', + 'GCodeCoordSystemOffset', + 'GCodeCublcSpline', 'GCodeCutterCompLeft', 'GCodeCutterCompRight', + 'GCodeCutterRadiusComp', 'GCodeCutterRadiusCompOff', - 'GCodeDynamicCutterCompLeft', - 'GCodeDynamicCutterCompRight', - 'GCodeAbsoluteArcDistanceMode', - 'GCodeAbsoluteDistanceMode', - 'GCodeIncrementalArcDistanceMode', - 'GCodeIncrementalDistanceMode', - 'GCodeLatheDiameterMode', - 'GCodeLatheRadiusMode', - 'GCodeInverseTimeMode', - 'GCodeUnitsPerMinuteMode', - 'GCodeUnitsPerRevolution', - 'GCodeAnalogOutputImmediate', - 'GCodeAnalogOutputSyncd', + 'GCodeDigitalOutput', 'GCodeDigitalOutputOff', 'GCodeDigitalOutputOffSyncd', 'GCodeDigitalOutputOn', 'GCodeDigitalOutputOnSyncd', - 'GCodeWaitOnInput', - 'GCodeArcMove', - 'GCodeArcMoveCCW', - 'GCodeArcMoveCW', - 'GCodeCancelCannedCycle', - 'GCodeCublcSpline', + 'GCodeDistanceMode', + 'GCodeDrillingCycle', + 'GCodeDrillingCycleChipBreaking', + 'GCodeDrillingCycleDwell', + 'GCodeDrillingCyclePeck', 'GCodeDwell', - 'GCodeLinearMove', - 'GCodeNURBS', - 'GCodeNURBSEnd', - 'GCodeQuadraticSpline', - 'GCodeRapidMove', - 'GCodeRigidTapping', - 'GCodeSpindleSyncMotion', - 'GCodeStraightProbe', - 'GCodeCoordSystemOffset', - 'GCodeGotoPredefinedPosition', - 'GCodeMoveInMachineCoords', - 'GCodeResetCoordSystemOffset', - 'GCodeRestoreCoordSystemOffset', - 'GCodeSet', - 'GCodeSetPredefinedPosition', - 'GCodeToolChange', - 'GCodeToolSetCurrent', - 'GCodeUserDefined', - 'GCodeAdaptiveFeed', + 'GCodeDynamicCutterCompLeft', + 'GCodeDynamicCutterCompRight', + 'GCodeDynamicToolLengthOffset', + 'GCodeEndProgram', + 'GCodeEndProgramPalletShuttle', + 'GCodeExactPathMode', + 'GCodeExactStopMode', 'GCodeFeedOverride', 'GCodeFeedRate', + 'GCodeFeedRateMode', 'GCodeFeedStop', + 'GCodeGotoPredefinedPosition', + 'GCodeIO', + 'GCodeIncrementalArcDistanceMode', + 'GCodeIncrementalDistanceMode', + 'GCodeInverseTimeMode', + 'GCodeLatheDiameterMode', + 'GCodeLatheRadiusMode', + 'GCodeLinearMove', + 'GCodeMotion', + 'GCodeMoveInMachineCoords', + 'GCodeNURBS', + 'GCodeNURBSEnd', + 'GCodeNonModal', + 'GCodeOrientSpindle', + 'GCodeOtherModal', + 'GCodePalletChangePause', + 'GCodePathBlendingMode', + 'GCodePathControlMode', + 'GCodePauseProgram', + 'GCodePauseProgramOptional', + 'GCodePlaneSelect', + 'GCodeProgramControl', + 'GCodeQuadraticSpline', + 'GCodeRapidMove', + 'GCodeResetCoordSystemOffset', + 'GCodeRestoreCoordSystemOffset', + 'GCodeRigidTapping', 'GCodeSelectCoordinateSystem', 'GCodeSelectCoordinateSystem1', 'GCodeSelectCoordinateSystem2', @@ -83,36 +100,40 @@ __all__ = [ 'GCodeSelectCoordinateSystem8', 'GCodeSelectCoordinateSystem9', 'GCodeSelectTool', - 'GCodeSpeedAndFeedOverrideOff', - 'GCodeSpeedAndFeedOverrideOn', - 'GCodeSpindleSpeed', - 'GCodeSpindleSpeedOverride', - 'GCodeExactPathMode', - 'GCodeExactStopMode', - 'GCodePathBlendingMode', 'GCodeSelectUVPlane', 'GCodeSelectVWPlane', 'GCodeSelectWUPlane', 'GCodeSelectXYPlane', 'GCodeSelectYZPlane', 'GCodeSelectZXPlane', - 'GCodeEndProgram', - 'GCodeEndProgramPalletShuttle', - 'GCodePalletChangePause', - 'GCodePauseProgram', - 'GCodePauseProgramOptional', - 'GCodeOrientSpindle', + 'GCodeSet', + 'GCodeSetPredefinedPosition', + 'GCodeSpeedAndFeedOverrideOff', + 'GCodeSpeedAndFeedOverrideOn', + 'GCodeSpindle', 'GCodeSpindleConstantSurfaceSpeedMode', 'GCodeSpindleRPMMode', + 'GCodeSpindleSpeed', + 'GCodeSpindleSpeedMode', + 'GCodeSpindleSpeedOverride', + 'GCodeSpindleSyncMotion', + 'GCodeStartSpindle', 'GCodeStartSpindleCCW', 'GCodeStartSpindleCW', 'GCodeStopSpindle', - 'GCodeAddToolLengthOffset', - 'GCodeCancelToolLengthOffset', - 'GCodeDynamicToolLengthOffset', + 'GCodeStraightProbe', + 'GCodeThreadingCycle', + 'GCodeToolChange', + 'GCodeToolLength', 'GCodeToolLengthOffset', + 'GCodeToolSetCurrent', + 'GCodeUnit', + 'GCodeUnitsPerMinuteMode', + 'GCodeUnitsPerRevolution', 'GCodeUseInches', 'GCodeUseMillimeters', + 'GCodeUserDefined', + 'GCodeWaitOnInput', ] # Machine @@ -139,190 +160,210 @@ from .words import ( # GCode from .gcodes import ( - GCode, words2gcodes, + words2gcodes, text2gcodes, split_gcodes, # $ python -c "from pygcode.gcodes import _gcode_class_infostr; print(_gcode_class_infostr())" - # - GCode: - # - GCodeCannedCycle: - # G89 - GCodeBoringCycleDwellFeedOut: G89: Boring Cycle, Dwell, Feed Out - # G85 - GCodeBoringCycleFeedOut: G85: Boring Cycle, Feed Out - # G81 - GCodeDrillingCycle: G81: Drilling Cycle - # G73 - GCodeDrillingCycleChipBreaking: G73: Drilling Cycle, ChipBreaking - # G82 - GCodeDrillingCycleDwell: G82: Drilling Cycle, Dwell - # G83 - GCodeDrillingCyclePeck: G83: Drilling Cycle, Peck - # G76 - GCodeThreadingCycle: G76: Threading Cycle - # - GCodeCannedReturnMode: - # G98 - GCodeCannedCycleReturnLevel: G98: Canned Cycle Return Level - # - GCodeCoolant: - # M08 - GCodeCoolantFloodOn: M8: turn flood coolant on - # M07 - GCodeCoolantMistOn: M7: turn mist coolant on - # M09 - GCodeCoolantOff: M9: turn all coolant off - # - GCodeCutterRadiusComp: - # G41 - GCodeCutterCompLeft: G41: Cutter Radius Compensation (left) - # G42 - GCodeCutterCompRight: G42: Cutter Radius Compensation (right) - # G40 - GCodeCutterRadiusCompOff: G40: Cutter Radius Compensation Off - # G41.1 - GCodeDynamicCutterCompLeft: G41.1: Dynamic Cutter Radius Compensation (left) - # G42.1 - GCodeDynamicCutterCompRight: G42.1: Dynamic Cutter Radius Compensation (right) - # - GCodeDistanceMode: - # G90.1 - GCodeAbsoluteArcDistanceMode: G90.1: Absolute Distance Mode for Arc IJK Parameters - # G90 - GCodeAbsoluteDistanceMode: G90: Absolute Distance Mode - # G91.1 - GCodeIncrementalArcDistanceMode: G91.1: Incremental Distance Mode for Arc IJK Parameters - # G91 - GCodeIncrementalDistanceMode: G91: Incremental Distance Mode - # G07 - GCodeLatheDiameterMode: G7: Lathe Diameter Mode - # G08 - GCodeLatheRadiusMode: G8: Lathe Radius Mode - # - GCodeFeedRateMode: - # G93 - GCodeInverseTimeMode: G93: Inverse Time Mode - # G94 - GCodeUnitsPerMinuteMode: G94: Units Per MinuteMode - # G95 - GCodeUnitsPerRevolution: G95: Units Per Revolution - # - GCodeIO: - # - GCodeAnalogOutput: Analog Output - # M68 - GCodeAnalogOutputImmediate: M68: Analog Output, Immediate - # M67 - GCodeAnalogOutputSyncd: M67: Analog Output, Synchronized - # - GCodeDigitalOutput: Digital Output Control - # M65 - GCodeDigitalOutputOff: M65: turn off digital output immediately - # M63 - GCodeDigitalOutputOffSyncd: M63: turn off digital output synchronized with motion - # M64 - GCodeDigitalOutputOn: M64: turn on digital output immediately - # M62 - GCodeDigitalOutputOnSyncd: M62: turn on digital output synchronized with motion - # M66 - GCodeWaitOnInput: M66: Wait on Input - # - GCodeMotion: - # - GCodeArcMove: Arc Move - # G03 - GCodeArcMoveCCW: G3: Arc Move (counter-clockwise) - # G02 - GCodeArcMoveCW: G2: Arc Move (clockwise) - # G80 - GCodeCancelCannedCycle: G80: Cancel Canned Cycle - # G05 - GCodeCublcSpline: G5: Cubic Spline - # G04 - GCodeDwell: G4: Dwell - # G01 - GCodeLinearMove: G1: Linear Move - # G05.2 - GCodeNURBS: G5.2: Non-uniform rational basis spline (NURBS) - # G05.3 - GCodeNURBSEnd: G5.3: end NURBS mode - # G05.1 - GCodeQuadraticSpline: G5.1: Quadratic Spline - # G00 - GCodeRapidMove: G0: Rapid Move - # G33.1 - GCodeRigidTapping: G33.1: Rigid Tapping - # G33 - GCodeSpindleSyncMotion: G33: Spindle Synchronized Motion - # - GCodeStraightProbe: G38.2-G38.5: Straight Probe - # - GCodeNonModal: - # G92 - GCodeCoordSystemOffset: G92: Coordinate System Offset - # - GCodeGotoPredefinedPosition: G28,G30: Goto Predefined Position (rapid movement) - # G53 - GCodeMoveInMachineCoords: G53: Move in Machine Coordinates - # - GCodeResetCoordSystemOffset: G92.1,G92.2: Reset Coordinate System Offset - # G92.3 - GCodeRestoreCoordSystemOffset: G92.3: Restore Coordinate System Offset - # G10 - GCodeSet: G10: Set stuff - # - GCodeSetPredefinedPosition: G28.1,G30.1: Set Predefined Position - # M06 - GCodeToolChange: M6: Tool Change - # M61 - GCodeToolSetCurrent: M61: Set Current Tool - # - GCodeUserDefined: M101-M199: User Defined Commands - # - GCodeOtherModal: - # M52 - GCodeAdaptiveFeed: M52: Adaptive Feed Control - # M50 - GCodeFeedOverride: M50: Feed Override Control - # - GCodeFeedRate: F: Set Feed Rate - # M53 - GCodeFeedStop: M53: Feed Stop Control - # - GCodeSelectCoordinateSystem: Select Coordinate System - # G54 - GCodeSelectCoordinateSystem1: Select Coordinate System 1 - # G55 - GCodeSelectCoordinateSystem2: Select Coordinate System 2 - # G56 - GCodeSelectCoordinateSystem3: Select Coordinate System 3 - # G57 - GCodeSelectCoordinateSystem4: Select Coordinate System 4 - # G58 - GCodeSelectCoordinateSystem5: Select Coordinate System 5 - # G59 - GCodeSelectCoordinateSystem6: Select Coordinate System 6 - # G59.1 - GCodeSelectCoordinateSystem7: Select Coordinate System 7 - # G59.2 - GCodeSelectCoordinateSystem8: Select Coordinate System 8 - # G59.3 - GCodeSelectCoordinateSystem9: Select Coordinate System 9 - # - GCodeSelectTool: T: Select Tool - # M49 - GCodeSpeedAndFeedOverrideOff: M49: Speed and Feed Override Control Off - # M48 - GCodeSpeedAndFeedOverrideOn: M48: Speed and Feed Override Control On - # - GCodeSpindleSpeed: S: Set Spindle Speed - # M51 - GCodeSpindleSpeedOverride: M51: Spindle Speed Override Control - # - GCodePathControlMode: - # G61 - GCodeExactPathMode: G61: Exact path mode - # G61.1 - GCodeExactStopMode: G61.1: Exact stop mode - # G64 - GCodePathBlendingMode: G64: Path Blending - # - GCodePlaneSelect: - # G17.1 - GCodeSelectUVPlane: G17.1: select UV plane - # G19.1 - GCodeSelectVWPlane: G19.1: select VW plane - # G18.1 - GCodeSelectWUPlane: G18.1: select WU plane - # G17 - GCodeSelectXYPlane: G17: select XY plane (default) - # G19 - GCodeSelectYZPlane: G19: select YZ plane - # G18 - GCodeSelectZXPlane: G18: select ZX plane - # - GCodeProgramControl: - # M02 - GCodeEndProgram: M2: Program End - # M30 - GCodeEndProgramPalletShuttle: M30: exchange pallet shuttles and end the program - # M60 - GCodePalletChangePause: M60: Pallet Change Pause - # M00 - GCodePauseProgram: M0: Program Pause - # M01 - GCodePauseProgramOptional: M1: Program Pause (optional) - # - GCodeSpindle: - # M19 - GCodeOrientSpindle: M19: Orient Spindle - # G96 - GCodeSpindleConstantSurfaceSpeedMode: G96: Spindle Constant Surface Speed - # G97 - GCodeSpindleRPMMode: G97: Spindle RPM Speed - # M04 - GCodeStartSpindleCCW: M4: Start Spindle Counter-Clockwise - # M03 - GCodeStartSpindleCW: M3: Start Spindle Clockwise - # M05 - GCodeStopSpindle: M5: Stop Spindle - # - GCodeToolLength: - # G43.2 - GCodeAddToolLengthOffset: G43.2: Appkly Additional Tool Length Offset - # G49 - GCodeCancelToolLengthOffset: G49: Cancel Tool Length Compensation - # G43.1 - GCodeDynamicToolLengthOffset: G43.1: Dynamic Tool Length Offset - # G43 - GCodeToolLengthOffset: G43: Tool Length Offset - # - GCodeUnit: - # G20 - GCodeUseInches: G20: use inches for length units - # G21 - GCodeUseMillimeters: G21: use millimeters for length units + # - GCode: + # - GCodeCannedCycle: + # G89 - GCodeBoringCycleDwellFeedOut: G89: Boring Cycle, Dwell, Feed Out + # G85 - GCodeBoringCycleFeedOut: G85: Boring Cycle, Feed Out + # G81 - GCodeDrillingCycle: G81: Drilling Cycle + # G73 - GCodeDrillingCycleChipBreaking: G73: Drilling Cycle, ChipBreaking + # G82 - GCodeDrillingCycleDwell: G82: Drilling Cycle, Dwell + # G83 - GCodeDrillingCyclePeck: G83: Drilling Cycle, Peck + # G76 - GCodeThreadingCycle: G76: Threading Cycle + # - GCodeCannedReturnMode: + # G98 - GCodeCannedCycleReturnLevel: G98: Canned Cycle Return Level + # - GCodeCoolant: + # M08 - GCodeCoolantFloodOn: M8: turn flood coolant on + # M07 - GCodeCoolantMistOn: M7: turn mist coolant on + # M09 - GCodeCoolantOff: M9: turn all coolant off + # - GCodeCutterRadiusComp: + # G41 - GCodeCutterCompLeft: G41: Cutter Radius Compensation (left) + # G42 - GCodeCutterCompRight: G42: Cutter Radius Compensation (right) + # G40 - GCodeCutterRadiusCompOff: G40: Cutter Radius Compensation Off + # G41.1 - GCodeDynamicCutterCompLeft: G41.1: Dynamic Cutter Radius Compensation (left) + # G42.1 - GCodeDynamicCutterCompRight: G42.1: Dynamic Cutter Radius Compensation (right) + # - GCodeDistanceMode: + # G90.1 - GCodeAbsoluteArcDistanceMode: G90.1: Absolute Distance Mode for Arc IJK Parameters + # G90 - GCodeAbsoluteDistanceMode: G90: Absolute Distance Mode + # G91.1 - GCodeIncrementalArcDistanceMode: G91.1: Incremental Distance Mode for Arc IJK Parameters + # G91 - GCodeIncrementalDistanceMode: G91: Incremental Distance Mode + # G07 - GCodeLatheDiameterMode: G7: Lathe Diameter Mode + # G08 - GCodeLatheRadiusMode: G8: Lathe Radius Mode + # - GCodeFeedRateMode: + # G93 - GCodeInverseTimeMode: G93: Inverse Time Mode + # G94 - GCodeUnitsPerMinuteMode: G94: Units Per MinuteMode + # G95 - GCodeUnitsPerRevolution: G95: Units Per Revolution + # - GCodeIO: + # - GCodeAnalogOutput: Analog Output + # M68 - GCodeAnalogOutputImmediate: M68: Analog Output, Immediate + # M67 - GCodeAnalogOutputSyncd: M67: Analog Output, Synchronized + # - GCodeDigitalOutput: Digital Output Control + # M65 - GCodeDigitalOutputOff: M65: turn off digital output immediately + # M63 - GCodeDigitalOutputOffSyncd: M63: turn off digital output synchronized with motion + # M64 - GCodeDigitalOutputOn: M64: turn on digital output immediately + # M62 - GCodeDigitalOutputOnSyncd: M62: turn on digital output synchronized with motion + # M66 - GCodeWaitOnInput: M66: Wait on Input + # - GCodeMotion: + # - GCodeArcMove: Arc Move + # G03 - GCodeArcMoveCCW: G3: Arc Move (counter-clockwise) + # G02 - GCodeArcMoveCW: G2: Arc Move (clockwise) + # G80 - GCodeCancelCannedCycle: G80: Cancel Canned Cycle + # G05 - GCodeCublcSpline: G5: Cubic Spline + # G04 - GCodeDwell: G4: Dwell + # G01 - GCodeLinearMove: G1: Linear Move + # G05.2 - GCodeNURBS: G5.2: Non-uniform rational basis spline (NURBS) + # G05.3 - GCodeNURBSEnd: G5.3: end NURBS mode + # G05.1 - GCodeQuadraticSpline: G5.1: Quadratic Spline + # G00 - GCodeRapidMove: G0: Rapid Move + # G33.1 - GCodeRigidTapping: G33.1: Rigid Tapping + # G33 - GCodeSpindleSyncMotion: G33: Spindle Synchronized Motion + # - GCodeStraightProbe: G38.2-G38.5: Straight Probe + # - GCodeNonModal: + # G92 - GCodeCoordSystemOffset: G92: Coordinate System Offset + # - GCodeGotoPredefinedPosition: G28,G30: Goto Predefined Position (rapid movement) + # G53 - GCodeMoveInMachineCoords: G53: Move in Machine Coordinates + # - GCodeResetCoordSystemOffset: G92.1,G92.2: Reset Coordinate System Offset + # G92.3 - GCodeRestoreCoordSystemOffset: G92.3: Restore Coordinate System Offset + # G10 - GCodeSet: G10: Set stuff + # - GCodeSetPredefinedPosition: G28.1,G30.1: Set Predefined Position + # M06 - GCodeToolChange: M6: Tool Change + # M61 - GCodeToolSetCurrent: M61: Set Current Tool + # - GCodeUserDefined: M101-M199: User Defined Commands + # - GCodeOtherModal: + # M52 - GCodeAdaptiveFeed: M52: Adaptive Feed Control + # M50 - GCodeFeedOverride: M50: Feed Override Control + # - GCodeFeedRate: F: Set Feed Rate + # M53 - GCodeFeedStop: M53: Feed Stop Control + # - GCodeSelectCoordinateSystem: Select Coordinate System + # G54 - GCodeSelectCoordinateSystem1: Select Coordinate System 1 + # G55 - GCodeSelectCoordinateSystem2: Select Coordinate System 2 + # G56 - GCodeSelectCoordinateSystem3: Select Coordinate System 3 + # G57 - GCodeSelectCoordinateSystem4: Select Coordinate System 4 + # G58 - GCodeSelectCoordinateSystem5: Select Coordinate System 5 + # G59 - GCodeSelectCoordinateSystem6: Select Coordinate System 6 + # G59.1 - GCodeSelectCoordinateSystem7: Select Coordinate System 7 + # G59.2 - GCodeSelectCoordinateSystem8: Select Coordinate System 8 + # G59.3 - GCodeSelectCoordinateSystem9: Select Coordinate System 9 + # - GCodeSelectTool: T: Select Tool + # M49 - GCodeSpeedAndFeedOverrideOff: M49: Speed and Feed Override Control Off + # M48 - GCodeSpeedAndFeedOverrideOn: M48: Speed and Feed Override Control On + # - GCodeSpindleSpeed: S: Set Spindle Speed + # M51 - GCodeSpindleSpeedOverride: M51: Spindle Speed Override Control + # - GCodePathControlMode: + # G61 - GCodeExactPathMode: G61: Exact path mode + # G61.1 - GCodeExactStopMode: G61.1: Exact stop mode + # G64 - GCodePathBlendingMode: G64: Path Blending + # - GCodePlaneSelect: + # G17.1 - GCodeSelectUVPlane: G17.1: select UV plane + # G19.1 - GCodeSelectVWPlane: G19.1: select VW plane + # G18.1 - GCodeSelectWUPlane: G18.1: select WU plane + # G17 - GCodeSelectXYPlane: G17: select XY plane (default) + # G19 - GCodeSelectYZPlane: G19: select YZ plane + # G18 - GCodeSelectZXPlane: G18: select ZX plane + # - GCodeProgramControl: + # M02 - GCodeEndProgram: M2: Program End + # M30 - GCodeEndProgramPalletShuttle: M30: exchange pallet shuttles and end the program + # M60 - GCodePalletChangePause: M60: Pallet Change Pause + # M00 - GCodePauseProgram: M0: Program Pause + # M01 - GCodePauseProgramOptional: M1: Program Pause (optional) + # - GCodeSpindle: + # M19 - GCodeOrientSpindle: M19: Orient Spindle + # - GCodeSpindleSpeedMode: + # G96 - GCodeSpindleConstantSurfaceSpeedMode: G96: Spindle Constant Surface Speed + # G97 - GCodeSpindleRPMMode: G97: Spindle RPM Speed + # - GCodeStartSpindle: M3,M4: Start Spindle Clockwise + # M04 - GCodeStartSpindleCCW: M4: Start Spindle Counter-Clockwise + # M03 - GCodeStartSpindleCW: M3: Start Spindle Clockwise + # M05 - GCodeStopSpindle: M5: Stop Spindle + # - GCodeToolLength: + # G43.2 - GCodeAddToolLengthOffset: G43.2: Appkly Additional Tool Length Offset + # G49 - GCodeCancelToolLengthOffset: G49: Cancel Tool Length Compensation + # G43.1 - GCodeDynamicToolLengthOffset: G43.1: Dynamic Tool Length Offset + # G43 - GCodeToolLengthOffset: G43: Tool Length Offset + # - GCodeUnit: + # G20 - GCodeUseInches: G20: use inches for length units + # G21 - GCodeUseMillimeters: G21: use millimeters for length units + # $ python -c "from pygcode.gcodes import GCode, _subclasses; print(',\\n'.join(sorted(g.__name__ for g in _subclasses(GCode))))"python -c "from pygcode.gcodes import GCode, _subclasses; print(',\\n'.join(sorted(g.__name__ for g in _subclasses(GCode))))" + GCode, + GCodeAbsoluteArcDistanceMode, + GCodeAbsoluteDistanceMode, + GCodeAdaptiveFeed, + GCodeAddToolLengthOffset, + GCodeAnalogOutput, + GCodeAnalogOutputImmediate, + GCodeAnalogOutputSyncd, + GCodeArcMove, + GCodeArcMoveCCW, + GCodeArcMoveCW, GCodeBoringCycleDwellFeedOut, GCodeBoringCycleFeedOut, - GCodeDrillingCycle, - GCodeDrillingCycleChipBreaking, - GCodeDrillingCycleDwell, - GCodeDrillingCyclePeck, - GCodeThreadingCycle, + GCodeCancelCannedCycle, + GCodeCancelToolLengthOffset, + GCodeCannedCycle, GCodeCannedCycleReturnLevel, + GCodeCannedReturnMode, + GCodeCoolant, GCodeCoolantFloodOn, GCodeCoolantMistOn, GCodeCoolantOff, + GCodeCoordSystemOffset, + GCodeCublcSpline, GCodeCutterCompLeft, GCodeCutterCompRight, + GCodeCutterRadiusComp, GCodeCutterRadiusCompOff, - GCodeDynamicCutterCompLeft, - GCodeDynamicCutterCompRight, - GCodeAbsoluteArcDistanceMode, - GCodeAbsoluteDistanceMode, - GCodeIncrementalArcDistanceMode, - GCodeIncrementalDistanceMode, - GCodeLatheDiameterMode, - GCodeLatheRadiusMode, - GCodeInverseTimeMode, - GCodeUnitsPerMinuteMode, - GCodeUnitsPerRevolution, - GCodeAnalogOutputImmediate, - GCodeAnalogOutputSyncd, + GCodeDigitalOutput, GCodeDigitalOutputOff, GCodeDigitalOutputOffSyncd, GCodeDigitalOutputOn, GCodeDigitalOutputOnSyncd, - GCodeWaitOnInput, - GCodeArcMove, - GCodeArcMoveCCW, - GCodeArcMoveCW, - GCodeCancelCannedCycle, - GCodeCublcSpline, + GCodeDistanceMode, + GCodeDrillingCycle, + GCodeDrillingCycleChipBreaking, + GCodeDrillingCycleDwell, + GCodeDrillingCyclePeck, GCodeDwell, - GCodeLinearMove, - GCodeNURBS, - GCodeNURBSEnd, - GCodeQuadraticSpline, - GCodeRapidMove, - GCodeRigidTapping, - GCodeSpindleSyncMotion, - GCodeStraightProbe, - GCodeCoordSystemOffset, - GCodeGotoPredefinedPosition, - GCodeMoveInMachineCoords, - GCodeResetCoordSystemOffset, - GCodeRestoreCoordSystemOffset, - GCodeSet, - GCodeSetPredefinedPosition, - GCodeToolChange, - GCodeToolSetCurrent, - GCodeUserDefined, - GCodeAdaptiveFeed, + GCodeDynamicCutterCompLeft, + GCodeDynamicCutterCompRight, + GCodeDynamicToolLengthOffset, + GCodeEndProgram, + GCodeEndProgramPalletShuttle, + GCodeExactPathMode, + GCodeExactStopMode, GCodeFeedOverride, GCodeFeedRate, + GCodeFeedRateMode, GCodeFeedStop, + GCodeGotoPredefinedPosition, + GCodeIO, + GCodeIncrementalArcDistanceMode, + GCodeIncrementalDistanceMode, + GCodeInverseTimeMode, + GCodeLatheDiameterMode, + GCodeLatheRadiusMode, + GCodeLinearMove, + GCodeMotion, + GCodeMoveInMachineCoords, + GCodeNURBS, + GCodeNURBSEnd, + GCodeNonModal, + GCodeOrientSpindle, + GCodeOtherModal, + GCodePalletChangePause, + GCodePathBlendingMode, + GCodePathControlMode, + GCodePauseProgram, + GCodePauseProgramOptional, + GCodePlaneSelect, + GCodeProgramControl, + GCodeQuadraticSpline, + GCodeRapidMove, + GCodeResetCoordSystemOffset, + GCodeRestoreCoordSystemOffset, + GCodeRigidTapping, GCodeSelectCoordinateSystem, GCodeSelectCoordinateSystem1, GCodeSelectCoordinateSystem2, @@ -334,34 +375,38 @@ from .gcodes import ( GCodeSelectCoordinateSystem8, GCodeSelectCoordinateSystem9, GCodeSelectTool, - GCodeSpeedAndFeedOverrideOff, - GCodeSpeedAndFeedOverrideOn, - GCodeSpindleSpeed, - GCodeSpindleSpeedOverride, - GCodeExactPathMode, - GCodeExactStopMode, - GCodePathBlendingMode, GCodeSelectUVPlane, GCodeSelectVWPlane, GCodeSelectWUPlane, GCodeSelectXYPlane, GCodeSelectYZPlane, GCodeSelectZXPlane, - GCodeEndProgram, - GCodeEndProgramPalletShuttle, - GCodePalletChangePause, - GCodePauseProgram, - GCodePauseProgramOptional, - GCodeOrientSpindle, + GCodeSet, + GCodeSetPredefinedPosition, + GCodeSpeedAndFeedOverrideOff, + GCodeSpeedAndFeedOverrideOn, + GCodeSpindle, GCodeSpindleConstantSurfaceSpeedMode, GCodeSpindleRPMMode, + GCodeSpindleSpeed, + GCodeSpindleSpeedMode, + GCodeSpindleSpeedOverride, + GCodeSpindleSyncMotion, + GCodeStartSpindle, GCodeStartSpindleCCW, GCodeStartSpindleCW, GCodeStopSpindle, - GCodeAddToolLengthOffset, - GCodeCancelToolLengthOffset, - GCodeDynamicToolLengthOffset, + GCodeStraightProbe, + GCodeThreadingCycle, + GCodeToolChange, + GCodeToolLength, GCodeToolLengthOffset, + GCodeToolSetCurrent, + GCodeUnit, + GCodeUnitsPerMinuteMode, + GCodeUnitsPerRevolution, GCodeUseInches, GCodeUseMillimeters, + GCodeUserDefined, + GCodeWaitOnInput, ) diff --git a/pygcode/gcodes.py b/pygcode/gcodes.py index 6b20d9e..167aea2 100644 --- a/pygcode/gcodes.py +++ b/pygcode/gcodes.py @@ -1,9 +1,9 @@ from collections import defaultdict from copy import copy -from .words import Word +from .words import Word, text2words -from .exceptions import GCodeParameterError +from .exceptions import GCodeParameterError, GCodeWordStrError # Terminology of a "G-Code" # For the purposes of this library, so-called "G" codes do not necessarily @@ -138,6 +138,7 @@ class GCode(object): # Defining Word word_key = None # Word instance to use in lookup word_matches = None # function (secondary) + default_word = None # Parameters associated to this gcode param_letters = set() @@ -149,18 +150,24 @@ class GCode(object): # Execution Order exec_order = 999 # if not otherwise specified, run last - def __init__(self, word, *params): + def __init__(self, *words): """ :param word: Word instance defining gcode (eg: Word('G0') for rapid movement) :param params: list of Word instances (eg: Word('X-1.2') as x-coordinate) """ - assert isinstance(word, Word), "invalid gcode word %r" % code_word - self.word = word + gcode_word_list = words[:1] + param_words = words[1:] + if gcode_word_list: + gcode_word = gcode_word_list[0] + else: + gcode_word = self._default_word() + assert isinstance(gcode_word, Word), "invalid gcode word %r" % gcode_word + self.word = gcode_word self.params = {} # Add Given Parameters - for param in params: - self.add_parameter(param) + for param_word in param_words: + self.add_parameter(param_word) def __repr__(self): param_str = '' @@ -188,10 +195,23 @@ class GCode(object): parameters=param_str, ) - # Sort by exec_order + def _default_word(self): + if self.default_word: + return copy(self.default_word) + elif self.word_key: + return copy(self.word_key) + raise AssertionError("class %r has no default word" % self.__class__) + + # Comparisons def __lt__(self, other): + """Sort by execution order""" return self.exec_order < other.exec_order + def __gt__(self, other): + """Sort by execution order""" + return self.exec_order > other.exec_order + + # Parameters def add_parameter(self, word): """ Add given word as a parameter for this gcode @@ -355,6 +375,7 @@ class GCodeStraightProbe(GCodeMotion): @classmethod def word_matches(cls, w): return (w.letter == 'G') and (38.2 <= w.value <= 38.5) + default_word = Word('G', 38.2) class GCodeSpindleSyncMotion(GCodeMotion): @@ -512,17 +533,20 @@ class GCodeSpindle(GCode): exec_order = 90 -class GCodeStartSpindleCW(GCodeSpindle): +class GCodeStartSpindle(GCodeSpindle): + """M3,M4: Start Spindle Clockwise""" + modal_group = MODAL_GROUP_MAP['spindle'] + + +class GCodeStartSpindleCW(GCodeStartSpindle): """M3: Start Spindle Clockwise""" #param_letters = set('S') # S is it's own gcode, makes no sense to be here word_key = Word('M', 3) - modal_group = MODAL_GROUP_MAP['spindle'] -class GCodeStartSpindleCCW(GCodeSpindle): +class GCodeStartSpindleCCW(GCodeStartSpindle): """M4: Start Spindle Counter-Clockwise""" #param_letters = set('S') # S is it's own gcode, makes no sense to be here word_key = Word('M', 4) - modal_group = MODAL_GROUP_MAP['spindle'] class GCodeStopSpindle(GCodeSpindle): @@ -537,18 +561,20 @@ class GCodeOrientSpindle(GCodeSpindle): word_key = Word('M', 19) -class GCodeSpindleConstantSurfaceSpeedMode(GCodeSpindle): +class GCodeSpindleSpeedMode(GCodeSpindle): + modal_group = MODAL_GROUP_MAP['spindle_speed_mode'] + + +class GCodeSpindleConstantSurfaceSpeedMode(GCodeSpindleSpeedMode): """G96: Spindle Constant Surface Speed""" param_letters = set('DS') word_key = Word('G', 96) - modal_group = MODAL_GROUP_MAP['spindle_speed_mode'] -class GCodeSpindleRPMMode(GCodeSpindle): +class GCodeSpindleRPMMode(GCodeSpindleSpeedMode): """G97: Spindle RPM Speed""" param_letters = set('D') word_key = Word('G', 97) - modal_group = MODAL_GROUP_MAP['spindle_speed_mode'] @@ -807,6 +833,7 @@ class GCodeFeedRate(GCodeOtherModal): @classmethod def word_matches(cls, w): return w.letter == 'F' + default_word = Word('F', 0) modal_group = MODAL_GROUP_MAP['feed_rate'] exec_order = 40 @@ -816,6 +843,7 @@ class GCodeSpindleSpeed(GCodeOtherModal): @classmethod def word_matches(cls, w): return w.letter == 'S' + default_word = Word('S', 0) # Modal Group: (see description in GCodeFeedRate) modal_group = MODAL_GROUP_MAP['spindle_speed'] exec_order = 50 @@ -826,6 +854,7 @@ class GCodeSelectTool(GCodeOtherModal): @classmethod def word_matches(cls, w): return w.letter == 'T' + default_word = Word('T', 0) # Modal Group: (see description in GCodeFeedRate) modal_group = MODAL_GROUP_MAP['tool'] exec_order = 60 @@ -1052,6 +1081,7 @@ class GCodeGotoPredefinedPosition(GCodeNonModal): @classmethod def word_matches(cls, w): return (w.letter == 'G') and (w.value in [28, 30]) + default_word = Word('G', 28) exec_order = 230 @@ -1060,6 +1090,7 @@ class GCodeSetPredefinedPosition(GCodeNonModal): @classmethod def word_matches(cls, w): return (w.letter == 'G') and (w.value in [28.1, 30.1]) + default_word = Word('G', 28.1) exec_order = 230 @@ -1080,6 +1111,7 @@ class GCodeResetCoordSystemOffset(GCodeNonModal): @classmethod def word_matches(cls, w): return (w.letter == 'G') and (w.value in [92.1, 92.2]) + default_word = Word('G', 92.1) exec_order = 230 # TODO: machine.state.offset *= 0 @@ -1098,6 +1130,7 @@ class GCodeUserDefined(GCodeNonModal): #@classmethod #def word_matches(cls, w): # return (w.letter == 'M') and (101 <= w.value <= 199) + #default_word = Word('M', 101) exec_order = 130 modal_group = MODAL_GROUP_MAP['user_defined'] @@ -1171,6 +1204,7 @@ def build_maps(): _gcode_maps_created = True +# ======================= Words -> GCodes ======================= def word_gcode_class(word, exhaustive=False): """ Map word to corresponding GCode class @@ -1179,7 +1213,7 @@ def word_gcode_class(word, exhaustive=False): :return: class inheriting GCode """ - if _gcode_maps_created is False: + if not _gcode_maps_created: build_maps() # quickly eliminate parameters @@ -1197,6 +1231,7 @@ def word_gcode_class(word, exhaustive=False): return None + def words2gcodes(words): """ Group words into g-codes (includes both G & M codes) @@ -1257,3 +1292,51 @@ def words2gcodes(words): gcodes.append(gcode) return (gcodes, parameter_map[None]) + + +def text2gcodes(text): + """ + Convert text to GCode instances (must be fully formed; no modal parameters) + :param text: line from a g-code file + :return: tuple([<GCode>, <GCode>, ...], list(<unused words>)) + """ + words = list(text2words(text)) + (gcodes, modal_words) = words2gcodes(words) + if modal_words: + raise GCodeWordStrError("gcode text not fully formed, unassigned parameters: %r" % modal_words) + return gcodes + + +# ======================= Utilities ======================= + +def split_gcodes(gcode_list, splitter_class, sort_list=True): + """ + Splits a list of GCode instances into 3, the center list containing the splitter_class gcode + :param gcode_list: list of GCode instances to split + :param splitter_class: class of gcode identifying split from left to right + :return: list of: [[<gcodes before splitter>], [<splitter instance>], [<gcodes after splitter>]] + """ + # for example: + # g_list = sorted([g1, g2, g3, g4]) + # split_gcodes(g_list, type(g2)) == [[g1], [g2], [g3, g4]] + # 3 lists are always returned, even if empty; if 2nd list is empty, + # then the 3rd will be as well. + if sort_list: # sort by execution order first + gcode_list = sorted(gcode_list) + + split = [gcode_list, [], []] # default (if no splitter can be found) + + # Find splitter index (only one can be found) + split_index = None + for (i, gcode) in enumerate(gcode_list): + if isinstance(gcode, splitter_class): + split_index = i + break + + # Form split: pivoting around split_index + if split_index is not None: + split[0] = gcode_list[:split_index] + split[1] = [gcode_list[split_index]] + split[2] = gcode_list[split_index+1:] + + return split diff --git a/pygcode/line.py b/pygcode/line.py index a1bd290..881b5f1 100644 --- a/pygcode/line.py +++ b/pygcode/line.py @@ -12,8 +12,7 @@ class Line(object): # Split line into block text, and comments if text is not None: (block_str, comment) = split_line(text) - if block_str: - self.block = Block(block_str) + self.block = Block(block_str) if comment: self.comment = comment @@ -23,5 +22,10 @@ class Line(object): return str(self) return self._text + @property + def gcodes(self): + """self.block.gcodes passthrough""" + return self.block.gcodes + def __str__(self): return ' '.join([str(x) for x in [self.block, self.comment] if x]) diff --git a/pygcode/machine.py b/pygcode/machine.py index 8dc1db8..9ff7300 100644 --- a/pygcode/machine.py +++ b/pygcode/machine.py @@ -253,7 +253,7 @@ class Mode(object): def set_mode(self, *gcode_list): """ - Set machine mode from given gcodes + Set machine mode from given gcodes (will not be processed) :param gcode_list: list of GCode instances (given as individual parameters) :return: dict of form: {<modal group>: <new mode GCode>, ...} """ @@ -291,12 +291,17 @@ class Mode(object): else: self.__dict__[key] = value - def __str__(self): + @property + def gcodes(self): + """List of modal gcodes""" gcode_list = [] for modal_group in sorted(MODAL_GROUP_MAP.values()): if self.modal_groups[modal_group]: gcode_list.append(self.modal_groups[modal_group]) - return ' '.join(str(g) for g in gcode_list) + return gcode_list + + def __str__(self): + return ' '.join(str(g) for g in self.gcodes) def __repr__(self): return "<{class_name}: {gcodes}>".format( @@ -318,9 +323,10 @@ class Machine(object): self.state = self.STATE_CLASS(axes=self.axes) # Position type (with default axes the same as this machine) + units_mode = getattr(self.mode, 'units', None) self.Position = type('Position', (Position,), { 'default_axes': self.axes, - 'default_unit': self.mode.units.unit_id, + 'default_unit': units_mode.unit_id if units_mode else UNIT_METRIC, }) # Absolute machine position @@ -335,6 +341,7 @@ class Machine(object): self.state.cur_coord_sys = coord_sys_mode.coord_system_id def modal_gcode(self, modal_params): + if not modal_params: return None if self.mode.motion is None: @@ -349,7 +356,7 @@ class Machine(object): return modal_gcodes[0] return None - def process(self, *gcode_list, **kwargs): + def process_gcodes(self, *gcode_list, **kwargs): """ Process gcodes :param gcode_list: list of GCode instances @@ -377,7 +384,11 @@ class Machine(object): # - Crop a file (eg: resume half way through) def process_block(self, block): - self.process(*block.gcodes, modal_params=block.modal_params) + self.process_gcodes(*block.gcodes, modal_params=block.modal_params) + + def process_str(self, block_str): + line = Line(block_str) + self.process_block(line.block) @property def pos(self): diff --git a/tests/test_gcodes.py b/tests/test_gcodes.py index ef3374a..34e4a22 100644 --- a/tests/test_gcodes.py +++ b/tests/test_gcodes.py @@ -11,7 +11,10 @@ add_pygcode_to_path() # Units under test from pygcode import gcodes from pygcode import words -class TestGCodeWordMapping(unittest.TestCase): + +from pygcode.exceptions import GCodeWordStrError + +class GCodeWordMappingTests(unittest.TestCase): def test_word_map_integrity(self): gcodes.build_maps() @@ -24,7 +27,7 @@ class TestGCodeWordMapping(unittest.TestCase): "conflict with %s and %s" % (fn_class, key_class) ) -class TestGCodeModalGroups(unittest.TestCase): +class GCodeModalGroupTests(unittest.TestCase): def test_modal_groups(self): # Modal groups taken (and slightly modified) from LinuxCNC documentation: # link: http://linuxcnc.org/docs/html/gcode/overview.html#_modal_groups @@ -80,7 +83,7 @@ class TestGCodeModalGroups(unittest.TestCase): ) -class TestWordsToGCodes(unittest.TestCase): +class Words2GCodesTests(unittest.TestCase): def test_stuff(self): # FIXME: function name line = 'G1 X82.6892 Y-38.6339 F1500' word_list = list(words.text2words(line)) @@ -99,3 +102,40 @@ class TestWordsToGCodes(unittest.TestCase): self.assertEqual(gcode_list[0].Y, -38.6339) # F1500 self.assertEqual(gcode_list[1].word, words.Word('F', 1500)) + + +class Text2GCodesTests(unittest.TestCase): + def test_basic(self): + gcs = gcodes.text2gcodes('G1 X1 Y2 G90') + self.assertEqual(len(gcs), 2) + # G1 X1 Y2 + self.assertEqual(gcs[0].word, words.Word('G', 1)) + self.assertEqual(gcs[0].X, 1) + self.assertEqual(gcs[0].Y, 2) + # G90 + self.assertEqual(gcs[1].word, words.Word('G', 90)) + + def test_modal_params(self): + with self.assertRaises(GCodeWordStrError): + gcodes.text2gcodes('X1 Y2') + + +class GCodeSplitTests(unittest.TestCase): + + def test_split(self): + g_list = gcodes.text2gcodes('G91 S1000 G1 X1 Y2 M3') + split = gcodes.split_gcodes(g_list, gcodes.GCodeStartSpindle) + self.assertEqual([len(x) for x in split], [1, 1, 2]) + self.assertTrue(any(isinstance(g, gcodes.GCodeSpindleSpeed) for g in split[0])) + self.assertTrue(isinstance(split[1][0], gcodes.GCodeStartSpindle)) + self.assertTrue(any(isinstance(g, gcodes.GCodeDistanceMode) for g in split[2])) + self.assertTrue(any(isinstance(g, gcodes.GCodeMotion) for g in split[2])) + + def test_split_unsorted(self): + g_list = gcodes.text2gcodes('G91 G1 X1 Y2 M3 S1000') + split = gcodes.split_gcodes(g_list, gcodes.GCodeStartSpindle, sort_list=False) + self.assertEqual([len(x) for x in split], [2, 1, 1]) + self.assertTrue(any(isinstance(g, gcodes.GCodeDistanceMode) for g in split[0])) + self.assertTrue(any(isinstance(g, gcodes.GCodeMotion) for g in split[0])) + self.assertTrue(isinstance(split[1][0], gcodes.GCodeStartSpindle)) + self.assertTrue(any(isinstance(g, gcodes.GCodeSpindleSpeed) for g in split[2])) From a1b3fc41d2c2093efd47aea2edcc77f91bae1bd5 Mon Sep 17 00:00:00 2001 From: Peter Boin <nymphii@gmail.com> Date: Mon, 10 Jul 2017 00:32:17 +1000 Subject: [PATCH 16/32] normalize code coming along --- README.md | 2 + pygcode/gcodes.py | 39 +++++++++- pygcode/machine.py | 21 ++++++ pygcode/transform.py | 134 +++++++++++++++++++++++++++++++++++ pygcode/utils.py | 65 +++++++++++++++++ scripts/pygcode-normalize.py | 98 +++++++++++++++++++++++++ 6 files changed, 357 insertions(+), 2 deletions(-) create mode 100644 pygcode/transform.py create mode 100644 pygcode/utils.py create mode 100755 scripts/pygcode-normalize.py diff --git a/README.md b/README.md index cc50c33..ca8ab5b 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ I'll be learning along the way, but the plan is to follow the lead of [GRBL](htt `pip install pygcode` +FIXME: well, that's the plan... give me some time to get it going though. + ## Usage Just brainstorming here... diff --git a/pygcode/gcodes.py b/pygcode/gcodes.py index 167aea2..cd23752 100644 --- a/pygcode/gcodes.py +++ b/pygcode/gcodes.py @@ -1,6 +1,8 @@ +import sys from collections import defaultdict from copy import copy +from .utils import Vector3, Quaternion, quat2coord_system from .words import Word, text2words from .exceptions import GCodeParameterError, GCodeWordStrError @@ -249,15 +251,19 @@ class GCode(object): if l in self.modal_param_letters ]) - def get_param_dict(self, letters=None): + def get_param_dict(self, letters=None, lc=False): """ Get gcode parameters as a dict gcode parameter like "X3.1, Y-2" would return {'X': 3.1, 'Y': -2} :param letters: iterable whitelist of letters to include as dict keys + :param lc: lower case parameter letters :return: dict of gcode parameters' (letter, value) pairs """ + letter_mod = lambda x: x + if lc: + letter_mod = lambda x: x.lower() return dict( - (w.letter, w.value) for w in self.params.values() + (letter_mod(w.letter), w.value) for w in self.params.values() if (letters is None) or (w.letter in letters) ) @@ -470,6 +476,7 @@ class GCodeAbsoluteDistanceMode(GCodeDistanceMode): word_key = Word('G', 90) modal_group = MODAL_GROUP_MAP['distance'] + class GCodeIncrementalDistanceMode(GCodeDistanceMode): """G91: Incremental Distance Mode""" word_key = Word('G', 91) @@ -701,20 +708,48 @@ class GCodePlaneSelect(GCode): modal_group = MODAL_GROUP_MAP['plane_selection'] exec_order = 150 + # -- Plane Orientation Quaternion + # Such that... + # vectorXY = Vector3(<your coords in X/Y plane>) + # vectorZX = GCodeSelectZXPlane.quat * vectorXY + # vectorZX += some_offset_vector + # vectorXY = GCodeSelectZXPlane.quat.conjugate() * vectorZX + # note: all quaternions use the XY plane as a basis + # To transform from ZX to YZ planes via these quaternions, you must + # first translate it to XY, like so: + # vectorYZ = GCodeSelectYZPlane.quat * (GCodeSelectZXPlane.quat.conjugate() * vectorZX) + quat = None # Quaternion + + # -- Plane Normal + # Vector normal to plane (such that XYZ axes follow the right-hand rule) + normal = None # Vector3 + class GCodeSelectXYPlane(GCodePlaneSelect): """G17: select XY plane (default)""" word_key = Word('G', 17) + quat = Quaternion() # no effect + normal = Vector3(0, 0, 1) class GCodeSelectZXPlane(GCodePlaneSelect): """G18: select ZX plane""" word_key = Word('G', 18) + quat = quat2coord_system( + Vector3(1, 0, 0), Vector3(0, 1, 0), + Vector3(0, 0, 1), Vector3(1, 0, 0) + ) + normal = Vector3(0, 1, 0) class GCodeSelectYZPlane(GCodePlaneSelect): """G19: select YZ plane""" word_key = Word('G', 19) + quat = quat2coord_system( + Vector3(1, 0, 0), Vector3(0, 1, 0), + Vector3(0, 1, 0), Vector3(0, 0, 1) + ) + normal = Vector3(1, 0, 0) class GCodeSelectUVPlane(GCodePlaneSelect): diff --git a/pygcode/machine.py b/pygcode/machine.py index 9ff7300..a0be733 100644 --- a/pygcode/machine.py +++ b/pygcode/machine.py @@ -6,10 +6,13 @@ from .gcodes import ( # Modal GCodes GCodeIncrementalDistanceMode, GCodeUseInches, GCodeUseMillimeters, + # Utilities + words2gcodes, ) from .block import Block from .line import Line from .words import Word +from .utils import Vector3, Quaternion from .exceptions import MachineInvalidAxis, MachineInvalidState @@ -134,6 +137,10 @@ class Position(object): def values(self): return dict(self._value) + @property + def vector(self): + return Vector3(self._value['X'], self._value['Y'], self._value['Z']) + def __repr__(self): return "<{class_name}: {coordinates}>".format( class_name=self.__class__.__name__, @@ -356,12 +363,26 @@ class Machine(object): return modal_gcodes[0] return None + def block_modal_gcodes(self, block): + """ + Block's GCode list in current machine mode + :param block: Block instance + :return: list of gcodes, block.gcodes + <modal gcode, if there is one> + """ + assert isinstance(block, Block), "invalid parameter" + gcodes = copy(block.gcodes) + modal_gcode = self.modal_gcode(block.modal_params) + if modal_gcode: + gcodes.append(modal_gcode) + return sorted(gcodes) + def process_gcodes(self, *gcode_list, **kwargs): """ Process gcodes :param gcode_list: list of GCode instances :param modal_params: list of Word instances to be applied to current movement mode """ + gcode_list = list(gcode_list) # make appendable # Add modal gcode to list of given gcodes modal_params = kwargs.get('modal_params', []) if modal_params: diff --git a/pygcode/transform.py b/pygcode/transform.py new file mode 100644 index 0000000..ea10821 --- /dev/null +++ b/pygcode/transform.py @@ -0,0 +1,134 @@ +from math import acos + +from .gcodes import GCodeArcMove, GCodeArcMoveCW, GCodeArcMoveCCW +from .gcodes import GCodeSelectXYPlane, GCodeSelectYZPlane, GCodeSelectZXPlane +from .gcodes import GCodeAbsoluteDistanceMode, GCodeIncrementalDistanceMode +from .gcodes import GCodeAbsoluteArcDistanceMode, GCodeIncrementalArcDistanceMode + +from .machine import Position +from .utils import Vector3, Quaternion, plane_projection + +# ==================== Arcs (G2,G3) --> Linear Motion (G1) ==================== + + +class ArcLinearizeMethod(object): + pass + + def __init__(self, max_error, radius) + self.max_error = max_error + self.radius = radius + + def get_max_wedge_angle(self): + """Calculate angular coverage of a single line reaching maximum allowable error""" + raise NotImplementedError("not overridden") + + +class ArcLinearizeInside(ArcLinearizeMethod): + """Start and end points of each line are on the original arc""" + # Attributes / Trade-offs: + # - Errors cause arc to be slightly smaller + # - pocket milling action will remove less material + # - perimeter milling action will remove more material + # - Each line is the same length + # - Simplest maths, easiest to explain & visually verify + + def get_max_wedge_angle(self): + return 2 * acos((self.radius - self.max_error) / self.radius) + + + + + +class ArcLinearizeOutside(ArcLinearizeMethod): + """Mid-points of each line are on the original arc, first and last lines are 1/2 length""" + # Attributes / Trade-offs: + # - Errors cause arc to be slightly larger + # - pocket milling action will remove more material + # - perimeter milling action will remove less material + # - 1st and last lines are 1/2 length of the others + + +class ArcLinearizeMid(ArcLinearizeMethod): + """Lines cross original arc from tangent of arc radius - precision/2, until it reaches arc radius + precision/2""" + # Attributes / Trade-offs: + # - Closest to original arc (error distances are equal inside and outside the arc) + # - Most complex to calculate (but who cares, that's only done once) + # - default linearizing method as it's probably the best + + +DEFAULT_LA_METHOD = ArcLinearizeMid +DEFAULT_LA_PLANE = GCodeSelectXYPlane +DEFAULT_LA_DISTMODE = GCodeAbsoluteDistanceMode +DEFAULT_LA_ARCDISTMODE = GCodeIncrementalArcDistanceMode + +def linearize_arc(arc_gcode, start_pos, plane=None, method_class=None, + dist_mode=None, arc_dist_mode=None, + max_error=0.01, precision_fmt="{0:.3f}"): + # set defaults + if method_class is None: + method_class = DEFAULT_LA_method_class + if plane_selection is None: + plane_selection = DEFAULT_LA_PLANE + if dist_mode is None: + dist_mode = DEFAULT_LA_DISTMODE + if arc_dist_mode is None: + arc_dist_mode = DEFAULT_LA_ARCDISTMODE + + # Parameter Type Assertions + assert isinstance(arc_gcode, GCodeArcMove), "bad arc_gcode type: %r" % arc_gcode + assert isinstance(start_pos, Position), "bad start_pos type: %r" % start_pos + assert isinstance(plane, GCodePlaneSelect), "bad plane type: %r" % plane + assert issubclass(method_class, ArcLinearizeMethod), "bad method_class type: %r" % method_class + assert isinstance(dist_mode, (GCodeAbsoluteDistanceMode, GCodeIncrementalDistanceMode)), "bad dist_mode type: %r" % dist_mode + assert isinstance(arc_dist_mode, (GCodeAbsoluteArcDistanceMode, GCodeIncrementalArcDistanceMode)), "bad arc_dist_mode type: %r" % arc_dist_mode + assert max_error > 0, "max_error must be > 0" + + # Arc Start + arc_start = start_pos.vector + # Arc End + arc_end_coords = dict((l, 0.0) for l in 'xyz') + arc_end_coords.update(g.arc_gcode('XYZ', lc=True)) + arc_end = Vector3(**arc_end_coords) + if isinstance(dist_mode, GCodeIncrementalDistanceMode): + arc_end += start_pos.vector + # Arc Center + arc_center_ijk = dict((l, 0.0) for l in 'IJK') + arc_center_ijk.update(g.arc_gcode('IJK')) + arc_center_coords = dict(({'I':'x','J':'y','K':'z'}[k], v) for (k, v) in arc_center_ijk.items()) + arc_center = Vector3(**arc_center_coords) + if isinstance(arc_dist_mode, GCodeIncrementalArcDistanceMode): + arc_center += start_pos.vector + + # Planar Projections + arc_p_start = plane_projection(arc_start, plane.normal) + arc_p_end = plane_projection(arc_p_end, plane.normal) + arc_p_center = plane_projection(arc_center, plane.normal) + + # Radii, center-point adjustment + r1 = arc_p_start - arc_p_center + r2 = arc_p_end - arc_p_center + radius = (abs(r1) + abs(r2)) / 2.0 + + arc_p_center = ( # average radii along the same vectors + (arc_p_start - (r1.normalized() * radius)) + + (arc_p_end - (r2.normalized() * radius)) + ) / 2.0 + # FIXME: nice idea, but I don't think it's correct... + # ie: re-calculation of r1 & r2 will not yield r1 == r2 + # I think I have to think more pythagoreanly... yeah, that's a word now + + method = method_class( + max_error=max_error, + radius=radius, + ) + + #plane_projection(vect, normal) + + pass + # Steps: + # - calculate: + # - + # - calculate number of linear segments + + +# ==================== Arc Precision Adjustment ==================== diff --git a/pygcode/utils.py b/pygcode/utils.py new file mode 100644 index 0000000..3a8f8ed --- /dev/null +++ b/pygcode/utils.py @@ -0,0 +1,65 @@ +import sys + +if sys.version_info < (3, 0): + from euclid import Vector3, Quaternion +else: + from euclid3 import Vector3, Quaternion + + +# ==================== Geometric Utilities ==================== +def quat2align(to_align, with_this, normalize=True): + """ + Calculate Quaternion that will rotate a given vector to align with another + can accumulate with perpendicular alignemnt vectors like so: + (x_axis, z_axis) = (Vector3(1, 0, 0), Vector3(0, 0, 1)) + q1 = quat2align(v1, z_axis) + q2 = quat2align(q1 * v2, x_axis) + # assuming v1 is perpendicular to v2 + q3 = q2 * q1 # application of q3 to any vector will re-orient it to the + # coordinate system defined by (v1,v2), as (z,x) respectively. + :param to_align: Vector3 instance to be rotated + :param with_this: Vector3 instance as target for alignment + :result: Quaternion such that: q * to_align == with_this + """ + # Normalize Vectors + if normalize: + to_align = to_align.normalized() + with_this = with_this.normalized() + # Calculate Quaternion + return Quaternion.new_rotate_axis( + angle=to_align.angle(with_this), + axis=to_align.cross(with_this), + ) + + +def quat2coord_system(origin1, origin2, align1, align2): + """ + Calculate Quaternion to apply to any vector to re-orientate it to another + (target) coordinate system. + (note: both origin and align coordinate systems must use right-hand-rule) + :param origin1: origin(1|2) are perpendicular vectors in the original coordinate system + :param origin2: see origin1 + :param align1: align(1|2) are 2 perpendicular vectors in the target coordinate system + :param align2: see align1 + :return: Quaternion such that q * origin1 = align1, and q * origin2 = align2 + """ + # Normalize Vectors + origin1 = origin1.normalized() + origin2 = origin2.normalized() + align1 = align1.normalized() + align2 = align2.normalized() + # Calculate Quaternion + q1 = quat2align(origin1, align1, normalize=False) + q2 = quat2align(q1 * origin2, align2, normalize=False) + return q2 * q1 + + +def plane_projection(vect, normal): + """ + Project vect to a plane represented by normal + :param vect: vector to be projected (Vector3) + :param normal: normal of plane to project on to (Vector3) + :return: vect projected onto plane represented by normal + """ + n = normal.normalized() + return v - (n * v.dot(n)) diff --git a/scripts/pygcode-normalize.py b/scripts/pygcode-normalize.py new file mode 100755 index 0000000..f27c918 --- /dev/null +++ b/scripts/pygcode-normalize.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python +import argparse + +for pygcode_lib_type in ('installed_lib', 'relative_lib'): + try: + # pygcode + from pygcode import Machine, Mode, Line + from pygcode import GCodeArcMove, GCodeArcMoveCW, GCodeArcMoveCCW + from pygcode import split_gcodes + from pygcode.transform import linearize_arc + + except ImportError: + import sys, os, inspect + # Add pygcode (relative to this test-path) to the system path + _this_path = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) + sys.path.insert(0, os.path.join(_this_path, '..')) + if pygcode_lib_type == 'installed_lib': + continue # import was attempted before sys.path addition. retry import + raise # otherwise the raised ImportError is a genuine problem + break + + +# =================== Command Line Arguments =================== +# --- Defaults +DEFAULT_PRECISION = 0.005 # mm +DEFAULT_MACHINE_MODE = 'G0 G54 G17 G21 G90 G94 M5 M9 T0 F0 S0' + +# --- Create Parser +parser = argparse.ArgumentParser(description='Normalize gcode for machine consistency using different CAM software') +parser.add_argument( + 'infile', type=argparse.FileType('r'), nargs=1, + help="gcode file to normalize", +) + +parser.add_argument( + '--precision', '-p', dest='precision', type=float, default=DEFAULT_PRECISION, + help="maximum positional error when generating gcodes (eg: arcs to lines)", +) + +# Machine +parser.add_argument( + '--machine_mode', '-mm', dest='machine_mode', default=DEFAULT_MACHINE_MODE, + help="Machine's startup mode as gcode (default: '%s')" % DEFAULT_MACHINE_MODE, +) + +# Arcs +parser.add_argument( + '--arcs_linearize', '-al', dest='arcs_linearize', + action='store_const', const=True, default=False, + help="convert G2/3 commands to a series of linear G1 linear interpolations", +) +parser.add_argument( + '--arc_alignment', '-aa', dest='arc_alignment', type=str, choices=('XYZ','IJK','R'), + default=None, + help="enforce precision on arcs, if XYZ the destination is altered to match the radius" + "if IJK or R then the arc'c centre point is moved to assure precision", +) + +# --- Parse Arguments +args = parser.parse_args() + + +# =================== Create Virtual CNC Machine =================== +class MyMode(Mode): + default_mode = args.machine_mode + +class MyMachine(Machine): + MODE_CLASS = MyMode + +machine = MyMachine() + +# =================== Utility Functions =================== +def gcodes2str(gcodes): + return ' '.join("%s" % g for g in gcodes) + + +# =================== Process File =================== +print(args) + +for line_str in args.infile[0].readlines(): + line = Line(line_str) + + effective_gcodes = machine.block_modal_gcodes(line.block) + + if any(isinstance(g, GCodeArcMove) for g in effective_gcodes): + print("---------> Found an Arc <----------") + (before, (arc,), after) = split_gcodes(effective_gcodes, GCodeArcMove) + if before: + print(gcodes2str(before)) + print(str(arc)) + if after: + print(gcodes2str(after)) + + + + print("%r, %s" % (sorted(line.block.gcodes), line.block.modal_params)) + + machine.process_block(line.block) From c3f822f8025b960cabb478b7da71b6704a7a2dc3 Mon Sep 17 00:00:00 2001 From: Peter Boin <nymphii@gmail.com> Date: Tue, 11 Jul 2017 00:11:32 +1000 Subject: [PATCH 17/32] arc calcs for linearizing (wip) --- pygcode/gcodes.py | 48 ++++++++++-- pygcode/machine.py | 12 ++- pygcode/transform.py | 144 +++++++++++++++++++++++++++-------- pygcode/utils.py | 3 +- scripts/pygcode-normalize.py | 33 ++++++-- 5 files changed, 190 insertions(+), 50 deletions(-) diff --git a/pygcode/gcodes.py b/pygcode/gcodes.py index cd23752..6c08bdd 100644 --- a/pygcode/gcodes.py +++ b/pygcode/gcodes.py @@ -227,6 +227,17 @@ class GCode(object): self.params[word.letter] = word + # Assert Parameters + def assert_params(self): + """ + Assert validity of gcode's parameters. + This verification is irrespective of machine, or machine's state; + verification is g-code language-based verification + :raises: GCodeParameterError + """ + # to be overridden in inheriting classes + pass + def __getattr__(self, key): # Return parameter values (if valid parameter for gcode) if key in self.param_letters: @@ -334,6 +345,29 @@ class GCodeArcMove(GCodeMotion): """Arc Move""" param_letters = GCodeMotion.param_letters | set('IJKRP') + def assert_params(self): + param_letters = set(self.params.keys()) + # Parameter groups + params_xyz = set('XYZ') & set(param_letters) + params_ijk = set('IJK') & set(param_letters) + params_r = set('R') & set(param_letters) + params_ijkr = params_ijk | params_r + + # --- Parameter Groups + # XYZ: at least 1 + if not params_xyz: + raise GCodeParameterError("no XYZ parameters set for destination: %r" % arc_gcode) + # IJK or R: only in 1 group + if params_ijk and params_r: + raise GCodeParameterError("both IJK and R parameters defined: %r" % arc_gcode) + # IJKR: at least 1 + if not params_ijkr: + raise GCodeParameterError("neither IJK or R parameters defined: %r" % arc_gcode) + + # --- Parameter Values + if params_r and (self.R == 0): + raise GCodeParameterError("cannot plot a circle with a radius of zero: %r" % arc_gcode) + class GCodeArcMoveCW(GCodeArcMove): """G2: Arc Move (clockwise)""" @@ -729,27 +763,27 @@ class GCodeSelectXYPlane(GCodePlaneSelect): """G17: select XY plane (default)""" word_key = Word('G', 17) quat = Quaternion() # no effect - normal = Vector3(0, 0, 1) + normal = Vector3(0., 0., 1.) class GCodeSelectZXPlane(GCodePlaneSelect): """G18: select ZX plane""" word_key = Word('G', 18) quat = quat2coord_system( - Vector3(1, 0, 0), Vector3(0, 1, 0), - Vector3(0, 0, 1), Vector3(1, 0, 0) + Vector3(1., 0., 0.), Vector3(0., 1., 0.), + Vector3(0., 0., 1.), Vector3(1., 0., 0.) ) - normal = Vector3(0, 1, 0) + normal = Vector3(0., 1., 0.) class GCodeSelectYZPlane(GCodePlaneSelect): """G19: select YZ plane""" word_key = Word('G', 19) quat = quat2coord_system( - Vector3(1, 0, 0), Vector3(0, 1, 0), - Vector3(0, 1, 0), Vector3(0, 0, 1) + Vector3(1., 0., 0.), Vector3(0., 1., 0.), + Vector3(0., 1., 0.), Vector3(0., 0., 1.) ) - normal = Vector3(1, 0, 0) + normal = Vector3(1., 0., 0.) class GCodeSelectUVPlane(GCodePlaneSelect): diff --git a/pygcode/machine.py b/pygcode/machine.py index a0be733..1c625bc 100644 --- a/pygcode/machine.py +++ b/pygcode/machine.py @@ -52,6 +52,10 @@ class Position(object): self._value = defaultdict(lambda: 0.0, dict((k, 0.0) for k in self.axes)) self._value.update(kwargs) + def update(self, **coords): + for (k, v) in coords.items(): + setattr(self, k, v) + # Attributes Get/Set def __getattr__(self, key): if key in self.axes: @@ -428,8 +432,10 @@ class Machine(object): # =================== Machine Actions =================== def move_to(self, rapid=False, **coords): """Move machine to given position""" - given_position = Position(axes=self.axes, **coords) if isinstance(self.mode.distance, GCodeIncrementalDistanceMode): - self.pos += given_position + pos_delta = Position(axes=self.axes, **coords) + self.pos += pos_delta else: # assumed: GCodeAbsoluteDistanceMode - self.pos = given_position + new_pos = self.pos + new_pos.update(**coords) # only change given coordinates + self.pos = new_pos diff --git a/pygcode/transform.py b/pygcode/transform.py index ea10821..60fb6af 100644 --- a/pygcode/transform.py +++ b/pygcode/transform.py @@ -1,20 +1,21 @@ -from math import acos +from math import acos, atan2, pi, sqrt from .gcodes import GCodeArcMove, GCodeArcMoveCW, GCodeArcMoveCCW -from .gcodes import GCodeSelectXYPlane, GCodeSelectYZPlane, GCodeSelectZXPlane +from .gcodes import GCodePlaneSelect, GCodeSelectXYPlane, GCodeSelectYZPlane, GCodeSelectZXPlane from .gcodes import GCodeAbsoluteDistanceMode, GCodeIncrementalDistanceMode from .gcodes import GCodeAbsoluteArcDistanceMode, GCodeIncrementalArcDistanceMode from .machine import Position +from .exceptions import GCodeParameterError from .utils import Vector3, Quaternion, plane_projection -# ==================== Arcs (G2,G3) --> Linear Motion (G1) ==================== +# ==================== Arcs (G2,G3) --> Linear Motion (G1) ==================== class ArcLinearizeMethod(object): pass - def __init__(self, max_error, radius) + def __init__(self, max_error, radius): self.max_error = max_error self.radius = radius @@ -67,12 +68,12 @@ def linearize_arc(arc_gcode, start_pos, plane=None, method_class=None, # set defaults if method_class is None: method_class = DEFAULT_LA_method_class - if plane_selection is None: - plane_selection = DEFAULT_LA_PLANE + if plane is None: + plane = DEFAULT_LA_PLANE() if dist_mode is None: - dist_mode = DEFAULT_LA_DISTMODE + dist_mode = DEFAULT_LA_DISTMODE() if arc_dist_mode is None: - arc_dist_mode = DEFAULT_LA_ARCDISTMODE + arc_dist_mode = DEFAULT_LA_ARCDISTMODE() # Parameter Type Assertions assert isinstance(arc_gcode, GCodeArcMove), "bad arc_gcode type: %r" % arc_gcode @@ -86,40 +87,121 @@ def linearize_arc(arc_gcode, start_pos, plane=None, method_class=None, # Arc Start arc_start = start_pos.vector # Arc End - arc_end_coords = dict((l, 0.0) for l in 'xyz') - arc_end_coords.update(g.arc_gcode('XYZ', lc=True)) + arc_end_coords = dict(zip('xyz', arc_start.xyz)) + arc_end_coords.update(arc_gcode.get_param_dict('XYZ', lc=True)) arc_end = Vector3(**arc_end_coords) if isinstance(dist_mode, GCodeIncrementalDistanceMode): arc_end += start_pos.vector - # Arc Center - arc_center_ijk = dict((l, 0.0) for l in 'IJK') - arc_center_ijk.update(g.arc_gcode('IJK')) - arc_center_coords = dict(({'I':'x','J':'y','K':'z'}[k], v) for (k, v) in arc_center_ijk.items()) - arc_center = Vector3(**arc_center_coords) - if isinstance(arc_dist_mode, GCodeIncrementalArcDistanceMode): - arc_center += start_pos.vector # Planar Projections arc_p_start = plane_projection(arc_start, plane.normal) - arc_p_end = plane_projection(arc_p_end, plane.normal) - arc_p_center = plane_projection(arc_center, plane.normal) + arc_p_end = plane_projection(arc_end, plane.normal) + + # Arc radius, calcualted one of 2 ways: + # - R: arc radius is provided + # - IJK: arc's center-point is given, errors mitigated + arc_gcode.assert_params() + if 'R' in arc_gcode.params: + # R: radius magnitude specified + if abs(arc_p_start - arc_p_end) < max_error: + raise GCodeParameterError( + "arc starts and finishes in the same spot; cannot " + "speculate where circle's center is: %r" % arc_gcode + ) + + arc_radius = abs(arc_gcode.R) # arc radius (magnitude) + + else: + # IJK: radius vertex specified + arc_center_ijk = dict((l, 0.) for l in 'IJK') + arc_center_ijk.update(arc_gcode.get_param_dict('IJK')) + arc_center_coords = dict(({'I':'x','J':'y','K':'z'}[k], v) for (k, v) in arc_center_ijk.items()) + arc_center = Vector3(**arc_center_coords) + if isinstance(arc_dist_mode, GCodeIncrementalArcDistanceMode): + arc_center += start_pos.vector + + # planar projection + arc_p_center = plane_projection(arc_center, plane.normal) + + # Radii + r1 = arc_p_start - arc_p_center + r2 = arc_p_end - arc_p_center + + # average the 2 radii to get the most accurate radius + arc_radius = (abs(r1) + abs(r2)) / 2. + + # Find Circle's Center (given radius) + arc_span = arc_p_end - arc_p_start # vector spanning from start -> end + arc_span_mid = arc_span * 0.5 # arc_span's midpoint + if arc_radius < abs(arc_span_mid): + raise GCodeParameterError("circle cannot reach endpoint at this radius: %r" % arc_gcode) + # vector from arc_span midpoint -> circle's centre + radius_mid_vect = arc_span_mid.normalized().cross(plane.normal) * sqrt(arc_radius**2 - abs(arc_span_mid)**2) + + if 'R' in arc_gcode.params: + # R: radius magnitude specified + if isinstance(arc_gcode, GCodeArcMoveCW) == (arc_gcode.R < 0): + arc_p_center = arc_p_start + arc_span_mid - radius_mid_vect + else: + arc_p_center = arc_p_start + arc_span_mid + radius_mid_vect + else: + # IJK: radius vertex specified + # arc_p_center is defined as per IJK params, this is an adjustment + arc_p_center_options = [ + arc_p_start + arc_span_mid - radius_mid_vect, + arc_p_start + arc_span_mid + radius_mid_vect + ] + if abs(arc_p_center_options[0] - arc_p_center) < abs(arc_p_center_options[1] - arc_p_center): + arc_p_center = arc_p_center_options[0] + else: + arc_p_center = arc_p_center_options[1] + + # Arc's angle (first rotated back to xy plane) + xy_c2start = plane.quat * (arc_p_start - arc_p_center) + xy_c2end = plane.quat * (arc_p_end - arc_p_center) + (a1, a2) = (atan2(*xy_c2start.yx), atan2(*xy_c2end.yx)) + if isinstance(arc_gcode, GCodeArcMoveCW): + arc_angle = (a1 - a2) % (2 * pi) + else: + arc_angle = -((a2 - a1) % (2 * pi)) + + # Helical interpolation + helical_start = plane.normal * arc_start.dot(plane.normal) + helical_end = plane.normal * arc_end.dot(plane.normal) + + # Parameters determined above: + # - arc_p_start arc start point + # - arc_p_end arc end point + # - arc_p_center arc center + # - arc_angle angle between start & end (>0 is ccw, <0 is cw) (radians) + # - helical_start distance along plane.normal of arc start + # - helical_disp distance along plane.normal of arc end + + # TODO: debug printing + print(( + "linearize_arc params\n" + " - arc_p_start {arc_p_start}\n" + " - arc_p_end {arc_p_end}\n" + " - arc_p_center {arc_p_center}\n" + " - arc_radius {arc_radius}\n" + " - arc_angle {arc_angle:.4f} ({arc_angle_deg:.3f} deg)\n" + " - helical_start {helical_start}\n" + " - helical_end {helical_end}\n" + ).format( + arc_p_start=arc_p_start, + arc_p_end=arc_p_end, + arc_p_center=arc_p_center, + arc_radius=arc_radius, + arc_angle=arc_angle, arc_angle_deg=arc_angle * (180/pi), + helical_start=helical_start, + helical_end=helical_end, + )) - # Radii, center-point adjustment - r1 = arc_p_start - arc_p_center - r2 = arc_p_end - arc_p_center - radius = (abs(r1) + abs(r2)) / 2.0 - arc_p_center = ( # average radii along the same vectors - (arc_p_start - (r1.normalized() * radius)) + - (arc_p_end - (r2.normalized() * radius)) - ) / 2.0 - # FIXME: nice idea, but I don't think it's correct... - # ie: re-calculation of r1 & r2 will not yield r1 == r2 - # I think I have to think more pythagoreanly... yeah, that's a word now method = method_class( max_error=max_error, - radius=radius, + radius=arc_radius, ) #plane_projection(vect, normal) diff --git a/pygcode/utils.py b/pygcode/utils.py index 3a8f8ed..5a93b94 100644 --- a/pygcode/utils.py +++ b/pygcode/utils.py @@ -61,5 +61,6 @@ def plane_projection(vect, normal): :param normal: normal of plane to project on to (Vector3) :return: vect projected onto plane represented by normal """ + # ref: https://en.wikipedia.org/wiki/Vector_projection n = normal.normalized() - return v - (n * v.dot(n)) + return vect - (n * vect.dot(n)) diff --git a/scripts/pygcode-normalize.py b/scripts/pygcode-normalize.py index f27c918..d5e3eb3 100755 --- a/scripts/pygcode-normalize.py +++ b/scripts/pygcode-normalize.py @@ -8,6 +8,7 @@ for pygcode_lib_type in ('installed_lib', 'relative_lib'): from pygcode import GCodeArcMove, GCodeArcMoveCW, GCodeArcMoveCCW from pygcode import split_gcodes from pygcode.transform import linearize_arc + from pygcode.transform import ArcLinearizeInside, ArcLinearizeOutside, ArcLinearizeMid except ImportError: import sys, os, inspect @@ -79,20 +80,36 @@ print(args) for line_str in args.infile[0].readlines(): line = Line(line_str) + if line.comment: + print("===== %s" % line.comment.text) effective_gcodes = machine.block_modal_gcodes(line.block) if any(isinstance(g, GCodeArcMove) for g in effective_gcodes): print("---------> Found an Arc <----------") - (before, (arc,), after) = split_gcodes(effective_gcodes, GCodeArcMove) - if before: - print(gcodes2str(before)) - print(str(arc)) - if after: - print(gcodes2str(after)) + (befores, (arc,), afters) = split_gcodes(effective_gcodes, GCodeArcMove) + # TODO: debug printing (for now) + if befores: + print("befores: %s" % gcodes2str(befores)) + machine.process_gcodes(*befores) + print("arc: %s" % str(arc)) + linearize_arc( + arc_gcode=arc, + start_pos=machine.pos, + plane=machine.mode.plane_selection, + method_class=ArcLinearizeInside, # FIXME: selectable from args + dist_mode=machine.mode.distance, + arc_dist_mode=machine.mode.arc_ijk_distance, + max_error=args.precision, + ) + machine.process_gcodes(arc) + + if afters: + print("afters: %s" % gcodes2str(afters)) + machine.process_gcodes(*afters) + else: + machine.process_block(line.block) print("%r, %s" % (sorted(line.block.gcodes), line.block.modal_params)) - - machine.process_block(line.block) From 06a16ea1acb40a6911f74838b652d8ac98a77744 Mon Sep 17 00:00:00 2001 From: Peter Boin <nymphii@gmail.com> Date: Tue, 11 Jul 2017 02:42:39 +1000 Subject: [PATCH 18/32] first working arc linearizing --- pygcode/gcodes.py | 4 +- pygcode/transform.py | 125 ++++++++++++++++++++++++----------- pygcode/words.py | 2 +- scripts/pygcode-normalize.py | 78 ++++++++++++++++------ 4 files changed, 151 insertions(+), 58 deletions(-) diff --git a/pygcode/gcodes.py b/pygcode/gcodes.py index 6c08bdd..f10186a 100644 --- a/pygcode/gcodes.py +++ b/pygcode/gcodes.py @@ -152,7 +152,7 @@ class GCode(object): # Execution Order exec_order = 999 # if not otherwise specified, run last - def __init__(self, *words): + def __init__(self, *words, **params): """ :param word: Word instance defining gcode (eg: Word('G0') for rapid movement) :param params: list of Word instances (eg: Word('X-1.2') as x-coordinate) @@ -170,6 +170,8 @@ class GCode(object): # Add Given Parameters for param_word in param_words: self.add_parameter(param_word) + for (k, v) in params.items(): + self.add_parameter(Word(k, v)) def __repr__(self): param_str = '' diff --git a/pygcode/transform.py b/pygcode/transform.py index 60fb6af..e3b0c00 100644 --- a/pygcode/transform.py +++ b/pygcode/transform.py @@ -1,5 +1,6 @@ -from math import acos, atan2, pi, sqrt +from math import acos, atan2, pi, sqrt, ceil +from .gcodes import GCodeLinearMove from .gcodes import GCodeArcMove, GCodeArcMoveCW, GCodeArcMoveCCW from .gcodes import GCodePlaneSelect, GCodeSelectXYPlane, GCodeSelectYZPlane, GCodeSelectZXPlane from .gcodes import GCodeAbsoluteDistanceMode, GCodeIncrementalDistanceMode @@ -15,14 +16,27 @@ from .utils import Vector3, Quaternion, plane_projection class ArcLinearizeMethod(object): pass - def __init__(self, max_error, radius): + def __init__(self, max_error, plane_normal, + arc_p_start, arc_p_end, arc_p_center, + arc_radius, arc_angle, helical_start, helical_end): self.max_error = max_error - self.radius = radius + self.plane_normal = plane_normal + self.arc_p_start = arc_p_start + self.arc_p_end = arc_p_end + self.arc_p_center = arc_p_center + self.arc_radius = arc_radius + self.arc_angle = arc_angle + self.helical_start = helical_start + self.helical_end = helical_end def get_max_wedge_angle(self): """Calculate angular coverage of a single line reaching maximum allowable error""" raise NotImplementedError("not overridden") + def iter_vertices(self): + """Yield absolute (<start vertex>, <end vertex>) for each line for the arc""" + raise NotImplementedError("not overridden") + class ArcLinearizeInside(ArcLinearizeMethod): """Start and end points of each line are on the original arc""" @@ -34,9 +48,30 @@ class ArcLinearizeInside(ArcLinearizeMethod): # - Simplest maths, easiest to explain & visually verify def get_max_wedge_angle(self): - return 2 * acos((self.radius - self.max_error) / self.radius) + return abs(2 * acos((self.arc_radius - self.max_error) / self.arc_radius)) + def iter_vertices(self): + wedge_count = int(ceil(abs(self.arc_angle) / self.get_max_wedge_angle())) + wedge_angle = self.arc_angle / wedge_count + start_radius = self.arc_p_start - self.arc_p_center + helical_delta = (self.helical_end - self.helical_start) / wedge_count + l_p_start = start_radius + self.arc_p_center + l_start = l_p_start + self.helical_start + for i in range(wedge_count): + q_end = Quaternion.new_rotate_axis( + angle=wedge_angle * (i+1), + axis=-self.plane_normal, + ) + # Projected on selected plane + l_p_end = (q_end * start_radius) + self.arc_p_center + # Helical displacement + l_end = l_p_end + (self.helical_start + (helical_delta * (i+1))) + + yield (l_start, l_end) + + # start of next line is the end of this line + (l_p_start, l_start) = (l_p_end, l_end) @@ -64,7 +99,7 @@ DEFAULT_LA_ARCDISTMODE = GCodeIncrementalArcDistanceMode def linearize_arc(arc_gcode, start_pos, plane=None, method_class=None, dist_mode=None, arc_dist_mode=None, - max_error=0.01, precision_fmt="{0:.3f}"): + max_error=0.01, decimal_places=3): # set defaults if method_class is None: method_class = DEFAULT_LA_method_class @@ -177,40 +212,56 @@ def linearize_arc(arc_gcode, start_pos, plane=None, method_class=None, # - helical_start distance along plane.normal of arc start # - helical_disp distance along plane.normal of arc end - # TODO: debug printing - print(( - "linearize_arc params\n" - " - arc_p_start {arc_p_start}\n" - " - arc_p_end {arc_p_end}\n" - " - arc_p_center {arc_p_center}\n" - " - arc_radius {arc_radius}\n" - " - arc_angle {arc_angle:.4f} ({arc_angle_deg:.3f} deg)\n" - " - helical_start {helical_start}\n" - " - helical_end {helical_end}\n" - ).format( - arc_p_start=arc_p_start, - arc_p_end=arc_p_end, - arc_p_center=arc_p_center, - arc_radius=arc_radius, - arc_angle=arc_angle, arc_angle_deg=arc_angle * (180/pi), - helical_start=helical_start, - helical_end=helical_end, - )) + #print(( + # "linearize_arc params\n" + # " - arc_p_start {arc_p_start}\n" + # " - arc_p_end {arc_p_end}\n" + # " - arc_p_center {arc_p_center}\n" + # " - arc_radius {arc_radius}\n" + # " - arc_angle {arc_angle:.4f} ({arc_angle_deg:.3f} deg)\n" + # " - helical_start {helical_start}\n" + # " - helical_end {helical_end}\n" + #).format( + # arc_p_start=arc_p_start, + # arc_p_end=arc_p_end, + # arc_p_center=arc_p_center, + # arc_radius=arc_radius, + # arc_angle=arc_angle, arc_angle_deg=arc_angle * (180/pi), + # helical_start=helical_start, + # helical_end=helical_end, + #)) + method_class_params = { + 'max_error': max_error, + 'plane_normal': plane.normal, + 'arc_p_start': arc_p_start, + 'arc_p_end': arc_p_end, + 'arc_p_center': arc_p_center, + 'arc_radius': arc_radius, + 'arc_angle': arc_angle, + 'helical_start': helical_start, + 'helical_end': helical_end, + } + method = method_class(**method_class_params) - - method = method_class( - max_error=max_error, - radius=arc_radius, - ) - - #plane_projection(vect, normal) - - pass - # Steps: - # - calculate: - # - - # - calculate number of linear segments + #import ipdb; ipdb.set_trace() + if isinstance(dist_mode, GCodeAbsoluteDistanceMode): + # Absolute coordinates + for line_vertices in method.iter_vertices(): + (l_start, l_end) = line_vertices + yield GCodeLinearMove(**dict(zip('XYZ', l_end.xyz))) + else: + # Incremental coordinates (beware cumulative errors) + cur_pos = arc_start + for line_vertices in method.iter_vertices(): + (l_start, l_end) = line_vertices + l_delta = l_end - cur_pos + + # round delta coordinates (introduces errors) + for axis in 'xyz': + setattr(l_delta, axis, round(getattr(l_delta, axis), decimal_places)) + yield GCodeLinearMove(**dict(zip('XYZ', l_delta.xyz))) + cur_pos += l_delta # mitigate errors by also adding them the accumulated cur_pos # ==================== Arc Precision Adjustment ==================== diff --git a/pygcode/words.py b/pygcode/words.py index 059dc42..776859f 100644 --- a/pygcode/words.py +++ b/pygcode/words.py @@ -16,7 +16,7 @@ def _clean_codestr(value): return "%g" % value CLEAN_NONE = lambda v: v -CLEAN_FLOAT = lambda v: "%g" % v +CLEAN_FLOAT = lambda v: "{0:g}".format(round(v, 3)) CLEAN_CODE = _clean_codestr CLEAN_INT = lambda v: "%g" % v diff --git a/scripts/pygcode-normalize.py b/scripts/pygcode-normalize.py index d5e3eb3..613ea00 100755 --- a/scripts/pygcode-normalize.py +++ b/scripts/pygcode-normalize.py @@ -1,5 +1,7 @@ #!/usr/bin/env python import argparse +import re +from collections import defaultdict for pygcode_lib_type in ('installed_lib', 'relative_lib'): try: @@ -46,10 +48,18 @@ parser.add_argument( # Arcs parser.add_argument( - '--arcs_linearize', '-al', dest='arcs_linearize', + '--arc_linearize', '-al', dest='arc_linearize', action='store_const', const=True, default=False, help="convert G2/3 commands to a series of linear G1 linear interpolations", ) + +parser.add_argument( + '--arc_lin_method', '-alm', dest='arc_lin_method', default='i', + help="Method of linearizing arcs, i=inner, o=outer, m=middle. List 2 " + "for <ccw>,<cw>, eg 'i,o'. also: 'm' is equivalent to 'm,m' ", + metavar='{i,o,m}[,{i,o,m]', +) + parser.add_argument( '--arc_alignment', '-aa', dest='arc_alignment', type=str, choices=('XYZ','IJK','R'), default=None, @@ -61,6 +71,31 @@ parser.add_argument( args = parser.parse_args() +# arc linearizing method (manually parsing) +ARC_LIN_CLASS_MAP = { + 'i': ArcLinearizeInside, + 'o': ArcLinearizeOutside, + 'm': ArcLinearizeMid, +} + +arc_lin_method_regex = re.compile(r'^(?P<g2>[iom])(,(?P<g3>[iom]))?$', re.I) +if args.arc_lin_method: + match = arc_lin_method_regex.search(args.arc_lin_method) + if not match: + raise RuntimeError("parameter for --arc_lin_method is invalid: '%s'" % args.arc_lin_method) + + # changing args.arc_lin_method (because I'm a fiend) + args.arc_lin_method = {} + args.arc_lin_method['G2'] = ARC_LIN_CLASS_MAP[match.group('g2')] + if match.group('g3'): + args.arc_lin_method['G3'] = ARC_LIN_CLASS_MAP[match.group('g3')] + else: + args.arc_lin_method['G3'] = args.arc_lin_method['G2'] +else: + args.arc_lin_method = defaultdict(lambda: ArcLinearizeInside) # just to be sure + + + # =================== Create Virtual CNC Machine =================== class MyMode(Mode): default_mode = args.machine_mode @@ -76,40 +111,45 @@ def gcodes2str(gcodes): # =================== Process File =================== -print(args) for line_str in args.infile[0].readlines(): line = Line(line_str) - if line.comment: - print("===== %s" % line.comment.text) + #if line.comment: + # print("===== %s" % line.comment.text) effective_gcodes = machine.block_modal_gcodes(line.block) - if any(isinstance(g, GCodeArcMove) for g in effective_gcodes): - print("---------> Found an Arc <----------") + if args.arc_linearize and any(isinstance(g, GCodeArcMove) for g in effective_gcodes): + #print("---------> Found an Arc <----------") (befores, (arc,), afters) = split_gcodes(effective_gcodes, GCodeArcMove) # TODO: debug printing (for now) if befores: - print("befores: %s" % gcodes2str(befores)) + print(gcodes2str(befores)) machine.process_gcodes(*befores) - print("arc: %s" % str(arc)) - linearize_arc( - arc_gcode=arc, - start_pos=machine.pos, - plane=machine.mode.plane_selection, - method_class=ArcLinearizeInside, # FIXME: selectable from args - dist_mode=machine.mode.distance, - arc_dist_mode=machine.mode.arc_ijk_distance, - max_error=args.precision, - ) + #print("arc: %s" % str(arc)) + linearize_params = { + 'arc_gcode': arc, + 'start_pos': machine.pos, + 'plane': machine.mode.plane_selection, + 'method_class': args.arc_lin_method["%s%i" % (arc.word.letter, arc.word.value)], + 'dist_mode': machine.mode.distance, + 'arc_dist_mode': machine.mode.arc_ijk_distance, + 'max_error': args.precision, + 'decimal_places': 3, + } + for linear_gcode in linearize_arc(**linearize_params): + print(linear_gcode) machine.process_gcodes(arc) if afters: - print("afters: %s" % gcodes2str(afters)) + print(gcodes2str(afters)) machine.process_gcodes(*afters) + if line.comment: + print(str(line.comment)) else: + print(str(line)) machine.process_block(line.block) - print("%r, %s" % (sorted(line.block.gcodes), line.block.modal_params)) + #print("%r, %s" % (sorted(line.block.gcodes), line.block.modal_params)) From 0c5670c4fff3ac16a94e87ed8ef50505d775ad44 Mon Sep 17 00:00:00 2001 From: Peter Boin <nymphii@gmail.com> Date: Tue, 11 Jul 2017 11:06:32 +1000 Subject: [PATCH 19/32] gcode parameter additions / change by attribute --- pygcode/gcodes.py | 10 ++++++++++ pygcode/words.py | 5 +++++ 2 files changed, 15 insertions(+) diff --git a/pygcode/gcodes.py b/pygcode/gcodes.py index f10186a..9a4f5ce 100644 --- a/pygcode/gcodes.py +++ b/pygcode/gcodes.py @@ -253,6 +253,16 @@ class GCode(object): key=key )) + def __setattr__(self, key, value): + if key in self.param_letters: + if key in self.params: + self.params[key].value = value + else: + self.add_parameter(Word(key, value)) + + else: + self.__dict__[key] = value + @property def description(self): return self.__doc__ diff --git a/pygcode/words.py b/pygcode/words.py index 776859f..df0d6b9 100644 --- a/pygcode/words.py +++ b/pygcode/words.py @@ -275,6 +275,11 @@ class Word(object): return self._value_class(self._value_str) return self._value + @value.setter + def value(self, new_value): + self._value = self._value_class(new_value) + self._value_str = None + # Order def __lt__(self, other): return self.letter < other.letter From 0d398d5330f1b42798ee749d7cd0d4729282a636 Mon Sep 17 00:00:00 2001 From: Peter Boin <nymphii@gmail.com> Date: Tue, 11 Jul 2017 11:35:49 +1000 Subject: [PATCH 20/32] removed clean_str concept from words --- pygcode/__init__.py | 9 +++++---- pygcode/block.py | 2 +- pygcode/gcodes.py | 11 ++++++----- pygcode/words.py | 41 ++++++++--------------------------------- 4 files changed, 20 insertions(+), 43 deletions(-) diff --git a/pygcode/__init__.py b/pygcode/__init__.py index 5a3d921..fba96cb 100644 --- a/pygcode/__init__.py +++ b/pygcode/__init__.py @@ -12,6 +12,7 @@ __all__ = [ # GCodes 'words2gcodes', 'text2gcodes', 'split_gcodes', + # $ python -c "from pygcode.gcodes import GCode, _subclasses as sc; print(',\\n '.join(sorted('\\'%s\\'' % g.__name__ for g in sc(GCode))))"python -c "from pygcode.gcodes import GCode, _subclasses as sc; print(',\\n '.join(sorted('\\'%s\\'' % g.__name__ for g in sc(GCode))))" 'GCode', 'GCodeAbsoluteArcDistanceMode', 'GCodeAbsoluteDistanceMode', @@ -133,7 +134,7 @@ __all__ = [ 'GCodeUseInches', 'GCodeUseMillimeters', 'GCodeUserDefined', - 'GCodeWaitOnInput', + 'GCodeWaitOnInput' ] # Machine @@ -162,7 +163,7 @@ from .words import ( from .gcodes import ( words2gcodes, text2gcodes, split_gcodes, - # $ python -c "from pygcode.gcodes import _gcode_class_infostr; print(_gcode_class_infostr())" + # $ python -c "from pygcode.gcodes import _gcode_class_infostr as x; print(x(prefix=' # '))" # - GCode: # - GCodeCannedCycle: # G89 - GCodeBoringCycleDwellFeedOut: G89: Boring Cycle, Dwell, Feed Out @@ -286,7 +287,7 @@ from .gcodes import ( # G20 - GCodeUseInches: G20: use inches for length units # G21 - GCodeUseMillimeters: G21: use millimeters for length units - # $ python -c "from pygcode.gcodes import GCode, _subclasses; print(',\\n'.join(sorted(g.__name__ for g in _subclasses(GCode))))"python -c "from pygcode.gcodes import GCode, _subclasses; print(',\\n'.join(sorted(g.__name__ for g in _subclasses(GCode))))" + # $ python -c "from pygcode.gcodes import GCode, _subclasses as sc; print(',\\n '.join(sorted(g.__name__ for g in sc(GCode))))" GCode, GCodeAbsoluteArcDistanceMode, GCodeAbsoluteDistanceMode, @@ -408,5 +409,5 @@ from .gcodes import ( GCodeUseInches, GCodeUseMillimeters, GCodeUserDefined, - GCodeWaitOnInput, + GCodeWaitOnInput ) diff --git a/pygcode/block.py b/pygcode/block.py index afc267d..5a9fdf2 100644 --- a/pygcode/block.py +++ b/pygcode/block.py @@ -77,4 +77,4 @@ class Block(object): )) def __str__(self): - return ' '.join(str(x) for x in (self.gcodes + [p.clean_str for p in self.modal_params])) + return ' '.join(str(x) for x in (self.gcodes + self.modal_params)) diff --git a/pygcode/gcodes.py b/pygcode/gcodes.py index 9a4f5ce..d5898c6 100644 --- a/pygcode/gcodes.py +++ b/pygcode/gcodes.py @@ -191,11 +191,11 @@ class GCode(object): param_str = '' if self.params: param_str += ' ' + ' '.join([ - "{}".format(self.params[k].clean_str) + "{}".format(self.params[k]) for k in sorted(self.params.keys()) ]) return "{gcode}{parameters}".format( - gcode=self.word.clean_str, + gcode=self.word, parameters=param_str, ) @@ -1237,7 +1237,7 @@ def _subclasses(root_class): yield cls -def _gcode_class_infostr(base_class=GCode): +def _gcode_class_infostr(base_class=GCode, prefix=''): """ List all ineheriting classes for the given gcode class :param base_class: root of hierarcy @@ -1247,8 +1247,9 @@ def _gcode_class_infostr(base_class=GCode): for (cls, level) in _subclasses_level(base_class): word_str = '' if cls.word_key: - word_str = cls.word_key.clean_str - info_str += "{word} {indent}- {name}: {description}\n".format( + word_str = str(cls.word_key) + info_str += "{prefix}{word} {indent}- {name}: {description}\n".format( + prefix=prefix, word="%-5s" % word_str, indent=(level * " "), name=cls.__name__, diff --git a/pygcode/words.py b/pygcode/words.py index df0d6b9..3368f72 100644 --- a/pygcode/words.py +++ b/pygcode/words.py @@ -208,21 +208,15 @@ class Word(object): (letter, value) = args else: # Word('G90') - word_str = args[0] - letter = word_str[0] # first letter - value = word_str[1:] # rest of string + letter = args[0][0] # first letter + value = args[0][1:] # rest of string + letter = letter.upper() - self.letter = letter.upper() + self._value_class = WORD_MAP[letter]['class'] + self._value_clean = WORD_MAP[letter]['clean_value'] - self._value_str = None - self._value = None - if isinstance(value, six.string_types): - self._value_str = value - else: - self._value = value - - self._value_class = WORD_MAP[self.letter]['class'] - self._value_clean = WORD_MAP[self.letter]['clean_value'] + self.letter = letter + self._value = self._value_class(value) def __str__(self): return "{letter}{value}".format( @@ -230,14 +224,6 @@ class Word(object): 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): return "<{class_name}: {string}>".format( class_name=self.__class__.__name__, @@ -255,30 +241,19 @@ class Word(object): def __hash__(self): return hash((self.letter, self.value)) - - # Value Properties @property def value_str(self): - """Value string, or string representation of value""" - if self._value_str is None: - return str(self._value) - return self._value_str - - @property - def value_cleanstr(self): """Clean string representation, for consistent file output""" return self._value_clean(self.value) + # Value Properties @property def value(self): - if self._value is None: - return self._value_class(self._value_str) return self._value @value.setter def value(self, new_value): self._value = self._value_class(new_value) - self._value_str = None # Order def __lt__(self, other): From 436ef533986688e297f8be5c83dac83e27a5e053 Mon Sep 17 00:00:00 2001 From: Peter Boin <nymphii@gmail.com> Date: Wed, 12 Jul 2017 00:49:14 +1000 Subject: [PATCH 21/32] outer arc linearization --- pygcode/transform.py | 50 +++++++++++++++++++++++++++++------- scripts/pygcode-normalize.py | 10 +++++++- 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/pygcode/transform.py b/pygcode/transform.py index e3b0c00..b3343a6 100644 --- a/pygcode/transform.py +++ b/pygcode/transform.py @@ -1,4 +1,4 @@ -from math import acos, atan2, pi, sqrt, ceil +from math import cos, acos, atan2, pi, sqrt, ceil from .gcodes import GCodeLinearMove from .gcodes import GCodeArcMove, GCodeArcMoveCW, GCodeArcMoveCCW @@ -14,8 +14,6 @@ from .utils import Vector3, Quaternion, plane_projection # ==================== Arcs (G2,G3) --> Linear Motion (G1) ==================== class ArcLinearizeMethod(object): - pass - def __init__(self, max_error, plane_normal, arc_p_start, arc_p_end, arc_p_center, arc_radius, arc_angle, helical_start, helical_end): @@ -29,6 +27,9 @@ class ArcLinearizeMethod(object): self.helical_start = helical_start self.helical_end = helical_end + if self.max_error > self.arc_radius: + self.max_error = self.arc_radius + def get_max_wedge_angle(self): """Calculate angular coverage of a single line reaching maximum allowable error""" raise NotImplementedError("not overridden") @@ -56,7 +57,7 @@ class ArcLinearizeInside(ArcLinearizeMethod): start_radius = self.arc_p_start - self.arc_p_center helical_delta = (self.helical_end - self.helical_start) / wedge_count - l_p_start = start_radius + self.arc_p_center + l_p_start = self.arc_p_start l_start = l_p_start + self.helical_start for i in range(wedge_count): q_end = Quaternion.new_rotate_axis( @@ -70,9 +71,8 @@ class ArcLinearizeInside(ArcLinearizeMethod): yield (l_start, l_end) - # start of next line is the end of this line - (l_p_start, l_start) = (l_p_end, l_end) - + # start of next line is the end of this one + l_start = l_end class ArcLinearizeOutside(ArcLinearizeMethod): @@ -83,6 +83,38 @@ class ArcLinearizeOutside(ArcLinearizeMethod): # - perimeter milling action will remove less material # - 1st and last lines are 1/2 length of the others + def get_max_wedge_angle(self): + return abs(2 * acos(self.arc_radius / (self.arc_radius + self.max_error))) + + def iter_vertices(self): + # n wedges distributed like: + # - 1/2 wedge : first line + # - n-1 wedges : outer perimeter + # - last 1/2 wedge : last line + wedge_count = int(ceil(abs(self.arc_angle) / self.get_max_wedge_angle())) + wedge_angle = self.arc_angle / wedge_count + start_radius = self.arc_p_start - self.arc_p_center + # radius of outer circle (across which the linear lines will span) + error_radius = start_radius.normalized() * (self.arc_radius / cos(wedge_angle / 2)) + + l_p_start = start_radius + self.arc_p_center + l_start = l_p_start + self.helical_start + for i in range(wedge_count): + cur_angle = (wedge_angle * i) + (wedge_angle / 2) + q_end = Quaternion.new_rotate_axis(angle=cur_angle, axis=-self.plane_normal) + + # Projected on selected plane + l_p_end = (q_end * error_radius) + self.arc_p_center + # Helical displacement + l_end = l_p_end + self.helical_start + ((self.helical_end - self.helical_start) * (cur_angle / self.arc_angle)) + + yield (l_start, l_end) + + # start of next line is the end of this one + l_start = l_end + + yield (l_start, self.arc_p_end + self.helical_end) + class ArcLinearizeMid(ArcLinearizeMethod): """Lines cross original arc from tangent of arc radius - precision/2, until it reaches arc radius + precision/2""" @@ -244,7 +276,7 @@ def linearize_arc(arc_gcode, start_pos, plane=None, method_class=None, } method = method_class(**method_class_params) - #import ipdb; ipdb.set_trace() + # Iterate & yield each linear line (start, end) vertices if isinstance(dist_mode, GCodeAbsoluteDistanceMode): # Absolute coordinates for line_vertices in method.iter_vertices(): @@ -256,7 +288,7 @@ def linearize_arc(arc_gcode, start_pos, plane=None, method_class=None, for line_vertices in method.iter_vertices(): (l_start, l_end) = line_vertices l_delta = l_end - cur_pos - + # round delta coordinates (introduces errors) for axis in 'xyz': setattr(l_delta, axis, round(getattr(l_delta, axis), decimal_places)) diff --git a/scripts/pygcode-normalize.py b/scripts/pygcode-normalize.py index 613ea00..8d3e534 100755 --- a/scripts/pygcode-normalize.py +++ b/scripts/pygcode-normalize.py @@ -1,4 +1,11 @@ #!/usr/bin/env python + +# Script to take (theoretically) any g-code file as input, and output a +# normalized version of it. +# +# Script outcome can have cursory verification with: +# https://nraynaud.github.io/webgcode/ + import argparse import re from collections import defaultdict @@ -37,7 +44,8 @@ parser.add_argument( parser.add_argument( '--precision', '-p', dest='precision', type=float, default=DEFAULT_PRECISION, - help="maximum positional error when generating gcodes (eg: arcs to lines)", + help="maximum positional error when generating gcodes (eg: arcs to lines) " + "(default: %g)" % DEFAULT_PRECISION, ) # Machine From 2c0de02b6d223c4d9c3a611a22ed056a6850b78e Mon Sep 17 00:00:00 2001 From: Peter Boin <nymphii@gmail.com> Date: Wed, 12 Jul 2017 16:53:43 +1000 Subject: [PATCH 22/32] bugfix: ws at start of str(Line()) --- pygcode/block.py | 5 +++++ pygcode/transform.py | 19 ------------------- scripts/pygcode-normalize.py | 4 ---- 3 files changed, 5 insertions(+), 23 deletions(-) diff --git a/pygcode/block.py b/pygcode/block.py index 5a9fdf2..bb8b716 100644 --- a/pygcode/block.py +++ b/pygcode/block.py @@ -76,5 +76,10 @@ class Block(object): key=k )) + def __bool__(self): + return bool(self.words) + + __nonzero__ = __bool__ # python < 3 compatability + def __str__(self): return ' '.join(str(x) for x in (self.gcodes + self.modal_params)) diff --git a/pygcode/transform.py b/pygcode/transform.py index b3343a6..18250a2 100644 --- a/pygcode/transform.py +++ b/pygcode/transform.py @@ -244,25 +244,6 @@ def linearize_arc(arc_gcode, start_pos, plane=None, method_class=None, # - helical_start distance along plane.normal of arc start # - helical_disp distance along plane.normal of arc end - #print(( - # "linearize_arc params\n" - # " - arc_p_start {arc_p_start}\n" - # " - arc_p_end {arc_p_end}\n" - # " - arc_p_center {arc_p_center}\n" - # " - arc_radius {arc_radius}\n" - # " - arc_angle {arc_angle:.4f} ({arc_angle_deg:.3f} deg)\n" - # " - helical_start {helical_start}\n" - # " - helical_end {helical_end}\n" - #).format( - # arc_p_start=arc_p_start, - # arc_p_end=arc_p_end, - # arc_p_center=arc_p_center, - # arc_radius=arc_radius, - # arc_angle=arc_angle, arc_angle_deg=arc_angle * (180/pi), - # helical_start=helical_start, - # helical_end=helical_end, - #)) - method_class_params = { 'max_error': max_error, 'plane_normal': plane.normal, diff --git a/scripts/pygcode-normalize.py b/scripts/pygcode-normalize.py index 8d3e534..2e8bb56 100755 --- a/scripts/pygcode-normalize.py +++ b/scripts/pygcode-normalize.py @@ -157,7 +157,3 @@ for line_str in args.infile[0].readlines(): else: print(str(line)) machine.process_block(line.block) - - - - #print("%r, %s" % (sorted(line.block.gcodes), line.block.modal_params)) From efeb76592db0a703175dd385e103287fe7c46f98 Mon Sep 17 00:00:00 2001 From: Peter Boin <nymphii@gmail.com> Date: Sat, 15 Jul 2017 23:20:15 +1000 Subject: [PATCH 23/32] improvements to arc linearization --- pygcode/gcodes.py | 28 ++++- pygcode/machine.py | 4 +- pygcode/transform.py | 195 +++++++++++++++++++++++++---------- pygcode/utils.py | 30 ++++++ pygcode/words.py | 21 +++- scripts/pygcode-normalize.py | 90 ++++++++++++---- 6 files changed, 283 insertions(+), 85 deletions(-) diff --git a/pygcode/gcodes.py b/pygcode/gcodes.py index d5898c6..d22748e 100644 --- a/pygcode/gcodes.py +++ b/pygcode/gcodes.py @@ -167,6 +167,10 @@ class GCode(object): self.word = gcode_word self.params = {} + # Whitespace as prefix + # if True, str(self) will repalce self.word code with whitespace + self._whitespace_prefix = False + # Add Given Parameters for param_word in param_words: self.add_parameter(param_word) @@ -194,8 +198,11 @@ class GCode(object): "{}".format(self.params[k]) for k in sorted(self.params.keys()) ]) - return "{gcode}{parameters}".format( - gcode=self.word, + word_str = str(self.word) + if self._whitespace_prefix: + word_str = ' ' * len(word_str) + return "{word_str}{parameters}".format( + word_str=word_str, parameters=param_str, ) @@ -468,36 +475,42 @@ class GCodeDrillingCycle(GCodeCannedCycle): """G81: Drilling Cycle""" param_letters = GCodeCannedCycle.param_letters | set('RLP') word_key = Word('G', 81) + modal_param_letters = GCodeCannedCycle.param_letters | set('RLP') class GCodeDrillingCycleDwell(GCodeCannedCycle): """G82: Drilling Cycle, Dwell""" param_letters = GCodeCannedCycle.param_letters | set('RLP') word_key = Word('G', 82) + modal_param_letters = GCodeCannedCycle.param_letters | set('RLP') class GCodeDrillingCyclePeck(GCodeCannedCycle): """G83: Drilling Cycle, Peck""" param_letters = GCodeCannedCycle.param_letters | set('RLQ') word_key = Word('G', 83) + modal_param_letters = GCodeCannedCycle.param_letters | set('RLQ') class GCodeDrillingCycleChipBreaking(GCodeCannedCycle): """G73: Drilling Cycle, ChipBreaking""" param_letters = GCodeCannedCycle.param_letters | set('RLQ') word_key = Word('G', 73) + modal_param_letters = GCodeCannedCycle.param_letters | set('RLQ') class GCodeBoringCycleFeedOut(GCodeCannedCycle): """G85: Boring Cycle, Feed Out""" param_letters = GCodeCannedCycle.param_letters | set('RLP') word_key = Word('G', 85) + modal_param_letters = GCodeCannedCycle.param_letters | set('RLP') class GCodeBoringCycleDwellFeedOut(GCodeCannedCycle): """G89: Boring Cycle, Dwell, Feed Out""" param_letters = GCodeCannedCycle.param_letters | set('RLP') word_key = Word('G', 89) + modal_param_letters = GCodeCannedCycle.param_letters | set('RLP') class GCodeThreadingCycle(GCodeCannedCycle): @@ -561,6 +574,7 @@ class GCodeFeedRateMode(GCode): modal_group = MODAL_GROUP_MAP['feed_rate_mode'] exec_order = 30 + class GCodeInverseTimeMode(GCodeFeedRateMode): """G93: Inverse Time Mode""" word_key = Word('G', 93) @@ -883,16 +897,24 @@ class GCodePathBlendingMode(GCodePathControlMode): # CODE PARAMETERS DESCRIPTION # G98 Canned Cycle Return Level + class GCodeCannedReturnMode(GCode): modal_group = MODAL_GROUP_MAP['canned_cycles_return'] exec_order = 220 class GCodeCannedCycleReturnLevel(GCodeCannedReturnMode): - """G98: Canned Cycle Return Level""" + """G98: Canned Cycle Return to the level set prior to cycle start""" + # "retract to the position that axis was in just before this series of one or more contiguous canned cycles was started" word_key = Word('G', 98) +class GCodeCannedCycleReturnToR(GCodeCannedReturnMode): + """G99: Canned Cycle Return to the level set by R""" + # "retract to the position specified by the R word of the canned cycle" + word_key = Word('G', 99) + + # ======================= Other Modal Codes ======================= # CODE PARAMETERS DESCRIPTION # F Set Feed Rate diff --git a/pygcode/machine.py b/pygcode/machine.py index 1c625bc..4f0e42b 100644 --- a/pygcode/machine.py +++ b/pygcode/machine.py @@ -357,7 +357,9 @@ class Machine(object): return None if self.mode.motion is None: raise MachineInvalidState("unable to assign modal parameters when no motion mode is set") - (modal_gcodes, unasigned_words) = words2gcodes([self.mode.motion.word] + modal_params) + params = copy(self.mode.motion.params) # dict + params.update(dict((w.letter, w) for w in modal_params)) # override retained modal parameters + (modal_gcodes, unasigned_words) = words2gcodes([self.mode.motion.word] + params.values()) if unasigned_words: raise MachineInvalidState("modal parameters '%s' cannot be assigned when in mode: %r" % ( ' '.join(str(x) for x in unasigned_words), self.mode diff --git a/pygcode/transform.py b/pygcode/transform.py index 18250a2..8467f78 100644 --- a/pygcode/transform.py +++ b/pygcode/transform.py @@ -1,4 +1,4 @@ -from math import cos, acos, atan2, pi, sqrt, ceil +from math import sin, cos, tan, asin, acos, atan2, pi, sqrt, ceil from .gcodes import GCodeLinearMove from .gcodes import GCodeArcMove, GCodeArcMoveCW, GCodeArcMoveCCW @@ -14,6 +14,11 @@ from .utils import Vector3, Quaternion, plane_projection # ==================== Arcs (G2,G3) --> Linear Motion (G1) ==================== class ArcLinearizeMethod(object): + # Chord Phase Offest: + # False : each line will span an equal portion of the arc + # True : the first & last chord will span 1/2 the angular distance of all other chords + chord_phase_offset = False + def __init__(self, max_error, plane_normal, arc_p_start, arc_p_end, arc_p_center, arc_radius, arc_angle, helical_start, helical_end): @@ -30,13 +35,103 @@ class ArcLinearizeMethod(object): if self.max_error > self.arc_radius: self.max_error = self.arc_radius + # Initializing + self._max_wedge_angle = None + self._wedge_count = None + self._wedge_angle = None + self._inner_radius = None + self._outer_radius = None + + # Overridden Functions def get_max_wedge_angle(self): """Calculate angular coverage of a single line reaching maximum allowable error""" raise NotImplementedError("not overridden") + def get_inner_radius(self): + """Radius each line is tangential to""" + # IMPORTANT: when overriding, calculate this using self.wedge_angle, + # (self.wedge_angle will almost always be < self.max_wedge_angle) + raise NotImplementedError("not overridden") + + def get_outer_radius(self): + """Radius from which each line forms a chord""" + # IMPORTANT: when overriding, calculate this using self.wedge_angle, + # (self.wedge_angle will almost always be < self.max_wedge_angle) + raise NotImplementedError("not overridden") + + # Properties + @property + def max_wedge_angle(self): + if self._max_wedge_angle is None: + self._max_wedge_angle = self.get_max_wedge_angle() + return self._max_wedge_angle + + @property + def wedge_count(self): + """ + Number of full wedges covered across the arc. + NB: if there is phase offset, then the actual number of linearized lines + is this + 1, because the first and last are considered to be the + same 'wedge'. + """ + if self._wedge_count is None: + self._wedge_count = int(ceil(abs(self.arc_angle) / self.max_wedge_angle)) + return self._wedge_count + + @property + def wedge_angle(self): + """Angle each major chord stretches across the original arc""" + if self._wedge_angle is None: + self._wedge_angle = self.arc_angle / self.wedge_count + return self._wedge_angle + + @property + def inner_radius(self): + if self._inner_radius is None: + self._inner_radius = self.get_inner_radius() + return self._inner_radius + + @property + def outer_radius(self): + if self._outer_radius is None: + self._outer_radius = self.get_outer_radius() + return self._outer_radius + + # Iter def iter_vertices(self): """Yield absolute (<start vertex>, <end vertex>) for each line for the arc""" - raise NotImplementedError("not overridden") + start_vertex = self.arc_p_start - self.arc_p_center + outer_vertex = start_vertex.normalized() * self.outer_radius + d_helical = self.helical_end - self.helical_start + + l_p_start = self.arc_p_center + start_vertex + l_start = l_p_start + self.helical_start + + for i in range(self.wedge_count): + wedge_number = i + 1 + # Current angle + cur_angle = self.wedge_angle * wedge_number + if self.chord_phase_offset: + cur_angle -= self.wedge_angle / 2. + elif wedge_number >= self.wedge_count: + break # stop 1 iteration short + # alow last arc to simply span across: + # <the end of the last line> -> <circle's end point> + + # Next end point as projected on selected plane + q_end = Quaternion.new_rotate_axis(angle=cur_angle, axis=-self.plane_normal) + l_p_end = (q_end * outer_vertex) + self.arc_p_center + # += helical displacement (difference along plane's normal) + helical_displacement = self.helical_start + (d_helical * (cur_angle / self.arc_angle)) + l_end = l_p_end + helical_displacement + + yield (l_start, l_end) + + # start of next line is the end of this one + l_start = l_end + + # Last line always ends at the circle's end + yield (l_start, self.arc_p_end + self.helical_end) class ArcLinearizeInside(ArcLinearizeMethod): @@ -48,31 +143,19 @@ class ArcLinearizeInside(ArcLinearizeMethod): # - Each line is the same length # - Simplest maths, easiest to explain & visually verify + chord_phase_offset = False + def get_max_wedge_angle(self): + """Calculate angular coverage of a single line reaching maximum allowable error""" return abs(2 * acos((self.arc_radius - self.max_error) / self.arc_radius)) - def iter_vertices(self): - wedge_count = int(ceil(abs(self.arc_angle) / self.get_max_wedge_angle())) - wedge_angle = self.arc_angle / wedge_count - start_radius = self.arc_p_start - self.arc_p_center - helical_delta = (self.helical_end - self.helical_start) / wedge_count + def get_inner_radius(self): + """Radius each line is tangential to""" + return abs(cos(self.wedge_angle / 2.) * self.arc_radius) - l_p_start = self.arc_p_start - l_start = l_p_start + self.helical_start - for i in range(wedge_count): - q_end = Quaternion.new_rotate_axis( - angle=wedge_angle * (i+1), - axis=-self.plane_normal, - ) - # Projected on selected plane - l_p_end = (q_end * start_radius) + self.arc_p_center - # Helical displacement - l_end = l_p_end + (self.helical_start + (helical_delta * (i+1))) - - yield (l_start, l_end) - - # start of next line is the end of this one - l_start = l_end + def get_outer_radius(self): + """Radius from which each line forms a chord""" + return self.arc_radius class ArcLinearizeOutside(ArcLinearizeMethod): @@ -83,37 +166,19 @@ class ArcLinearizeOutside(ArcLinearizeMethod): # - perimeter milling action will remove less material # - 1st and last lines are 1/2 length of the others + chord_phase_offset = True + def get_max_wedge_angle(self): + """Calculate angular coverage of a single line reaching maximum allowable error""" return abs(2 * acos(self.arc_radius / (self.arc_radius + self.max_error))) - def iter_vertices(self): - # n wedges distributed like: - # - 1/2 wedge : first line - # - n-1 wedges : outer perimeter - # - last 1/2 wedge : last line - wedge_count = int(ceil(abs(self.arc_angle) / self.get_max_wedge_angle())) - wedge_angle = self.arc_angle / wedge_count - start_radius = self.arc_p_start - self.arc_p_center - # radius of outer circle (across which the linear lines will span) - error_radius = start_radius.normalized() * (self.arc_radius / cos(wedge_angle / 2)) + def get_inner_radius(self): + """Radius each line is tangential to""" + return self.arc_radius - l_p_start = start_radius + self.arc_p_center - l_start = l_p_start + self.helical_start - for i in range(wedge_count): - cur_angle = (wedge_angle * i) + (wedge_angle / 2) - q_end = Quaternion.new_rotate_axis(angle=cur_angle, axis=-self.plane_normal) - - # Projected on selected plane - l_p_end = (q_end * error_radius) + self.arc_p_center - # Helical displacement - l_end = l_p_end + self.helical_start + ((self.helical_end - self.helical_start) * (cur_angle / self.arc_angle)) - - yield (l_start, l_end) - - # start of next line is the end of this one - l_start = l_end - - yield (l_start, self.arc_p_end + self.helical_end) + def get_outer_radius(self): + """Radius from which each line forms a chord""" + return abs(self.arc_radius / cos(self.wedge_angle / 2.)) class ArcLinearizeMid(ArcLinearizeMethod): @@ -123,6 +188,23 @@ class ArcLinearizeMid(ArcLinearizeMethod): # - Most complex to calculate (but who cares, that's only done once) # - default linearizing method as it's probably the best + chord_phase_offset = True + + def get_max_wedge_angle(self): + """Calculate angular coverage of a single line reaching maximum allowable error""" + d_radius = self.max_error / 2. + return abs(2. * acos((self.arc_radius - d_radius) / (self.arc_radius + d_radius))) + + def get_inner_radius(self): + """Radius each line is tangential to""" + d_radius = self.arc_radius * (tan(self.wedge_angle / 4.) ** 2) + return self.arc_radius - d_radius + + def get_outer_radius(self): + """Radius from which each line forms a chord""" + d_radius = self.arc_radius * (tan(self.wedge_angle / 4.) ** 2) + return self.arc_radius + d_radius + DEFAULT_LA_METHOD = ArcLinearizeMid DEFAULT_LA_PLANE = GCodeSelectXYPlane @@ -154,11 +236,14 @@ def linearize_arc(arc_gcode, start_pos, plane=None, method_class=None, # Arc Start arc_start = start_pos.vector # Arc End - arc_end_coords = dict(zip('xyz', arc_start.xyz)) - arc_end_coords.update(arc_gcode.get_param_dict('XYZ', lc=True)) - arc_end = Vector3(**arc_end_coords) - if isinstance(dist_mode, GCodeIncrementalDistanceMode): - arc_end += start_pos.vector + if isinstance(dist_mode, GCodeAbsoluteDistanceMode): + # given coordinates override those already defined + arc_end_coords = dict(zip('xyz', arc_start.xyz)) + arc_end_coords.update(arc_gcode.get_param_dict('XYZ', lc=True)) + arc_end = Vector3(**arc_end_coords) + else: + # given coordinates are += to arc's start coords + arc_end = arc_start + Vector3(**arc_gcode.get_param_dict('XYZ', lc=True)) # Planar Projections arc_p_start = plane_projection(arc_start, plane.normal) diff --git a/pygcode/utils.py b/pygcode/utils.py index 5a93b94..00df8ab 100644 --- a/pygcode/utils.py +++ b/pygcode/utils.py @@ -1,4 +1,5 @@ import sys +from copy import copy, deepcopy if sys.version_info < (3, 0): from euclid import Vector3, Quaternion @@ -64,3 +65,32 @@ def plane_projection(vect, normal): # ref: https://en.wikipedia.org/wiki/Vector_projection n = normal.normalized() return vect - (n * vect.dot(n)) + +# ==================== GCode Utilities ==================== + +def omit_redundant_modes(gcode_iter): + """ + Replace redundant machine motion modes with whitespace, + :param gcode_iter: iterable to return with modifications + """ + + from .machine import Machine, Mode + from .gcodes import MODAL_GROUP_MAP + class NullModeMachine(Machine): + MODE_CLASS = type('NullMode', (Mode,), {'default_mode': ''}) + m = NullModeMachine() + + for g in gcode_iter: + if (g.modal_group is not None) and (m.mode.modal_groups[g.modal_group] is not None): + # g-code has a modal groups, and the machine's mode + # (of the same modal group) is not None + if m.mode.modal_groups[g.modal_group].word == g.word: + # machine's mode & g-code's mode match (no machine change) + if g.modal_group == MODAL_GROUP_MAP['motion']: + # finally: g-code sets a motion mode in the machine + g = copy(g) # duplicate gcode object + # stop redundant g-code word from being printed + g._whitespace_prefix = True + + m.process_gcodes(g) + yield g diff --git a/pygcode/words.py b/pygcode/words.py index 3368f72..25d9197 100644 --- a/pygcode/words.py +++ b/pygcode/words.py @@ -216,7 +216,7 @@ class Word(object): self._value_clean = WORD_MAP[letter]['clean_value'] self.letter = letter - self._value = self._value_class(value) + self.value = value def __str__(self): return "{letter}{value}".format( @@ -230,6 +230,20 @@ class Word(object): string=str(self), ) + # Sorting + def __lt__(self, other): + return (self.letter, self.value) < (other.letter, other.value) + + def __gt__(self, other): + return (self.letter, self.value) > (other.letter, other.value) + + def __le__(self, other): + return (self.letter, self.value) <= (other.letter, other.value) + + def __ge__(self, other): + return (self.letter, self.value) >= (other.letter, other.value) + + # Equality def __eq__(self, other): if isinstance(other, six.string_types): other = str2word(other) @@ -238,6 +252,7 @@ class Word(object): def __ne__(self, other): return not self.__eq__(other) + # Hashing def __hash__(self): return hash((self.letter, self.value)) @@ -255,10 +270,6 @@ class Word(object): def value(self, new_value): self._value = self._value_class(new_value) - # Order - def __lt__(self, other): - return self.letter < other.letter - @property def description(self): return "%s: %s" % (self.letter, WORD_MAP[self.letter]['description']) diff --git a/scripts/pygcode-normalize.py b/scripts/pygcode-normalize.py index 2e8bb56..a312ead 100755 --- a/scripts/pygcode-normalize.py +++ b/scripts/pygcode-normalize.py @@ -13,11 +13,16 @@ from collections import defaultdict for pygcode_lib_type in ('installed_lib', 'relative_lib'): try: # pygcode + from pygcode import Word from pygcode import Machine, Mode, Line from pygcode import GCodeArcMove, GCodeArcMoveCW, GCodeArcMoveCCW + from pygcode import GCodeCannedCycle from pygcode import split_gcodes + from pygcode import Comment from pygcode.transform import linearize_arc from pygcode.transform import ArcLinearizeInside, ArcLinearizeOutside, ArcLinearizeMid + from pygcode.gcodes import _subclasses + from pygcode.utils import omit_redundant_modes except ImportError: import sys, os, inspect @@ -34,6 +39,8 @@ for pygcode_lib_type in ('installed_lib', 'relative_lib'): # --- Defaults DEFAULT_PRECISION = 0.005 # mm DEFAULT_MACHINE_MODE = 'G0 G54 G17 G21 G90 G94 M5 M9 T0 F0 S0' +DEFAULT_ARC_LIN_METHOD = 'm' +DEFAULT_CANNED_CODES = ','.join(str(w) for w in sorted(c.word_key for c in _subclasses(GCodeCannedCycle) if c.word_key)) # --- Create Parser parser = argparse.ArgumentParser(description='Normalize gcode for machine consistency using different CAM software') @@ -54,32 +61,55 @@ parser.add_argument( help="Machine's startup mode as gcode (default: '%s')" % DEFAULT_MACHINE_MODE, ) -# Arcs -parser.add_argument( +# Arc Linearizing +group = parser.add_argument_group( + "Arc Linearizing", + "Converting arcs (G2/G3 codes) into linear interpolations (G1 codes) to " + "aproximate the original arc. Indistinguishable from an original arc when " + "--precision is set low enough." +) +group.add_argument( '--arc_linearize', '-al', dest='arc_linearize', action='store_const', const=True, default=False, - help="convert G2/3 commands to a series of linear G1 linear interpolations", + help="convert G2,G3 commands to a series of linear interpolations (G1 codes)", +) +group.add_argument( + '--arc_lin_method', '-alm', dest='arc_lin_method', default=DEFAULT_ARC_LIN_METHOD, + help="Method of linearizing arcs, i=inner, o=outer, m=mid. List 2 " + "for <ccw>,<cw>, eg 'i,o'. 'i' is equivalent to 'i,i'. " + "(default: '%s')" % DEFAULT_ARC_LIN_METHOD, + metavar='{i,o,m}[,{i,o,m}]', ) -parser.add_argument( - '--arc_lin_method', '-alm', dest='arc_lin_method', default='i', - help="Method of linearizing arcs, i=inner, o=outer, m=middle. List 2 " - "for <ccw>,<cw>, eg 'i,o'. also: 'm' is equivalent to 'm,m' ", - metavar='{i,o,m}[,{i,o,m]', +# Canned Cycles +group = parser.add_argument_group( + "Canned Cycle Simplification", + "Convert canned cycles into basic linear or scalar codes, such as linear " + "interpolation (G1), and pauses (or 'dwells', G4)" +) +group.add_argument( + '--canned_simplify', '-cs', dest='canned_simplify', + action='store_const', const=True, default=False, + help="Convert canned cycles into basic linear movements", +) +group.add_argument( + '---canned_codes', '-cc', dest='canned_codes', default=DEFAULT_CANNED_CODES, + help="List of canned gcodes to simplify, (default is '%s')" % DEFAULT_CANNED_CODES, ) -parser.add_argument( - '--arc_alignment', '-aa', dest='arc_alignment', type=str, choices=('XYZ','IJK','R'), - default=None, - help="enforce precision on arcs, if XYZ the destination is altered to match the radius" - "if IJK or R then the arc'c centre point is moved to assure precision", -) +#parser.add_argument( +# '--arc_alignment', '-aa', dest='arc_alignment', type=str, choices=('XYZ','IJK','R'), +# default=None, +# help="enforce precision on arcs, if XYZ the destination is altered to match the radius" +# "if IJK or R then the arc'c centre point is moved to assure precision", +#) # --- Parse Arguments args = parser.parse_args() -# arc linearizing method (manually parsing) +# --- Manually Parsing : Arc Linearizing Method +# args.arc_lin_method = {Word('G2'): <linearize method class>, ... } ARC_LIN_CLASS_MAP = { 'i': ArcLinearizeInside, 'o': ArcLinearizeOutside, @@ -94,15 +124,24 @@ if args.arc_lin_method: # changing args.arc_lin_method (because I'm a fiend) args.arc_lin_method = {} - args.arc_lin_method['G2'] = ARC_LIN_CLASS_MAP[match.group('g2')] + args.arc_lin_method[Word('g2')] = ARC_LIN_CLASS_MAP[match.group('g2')] if match.group('g3'): - args.arc_lin_method['G3'] = ARC_LIN_CLASS_MAP[match.group('g3')] + args.arc_lin_method[Word('g3')] = ARC_LIN_CLASS_MAP[match.group('g3')] else: - args.arc_lin_method['G3'] = args.arc_lin_method['G2'] + args.arc_lin_method[Word('g3')] = args.arc_lin_method[Word('g2')] else: - args.arc_lin_method = defaultdict(lambda: ArcLinearizeInside) # just to be sure + # FIXME: change default to ArcLinearizeMid (when it's working) + args.arc_lin_method = defaultdict(lambda: ArcLinearizeMid) # just to be sure +# --- Manually Parsing : Canned Codes +# args.canned_codes = [Word('G73'), Word('G89'), ... ] +canned_code_words = set() +for word_str in re.split(r'\s*,\s*', args.canned_codes): + canned_code_words.add(Word(word_str)) + +args.canned_codes = canned_code_words + # =================== Create Virtual CNC Machine =================== class MyMode(Mode): @@ -135,17 +174,18 @@ for line_str in args.infile[0].readlines(): print(gcodes2str(befores)) machine.process_gcodes(*befores) #print("arc: %s" % str(arc)) + print(Comment("linearized: %r" % arc)) linearize_params = { 'arc_gcode': arc, 'start_pos': machine.pos, 'plane': machine.mode.plane_selection, - 'method_class': args.arc_lin_method["%s%i" % (arc.word.letter, arc.word.value)], + 'method_class': args.arc_lin_method[arc.word], 'dist_mode': machine.mode.distance, 'arc_dist_mode': machine.mode.arc_ijk_distance, 'max_error': args.precision, 'decimal_places': 3, } - for linear_gcode in linearize_arc(**linearize_params): + for linear_gcode in omit_redundant_modes(linearize_arc(**linearize_params)): print(linear_gcode) machine.process_gcodes(arc) @@ -154,6 +194,14 @@ for line_str in args.infile[0].readlines(): machine.process_gcodes(*afters) if line.comment: print(str(line.comment)) + + elif args.canned_simplify and any((g.word in args.canned_codes) for g in effective_gcodes): + (befores, (canned,), afters) = split_gcodes(effective_gcodes, GCodeCannedCycle) + print(Comment('canning simplified: %r' % canned)) + # TODO: simplify canned things + print(str(line)) + machine.process_block(line.block) + else: print(str(line)) machine.process_block(line.block) From 304413dfbe28a1d0966a3a0235cbaf5ff4162705 Mon Sep 17 00:00:00 2001 From: Peter Boin <nymphii@gmail.com> Date: Sat, 15 Jul 2017 23:50:22 +1000 Subject: [PATCH 24/32] code for split & processing now much prettier --- scripts/pygcode-normalize.py | 72 +++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 29 deletions(-) diff --git a/scripts/pygcode-normalize.py b/scripts/pygcode-normalize.py index a312ead..370796d 100755 --- a/scripts/pygcode-normalize.py +++ b/scripts/pygcode-normalize.py @@ -9,6 +9,7 @@ import argparse import re from collections import defaultdict +from contextlib import contextmanager for pygcode_lib_type in ('installed_lib', 'relative_lib'): try: @@ -157,48 +158,61 @@ def gcodes2str(gcodes): return ' '.join("%s" % g for g in gcodes) +@contextmanager +def split_and_process(gcode_list, gcode_class, comment): + """ + Split gcodes by given class, yields given class instance + :param gcode_list: list of GCode instances + :param gcode_class: class inheriting from GCode (directly, or indirectly) + :param comment: Comment instance, or None + """ + (befores, (g,), afters) = split_gcodes(gcode_list, gcode_class) + # print & process those before gcode_class instance + if befores: + print(gcodes2str(befores)) + machine.process_gcodes(*befores) + # yield, then process gcode_class instance + yield g + machine.process_gcodes(g) + # print & process those after gcode_class instance + if afters: + print(gcodes2str(afters)) + machine.process_gcodes(*afters) + # print comment (if given) + if comment: + print(str(line.comment)) + + # =================== Process File =================== for line_str in args.infile[0].readlines(): line = Line(line_str) - #if line.comment: - # print("===== %s" % line.comment.text) + # Effective G-Codes: + # fills in missing motion modal gcodes (using machine's current motion mode). effective_gcodes = machine.block_modal_gcodes(line.block) if args.arc_linearize and any(isinstance(g, GCodeArcMove) for g in effective_gcodes): - #print("---------> Found an Arc <----------") - (befores, (arc,), afters) = split_gcodes(effective_gcodes, GCodeArcMove) - # TODO: debug printing (for now) - if befores: - print(gcodes2str(befores)) - machine.process_gcodes(*befores) - #print("arc: %s" % str(arc)) - print(Comment("linearized: %r" % arc)) - linearize_params = { - 'arc_gcode': arc, - 'start_pos': machine.pos, - 'plane': machine.mode.plane_selection, - 'method_class': args.arc_lin_method[arc.word], - 'dist_mode': machine.mode.distance, - 'arc_dist_mode': machine.mode.arc_ijk_distance, - 'max_error': args.precision, - 'decimal_places': 3, - } - for linear_gcode in omit_redundant_modes(linearize_arc(**linearize_params)): - print(linear_gcode) - machine.process_gcodes(arc) - - if afters: - print(gcodes2str(afters)) - machine.process_gcodes(*afters) - if line.comment: - print(str(line.comment)) + with split_and_process(effective_gcodes, GCodeArcMove, line.comment) as arc: + linearize_params = { + 'arc_gcode': arc, + 'start_pos': machine.pos, + 'plane': machine.mode.plane_selection, + 'method_class': args.arc_lin_method[arc.word], + 'dist_mode': machine.mode.distance, + 'arc_dist_mode': machine.mode.arc_ijk_distance, + 'max_error': args.precision, + 'decimal_places': 3, + } + for linear_gcode in omit_redundant_modes(linearize_arc(**linearize_params)): + print(linear_gcode) elif args.canned_simplify and any((g.word in args.canned_codes) for g in effective_gcodes): (befores, (canned,), afters) = split_gcodes(effective_gcodes, GCodeCannedCycle) print(Comment('canning simplified: %r' % canned)) + # TODO: simplify canned things + print(str(line)) machine.process_block(line.block) From b7416036555fe4c7e08e029ed8e2d8fb16174a93 Mon Sep 17 00:00:00 2001 From: Peter Boin <nymphii@gmail.com> Date: Sun, 16 Jul 2017 02:06:06 +1000 Subject: [PATCH 25/32] test omit_redundant_modes --- tests/test_utils.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 tests/test_utils.py diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..fdd5114 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,33 @@ +import unittest +import re + +# Add relative pygcode to path +from testutils import add_pygcode_to_path, str_lines +add_pygcode_to_path() + +# Units under test +from pygcode.utils import omit_redundant_modes +from pygcode import text2gcodes, Line + +class UtilityTests(unittest.TestCase): + def test_omit_redundant_modes(self): + lines = [ + Line(line_str) + for line_str in re.split(r'\s*\n\s*', ''' + g1 x0 y0 ; yes + g1 x10 y-20 ; no + g0 x-3 y2 ; yes + g0 x0 y0 ; no + g0 x1 y1 ; no + g1 x20 y20 z5 ; yes + ''') + if line_str + ] + gcodes = [l.gcodes[0] for l in lines] + comments = [l.comment for l in lines] + for (i, g) in enumerate(omit_redundant_modes(gcodes)): + comment = comments[i].text if comments[i] else None + if comment == 'no': + self.assertIsNotNone(re.search(r'^\s', str(g))) + elif comment == 'yes': + self.assertIsNone(re.search(r'^\s', str(g))) From 9ca819d8c6978e4e44dd5d6d605ff9ad8b368902 Mon Sep 17 00:00:00 2001 From: Peter Boin <nymphii@gmail.com> Date: Tue, 18 Jul 2017 12:59:15 +1000 Subject: [PATCH 26/32] simplify canned drilling processes --- pygcode/gcodes.py | 86 +++++++++++++++++++++--- pygcode/transform.py | 126 ++++++++++++++++++++++++++++++++++- pygcode/utils.py | 2 +- scripts/pygcode-normalize.py | 29 ++++---- tests/test_gcodes.py | 52 +++++++++++++++ tests/test_words.py | 4 -- tests/testutils.py | 1 + 7 files changed, 272 insertions(+), 28 deletions(-) diff --git a/pygcode/gcodes.py b/pygcode/gcodes.py index d22748e..affba26 100644 --- a/pygcode/gcodes.py +++ b/pygcode/gcodes.py @@ -475,42 +475,42 @@ class GCodeDrillingCycle(GCodeCannedCycle): """G81: Drilling Cycle""" param_letters = GCodeCannedCycle.param_letters | set('RLP') word_key = Word('G', 81) - modal_param_letters = GCodeCannedCycle.param_letters | set('RLP') + modal_param_letters = GCodeCannedCycle.param_letters | set('RP') class GCodeDrillingCycleDwell(GCodeCannedCycle): """G82: Drilling Cycle, Dwell""" param_letters = GCodeCannedCycle.param_letters | set('RLP') word_key = Word('G', 82) - modal_param_letters = GCodeCannedCycle.param_letters | set('RLP') + modal_param_letters = GCodeCannedCycle.param_letters | set('RP') class GCodeDrillingCyclePeck(GCodeCannedCycle): """G83: Drilling Cycle, Peck""" param_letters = GCodeCannedCycle.param_letters | set('RLQ') word_key = Word('G', 83) - modal_param_letters = GCodeCannedCycle.param_letters | set('RLQ') + modal_param_letters = GCodeCannedCycle.param_letters | set('RQ') class GCodeDrillingCycleChipBreaking(GCodeCannedCycle): """G73: Drilling Cycle, ChipBreaking""" param_letters = GCodeCannedCycle.param_letters | set('RLQ') word_key = Word('G', 73) - modal_param_letters = GCodeCannedCycle.param_letters | set('RLQ') + modal_param_letters = GCodeCannedCycle.param_letters | set('RQ') class GCodeBoringCycleFeedOut(GCodeCannedCycle): """G85: Boring Cycle, Feed Out""" param_letters = GCodeCannedCycle.param_letters | set('RLP') word_key = Word('G', 85) - modal_param_letters = GCodeCannedCycle.param_letters | set('RLP') + modal_param_letters = GCodeCannedCycle.param_letters | set('RP') class GCodeBoringCycleDwellFeedOut(GCodeCannedCycle): """G89: Boring Cycle, Dwell, Feed Out""" param_letters = GCodeCannedCycle.param_letters | set('RLP') word_key = Word('G', 89) - modal_param_letters = GCodeCannedCycle.param_letters | set('RLP') + modal_param_letters = GCodeCannedCycle.param_letters | set('RP') class GCodeThreadingCycle(GCodeCannedCycle): @@ -895,8 +895,8 @@ class GCodePathBlendingMode(GCodePathControlMode): # ======================= Return Mode in Canned Cycles ======================= # CODE PARAMETERS DESCRIPTION -# G98 Canned Cycle Return Level - +# G98 Canned Cycle Return Level to previous +# G99 Canned Cycle Return to the level set by R class GCodeCannedReturnMode(GCode): modal_group = MODAL_GROUP_MAP['canned_cycles_return'] @@ -1444,3 +1444,73 @@ def split_gcodes(gcode_list, splitter_class, sort_list=True): split[2] = gcode_list[split_index+1:] return split + + +def _gcodes_abs2rel(start_pos, dist_mode=None, axes='XYZ'): + """ + Decorator to convert returned motion gcode coordinates to incremental. + Intended to be used internally (mainly because it's a little shonky) + Decorated function is only expected to return GCodeRapidMove or GCodeLinearMove + instances + :param start_pos: starting machine position (Position) + :param dist_mode: machine's distance mode (GCodeAbsoluteDistanceMode or GCodeIncrementalDistanceMode) + :param axes: axes machine accepts (set) + """ + # Usage: + # m = Machine() # defaults to absolute distance mode + # m.process_gcodes(GCodeRapidMove(X=10, Y=20, Z=3)) + # m.process_gcodes(GCodeIncrementalDistanceMode()) + # + # @_gcodes_abs2rel(start_pos=m.pos, dist_mode=m.mode.distance, axes=m.axes) + # def do_stuff(): + # yield GCodeRapidMove(X=0, Y=30, Z=3) + # yield GCodeLinearMove(X=0, Y=30, Z=-5) + # + # gcode_list = do_stuff() + # gocde_list[0] # == GCodeRapidMove(X=-10, Y=10) + # gocde_list[1] # == GCodeLinearMove(Z=-8) + + SUPPORTED_MOTIONS = ( + GCodeRapidMove, GCodeLinearMove, + ) + + def wrapper(func): + + def inner(*largs, **kwargs): + # Create Machine (with minimal information) + from .machine import Machine, Mode, Position + m = type('AbsoluteCoordMachine', (Machine,), { + 'MODE_CLASS': type('NullMode', (Mode,), {'default_mode': 'G90'}), + 'axes': axes, + })() + m.pos = start_pos + + for gcode in func(*largs, **kwargs): + # Verification & passthrough's + if not isinstance(gcode, GCode): + yield gcode # whatever this thing is + else: + # Process gcode + pos_from = m.pos + m.process_gcodes(gcode) + pos_to = m.pos + + if gcode.modal_group != MODAL_GROUP_MAP['motion']: + yield gcode # only deal with motion gcodes + continue + elif not isinstance(gcode, SUPPORTED_MOTIONS): + raise NotImplementedError("%r to iterative coords is not supported (this is only a very simple function)" % gcode) + + # Convert coordinates to iterative + rel_pos = pos_to - pos_from + coord_words = [w for w in rel_pos.words if w.value] + if coord_words: # else relative coords are all zero; do nothing + yield words2gcodes([gcode.word] + coord_words)[0].pop() + + + # Return apropriate function + if (dist_mode is None) or isinstance(dist_mode, GCodeIncrementalDistanceMode): + return inner + else: + return func # bypass decorator entirely; nothing to be done + return wrapper diff --git a/pygcode/transform.py b/pygcode/transform.py index 8467f78..10a415b 100644 --- a/pygcode/transform.py +++ b/pygcode/transform.py @@ -1,10 +1,14 @@ from math import sin, cos, tan, asin, acos, atan2, pi, sqrt, ceil -from .gcodes import GCodeLinearMove +from .gcodes import GCodeLinearMove, GCodeRapidMove from .gcodes import GCodeArcMove, GCodeArcMoveCW, GCodeArcMoveCCW from .gcodes import GCodePlaneSelect, GCodeSelectXYPlane, GCodeSelectYZPlane, GCodeSelectZXPlane from .gcodes import GCodeAbsoluteDistanceMode, GCodeIncrementalDistanceMode from .gcodes import GCodeAbsoluteArcDistanceMode, GCodeIncrementalArcDistanceMode +from .gcodes import GCodeCannedCycle +from .gcodes import GCodeDrillingCyclePeck, GCodeDrillingCycleDwell, GCodeDrillingCycleChipBreaking +from .gcodes import GCodeCannedReturnMode, GCodeCannedCycleReturnLevel, GCodeCannedCycleReturnToR +from .gcodes import _gcodes_abs2rel from .machine import Position from .exceptions import GCodeParameterError @@ -97,7 +101,7 @@ class ArcLinearizeMethod(object): self._outer_radius = self.get_outer_radius() return self._outer_radius - # Iter + # Vertex Generator def iter_vertices(self): """Yield absolute (<start vertex>, <end vertex>) for each line for the arc""" start_vertex = self.arc_p_start - self.arc_p_center @@ -214,6 +218,17 @@ DEFAULT_LA_ARCDISTMODE = GCodeIncrementalArcDistanceMode def linearize_arc(arc_gcode, start_pos, plane=None, method_class=None, dist_mode=None, arc_dist_mode=None, max_error=0.01, decimal_places=3): + """ + Convert a G2,G3 arc into a series of approsimation G1 codes + :param arc_gcode: arc gcode to approximate (GCodeArcMove) + :param start_pos: current machine position (Position) + :param plane: machine's active plane (GCodePlaneSelect) + :param method_class: method of linear approximation (ArcLinearizeMethod) + :param dist_mode: machine's distance mode (GCodeAbsoluteDistanceMode or GCodeIncrementalDistanceMode) + :param arc_dist_mode: machine's arc distance mode (GCodeAbsoluteArcDistanceMode or GCodeIncrementalArcDistanceMode) + :param max_error: maximum distance approximation arcs can stray from original arc (float) + :param decimal_places: number of decimal places gocde will be rounded to, used to mitigate risks of accumulated eror when in incremental distance mode (int) + """ # set defaults if method_class is None: method_class = DEFAULT_LA_method_class @@ -362,4 +377,109 @@ def linearize_arc(arc_gcode, start_pos, plane=None, method_class=None, cur_pos += l_delta # mitigate errors by also adding them the accumulated cur_pos -# ==================== Arc Precision Adjustment ==================== +# ==================== Un-Canning ==================== + +DEFAULT_SCC_PLANE = GCodeSelectXYPlane +DEFAULT_SCC_DISTMODE = GCodeAbsoluteDistanceMode +DEFAULT_SCC_RETRACTMODE = GCodeCannedCycleReturnLevel + +def simplify_canned_cycle(canned_gcode, start_pos, + plane=None, dist_mode=None, retract_mode=None, + axes='XYZ'): + """ + Simplify canned cycle into it's basic linear components + :param canned_gcode: canned gcode to be simplified (GCodeCannedCycle) + :param start_pos: current machine position (Position) + :param plane: machine's active plane (GCodePlaneSelect) + :param dist_mode: machine's distance mode (GCodeAbsoluteDistanceMode or GCodeIncrementalDistanceMode) + :param axes: axes machine accepts (set) + """ + + # set defaults + if plane is None: + plane = DEFAULT_SCC_PLANE() + if dist_mode is None: + dist_mode = DEFAULT_SCC_DISTMODE() + if retract_mode is None: + retract_mode = DEFAULT_SCC_RETRACTMODE() + + # Parameter Type Assertions + assert isinstance(canned_gcode, GCodeCannedCycle), "bad canned_gcode type: %r" % canned_gcode + assert isinstance(start_pos, Position), "bad start_pos type: %r" % start_pos + assert isinstance(plane, GCodePlaneSelect), "bad plane type: %r" % plane + assert isinstance(dist_mode, (GCodeAbsoluteDistanceMode, GCodeIncrementalDistanceMode)), "bad dist_mode type: %r" % dist_mode + assert isinstance(retract_mode, GCodeCannedReturnMode), "bad retract_mode type: %r" % retract_mode + + # TODO: implement for planes other than XY + if not isinstance(plane, GCodeSelectXYPlane): + raise NotImplementedError("simplifying canned cycles for planes other than X/Y has not been implemented") + + @_gcodes_abs2rel(start_pos=start_pos, dist_mode=dist_mode, axes=axes) + def inner(): + cycle_count = 1 if (canned_gcode.L is None) else canned_gcode.L + cur_hole_p_axis = start_pos.vector + for i in range(cycle_count): + # Calculate Depths + if isinstance(dist_mode, GCodeAbsoluteDistanceMode): + retract_depth = canned_gcode.R + drill_depth = canned_gcode.Z + cur_hole_p_axis = Vector3(x=canned_gcode.X, y=canned_gcode.Y) + else: # incremental + retract_depth = start_pos.Z + canned_gcode.R + drill_depth = retract_depth + canned_gcode.Z + cur_hole_p_axis += Vector3(x=canned_gcode.X, y=canned_gcode.Y) + + if retract_depth < drill_depth: + raise NotImplementedError("drilling upward is not supported") + + if isinstance(retract_mode, GCodeCannedCycleReturnToR): + final_depth = retract_depth + else: + final_depth = start_pos.Z + + # Move above hole (height of retract_depth) + if retract_depth > start_pos.Z: + yield GCodeRapidMove(Z=retract_depth) + yield GCodeRapidMove(X=cur_hole_p_axis.x, Y=cur_hole_p_axis.y) + if retract_depth < start_pos.Z: + yield GCodeRapidMove(Z=retract_depth) + + # Drill hole + delta = drill_depth - retract_depth # full depth + if isinstance(canned_gcode, (GCodeDrillingCyclePeck, GCodeDrillingCycleChipBreaking)): + delta = -abs(canned_gcode.Q) + + cur_depth = retract_depth + last_depth = cur_depth + while True: + # Determine new depth + cur_depth += delta + if cur_depth < drill_depth: + cur_depth = drill_depth + + # Rapid to just above, then slowly drill through delta + just_above_base = last_depth + 0.1 + if just_above_base < retract_depth: + yield GCodeRapidMove(Z=just_above_base) + yield GCodeLinearMove(Z=cur_depth) + if cur_depth <= drill_depth: + break # loop stops at the bottom of the hole + else: + # back up + if isinstance(canned_gcode, GCodeDrillingCycleChipBreaking): + # retract "a bit" + yield GCodeRapidMove(Z=cur_depth + 0.5) # TODO: configurable retraction + else: + # default behaviour: GCodeDrillingCyclePeck + yield GCodeRapidMove(Z=retract_depth) + + last_depth = cur_depth + + # Dwell + if isinstance(canned_gcode, GCodeDrillingCycleDwell): + yield GCodeDwell(P=0.5) # TODO: configurable pause + + # Return + yield GCodeRapidMove(Z=final_depth) + + return inner() diff --git a/pygcode/utils.py b/pygcode/utils.py index 00df8ab..6c9216a 100644 --- a/pygcode/utils.py +++ b/pygcode/utils.py @@ -66,8 +66,8 @@ def plane_projection(vect, normal): n = normal.normalized() return vect - (n * vect.dot(n)) -# ==================== GCode Utilities ==================== +# ==================== GCode Utilities ==================== def omit_redundant_modes(gcode_iter): """ Replace redundant machine motion modes with whitespace, diff --git a/scripts/pygcode-normalize.py b/scripts/pygcode-normalize.py index 370796d..adecfd4 100755 --- a/scripts/pygcode-normalize.py +++ b/scripts/pygcode-normalize.py @@ -20,7 +20,7 @@ for pygcode_lib_type in ('installed_lib', 'relative_lib'): from pygcode import GCodeCannedCycle from pygcode import split_gcodes from pygcode import Comment - from pygcode.transform import linearize_arc + from pygcode.transform import linearize_arc, simplify_canned_cycle from pygcode.transform import ArcLinearizeInside, ArcLinearizeOutside, ArcLinearizeMid from pygcode.gcodes import _subclasses from pygcode.utils import omit_redundant_modes @@ -89,13 +89,13 @@ group = parser.add_argument_group( "interpolation (G1), and pauses (or 'dwells', G4)" ) group.add_argument( - '--canned_simplify', '-cs', dest='canned_simplify', + '--canned_expand', '-ce', dest='canned_expand', action='store_const', const=True, default=False, - help="Convert canned cycles into basic linear movements", + help="Expand canned cycles into basic linear movements, and pauses", ) group.add_argument( '---canned_codes', '-cc', dest='canned_codes', default=DEFAULT_CANNED_CODES, - help="List of canned gcodes to simplify, (default is '%s')" % DEFAULT_CANNED_CODES, + help="List of canned gcodes to expand, (default is '%s')" % DEFAULT_CANNED_CODES, ) #parser.add_argument( @@ -194,6 +194,7 @@ for line_str in args.infile[0].readlines(): if args.arc_linearize and any(isinstance(g, GCodeArcMove) for g in effective_gcodes): with split_and_process(effective_gcodes, GCodeArcMove, line.comment) as arc: + print(Comment("linearized arc: %r" % arc)) linearize_params = { 'arc_gcode': arc, 'start_pos': machine.pos, @@ -207,14 +208,18 @@ for line_str in args.infile[0].readlines(): for linear_gcode in omit_redundant_modes(linearize_arc(**linearize_params)): print(linear_gcode) - elif args.canned_simplify and any((g.word in args.canned_codes) for g in effective_gcodes): - (befores, (canned,), afters) = split_gcodes(effective_gcodes, GCodeCannedCycle) - print(Comment('canning simplified: %r' % canned)) - - # TODO: simplify canned things - - print(str(line)) - machine.process_block(line.block) + elif args.canned_expand and any((g.word in args.canned_codes) for g in effective_gcodes): + with split_and_process(effective_gcodes, GCodeCannedCycle, line.comment) as canned: + print(Comment("expanded: %r" % canned)) + simplify_canned_params = { + 'canned_gcode': canned, + 'start_pos': machine.pos, + 'plane': machine.mode.plane_selection, + 'dist_mode': machine.mode.distance, + 'axes': machine.axes, + } + for simplified_gcode in omit_redundant_modes(simplify_canned_cycle(**simplify_canned_params)): + print(simplified_gcode) else: print(str(line)) diff --git a/tests/test_gcodes.py b/tests/test_gcodes.py index 34e4a22..3da46a8 100644 --- a/tests/test_gcodes.py +++ b/tests/test_gcodes.py @@ -11,6 +11,7 @@ add_pygcode_to_path() # Units under test from pygcode import gcodes from pygcode import words +from pygcode import machine from pygcode.exceptions import GCodeWordStrError @@ -139,3 +140,54 @@ class GCodeSplitTests(unittest.TestCase): self.assertTrue(any(isinstance(g, gcodes.GCodeMotion) for g in split[0])) self.assertTrue(isinstance(split[1][0], gcodes.GCodeStartSpindle)) self.assertTrue(any(isinstance(g, gcodes.GCodeSpindleSpeed) for g in split[2])) + + +class GCodeAbsoluteToRelativeDecoratorTests(unittest.TestCase): + + def test_gcodes_abs2rel(self): + # setup gcode testlist + L = gcodes.GCodeLinearMove + R = gcodes.GCodeRapidMove + args = lambda x, y, z: dict(a for a in zip('XYZ', [x,y,z]) if a[1] is not None) + gcode_list = [ + # GCode instances Expected incremental output + (L(**args(0, 0, 0)), L(**args(-10, -20, -30))), + (L(**args(1, 2, 0)), L(**args(1, 2, None))), + (L(**args(3, 4, 0)), L(**args(2, 2, None))), + (R(**args(1, 2, 0)), R(**args(-2, -2, None))), + (R(**args(3, 4, 0)), R(**args(2, 2, None))), + (L(**args(3, 4, 0)), None), + (L(**args(3, 4, 8)), L(**args(None, None, 8))), + ] + + m = machine.Machine() + + # Incremental Output + m.set_mode(gcodes.GCodeAbsoluteDistanceMode()) + m.move_to(X=10, Y=20, Z=30) # initial position (absolute) + m.set_mode(gcodes.GCodeIncrementalDistanceMode()) + + @gcodes._gcodes_abs2rel(m.pos, dist_mode=m.mode.distance, axes=m.axes) + def expecting_rel(): + return [g[0] for g in gcode_list] + + trimmed_expecting_list = [x[1] for x in gcode_list if x[1] is not None] + for (i, g) in enumerate(expecting_rel()): + expected = trimmed_expecting_list[i] + self.assertEqual(type(g), type(expected)) + self.assertEqual(g.word, expected.word) + self.assertEqual(g.params, expected.params) + + # Absolute Output + m.set_mode(gcodes.GCodeAbsoluteDistanceMode()) + m.move_to(X=10, Y=20, Z=30) # initial position + + @gcodes._gcodes_abs2rel(m.pos, dist_mode=m.mode.distance, axes=m.axes) + def expecting_abs(): + return [g[0] for g in gcode_list] + + for (i, g) in enumerate(expecting_abs()): + expected = gcode_list[i][0] # expecting passthrough + self.assertEqual(type(g), type(expected)) + self.assertEqual(g.word, expected.word) + self.assertEqual(g.params, expected.params) diff --git a/tests/test_words.py b/tests/test_words.py index 5152f5d..b5482c1 100644 --- a/tests/test_words.py +++ b/tests/test_words.py @@ -1,7 +1,3 @@ -import sys -import os -import inspect - import unittest # Add relative pygcode to path diff --git a/tests/testutils.py b/tests/testutils.py index f6124aa..dcc494c 100644 --- a/tests/testutils.py +++ b/tests/testutils.py @@ -1,3 +1,4 @@ +# utilities for the testing suite (as opposed to the tests for utils.py) import sys import os import inspect From c9d141b1bf997e5bcd2d0b302ee1cc7e15d1d156 Mon Sep 17 00:00:00 2001 From: Peter Boin <nymphii@gmail.com> Date: Tue, 18 Jul 2017 13:04:44 +1000 Subject: [PATCH 27/32] removed file, it's not contributing anything --- pygcode/file.py | 28 ---------------------------- 1 file changed, 28 deletions(-) delete mode 100644 pygcode/file.py diff --git a/pygcode/file.py b/pygcode/file.py deleted file mode 100644 index fa9f395..0000000 --- a/pygcode/file.py +++ /dev/null @@ -1,28 +0,0 @@ -from .line import Line - -#from .machine import AbstractMachine - -class GCodeFile(object): - def __init__(self, filename=None): - self.filename = filename - - # Initialize - self.lines = [] - - def append(self, line): - assert isinstance(line, Line), "invalid line type" - self.lines.append(line) - - -class GCodeParser(object): - """Parse a gocde file""" - def __init__(self, filename): - self.filename = filename - self._fh = open(filename, 'r') - - def iterlines(self): - for line in self._fh.readlines(): - yield Line(line) - - def close(self): - self._fh.close() From ab276e3bd28a34643ed8f35c72f7de0717d6d613 Mon Sep 17 00:00:00 2001 From: Peter Boin <nymphii@gmail.com> Date: Tue, 18 Jul 2017 14:42:20 +1000 Subject: [PATCH 28/32] canned cycle processing --- pygcode/gcodes.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/pygcode/gcodes.py b/pygcode/gcodes.py index affba26..a11cc19 100644 --- a/pygcode/gcodes.py +++ b/pygcode/gcodes.py @@ -305,7 +305,7 @@ class GCode(object): :return: GCodeEffect instance; effect the gcode just had on machine """ from .machine import Machine # importing up (done live to avoid dependency loop) - assert isinstance(machine, Machine), "invalid parameter" + assert isinstance(machine, Machine), "invalid machine type: %r" % machine # Set mode self._process_mode(machine) @@ -400,11 +400,14 @@ class GCodeArcMoveCCW(GCodeArcMove): class GCodeDwell(GCodeMotion): """G4: Dwell""" - param_letters = GCodeMotion.param_letters | set('P') + param_letters = set('P') # doesn't accept axis parameters word_key = Word('G', 4) modal_group = None # one of the few motion commands that isn't modal exec_order = 140 + def _process(self, machine): + pass # no movements made + class GCodeCublcSpline(GCodeMotion): """G5: Cubic Spline""" @@ -470,6 +473,16 @@ class GCodeCannedCycle(GCode): modal_group = MODAL_GROUP_MAP['motion'] exec_order = 241 + def _process(self, machine): + moveto_coords = self.get_param_dict(letters=machine.axes) + if isinstance(machine.mode., GCodeCannedCycleReturnToR): + # canned return is to this.R, not this.Z (plane dependent) + moveto_coords.update({ + machine.mode.plane_selection.normal_axis: this.R, + }) + + machine.mode_to(**moveto_coords) + class GCodeDrillingCycle(GCodeCannedCycle): """G81: Drilling Cycle""" @@ -782,6 +795,7 @@ class GCodePlaneSelect(GCode): # -- Plane Normal # Vector normal to plane (such that XYZ axes follow the right-hand rule) + normal_axis = None # Letter of normal axis (upper case) normal = None # Vector3 @@ -789,6 +803,7 @@ class GCodeSelectXYPlane(GCodePlaneSelect): """G17: select XY plane (default)""" word_key = Word('G', 17) quat = Quaternion() # no effect + normal_axis = 'Z' normal = Vector3(0., 0., 1.) @@ -799,6 +814,7 @@ class GCodeSelectZXPlane(GCodePlaneSelect): Vector3(1., 0., 0.), Vector3(0., 1., 0.), Vector3(0., 0., 1.), Vector3(1., 0., 0.) ) + normal_axis = 'Y' normal = Vector3(0., 1., 0.) @@ -809,6 +825,7 @@ class GCodeSelectYZPlane(GCodePlaneSelect): Vector3(1., 0., 0.), Vector3(0., 1., 0.), Vector3(0., 1., 0.), Vector3(0., 0., 1.) ) + normal_axis = 'X' normal = Vector3(1., 0., 0.) @@ -1449,9 +1466,9 @@ def split_gcodes(gcode_list, splitter_class, sort_list=True): def _gcodes_abs2rel(start_pos, dist_mode=None, axes='XYZ'): """ Decorator to convert returned motion gcode coordinates to incremental. - Intended to be used internally (mainly because it's a little shonky) - Decorated function is only expected to return GCodeRapidMove or GCodeLinearMove - instances + Intended to be used internally (mainly because it's a little shonky). + Function being decorated is only expected to return GCodeRapidMove or + GCodeLinearMove instances. :param start_pos: starting machine position (Position) :param dist_mode: machine's distance mode (GCodeAbsoluteDistanceMode or GCodeIncrementalDistanceMode) :param axes: axes machine accepts (set) From dcdacd68c092e51a033e14e04ca21484b300c898 Mon Sep 17 00:00:00 2001 From: Peter Boin <nymphii@gmail.com> Date: Tue, 18 Jul 2017 14:43:49 +1000 Subject: [PATCH 29/32] derps --- pygcode/gcodes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pygcode/gcodes.py b/pygcode/gcodes.py index a11cc19..4d342ba 100644 --- a/pygcode/gcodes.py +++ b/pygcode/gcodes.py @@ -475,13 +475,13 @@ class GCodeCannedCycle(GCode): def _process(self, machine): moveto_coords = self.get_param_dict(letters=machine.axes) - if isinstance(machine.mode., GCodeCannedCycleReturnToR): + if isinstance(machine.mode.canned_cycles_return, GCodeCannedCycleReturnToR): # canned return is to this.R, not this.Z (plane dependent) moveto_coords.update({ machine.mode.plane_selection.normal_axis: this.R, }) - machine.mode_to(**moveto_coords) + machine.move_to(**moveto_coords) class GCodeDrillingCycle(GCodeCannedCycle): From 90d979bb05a2e3ef66cf8acf3813d468df83c78c Mon Sep 17 00:00:00 2001 From: Peter Boin <nymphii@gmail.com> Date: Tue, 18 Jul 2017 15:05:15 +1000 Subject: [PATCH 30/32] preparing for release --- setup.cfg | 2 ++ setup.py | 13 +++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 setup.cfg create mode 100644 setup.py diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..b88034e --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[metadata] +description-file = README.md diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..b54a1f3 --- /dev/null +++ b/setup.py @@ -0,0 +1,13 @@ +from distutils.core import setup +setup( + name = 'pygcode', + packages = ['pygcode'], + version = '0.1', + description = 'basic g-code parser, interpreter, and writer library', + author = 'Peter Boin', + author_email = 'peter.boin@gmail.com', + url = 'https://github.com/fragmuffin/pygcode', + download_url = 'https://github.com/fragmuffin/pygcode/archive/0.1.tar.gz', + keywords = ['gcode', 'cnc', 'parser', 'interpreter'], + classifiers = [], +) From 512a5ac31110e3ceb05ac03f737d12b6c2e1f24a Mon Sep 17 00:00:00 2001 From: Peter Boin <nymphii@gmail.com> Date: Tue, 18 Jul 2017 17:58:34 +1000 Subject: [PATCH 31/32] moved to src folder --- setup.cfg | 4 + setup.py | 109 ++++++++++++++++++++++--- {pygcode => src/pygcode}/__init__.py | 0 {pygcode => src/pygcode}/block.py | 0 {pygcode => src/pygcode}/comment.py | 0 {pygcode => src/pygcode}/exceptions.py | 0 {pygcode => src/pygcode}/gcodes.py | 0 {pygcode => src/pygcode}/line.py | 0 {pygcode => src/pygcode}/machine.py | 0 {pygcode => src/pygcode}/transform.py | 0 {pygcode => src/pygcode}/utils.py | 0 {pygcode => src/pygcode}/words.py | 0 12 files changed, 100 insertions(+), 13 deletions(-) rename {pygcode => src/pygcode}/__init__.py (100%) rename {pygcode => src/pygcode}/block.py (100%) rename {pygcode => src/pygcode}/comment.py (100%) rename {pygcode => src/pygcode}/exceptions.py (100%) rename {pygcode => src/pygcode}/gcodes.py (100%) rename {pygcode => src/pygcode}/line.py (100%) rename {pygcode => src/pygcode}/machine.py (100%) rename {pygcode => src/pygcode}/transform.py (100%) rename {pygcode => src/pygcode}/utils.py (100%) rename {pygcode => src/pygcode}/words.py (100%) diff --git a/setup.cfg b/setup.cfg index b88034e..f98a093 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,6 @@ +[bdist_wheel] +universal = 1 + [metadata] description-file = README.md +license_file = LICENSE diff --git a/setup.py b/setup.py index b54a1f3..277d36e 100644 --- a/setup.py +++ b/setup.py @@ -1,13 +1,96 @@ -from distutils.core import setup -setup( - name = 'pygcode', - packages = ['pygcode'], - version = '0.1', - description = 'basic g-code parser, interpreter, and writer library', - author = 'Peter Boin', - author_email = 'peter.boin@gmail.com', - url = 'https://github.com/fragmuffin/pygcode', - download_url = 'https://github.com/fragmuffin/pygcode/archive/0.1.tar.gz', - keywords = ['gcode', 'cnc', 'parser', 'interpreter'], - classifiers = [], -) +import codecs +import os +import re + +from setuptools import setup, find_packages + + +################################################################### + +NAME = "attrs" +PACKAGES = find_packages(where="src") +META_PATH = os.path.join("src", "attr", "__init__.py") +KEYWORDS = ["class", "attribute", "boilerplate"] +CLASSIFIERS = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Natural Language :: English", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Topic :: Software Development :: Libraries :: Python Modules", +] +INSTALL_REQUIRES = [] + +################################################################### + +HERE = os.path.abspath(os.path.dirname(__file__)) + + +def read(*parts): + """ + Build an absolute path from *parts* and and return the contents of the + resulting file. Assume UTF-8 encoding. + """ + with codecs.open(os.path.join(HERE, *parts), "rb", "utf-8") as f: + return f.read() + + +META_FILE = read(META_PATH) + + +def find_meta(meta): + """ + Extract __*meta*__ from META_FILE. + """ + meta_match = re.search( + r"^(?P<name>__{meta}__)\s*=\s*['\"](?P<value>[^'\"]*)['\"]".format(meta=meta), + META_FILE, re.M + ) + if meta_match: + return meta_match.group('value') + raise RuntimeError("Unable to find __{meta}__ string.".format(meta=meta)) + + +if __name__ == "__main__": + setup( + name=NAME, + description=find_meta("description"), + license=find_meta("license"), + url=find_meta("uri"), + version=find_meta("version"), + author=find_meta("author"), + author_email=find_meta("email"), + maintainer=find_meta("author"), + maintainer_email=find_meta("email"), + keywords=KEYWORDS, + long_description=read("README.rst"), + packages=PACKAGES, + package_dir={"": "src"}, + zip_safe=False, + classifiers=CLASSIFIERS, + install_requires=INSTALL_REQUIRES, + ) + +#VERSION = '0.1.dev' # *.dev = release candidate +# +#setup( +# name = 'pygcode', +# packages = ['pygcode'], +# version = VERSION, +# description = 'basic g-code parser, interpreter, and writer library', +# author = 'Peter Boin', +# author_email = 'peter.boin@gmail.com', +# url = 'https://github.com/fragmuffin/pygcode', +# download_url = 'https://github.com/fragmuffin/pygcode/archive/%s.tar.gz' % VERSION, +# keywords = ['gcode', 'cnc', 'parser', 'interpreter'], +# classifiers = [], +#) diff --git a/pygcode/__init__.py b/src/pygcode/__init__.py similarity index 100% rename from pygcode/__init__.py rename to src/pygcode/__init__.py diff --git a/pygcode/block.py b/src/pygcode/block.py similarity index 100% rename from pygcode/block.py rename to src/pygcode/block.py diff --git a/pygcode/comment.py b/src/pygcode/comment.py similarity index 100% rename from pygcode/comment.py rename to src/pygcode/comment.py diff --git a/pygcode/exceptions.py b/src/pygcode/exceptions.py similarity index 100% rename from pygcode/exceptions.py rename to src/pygcode/exceptions.py diff --git a/pygcode/gcodes.py b/src/pygcode/gcodes.py similarity index 100% rename from pygcode/gcodes.py rename to src/pygcode/gcodes.py diff --git a/pygcode/line.py b/src/pygcode/line.py similarity index 100% rename from pygcode/line.py rename to src/pygcode/line.py diff --git a/pygcode/machine.py b/src/pygcode/machine.py similarity index 100% rename from pygcode/machine.py rename to src/pygcode/machine.py diff --git a/pygcode/transform.py b/src/pygcode/transform.py similarity index 100% rename from pygcode/transform.py rename to src/pygcode/transform.py diff --git a/pygcode/utils.py b/src/pygcode/utils.py similarity index 100% rename from pygcode/utils.py rename to src/pygcode/utils.py diff --git a/pygcode/words.py b/src/pygcode/words.py similarity index 100% rename from pygcode/words.py rename to src/pygcode/words.py From 63744fee4005019360bf405e399bc899467d39b7 Mon Sep 17 00:00:00 2001 From: Peter Boin <nymphii@gmail.com> Date: Tue, 18 Jul 2017 18:28:19 +1000 Subject: [PATCH 32/32] converted readme to rst --- README.md => README.rst | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) rename README.md => README.rst (79%) diff --git a/README.md b/README.rst similarity index 79% rename from README.md rename to README.rst index ca8ab5b..af72bfd 100644 --- a/README.md +++ b/README.rst @@ -1,19 +1,27 @@ -# pygcode +pygcode +======= + GCODE Parser for Python -Currently in development, this is planned to be a pythonic interpreter and encoder for g-code. -I'll be learning along the way, but the plan is to follow the lead of [GRBL](https://github.com/gnea/grbl). +Currently in development, this is planned to be a pythonic interpreter +and encoder for g-code. I'll be learning along the way, but the plan is +to follow the lead of `GRBL <https://github.com/gnea/grbl>`__. -## Installation +Installation +------------ -`pip install pygcode` +``pip install pygcode`` -FIXME: well, that's the plan... give me some time to get it going though. +FIXME: well, that's the plan... give me some time to get it going +though. -## Usage +Usage +----- Just brainstorming here... +:: + import pygcode import math import euclid @@ -64,10 +72,15 @@ Just brainstorming here... gfile_in.close() gfile_out.close() +Supported G-Codes +----------------- -## Supported G-Codes -GCode support is planned to follow that of [GRBL](https://github.com/gnea/grbl) which follows [LinuxCNC](http://linuxcnc.org) (list of gcodes documented [here](http://linuxcnc.org/docs/html/gcode.html)). +GCode support is planned to follow that of +`GRBL <https://github.com/gnea/grbl>`__ which follows +`LinuxCNC <http://linuxcnc.org>`__ (list of gcodes documented +`here <http://linuxcnc.org/docs/html/gcode.html>`__). -But anything pre v1.0 will be a sub-set, focusing on the issues I'm having... I'm selfish that way. +But anything pre v1.0 will be a sub-set, focusing on the issues I'm +having... I'm selfish that way. TBD: list of gcodes (also as a TODO list)