From dcac4fcec5462567f541fa6a9c1a1b901972cdc7 Mon Sep 17 00:00:00 2001 From: Peter Boin Date: Wed, 5 Jul 2017 02:06:38 +1000 Subject: [PATCH] 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])