diff --git a/PythonicGcodeMachine/Gcode/Rs274/Ast.py b/PythonicGcodeMachine/Gcode/Rs274/Ast.py index 4ef47023eccf2bdc4f5aaaf9d499b4506a343c34..f333543a73d18b9a2446ec16d32f0c4ef2b5c6d9 100644 --- a/PythonicGcodeMachine/Gcode/Rs274/Ast.py +++ b/PythonicGcodeMachine/Gcode/Rs274/Ast.py @@ -19,6 +19,8 @@ #################################################################################################### """Module to implement an AST for RS-274 G-code. + +All classes are clonable. """ #################################################################################################### @@ -64,6 +66,7 @@ __all__ = [ #################################################################################################### import math +import re import colors @@ -84,6 +87,8 @@ class Program: str(program) + program2 = program.clone() + """ ############################################## @@ -93,6 +98,16 @@ class Program: ############################################## + def clone(self): + + program = self.__class__() + for line in self: + program += line.clone() + + return program + + ############################################## + def push(self, line): self._lines.append(line) @@ -134,7 +149,42 @@ class Program: #################################################################################################### -class LineItem: +class CloneMixin: + + @staticmethod + def _clone_value(value): + if hasattr(value, 'clone'): + return value.clone() + else: + return value + +#################################################################################################### + +class LineItem(CloneMixin): + + ############################################## + + def _check_value(self, value): + + if (isinstance(value, (int, float)) or + isinstance(value, RealValue)): + return value + else: + try: + str_value = str(value) + except: + raise ValueError("Invalid value {}".format(value)) + # Fixme: + from .Parser import GcodeParser, GcodeParserError + parser = GcodeParser() + try: + # Fixme: parser hack + ast = parser.parse('X' + value) + return ast[0].value + except GcodeParserError: + raise ValueError("Invalid G-code value {}".format(value)) + + ############################################## def ansi_str(self): return str(self) @@ -162,11 +212,13 @@ class Line: line += Comment('move') line += Word('X', 10) line += Comment('Y value') - line += Word('Y', 20) + line += Word('Y', 20.) line += ParameterSetting('1', 1.2) - # using expression + # using expression, AST way line += Word('Z', Addition(30, Multiply(Parameter(100), Cosine(30)))) + # string way + line += Word('Z', '[30 + [#100 * cos[30]]]') # Array interface for item in line: @@ -175,6 +227,18 @@ class Line: str(line) print(line.ansi_str()) # use ANSI colors, see Line.ANSI_... attributes + a_line = line.clone() + + Values can be passed as: + + * int or float, + * AST for expression, + * any object that "str" evaluate to a valid G-code expression. + + As a shortcut, a G/M-code operation can be passed as string:: + + line += 'G0' + Expression can be evaluated using :code:`float(obj.value)`, excepted when we must access a parameter value. @@ -200,6 +264,16 @@ class Line: ############################################## + def clone(self): + + line = self.__class__(self._deleted, self._line_number, self._comment) + for item in self: + line += item.clone() + + return line + + ############################################## + @property def deleted(self): return self._deleted @@ -233,13 +307,26 @@ class Line: ############################################## + def _push_item(self, item): + if not isinstance(item, LineItem): + item = Word.from_str(item) + # Fixme: try to parse ??? + self._items.append(item) + + def push_items(self, iterable): + """Method to push an iterable""" + for item in iterable: + self.push(item) + def push(self, item): - if isinstance(item, LineItem): - self._items.append(item) + """Method to push a valid item, a 'G/Mxxx' shortcut string, a list or tuple""" + if isinstance(item, (list, tuple, Line)): + self.push_items(item) else: - raise ValueError + self._push_item(item) def __iadd__(self, item): + """push shortcut""" self.push(item) return self @@ -273,6 +360,24 @@ class Line: ############################################## + def toggle(self): + """Toggle deleted flag""" + self._deleted = not self._deleted + + def remove_line_number(self): + self._line_number = None + + ############################################## + + def remove_comment(self): + self._comment = None + for i, item in enumerate(self): + if isinstance(item, Comment): + # self._items.pop(i) + del self._items[i] + + ############################################## + def __repr__(self): items = [] @@ -328,6 +433,11 @@ class Comment(LineItem): ############################################## + def clone(self): + return self.__class__(self._text) + + ############################################## + def set(self, text): if '(' in text: raise ValueError('Comment cannot contains a "("') @@ -367,6 +477,20 @@ class Word(LineItem): 'X', 'Y', 'Z', ) + WORD_RE = re.compile('(G|M)(\d+)') + + ############################################## + + @classmethod + def from_str(cls, obj): + + str_obj = str(obj) + match = cls.WORD_RE.match(str_obj) + if match is not None: + return cls(*match.groups()) + else: + raise ValueError(obj) + ############################################## def __init__(self, letter, value): @@ -375,6 +499,11 @@ class Word(LineItem): ############################################## + def clone(self): + return self.__class__(self._letter, self._clone_value(self._value)) + + ############################################## + @property def letter(self): return self._letter @@ -392,13 +521,12 @@ class Word(LineItem): @value.setter def value(self, value): - # float expression ... - self._value = value + self._value = self._check_value(value) ############################################## def __repr__(self): - return 'Word({0._letter} {0._value})'.format(self) + return 'Word({0._letter}, {0._value})'.format(self) def __str__(self): return '{0._letter}{0._value}'.format(self) @@ -412,7 +540,7 @@ class Word(LineItem): #################################################################################################### -class RealValue: +class RealValue(CloneMixin): pass #################################################################################################### @@ -426,6 +554,11 @@ class ParameterMixin: ############################################## + def clone(self): + return self.__class__(self._parameter) + + ############################################## + @property def parameter(self): return self._parameter @@ -450,6 +583,11 @@ class ParameterSetting(LineItem, ParameterMixin): ParameterMixin.__init__(self, parameter) self.value = value + ############################################## + + def clone(self): + return self.__class__(self._parameter, self._clone_value(self._value)) + ############################################## @property def value(self): @@ -457,8 +595,7 @@ class ParameterSetting(LineItem, ParameterMixin): @value.setter def value(self, value): - # float expression ... - self._value = value + self._value = self._check_value(value) ############################################## @@ -509,6 +646,11 @@ class UnaryOperation(RealValue): ############################################## + def clone(self): + return self.__class__(self._clone_value(self._arg)) + + ############################################## + @property def arg(self): return self._arg @@ -601,6 +743,11 @@ class BinaryOperation(RealValue): ############################################## + def clone(self): + return self.__class__(self._clone_value(self._arg1), self._clone_value(self.arg2)) + + ############################################## + @property def arg1(self): return self._arg1 diff --git a/examples/gcode/annotate-gcode.py b/examples/gcode/annotate-gcode.py index 9869a7bd4c541e2af689f5e0eee8facc23ed1c04..12d1fbc47d70d20405a9512e96838a721b903174 100644 --- a/examples/gcode/annotate-gcode.py +++ b/examples/gcode/annotate-gcode.py @@ -20,11 +20,15 @@ #################################################################################################### +#r# =========================== +#r# Annotate a G-code program +#r# =========================== #r# -#r# ================================================== -#r# Example to show how to annotate a G-code program -#r# ================================================== +#r# For API see #r# +#r# * :mod:`PythonicGcodeMachine.Gcode.Rs274` +#r# * :mod:`PythonicGcodeMachine.Gcode.Rs274.Ast` +#r# * :mod:`PythonicGcodeMachine.Gcode.Rs274.Parser` #################################################################################################### diff --git a/examples/gcode/generate-gcode.py b/examples/gcode/generate-gcode.py new file mode 100644 index 0000000000000000000000000000000000000000..2b5743e34ea9f2d0b2700e20807580dadeb310d1 --- /dev/null +++ b/examples/gcode/generate-gcode.py @@ -0,0 +1,146 @@ +#?################################################################################################## +#?# +#?# PythonicGcodeMachine - @licence_header_description@ +#?# Copyright (C) 2018 Fabrice Salvaire +#?# +#?# This program is free software: you can redistribute it and/or modify +#?# it under the terms of the GNU General Public License as published by +#?# the Free Software Foundation, either version 3 of the License, or +#?# (at your option) any later version. +#?# +#?# This program is distributed in the hope that it will be useful, +#?# but WITHOUT ANY WARRANTY; without even the implied warranty of +#?# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +#?# GNU General Public License for more details. +#?# +#?# You should have received a copy of the GNU General Public License +#?# along with this program. If not, see . +#?# +#?################################################################################################## + +#################################################################################################### + +#r# =========================== +#r# Generate a G-code program +#r# =========================== +#r# +#r# For API see +#r# +#r# * :mod:`PythonicGcodeMachine.Gcode.Rs274` +#r# * :mod:`PythonicGcodeMachine.Gcode.Rs274.Ast` +#r# * :mod:`PythonicGcodeMachine.Gcode.Rs274.Parser` + +#################################################################################################### + +from PythonicGcodeMachine.Gcode.Rs274 import * +from PythonicGcodeMachine.Gcode.Rs274.Ast import * + +#################################################################################################### + +#r# Create a G-code line (block) using AST API + +line = Line(deleted=False, line_number=1, comment='a G-code block') + +# Push some items +# Note: order doesn't matter, see RS-274 for details +line += Word('G', 0) +line += Comment('fast move') +line += Word('X', 10) +line += Word('Y', 20) + +print(line) +#o# + +#r# More simpler way to pass G/M-code + +a_line = Line() +a_line += 'G0' +a_line += Word('X', 10) +print(a_line) +#o# + +#r# Using the G-code parser + +parser = GcodeParser() + +a_line = parser.parse('G0 X0 Y0') +a_line += Word('Z', 0) +print(a_line) +#o# + +a_line = Line() +a_line += 'G0' + +parsed_line = parser.parse('X1 Y2') +print(list(parsed_line)) +first_item = parsed_line[0] +print(first_item) +a_line += parsed_line + +print(a_line) +#o# + +#r# Expression : the AST way + +line2 = line.clone() +line2 += Word('Z', Addition(30, Multiply(Parameter(100), Cosine(30)))) +print(line2) +#o# + +#r# Expression : the literal way + +line3 = line.clone() +line3 += Word('Z', '[30 + [#100 * cos[30]]]') +print(line3) +#o# + +#r# Invalid expression + +try: + line4 = line.clone() + line4 += Word('Z', '1 + 2]') + print(line4) +except ValueError: + pass + +#r# Create a G-code program + +program = Program() + +program += line + +line2.line_number = 2 +line2.comment = 'using expression' +program += line2 + +line3.deleted = True +line3.line_number = 3 +line3.comment = None +program += line3 + +print(program) +#o# + +#r# Line cleanup tools + +line = Line(deleted=False, line_number=1, comment='a G-code block') +line += 'G0' +line += Comment('fast move') +line += Word('X', 10) +line += Word('Y', 20) + +print(line) +#o# + +line.toggle() +print(line) +#o# + +line.toggle() +line.remove_line_number() +print(line) +#o# + +line.remove_comment() +print(line) +#o#