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'