From 9ca819d8c6978e4e44dd5d6d605ff9ad8b368902 Mon Sep 17 00:00:00 2001 From: Peter Boin Date: Tue, 18 Jul 2017 12:59:15 +1000 Subject: [PATCH] simplify canned drilling processes --- pygcode/gcodes.py | 86 +++++++++++++++++++++--- pygcode/transform.py | 126 ++++++++++++++++++++++++++++++++++- pygcode/utils.py | 2 +- scripts/pygcode-normalize.py | 29 ++++---- tests/test_gcodes.py | 52 +++++++++++++++ tests/test_words.py | 4 -- tests/testutils.py | 1 + 7 files changed, 272 insertions(+), 28 deletions(-) diff --git a/pygcode/gcodes.py b/pygcode/gcodes.py index d22748e..affba26 100644 --- a/pygcode/gcodes.py +++ b/pygcode/gcodes.py @@ -475,42 +475,42 @@ class GCodeDrillingCycle(GCodeCannedCycle): """G81: Drilling Cycle""" param_letters = GCodeCannedCycle.param_letters | set('RLP') word_key = Word('G', 81) - modal_param_letters = GCodeCannedCycle.param_letters | set('RLP') + modal_param_letters = GCodeCannedCycle.param_letters | set('RP') class GCodeDrillingCycleDwell(GCodeCannedCycle): """G82: Drilling Cycle, Dwell""" param_letters = GCodeCannedCycle.param_letters | set('RLP') word_key = Word('G', 82) - modal_param_letters = GCodeCannedCycle.param_letters | set('RLP') + modal_param_letters = GCodeCannedCycle.param_letters | set('RP') class GCodeDrillingCyclePeck(GCodeCannedCycle): """G83: Drilling Cycle, Peck""" param_letters = GCodeCannedCycle.param_letters | set('RLQ') word_key = Word('G', 83) - modal_param_letters = GCodeCannedCycle.param_letters | set('RLQ') + modal_param_letters = GCodeCannedCycle.param_letters | set('RQ') class GCodeDrillingCycleChipBreaking(GCodeCannedCycle): """G73: Drilling Cycle, ChipBreaking""" param_letters = GCodeCannedCycle.param_letters | set('RLQ') word_key = Word('G', 73) - modal_param_letters = GCodeCannedCycle.param_letters | set('RLQ') + modal_param_letters = GCodeCannedCycle.param_letters | set('RQ') class GCodeBoringCycleFeedOut(GCodeCannedCycle): """G85: Boring Cycle, Feed Out""" param_letters = GCodeCannedCycle.param_letters | set('RLP') word_key = Word('G', 85) - modal_param_letters = GCodeCannedCycle.param_letters | set('RLP') + modal_param_letters = GCodeCannedCycle.param_letters | set('RP') class GCodeBoringCycleDwellFeedOut(GCodeCannedCycle): """G89: Boring Cycle, Dwell, Feed Out""" param_letters = GCodeCannedCycle.param_letters | set('RLP') word_key = Word('G', 89) - modal_param_letters = GCodeCannedCycle.param_letters | set('RLP') + modal_param_letters = GCodeCannedCycle.param_letters | set('RP') class GCodeThreadingCycle(GCodeCannedCycle): @@ -895,8 +895,8 @@ class GCodePathBlendingMode(GCodePathControlMode): # ======================= Return Mode in Canned Cycles ======================= # CODE PARAMETERS DESCRIPTION -# G98 Canned Cycle Return Level - +# G98 Canned Cycle Return Level to previous +# G99 Canned Cycle Return to the level set by R class GCodeCannedReturnMode(GCode): modal_group = MODAL_GROUP_MAP['canned_cycles_return'] @@ -1444,3 +1444,73 @@ def split_gcodes(gcode_list, splitter_class, sort_list=True): split[2] = gcode_list[split_index+1:] return split + + +def _gcodes_abs2rel(start_pos, dist_mode=None, axes='XYZ'): + """ + Decorator to convert returned motion gcode coordinates to incremental. + Intended to be used internally (mainly because it's a little shonky) + Decorated function is only expected to return GCodeRapidMove or GCodeLinearMove + instances + :param start_pos: starting machine position (Position) + :param dist_mode: machine's distance mode (GCodeAbsoluteDistanceMode or GCodeIncrementalDistanceMode) + :param axes: axes machine accepts (set) + """ + # Usage: + # m = Machine() # defaults to absolute distance mode + # m.process_gcodes(GCodeRapidMove(X=10, Y=20, Z=3)) + # m.process_gcodes(GCodeIncrementalDistanceMode()) + # + # @_gcodes_abs2rel(start_pos=m.pos, dist_mode=m.mode.distance, axes=m.axes) + # def do_stuff(): + # yield GCodeRapidMove(X=0, Y=30, Z=3) + # yield GCodeLinearMove(X=0, Y=30, Z=-5) + # + # gcode_list = do_stuff() + # gocde_list[0] # == GCodeRapidMove(X=-10, Y=10) + # gocde_list[1] # == GCodeLinearMove(Z=-8) + + SUPPORTED_MOTIONS = ( + GCodeRapidMove, GCodeLinearMove, + ) + + def wrapper(func): + + def inner(*largs, **kwargs): + # Create Machine (with minimal information) + from .machine import Machine, Mode, Position + m = type('AbsoluteCoordMachine', (Machine,), { + 'MODE_CLASS': type('NullMode', (Mode,), {'default_mode': 'G90'}), + 'axes': axes, + })() + m.pos = start_pos + + for gcode in func(*largs, **kwargs): + # Verification & passthrough's + if not isinstance(gcode, GCode): + yield gcode # whatever this thing is + else: + # Process gcode + pos_from = m.pos + m.process_gcodes(gcode) + pos_to = m.pos + + if gcode.modal_group != MODAL_GROUP_MAP['motion']: + yield gcode # only deal with motion gcodes + continue + elif not isinstance(gcode, SUPPORTED_MOTIONS): + raise NotImplementedError("%r to iterative coords is not supported (this is only a very simple function)" % gcode) + + # Convert coordinates to iterative + rel_pos = pos_to - pos_from + coord_words = [w for w in rel_pos.words if w.value] + if coord_words: # else relative coords are all zero; do nothing + yield words2gcodes([gcode.word] + coord_words)[0].pop() + + + # Return apropriate function + if (dist_mode is None) or isinstance(dist_mode, GCodeIncrementalDistanceMode): + return inner + else: + return func # bypass decorator entirely; nothing to be done + return wrapper diff --git a/pygcode/transform.py b/pygcode/transform.py index 8467f78..10a415b 100644 --- a/pygcode/transform.py +++ b/pygcode/transform.py @@ -1,10 +1,14 @@ from math import sin, cos, tan, asin, acos, atan2, pi, sqrt, ceil -from .gcodes import GCodeLinearMove +from .gcodes import GCodeLinearMove, GCodeRapidMove from .gcodes import GCodeArcMove, GCodeArcMoveCW, GCodeArcMoveCCW from .gcodes import GCodePlaneSelect, GCodeSelectXYPlane, GCodeSelectYZPlane, GCodeSelectZXPlane from .gcodes import GCodeAbsoluteDistanceMode, GCodeIncrementalDistanceMode from .gcodes import GCodeAbsoluteArcDistanceMode, GCodeIncrementalArcDistanceMode +from .gcodes import GCodeCannedCycle +from .gcodes import GCodeDrillingCyclePeck, GCodeDrillingCycleDwell, GCodeDrillingCycleChipBreaking +from .gcodes import GCodeCannedReturnMode, GCodeCannedCycleReturnLevel, GCodeCannedCycleReturnToR +from .gcodes import _gcodes_abs2rel from .machine import Position from .exceptions import GCodeParameterError @@ -97,7 +101,7 @@ class ArcLinearizeMethod(object): self._outer_radius = self.get_outer_radius() return self._outer_radius - # Iter + # Vertex Generator def iter_vertices(self): """Yield absolute (, ) for each line for the arc""" start_vertex = self.arc_p_start - self.arc_p_center @@ -214,6 +218,17 @@ DEFAULT_LA_ARCDISTMODE = GCodeIncrementalArcDistanceMode def linearize_arc(arc_gcode, start_pos, plane=None, method_class=None, dist_mode=None, arc_dist_mode=None, max_error=0.01, decimal_places=3): + """ + Convert a G2,G3 arc into a series of approsimation G1 codes + :param arc_gcode: arc gcode to approximate (GCodeArcMove) + :param start_pos: current machine position (Position) + :param plane: machine's active plane (GCodePlaneSelect) + :param method_class: method of linear approximation (ArcLinearizeMethod) + :param dist_mode: machine's distance mode (GCodeAbsoluteDistanceMode or GCodeIncrementalDistanceMode) + :param arc_dist_mode: machine's arc distance mode (GCodeAbsoluteArcDistanceMode or GCodeIncrementalArcDistanceMode) + :param max_error: maximum distance approximation arcs can stray from original arc (float) + :param decimal_places: number of decimal places gocde will be rounded to, used to mitigate risks of accumulated eror when in incremental distance mode (int) + """ # set defaults if method_class is None: method_class = DEFAULT_LA_method_class @@ -362,4 +377,109 @@ def linearize_arc(arc_gcode, start_pos, plane=None, method_class=None, cur_pos += l_delta # mitigate errors by also adding them the accumulated cur_pos -# ==================== Arc Precision Adjustment ==================== +# ==================== Un-Canning ==================== + +DEFAULT_SCC_PLANE = GCodeSelectXYPlane +DEFAULT_SCC_DISTMODE = GCodeAbsoluteDistanceMode +DEFAULT_SCC_RETRACTMODE = GCodeCannedCycleReturnLevel + +def simplify_canned_cycle(canned_gcode, start_pos, + plane=None, dist_mode=None, retract_mode=None, + axes='XYZ'): + """ + Simplify canned cycle into it's basic linear components + :param canned_gcode: canned gcode to be simplified (GCodeCannedCycle) + :param start_pos: current machine position (Position) + :param plane: machine's active plane (GCodePlaneSelect) + :param dist_mode: machine's distance mode (GCodeAbsoluteDistanceMode or GCodeIncrementalDistanceMode) + :param axes: axes machine accepts (set) + """ + + # set defaults + if plane is None: + plane = DEFAULT_SCC_PLANE() + if dist_mode is None: + dist_mode = DEFAULT_SCC_DISTMODE() + if retract_mode is None: + retract_mode = DEFAULT_SCC_RETRACTMODE() + + # Parameter Type Assertions + assert isinstance(canned_gcode, GCodeCannedCycle), "bad canned_gcode type: %r" % canned_gcode + assert isinstance(start_pos, Position), "bad start_pos type: %r" % start_pos + assert isinstance(plane, GCodePlaneSelect), "bad plane type: %r" % plane + assert isinstance(dist_mode, (GCodeAbsoluteDistanceMode, GCodeIncrementalDistanceMode)), "bad dist_mode type: %r" % dist_mode + assert isinstance(retract_mode, GCodeCannedReturnMode), "bad retract_mode type: %r" % retract_mode + + # TODO: implement for planes other than XY + if not isinstance(plane, GCodeSelectXYPlane): + raise NotImplementedError("simplifying canned cycles for planes other than X/Y has not been implemented") + + @_gcodes_abs2rel(start_pos=start_pos, dist_mode=dist_mode, axes=axes) + def inner(): + cycle_count = 1 if (canned_gcode.L is None) else canned_gcode.L + cur_hole_p_axis = start_pos.vector + for i in range(cycle_count): + # Calculate Depths + if isinstance(dist_mode, GCodeAbsoluteDistanceMode): + retract_depth = canned_gcode.R + drill_depth = canned_gcode.Z + cur_hole_p_axis = Vector3(x=canned_gcode.X, y=canned_gcode.Y) + else: # incremental + retract_depth = start_pos.Z + canned_gcode.R + drill_depth = retract_depth + canned_gcode.Z + cur_hole_p_axis += Vector3(x=canned_gcode.X, y=canned_gcode.Y) + + if retract_depth < drill_depth: + raise NotImplementedError("drilling upward is not supported") + + if isinstance(retract_mode, GCodeCannedCycleReturnToR): + final_depth = retract_depth + else: + final_depth = start_pos.Z + + # Move above hole (height of retract_depth) + if retract_depth > start_pos.Z: + yield GCodeRapidMove(Z=retract_depth) + yield GCodeRapidMove(X=cur_hole_p_axis.x, Y=cur_hole_p_axis.y) + if retract_depth < start_pos.Z: + yield GCodeRapidMove(Z=retract_depth) + + # Drill hole + delta = drill_depth - retract_depth # full depth + if isinstance(canned_gcode, (GCodeDrillingCyclePeck, GCodeDrillingCycleChipBreaking)): + delta = -abs(canned_gcode.Q) + + cur_depth = retract_depth + last_depth = cur_depth + while True: + # Determine new depth + cur_depth += delta + if cur_depth < drill_depth: + cur_depth = drill_depth + + # Rapid to just above, then slowly drill through delta + just_above_base = last_depth + 0.1 + if just_above_base < retract_depth: + yield GCodeRapidMove(Z=just_above_base) + yield GCodeLinearMove(Z=cur_depth) + if cur_depth <= drill_depth: + break # loop stops at the bottom of the hole + else: + # back up + if isinstance(canned_gcode, GCodeDrillingCycleChipBreaking): + # retract "a bit" + yield GCodeRapidMove(Z=cur_depth + 0.5) # TODO: configurable retraction + else: + # default behaviour: GCodeDrillingCyclePeck + yield GCodeRapidMove(Z=retract_depth) + + last_depth = cur_depth + + # Dwell + if isinstance(canned_gcode, GCodeDrillingCycleDwell): + yield GCodeDwell(P=0.5) # TODO: configurable pause + + # Return + yield GCodeRapidMove(Z=final_depth) + + return inner() diff --git a/pygcode/utils.py b/pygcode/utils.py index 00df8ab..6c9216a 100644 --- a/pygcode/utils.py +++ b/pygcode/utils.py @@ -66,8 +66,8 @@ def plane_projection(vect, normal): n = normal.normalized() return vect - (n * vect.dot(n)) -# ==================== GCode Utilities ==================== +# ==================== GCode Utilities ==================== def omit_redundant_modes(gcode_iter): """ Replace redundant machine motion modes with whitespace, diff --git a/scripts/pygcode-normalize.py b/scripts/pygcode-normalize.py index 370796d..adecfd4 100755 --- a/scripts/pygcode-normalize.py +++ b/scripts/pygcode-normalize.py @@ -20,7 +20,7 @@ for pygcode_lib_type in ('installed_lib', 'relative_lib'): from pygcode import GCodeCannedCycle from pygcode import split_gcodes from pygcode import Comment - from pygcode.transform import linearize_arc + from pygcode.transform import linearize_arc, simplify_canned_cycle from pygcode.transform import ArcLinearizeInside, ArcLinearizeOutside, ArcLinearizeMid from pygcode.gcodes import _subclasses from pygcode.utils import omit_redundant_modes @@ -89,13 +89,13 @@ group = parser.add_argument_group( "interpolation (G1), and pauses (or 'dwells', G4)" ) group.add_argument( - '--canned_simplify', '-cs', dest='canned_simplify', + '--canned_expand', '-ce', dest='canned_expand', action='store_const', const=True, default=False, - help="Convert canned cycles into basic linear movements", + help="Expand canned cycles into basic linear movements, and pauses", ) group.add_argument( '---canned_codes', '-cc', dest='canned_codes', default=DEFAULT_CANNED_CODES, - help="List of canned gcodes to simplify, (default is '%s')" % DEFAULT_CANNED_CODES, + help="List of canned gcodes to expand, (default is '%s')" % DEFAULT_CANNED_CODES, ) #parser.add_argument( @@ -194,6 +194,7 @@ for line_str in args.infile[0].readlines(): if args.arc_linearize and any(isinstance(g, GCodeArcMove) for g in effective_gcodes): with split_and_process(effective_gcodes, GCodeArcMove, line.comment) as arc: + print(Comment("linearized arc: %r" % arc)) linearize_params = { 'arc_gcode': arc, 'start_pos': machine.pos, @@ -207,14 +208,18 @@ for line_str in args.infile[0].readlines(): for linear_gcode in omit_redundant_modes(linearize_arc(**linearize_params)): print(linear_gcode) - elif args.canned_simplify and any((g.word in args.canned_codes) for g in effective_gcodes): - (befores, (canned,), afters) = split_gcodes(effective_gcodes, GCodeCannedCycle) - print(Comment('canning simplified: %r' % canned)) - - # TODO: simplify canned things - - print(str(line)) - machine.process_block(line.block) + elif args.canned_expand and any((g.word in args.canned_codes) for g in effective_gcodes): + with split_and_process(effective_gcodes, GCodeCannedCycle, line.comment) as canned: + print(Comment("expanded: %r" % canned)) + simplify_canned_params = { + 'canned_gcode': canned, + 'start_pos': machine.pos, + 'plane': machine.mode.plane_selection, + 'dist_mode': machine.mode.distance, + 'axes': machine.axes, + } + for simplified_gcode in omit_redundant_modes(simplify_canned_cycle(**simplify_canned_params)): + print(simplified_gcode) else: print(str(line)) diff --git a/tests/test_gcodes.py b/tests/test_gcodes.py index 34e4a22..3da46a8 100644 --- a/tests/test_gcodes.py +++ b/tests/test_gcodes.py @@ -11,6 +11,7 @@ add_pygcode_to_path() # Units under test from pygcode import gcodes from pygcode import words +from pygcode import machine from pygcode.exceptions import GCodeWordStrError @@ -139,3 +140,54 @@ class GCodeSplitTests(unittest.TestCase): self.assertTrue(any(isinstance(g, gcodes.GCodeMotion) for g in split[0])) self.assertTrue(isinstance(split[1][0], gcodes.GCodeStartSpindle)) self.assertTrue(any(isinstance(g, gcodes.GCodeSpindleSpeed) for g in split[2])) + + +class GCodeAbsoluteToRelativeDecoratorTests(unittest.TestCase): + + def test_gcodes_abs2rel(self): + # setup gcode testlist + L = gcodes.GCodeLinearMove + R = gcodes.GCodeRapidMove + args = lambda x, y, z: dict(a for a in zip('XYZ', [x,y,z]) if a[1] is not None) + gcode_list = [ + # GCode instances Expected incremental output + (L(**args(0, 0, 0)), L(**args(-10, -20, -30))), + (L(**args(1, 2, 0)), L(**args(1, 2, None))), + (L(**args(3, 4, 0)), L(**args(2, 2, None))), + (R(**args(1, 2, 0)), R(**args(-2, -2, None))), + (R(**args(3, 4, 0)), R(**args(2, 2, None))), + (L(**args(3, 4, 0)), None), + (L(**args(3, 4, 8)), L(**args(None, None, 8))), + ] + + m = machine.Machine() + + # Incremental Output + m.set_mode(gcodes.GCodeAbsoluteDistanceMode()) + m.move_to(X=10, Y=20, Z=30) # initial position (absolute) + m.set_mode(gcodes.GCodeIncrementalDistanceMode()) + + @gcodes._gcodes_abs2rel(m.pos, dist_mode=m.mode.distance, axes=m.axes) + def expecting_rel(): + return [g[0] for g in gcode_list] + + trimmed_expecting_list = [x[1] for x in gcode_list if x[1] is not None] + for (i, g) in enumerate(expecting_rel()): + expected = trimmed_expecting_list[i] + self.assertEqual(type(g), type(expected)) + self.assertEqual(g.word, expected.word) + self.assertEqual(g.params, expected.params) + + # Absolute Output + m.set_mode(gcodes.GCodeAbsoluteDistanceMode()) + m.move_to(X=10, Y=20, Z=30) # initial position + + @gcodes._gcodes_abs2rel(m.pos, dist_mode=m.mode.distance, axes=m.axes) + def expecting_abs(): + return [g[0] for g in gcode_list] + + for (i, g) in enumerate(expecting_abs()): + expected = gcode_list[i][0] # expecting passthrough + self.assertEqual(type(g), type(expected)) + self.assertEqual(g.word, expected.word) + self.assertEqual(g.params, expected.params) diff --git a/tests/test_words.py b/tests/test_words.py index 5152f5d..b5482c1 100644 --- a/tests/test_words.py +++ b/tests/test_words.py @@ -1,7 +1,3 @@ -import sys -import os -import inspect - import unittest # Add relative pygcode to path diff --git a/tests/testutils.py b/tests/testutils.py index f6124aa..dcc494c 100644 --- a/tests/testutils.py +++ b/tests/testutils.py @@ -1,3 +1,4 @@ +# utilities for the testing suite (as opposed to the tests for utils.py) import sys import os import inspect