diff --git a/dist/README.md b/dist/README.md index 1f6547b..604bbbb 100644 --- a/dist/README.md +++ b/dist/README.md @@ -1,5 +1,48 @@ # Change History +---- +## 0.2.1 + +### Improvements + +**`pygcode-norm` script:** + +Added "Final Machine Actions:" + + Final Machine Actions: + standardize what's done at the end of a gcode program. + + --zero_xy, -zxy On completion, move straight up to + rapid_safety_height, then across to X0 Y0. + --zero_z, -zz On completion, move down to Z0 (done after zero_xy, if + set). + --rapid_safety_height RAPID_SAFETY_HEIGHT, -rsh RAPID_SAFETY_HEIGHT + Z value to move to before traversing workpiece (if not + set, max value will be attempted). + --spindle_off, -so On completion, turn spindle off. + + +Added ability to remove all codes & parameters that cannot be parsed. + + --rm_invalid_modal, -rmim + Simply remove everything that isn't understood. Use + with caution. + +**Library Improvements** + +* `Machine.abs2work()` and `Machine.work2abs()` position + converters, apply machine's offset to the given position without effecting + machine's current position. +* `Machine.clean_block()` removes content from a block that's not parsable (use with caution) +* `Machine.ignore_invalid_modal` bool class parameter, if set, will continue on merrily while ignoring + anything not parsable (similarly to `clean_block`) +* deployment version category validation in `setup.py` (ie: alpha, beta, and so on) + +### Bugfixes + +(none) + +---- ## 0.2.0 Moved to `alpha` @@ -20,6 +63,7 @@ Improvements to read more versatile formats * [#5](https://github.com/fragmuffin/pygcode/issues/5) Line number in program +---- ## 0.1.2 Changes to accommodate implementation of [grbl-stream](https://github.com/fragmuffin/grbl-stream) diff --git a/dist/pygcode-0.2.0.tar.gz b/dist/pygcode-0.2.0.tar.gz deleted file mode 100644 index 960bd8c..0000000 Binary files a/dist/pygcode-0.2.0.tar.gz and /dev/null differ diff --git a/dist/pygcode-0.2.0-py2.py3-none-any.whl b/dist/pygcode-0.2.1-py2.py3-none-any.whl similarity index 64% rename from dist/pygcode-0.2.0-py2.py3-none-any.whl rename to dist/pygcode-0.2.1-py2.py3-none-any.whl index 39a65e5..db24ad1 100644 Binary files a/dist/pygcode-0.2.0-py2.py3-none-any.whl and b/dist/pygcode-0.2.1-py2.py3-none-any.whl differ diff --git a/dist/pygcode-0.2.1.tar.gz b/dist/pygcode-0.2.1.tar.gz new file mode 100644 index 0000000..5c2fab9 Binary files /dev/null and b/dist/pygcode-0.2.1.tar.gz differ diff --git a/scripts/pygcode-norm b/scripts/pygcode-norm index c371547..8b721d9 100755 --- a/scripts/pygcode-norm +++ b/scripts/pygcode-norm @@ -18,12 +18,14 @@ for pygcode_lib_type in ('installed_lib', 'relative_lib'): from pygcode import Machine, Mode, Line from pygcode import GCodeArcMove, GCodeArcMoveCW, GCodeArcMoveCCW from pygcode import GCodeCannedCycle + from pygcode import GCodeRapidMove, GCodeStopSpindle, GCodeAbsoluteDistanceMode from pygcode import split_gcodes from pygcode import Comment from pygcode.transform import linearize_arc, simplify_canned_cycle from pygcode.transform import ArcLinearizeInside, ArcLinearizeOutside, ArcLinearizeMid from pygcode.gcodes import _subclasses from pygcode import utils + from pygcode.exceptions import MachineInvalidState except ImportError: import sys, os, inspect @@ -65,7 +67,9 @@ def arc_lin_method_type(value): def word_list_type(value): """ - :return: [Word('G73'), Word('G89'), ... ] + Convert csv string list into Word instances. + >>> word_list_type("G73,G89") == set([Word('G73'), Word('G89')]) + :return: set of Word instances """ canned_code_words = set() for word_str in re.split(r'\s*,\s*', value): @@ -82,29 +86,29 @@ DEFAULT_CANNED_CODES = ','.join(str(w) for w in sorted(c.word_key for c in _subc # --- Create Parser parser = argparse.ArgumentParser( - description="Normalize gcode for machine consistency when using different CAM software" + description="Normalize gcode for machine consistency when using different CAM software." ) parser.add_argument( 'infile', type=argparse.FileType('r'), - help="gcode file to normalize", + help="Gcode file to normalize.", ) parser.add_argument( '--singles', '-s', dest='singles', action='store_const', const=True, default=False, - help="only output one command per gcode line", + help="Only output one command per gcode line.", ) parser.add_argument( '--full', '-f', dest='full', action='store_const', const=True, default=False, - help="output full commands, any modal parameters will be acompanied with " - "the fully qualified gcode command", + help="Output full commands, any modal parameters will be acompanied with " + "the fully qualified gcode command.", ) # 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, + help="Machine's startup mode as gcode (default: '%s')." % DEFAULT_MACHINE_MODE, ) # Arc Linearizing @@ -117,20 +121,20 @@ group = parser.add_argument_group( group.add_argument( '--arc_linearize', '-al', dest='arc_linearize', action='store_const', const=True, default=False, - help="convert G2,G3 commands to a series of linear interpolations (G1 codes)", + help="Convert G2,G3 commands to a series of linear interpolations (G1 codes).", ) group.add_argument( '--arc_lin_method', '-alm', dest='arc_lin_method', type=arc_lin_method_type, default=DEFAULT_ARC_LIN_METHOD, help="Method of linearizing arcs, i=inner, o=outer, m=mid. List 2 " "for ,, eg 'i,o'. 'i' is equivalent to 'i,i'. " - "(default: '%s')" % DEFAULT_ARC_LIN_METHOD, + "(default: '%s')." % DEFAULT_ARC_LIN_METHOD, metavar='{i,o,m}[,{i,o,m}]', ) group.add_argument( '--arc_precision', '-alp', dest='arc_precision', type=float, default=DEFAULT_PRECISION, - help="maximum positional error when creating linear interpolation codes " - "(default: %g)" % DEFAULT_PRECISION, + help="Maximum positional error when creating linear interpolation codes " + "(default: %g)." % DEFAULT_PRECISION, ) #parser.add_argument( @@ -149,41 +153,73 @@ group = parser.add_argument_group( group.add_argument( '--canned_expand', '-ce', dest='canned_expand', action='store_const', const=True, default=False, - help="Expand canned cycles into basic linear movements, and pauses", + help="Expand canned cycles into basic linear movements, and pauses.", ) group.add_argument( '--canned_codes', '-cc', dest='canned_codes', type=word_list_type, default=DEFAULT_CANNED_CODES, - help="List of canned gcodes to expand, (default is '%s')" % DEFAULT_CANNED_CODES, + help="List of canned gcodes to expand, (default is '%s')." % DEFAULT_CANNED_CODES, +) + +# Finalize Code +group = parser.add_argument_group( + "Final Machine Actions", + "standardize what's done at the end of a gcode program." +) +group.add_argument( + '--zero_xy', '-zxy', dest="zero_xy", + action='store_const', const=True, default=False, + help="On completion, move straight up to rapid_safety_height, " + "then across to X0 Y0.", +) +group.add_argument( + '--zero_z', '-zz', dest="zero_z", + action='store_const', const=True, default=False, + help="On completion, move down to Z0 (done after zero_xy, if set).", +) +group.add_argument( + '--rapid_safety_height', '-rsh', dest="rapid_safety_height", + type=float, default=None, + help="Z value to move to before traversing workpiece (if not set, max " + "value will be attempted).", +) +group.add_argument( + '--spindle_off', '-so', dest="spindle_off", + action='store_const', const=True, default=False, + help="On completion, turn spindle off.", ) # Removing non-functional content group = parser.add_argument_group( "Removing Content", - "options for the removal of content" + "options for the removal of content." ) group.add_argument( '--rm_comments', '-rc', dest='rm_comments', action='store_const', const=True, default=False, - help="remove all comments (non-functional)", + help="Remove all comments (non-functional).", ) group.add_argument( '--rm_blanks', '-rb', dest='rm_blanks', action='store_const', const=True, default=False, - help="remove all empty lines (non-functional)", + help="Remove all empty lines (non-functional).", ) group.add_argument( '--rm_whitespace', '-rws', dest='rm_whitespace', action='store_const', const=True, default=False, - help="remove all whitespace from gcode blocks (non-functional)", + help="Remove all whitespace from gcode blocks (non-functional).", ) group.add_argument( '--rm_gcodes', '-rmg', dest='rm_gcodes', type=word_list_type, default=[], - help="remove gcode (and it's parameters) with words in the given list " + help="Remove gcode (and it's parameters) with words in the given list " "(eg: M6,G43) (note: only works for modal params with --full)", ) - +group.add_argument( + '--rm_invalid_modal', '-rmim', dest='rm_invalid_modal', + action='store_const', const=True, default=False, + help="Simply remove everything that isn't understood. Use with caution.", +) # --- Parse Arguments args = parser.parse_args() @@ -195,6 +231,7 @@ class MyMode(Mode): class MyMachine(Machine): MODE_CLASS = MyMode + ignore_invalid_modal = args.rm_invalid_modal machine = MyMachine() @@ -276,6 +313,9 @@ def split_and_process(gcode_list, gcode_class, comment): for line_str in args.infile.readlines(): line = Line(line_str) + if args.rm_invalid_modal: + machine.clean_block(line.block) + # Effective G-Codes: # fills in missing motion modal gcodes (using machine's current motion mode). effective_gcodes = machine.block_modal_gcodes(line.block) @@ -315,3 +355,23 @@ for line_str in args.infile.readlines(): else: write(line.block.gcodes, modal_params=line.block.modal_params, comment=line.comment, macro=line.macro) machine.process_block(line.block) + +# Finalizing Motion & Spindle +if any([args.spindle_off, args.zero_xy, args.zero_z]): + write([], comment=Comment("pygcode-norm: finalizing")) +if any([args.zero_xy, args.zero_z]) and not(isinstance(machine.mode.distance, GCodeAbsoluteDistanceMode)): + write([GCodeAbsoluteDistanceMode()]) +if args.spindle_off: + write([GCodeStopSpindle()], comment=Comment("spindle off")) + +if args.zero_xy: + rapid_safety_height = args.rapid_safety_height + if rapid_safety_height is None: + rapid_safety_height = machine.abs2work(machine.abs_range_max).Z + +if args.zero_xy: + write([GCodeRapidMove(Z=rapid_safety_height)], comment=Comment("move to safe height")) + write([GCodeRapidMove(X=0, Y=0)], comment=Comment("move to planar origin")) + +if args.zero_z: + write([GCodeRapidMove(Z=0)], comment=Comment("move to zero height")) diff --git a/setup.py b/setup.py index ca02036..ac2d511 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ import codecs import os import re +from distutils.version import LooseVersion from setuptools import setup, find_packages @@ -14,7 +15,7 @@ META_PATH = os.path.join("src", NAME, "__init__.py") KEYWORDS = ['gcode', 'cnc', 'parser', 'interpreter'] CLASSIFIERS = [ - "Development Status :: 2 - Pre-Alpha", # see src/pygcode/__init__.py + "Development Status :: 3 - Alpha", # see src/pygcode/__init__.py "Intended Audience :: Developers", "Intended Audience :: Manufacturing", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", @@ -65,13 +66,49 @@ def find_meta(meta): raise RuntimeError("Unable to find __{meta}__ string.".format(meta=meta)) +def assert_version_classifier(version_str): + """ + Verify version consistency: + version number must correspond to the correct "Development Status" classifier + :raises: ValueError if error found, but ideally this function does nothing + """ + V = lambda v: LooseVersion(v) + # cast version + version = V(version_str) + + # get "Development Status" classifier + dev_status_list = [x for x in CLASSIFIERS if x.startswith("Development Status ::")] + if len(dev_status_list) != 1: + raise ValueError("must be 1 'Development Status' in CLASSIFIERS") + classifier = dev_status_list.pop() + + version_map = [ + (V('0.1'), "Development Status :: 2 - Pre-Alpha"), + (V('0.2'), "Development Status :: 3 - Alpha"), + (V('0.3'), "Development Status :: 4 - Beta"), + (V('1.0'), "Development Status :: 5 - Production/Stable"), + ] + + for (test_ver, test_classifier) in reversed(sorted(version_map, key=lambda x: x[0])): + if version >= test_ver: + if classifier == test_classifier: + return # all good, now forget any of this ever happened + else: + raise ValueError("for version {ver} classifier should be \n'{good}'\nnot\n'{bad}'".format( + ver=str(version), good=test_classifier, bad=classifier + )) + + if __name__ == "__main__": + version = find_meta("version") + assert_version_classifier(version) + setup( name=NAME, description=find_meta("description"), license=find_meta("license"), url=find_meta("url"), - version=find_meta("version"), + version=version, author=find_meta("author"), author_email=find_meta("email"), maintainer=find_meta("author"), diff --git a/src/pygcode.egg-info/PKG-INFO b/src/pygcode.egg-info/PKG-INFO index 80ed422..577db9d 100644 --- a/src/pygcode.egg-info/PKG-INFO +++ b/src/pygcode.egg-info/PKG-INFO @@ -1,11 +1,12 @@ Metadata-Version: 1.1 Name: pygcode -Version: 0.2.0 +Version: 0.2.1 Summary: Basic g-code parser, interpreter, and encoder library. Home-page: https://github.com/fragmuffin/pygcode Author: Peter Boin Author-email: peter.boin@gmail.com License: GPLv3 +Description-Content-Type: UNKNOWN Description: ======= pygcode ======= @@ -33,7 +34,7 @@ Description: ======= Keywords: gcode,cnc,parser,interpreter Platform: UNKNOWN -Classifier: Development Status :: 2 - Pre-Alpha +Classifier: Development Status :: 3 - Alpha Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Manufacturing Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3) diff --git a/src/pygcode/__init__.py b/src/pygcode/__init__.py index 21a1d11..51c75a7 100644 --- a/src/pygcode/__init__.py +++ b/src/pygcode/__init__.py @@ -6,7 +6,7 @@ # 1.x - Development Status :: 5 - Production/Stable # .y - developments on that version (pre-release) # *.dev* - development release (intended purely to test deployment) -__version__ = "0.2.0" +__version__ = "0.2.1" __title__ = "pygcode" __description__ = "Basic g-code parser, interpreter, and encoder library." @@ -69,6 +69,7 @@ 'GCodeCutterCompRight', 'GCodeCutterRadiusComp', 'GCodeCutterRadiusCompOff', + 'GCodeDefinition', 'GCodeDigitalOutput', 'GCodeDigitalOutputOff', 'GCodeDigitalOutputOffSyncd', @@ -98,6 +99,7 @@ 'GCodeInverseTimeMode', 'GCodeLatheDiameterMode', 'GCodeLatheRadiusMode', + 'GCodeLineNumber', 'GCodeLinearMove', 'GCodeMotion', 'GCodeMoveInMachineCoords', @@ -113,6 +115,7 @@ 'GCodePauseProgramOptional', 'GCodePlaneSelect', 'GCodeProgramControl', + 'GCodeProgramName', 'GCodeQuadraticSpline', 'GCodeRapidMove', 'GCodeResetCoordSystemOffset', @@ -190,7 +193,6 @@ # GCode from .gcodes import ( words2gcodes, text2gcodes, split_gcodes, - # $ python -c "from pygcode.gcodes import _gcode_class_infostr as x; print(x(prefix=' # '))" # - GCode: # - GCodeCannedCycle: @@ -214,6 +216,9 @@ # 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) + # - GCodeDefinition: + # - GCodeLineNumber: N: Line Number + # - GCodeProgramName: O: Program Name # - GCodeDistanceMode: # G90.1 - GCodeAbsoluteArcDistanceMode: G90.1: Absolute Distance Mode for Arc IJK Parameters # G90 - GCodeAbsoluteDistanceMode: G90: Absolute Distance Mode @@ -346,6 +351,7 @@ GCodeCutterCompRight, GCodeCutterRadiusComp, GCodeCutterRadiusCompOff, + GCodeDefinition, GCodeDigitalOutput, GCodeDigitalOutputOff, GCodeDigitalOutputOffSyncd, @@ -375,6 +381,7 @@ GCodeInverseTimeMode, GCodeLatheDiameterMode, GCodeLatheRadiusMode, + GCodeLineNumber, GCodeLinearMove, GCodeMotion, GCodeMoveInMachineCoords, @@ -390,6 +397,7 @@ GCodePauseProgramOptional, GCodePlaneSelect, GCodeProgramControl, + GCodeProgramName, GCodeQuadraticSpline, GCodeRapidMove, GCodeResetCoordSystemOffset, diff --git a/src/pygcode/machine.py b/src/pygcode/machine.py index 286354d..46c2ad5 100644 --- a/src/pygcode/machine.py +++ b/src/pygcode/machine.py @@ -5,6 +5,7 @@ from .gcodes import ( MODAL_GROUP_MAP, GCode, # Modal GCodes + GCodeMotion, GCodeIncrementalDistanceMode, GCodeUseInches, GCodeUseMillimeters, # Utilities @@ -369,6 +370,7 @@ class Machine(object): STATE_CLASS = State axes = set('XYZ') + ignore_invalid_modal = False def __init__(self): self.mode = self.MODE_CLASS() @@ -407,12 +409,15 @@ def set_mode(self, *gcode_list): # TODO: convert coord systems between inches/mm, G20/G21 respectively def modal_gcode(self, modal_params): + """ + :param ignore_unassigned: if truthy, unassigned parameters will be ignored + """ if not modal_params: return None if self.mode.motion is None: - unasigned_words = modal_params + (modal_gcodes, unasigned_words) = ([], modal_params) # forces exception to be raised in next step else: params = copy(self.mode.motion.params) # dict @@ -421,9 +426,11 @@ def modal_gcode(self, modal_params): [self.mode.motion.word] + list(params.values()) ) - if unasigned_words: + if unasigned_words and (not self.ignore_invalid_modal): # Can't process with unknown words on the same line... - # raising: MachineInvalidState + # 2 choices: + # - raise MachineInvalidState + # - or remove unassigned parameters from line plausable_codes = [w for w in unasigned_words if w.letter in set('GM')] if plausable_codes: # words in list are probably valid, but unsupported, G-Codes @@ -456,6 +463,25 @@ def block_modal_gcodes(self, block): gcodes.append(modal_gcode) return sorted(gcodes) + def clean_block(self, block): + """ + Remove invalid modal parameters from given block + :param block: Block instance to clean + """ + assert isinstance(block, Block), "invalid parameter" + if self.mode.motion is None: + # no modal motion, modal parameters are all invalid + block.modal_params = [] + elif any(True for g in block.gcodes if g.modal_group == GCodeMotion.modal_group): + # block defines new motion, modal motion is irrelevant + block.modal_params = [] + else: + (modal_gcodes, unasigned_words) = words2gcodes( + [self.mode.motion.word] + list(block.modal_params) + ) + for w in unasigned_words: + block.modal_params.remove(w) + def process_gcodes(self, *gcode_list, **kwargs): """ Process gcodes @@ -491,19 +517,28 @@ def process_str(self, block_str): line = Line(block_str) self.process_block(line.block) + # Position conversions (considering offsets) + def abs2work(self, abs_pos): + assert isinstance(abs_pos, Position), "bad abs_pos type" + coord_sys_offset = getattr(self.state.coord_sys, 'offset', Position(axes=self.axes)) + temp_offset = self.state.offset + return (abs_pos - coord_sys_offset) - temp_offset + + def work2abs(self, work_pos): + assert isinstance(work_pos, Position), "bad work_pos type" + coord_sys_offset = getattr(self.state.coord_sys, 'offset', Position(axes=self.axes)) + temp_offset = self.state.offset + return (work_pos + temp_offset + coord_sys_offset) + @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 + return self.abs2work(self.abs_pos) @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 + self.abs_pos = self.work2abs(value) self._update_abs_range(self.abs_pos) def _update_abs_range(self, pos):