diff --git a/PythonicGcodeMachine/Gcode/Rs274/Ast.py b/PythonicGcodeMachine/Gcode/Rs274/Ast.py new file mode 100644 index 0000000000000000000000000000000000000000..c1728ecb59c38a4a38babda8772e553c9f363941 --- /dev/null +++ b/PythonicGcodeMachine/Gcode/Rs274/Ast.py @@ -0,0 +1,576 @@ +#################################################################################################### +# +# 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 . +# +#################################################################################################### + +"""Module to implement an AST for RS-274 G-code. +""" + +#################################################################################################### + +__all__ = [ + 'Program', + 'LineItem', + 'Line', + 'Comment', + 'Word', + 'RealValue', + 'ParameterMixin', + 'ParameterSetting', + 'Parameter', + 'UnaryOperation', + 'AbsoluteValue', + 'ArcCosine', + 'ArcSine', + 'ArcTangent', + 'Cosine', + 'ERaisedTo', + 'FixDown', + 'FixUp', + 'NaturalLogOf', + 'Round', + 'Sine', + 'SquareRoot', + 'Tangent', + 'BinaryOperation', + 'Power', + 'DividedBy', + 'Modulo', + 'Times', + 'And', + 'ExclusiveOr', + 'Minus', + 'NonExclusiveOr', + 'Plus', +] + +#################################################################################################### + +import math + +#################################################################################################### + +class Program: + + """Class to implement a G-code program + + Usage:: + + program = Program() + program += line + str(program) + + """ + + ############################################## + + def __init__(self): + self._lines = [] + + ############################################## + + def push(self, line): + self._lines.append(line) + + def __iadd__(self, item): + self.push(item) + return self + + ############################################## + + def __len__(self): + return len(self._lines) + + def __iter__(self): + return iter(self._lines) + + def __getitem__(self, _slice): + return self._lines[_slice] + + def iter_on_not_deleted(self): + for line in self._lines: + if line: + yield line + + ############################################## + + def __repr__(self): + + text = 'Program(\n' + for line in self: + text += repr(line) + '\n' + text += ')\n' + + return text + + ############################################## + + def __str__(self): + return '\n'.join(map(str, self)) + +#################################################################################################### + +class LineItem: + pass + +#################################################################################################### + +class Line: + + """Class to implement a G-code line + + Usage:: + + line = Line(def=False, line_number=1, comment='a comment') + line += Word('G', 0) + line += Comment('move') + line += Word('X', 10) + line += Comment('Y value') + line += Word('Y', 20) + line += ParameterSetting('1', 1.2) + str(line) + + """ + + ############################################## + + def __init__(self, deleted=False, line_number=None, comment=None): + + self.deleted = deleted + self.line_number = line_number + self.comment = comment + + self._items = [] + + ############################################## + + @property + def deleted(self): + return self._deleted + + @deleted.setter + def deleted(self, value): + self._deleted = bool(value) + + @property + def line_number(self): + return self._line_number + + @line_number.setter + def line_number(self, value): + if value is not None: + value = float(value) + if value.is_integer(): + value = int(value) + self._line_number = value + + @property + def comment(self): + return self._comment + + @comment.setter + def comment(self, value): + if value is not None: + self._comment = str(value) + else: + self._comment = None + + ############################################## + + def push(self, item): + if isinstance(item, LineItem): + self._items.append(item) + else: + raise ValueError + + def __iadd__(self, item): + self.push(item) + return self + + ############################################## + + def __bool__(self): + return not self._deleted + + ############################################## + + def __len__(self): + return len(self._items) + + def __iter__(self): + return iter(self._items) + + def __getitem__(self, _slice): + return self._items[_slice] + + ############################################## + + def __repr__(self): + + items = [] + if not self: + items.append('Deleted') + items += list(map(repr, self)) + if self._comment: + items.append(self._comment) + + return 'Line{}'.format(self, items) + + ############################################## + + def __str__(self): + + line = '' + if not self: + line += r'\ ' + if self._line_number: + line += 'N{} '.format(self._line_number) + line += ' '.join(map(str, self)) + if self._comment: + line += ' ; ' + self._comment + + return line + +#################################################################################################### + +class Comment(LineItem): + + ############################################## + + def __init__(self, text): + self.set(text) + + ############################################## + + def set(self, text): + if '(' in text: + raise ValueError('Comment cannot contains a "("') + self._text = str(text) + + ############################################## + + @property + def text(self): + return self._text + + @text.setter + def text(self, value): + self.set(value) + + ############################################## + + def __repr__(self): + return 'Comment({0._text})'.format(self) + + def __str__(self): + return '({0._text})'.format(self) + +#################################################################################################### + +class Word(LineItem): + + LETTERS = ( + 'A', 'B', 'C', 'D', + 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', # 'N', + 'P', 'Q', 'R', 'S', 'T', + 'X', 'Y', 'Z', + ) + + ############################################## + + def __init__(self, letter, value): + self.letter = letter + self.value = value + + ############################################## + + @property + def letter(self): + return self._letter + + @letter.setter + def letter(self, value): + value = str(value).upper() + if value not in self.LETTERS: + raise ValueError + self._letter = value + + @property + def value(self): + return self._value + + @value.setter + def value(self, value): + # float expression ... + self._value = value + + ############################################## + + def __repr__(self): + return 'Word({0._letter} {0._value})'.format(self) + + def __str__(self): + return '{0._letter}{0._value}'.format(self) + +#################################################################################################### + +class RealValue: + pass + +#################################################################################################### + +class ParameterMixin: + + ############################################## + + def __init__(self, parameter): + self.parameter = parameter + + ############################################## + + @property + def parameter(self): + return self._parameter + + @parameter.setter + def parameter(self, value): + try: + value = int(value) + except ValueError: + value = str(value) + self._parameter = value + +#################################################################################################### + +class ParameterSetting(LineItem, ParameterMixin): + + ############################################## + + def __init__(self, parameter, value): + ParameterMixin.__init__(self, parameter) + self.value = value + + ############################################## + @property + def value(self): + return self._value + + @value.setter + def value(self, value): + # float expression ... + self._value = value + + ############################################## + + def __repr__(self): + return 'ParameterSetting({0._parameter} = {0._value})'.format(self) + + def __str__(self): + return '#{0._parameter}={0._value}'.format(self) + +#################################################################################################### + +class Parameter(RealValue, ParameterMixin): + + ############################################## + + def __init__(self, parameter): + ParameterMixin.__init__(self, parameter) + + ############################################## + + def __repr__(self): + return 'Parameter({0._parameter})'.format(self) + + def __str__(self): + return '#{0._parameter}'.format(self) + +#################################################################################################### + +class UnaryOperation(RealValue): + + __function__ = None + __gcode__ = None + + ############################################## + + def __init__(self, arg): + self.arg = arg + + ############################################## + + @property + def arg(self): + return self._arg + + @arg.setter + def arg(self, value): + self._arg = value + + ############################################## + + def float(self): + return self.__function__(float(self._arg)) + + ############################################## + + def __repr__(self): + return '{}({})'.format(self.__class__.__name__, repr(self._arg)) + + ############################################## + + def __str__(self): + return '{0.__gcode__}[{0._arg}]'.format(self) + +#################################################################################################### + +class AbsoluteValue(UnaryOperation): + __function__ = abs + __gcode__ = 'abs' + +class ArcCosine(UnaryOperation): + __function__ = lambda x: math.acos(math.radians(x)) + __gcode__ = 'acos' + +class ArcSine(UnaryOperation): + __function__ = lambda x: math.degrees(math.asin(x)) + __gcode__ = 'asin' + +class ArcTangent(UnaryOperation): + __function__ = lambda x: math.degrees(math.atan(x)) + __gcode__ = 'atan' + +class Cosine(UnaryOperation): + __function__ = lambda x: math.degrees(math.cos(x)) + __gcode__ = 'cos' + +class ERaisedTo(UnaryOperation): + __function__ = math.exp + __gcode__ = 'exp' + +class FixDown(UnaryOperation): + __function__ = math.ceil + __gcode__ = 'fix' + +class FixUp(UnaryOperation): + __function__ = math.floor + __gcode__ = 'fup' + +class NaturalLogOf(UnaryOperation): + __function__ = math.log + __gcode__ = 'ln' + +class Round(UnaryOperation): + __function__ = round + __gcode__ = 'round' + +class Sine(UnaryOperation): + __function__ = lambda x: math.sin(math.radians(x)) + __gcode__ = 'sin' + +class SquareRoot(UnaryOperation): + __function__ = math.sqrt + __gcode__ = 'sqrt' + +class Tangent(UnaryOperation): + __function__ = lambda x: ath.tan(math.radians(x)) + __gcode__ = 'tan' + +#################################################################################################### + +class BinaryOperation(RealValue): + + __function__ = None + __gcode__ = None + + ############################################## + + def __init__(self, arg1, arg2): + self.arg1 = arg1 + self.arg2 = arg2 + + ############################################## + + @property + def arg1(self): + return self._arg1 + + @arg1.setter + def arg1(self, value): + self._arg1 = value + + @property + def arg2(self): + return self._arg2 + + @arg2.setter + def arg2(self, value): + self._arg2 = value + + ############################################## + + def float(self): + return self.__function__(float(self._arg1), float(self._arg2)) + + ############################################## + + def __repr__(self): + return '{}({})'.format(self.__class__.__name__, repr(self._arg)) + + ############################################## + + def __str__(self): + return '[{0._arg1} {0.__gcode__} {0._arg2}]'.format(self) + +#################################################################################################### + +class Power(BinaryOperation): + __function__ = lambda a, b: a**b + __gcode__ = '**' + +class DividedBy(BinaryOperation): + __function__ = lambda a, b: a / b + __gcode__ = '/' + +class Modulo(BinaryOperation): + __function__ = lambda a, b: a % b + __gcode__ = 'mod' + +class Times(BinaryOperation): + __function__ = lambda a, b: a * b + __gcode__ = '*' + +class And(BinaryOperation): + __function__ = lambda a, b: a & b + __gcode__ = 'and' + +class ExclusiveOr(BinaryOperation): + __function__ = lambda a, b: a ^ b + __gcode__ = 'xor' + +class Minus(BinaryOperation): + __function__ = lambda a, b: a - b + __gcode__ = '-' + +class NonExclusiveOr(BinaryOperation): + __function__ = lambda a, b: a | b + __gcode__ = 'or' + +class Plus(BinaryOperation): + __function__ = lambda a, b: a + b + __gcode__ = '+' diff --git a/PythonicGcodeMachine/Gcode/Rs274/Lexer.py b/PythonicGcodeMachine/Gcode/Rs274/Lexer.py index 0df8ff397a31581a1d82ce7a6141a9d18263d4a8..c444d7f3dda66ca9d422daeb2456ec8efbc5dbe6 100644 --- a/PythonicGcodeMachine/Gcode/Rs274/Lexer.py +++ b/PythonicGcodeMachine/Gcode/Rs274/Lexer.py @@ -18,7 +18,7 @@ # #################################################################################################### -__all__ = ['GcodeLexerError', 'GcodeLexer, '] +__all__ = ['GcodeLexerError', 'GcodeLexer'] #################################################################################################### @@ -38,7 +38,7 @@ class GcodeLexerError(ValueError): class GcodeLexer: - """Class to implement a CGode lexer. + """Class to implement a RS-274 G-code lexer. For references, see @@ -219,7 +219,6 @@ class GcodeLexer: ############################################## def tokenize(self, data): - self.input(data) while True: token = self._lexer.token() diff --git a/PythonicGcodeMachine/Gcode/Rs274/Parser.py b/PythonicGcodeMachine/Gcode/Rs274/Parser.py index 5e2e0c4f440923d81edf2844eb2631ba57d02aca..011b2354aa20404bcbac80ad254d72a4044effc0 100644 --- a/PythonicGcodeMachine/Gcode/Rs274/Parser.py +++ b/PythonicGcodeMachine/Gcode/Rs274/Parser.py @@ -25,7 +25,7 @@ __all__ = ['GcodeParserError', 'GcodeParser'] # https://rply.readthedocs.io/en/latest/ from ply import yacc -from .Lexer import GcodeLexer, GcodeLexerError +from .Lexer import GcodeLexer #################################################################################################### @@ -36,7 +36,7 @@ class GcodeParserError(ValueError): class GcodeParser: - """Class to implement a CGode parser. + """Class to implement a RS-274 G-code parser. For references, see @@ -91,6 +91,8 @@ class GcodeParser: """ + __lexer_cls__ = GcodeLexer + ############################################## # Start symbol @@ -267,8 +269,7 @@ class GcodeParser: def _build(self, **kwargs): """Build the parser""" - - self._lexer = GcodeLexer() + self._lexer = self.__lexer_cls__() self.tokens = self._lexer.tokens self._parser = yacc.yacc( module=self, @@ -279,7 +280,6 @@ class GcodeParser: ############################################## def parse(self, line): - line = line.strip() ast = self._parser.parse( line, diff --git a/PythonicGcodeMachine/Gcode/Rs274/__init__.py b/PythonicGcodeMachine/Gcode/Rs274/__init__.py index 37fdd45a0d6bed35fad763c9e5dbb87020fe76cb..256679a9b02005349073a342d876750caf8c644d 100644 --- a/PythonicGcodeMachine/Gcode/Rs274/__init__.py +++ b/PythonicGcodeMachine/Gcode/Rs274/__init__.py @@ -163,7 +163,7 @@ value may be an explicit number (such as 341 or -0.8807), a parameter value, an unary operation value. Number -~~~~~ +~~~~~~ The following rules are used for (explicit) numbers. In these rules a digit is a single character between 0 and 9. diff --git a/unit-test/Gcode/test_Ast.py b/unit-test/Gcode/test_Ast.py new file mode 100644 index 0000000000000000000000000000000000000000..530663c8ef174332b880e2d546f9165143a30ff5 --- /dev/null +++ b/unit-test/Gcode/test_Ast.py @@ -0,0 +1,81 @@ +#################################################################################################### +# +# 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 . +# +#################################################################################################### + +#################################################################################################### + +import unittest + +#################################################################################################### + +from PythonicGcodeMachine.Gcode.Rs274.Ast import * + +#################################################################################################### + +class TestAst(unittest.TestCase): + + ############################################## + + def test_basic(self): + + # program = Program() + + word = Word('G', 0) + self.assertEqual(str(word), 'G0') + word = Word('g', 0) + self.assertEqual(str(word), 'G0') + word = Word('G', 1.0) + self.assertEqual(str(word), 'G1.0') + + line = Line() + line.push(Word('G', 1)) + self.assertEqual(str(line), 'G1') + line.comment = 'a comment' + self.assertEqual(str(line), 'G1 ; a comment') + line.line_number = 1 + self.assertEqual(str(line), 'N1 G1 ; a comment') + line.line_number = 1.1 + self.assertEqual(str(line), 'N1.1 G1 ; a comment') + line.deleted = True + self.assertEqual(str(line), r'\ N1.1 G1 ; a comment') + + line = Line() + for value, letter in enumerate('GXYZ'): + line.push(Word(letter, value)) + self.assertEqual(str(line), 'G0 X1 Y2 Z3') + + line = Line() + for value, letter in enumerate('GXYZ'): + line += Word(letter, value) + self.assertEqual(str(line), 'G0 X1 Y2 Z3') + + line.push(ParameterSetting(1, 1.2)) + self.assertEqual(str(line), 'G0 X1 Y2 Z3 #1=1.2') + + ############################################## + + def test_expression(self): + + self.assertEqual(str(Plus(1, 2)), '[1 + 2]') + +#################################################################################################### + +if __name__ == '__main__': + + unittest.main() diff --git a/unit-test/Gcode/test_Parser.py b/unit-test/Gcode/test_Parser.py index fc0515e5f3c009e0ff8e1b7cf54d298e24484c6a..ee4905e8d0d24887197b9eb6ef86283eda992fcb 100644 --- a/unit-test/Gcode/test_Parser.py +++ b/unit-test/Gcode/test_Parser.py @@ -24,8 +24,8 @@ import unittest #################################################################################################### -from PythonicGcodeMachine.Gcode.Lexer import GcodeLexer, GcodeLexerError -from PythonicGcodeMachine.Gcode.Parser import GcodeParser, GcodeParserError +from PythonicGcodeMachine.Gcode.Rs274.Lexer import GcodeLexer, GcodeLexerError +from PythonicGcodeMachine.Gcode.Rs274.Parser import GcodeParser, GcodeParserError ####################################################################################################