diff --git a/src/Mod/CAM/CAMTests/TestSnapmakerPost.py b/src/Mod/CAM/CAMTests/TestSnapmakerPost.py index 117b509ee3..2186482bb9 100644 --- a/src/Mod/CAM/CAMTests/TestSnapmakerPost.py +++ b/src/Mod/CAM/CAMTests/TestSnapmakerPost.py @@ -76,7 +76,7 @@ class TestSnapmakerPost(PathTestUtils.PathTestBase): expected_header = """\ ;Header Start ;header_type: cnc -;machine: Snapmaker 2 A350 +;machine: Snapmaker 2 A350 50W CNC module ;Post Processor: snapmaker_post ;Cam File: boxtest.fcstd ;Output Time: \\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{0,6} @@ -108,7 +108,7 @@ M5 """ # test header and body with comments - gcode = self.get_gcode([], "--machine=A350 --toolhead=50W --spindle-percent") + gcode = self.get_gcode([], "--machine=A350 --toolhead=50W_CNC") g_lines = gcode.splitlines() e_lines = expected_header.splitlines() + expected_body.splitlines() @@ -121,12 +121,12 @@ M5 self.assertEqual(exp, line) # test body without header - gcode = self.get_gcode([], "--machine=A350 --toolhead=50W --spindle-percent --no-header") + gcode = self.get_gcode([], "--machine=A350 --toolhead=50W_CNC --no-header") self.assertEqual(gcode, expected_body) # test body without comments gcode = self.get_gcode( - [], "--machine=A350 --toolhead=50W --spindle-percent --no-header --no-comments" + [], "--machine=A350 --toolhead=50W_CNC --no-header --no-comments" ) expected = "".join( [line for line in expected_body.splitlines(keepends=True) if not line.startswith(";")] @@ -139,7 +139,7 @@ M5 expected = "G0 X10.000 Y20.000 Z30.000" gcode = self.get_gcode( - [command], "--machine=A350 --toolhead=50W --spindle-percent --no-header" + [command], "--machine=A350 --toolhead=50W_CNC --no-header" ) result = gcode.splitlines()[18] self.assertEqual(result, expected) @@ -151,7 +151,7 @@ M5 expected = "G0 X10.00 Y20.00 Z30.00" gcode = self.get_gcode( - [command], "--machine=A350 --toolhead=50W --spindle-percent --no-header --precision=2" + [command], "--machine=A350 --toolhead=50W_CNC --no-header --precision=2" ) result = gcode.splitlines()[18] self.assertEqual(result, expected) @@ -163,7 +163,7 @@ M5 gcode = self.get_gcode( [command], - "--machine=A350 --toolhead=50W --spindle-percent --no-header --line-numbers --line-number=10 --line-increment=2", + "--machine=A350 --toolhead=50W_CNC --no-header --line-numbers --line-number=10 --line-increment=2", ) result = gcode.splitlines()[18] self.assertEqual(result, expected) @@ -172,7 +172,7 @@ M5 """Test Pre-amble""" gcode = self.get_gcode( [], - "--machine=A350 --toolhead=50W --spindle-percent --no-header --preamble='G18 G55' --no-comments", + "--machine=A350 --toolhead=50W_CNC --no-header --preamble='G18 G55' --no-comments", ) result = gcode.splitlines()[0] self.assertEqual(result, "G18 G55") @@ -181,7 +181,7 @@ M5 """Test Post-amble""" gcode = self.get_gcode( [], - "--machine=A350 --toolhead=50W --spindle-percent --no-header --postamble='G0 Z50\nM2' --no-comments", + "--machine=A350 --toolhead=50W_CNC --no-header --postamble='G0 Z50\nM2' --no-comments", ) result = gcode.splitlines()[-2] self.assertEqual(result, "G0 Z50") @@ -195,7 +195,7 @@ M5 # test inches conversion expected = "G0 X0.3937 Y0.7874 Z1.1811" gcode = self.get_gcode( - [command], "--machine=A350 --toolhead=50W --spindle-percent --no-header --inches" + [command], "--machine=A350 --toolhead=50W_CNC --no-header --inches" ) self.assertEqual(gcode.splitlines()[3], "G20") result = gcode.splitlines()[18] @@ -205,7 +205,7 @@ M5 expected = "G0 X0.39 Y0.79 Z1.18" gcode = self.get_gcode( [command], - "--machine=A350 --toolhead=50W --spindle-percent --no-header --inches --precision=2", + "--machine=A350 --toolhead=50W_CNC --no-header --inches --precision=2", ) result = gcode.splitlines()[18] self.assertEqual(result, expected) @@ -218,7 +218,7 @@ M5 expected = "G0 Y30.000" gcode = self.get_gcode( - [c0, c1], "--machine=A350 --toolhead=50W --spindle-percent --no-header --axis-modal" + [c0, c1], "--machine=A350 --toolhead=50W_CNC --no-header --axis-modal" ) result = gcode.splitlines()[19] self.assertEqual(result, expected) @@ -230,7 +230,7 @@ M5 c1 = Path.Command("M3 S3000") gcode = self.get_gcode( - [c0, c1], "--machine=A350 --toolhead=50W --spindle-percent --no-header" + [c0, c1], "--machine=A350 --toolhead=50W_CNC --no-header" ) self.assertEqual(gcode.splitlines()[19:22], ["M5", "M76", "M6 T2"]) self.assertEqual( @@ -256,88 +256,155 @@ M5 gcode = self.get_gcode( [command], - "--machine=Original --toolhead=50W --spindle-percent --no-header", + "--machine=Original --no-header", ) result = gcode.splitlines()[18] self.assertEqual(result, expected) gcode = self.get_gcode( [command], - "--machine=A150 --toolhead=50W --spindle-percent --no-header", + "--machine=A150 --toolhead=50W_CNC --no-header", ) result = gcode.splitlines()[18] self.assertEqual(result, expected) gcode = self.get_gcode( [command], - "--machine=A250 --toolhead=50W --spindle-percent --no-header", + "--machine=A250 --toolhead=50W_CNC --no-header", ) result = gcode.splitlines()[18] self.assertEqual(result, expected) gcode = self.get_gcode( [command], - "--machine=A250T --toolhead=50W --spindle-percent --no-header", + "--machine=A250T --toolhead=50W_CNC --no-header", ) result = gcode.splitlines()[18] self.assertEqual(result, expected) gcode = self.get_gcode( [command], - "--machine=A250T --toolhead=200W --no-header", + "--machine=A250T --toolhead=200W_CNC --no-header", ) result = gcode.splitlines()[18] self.assertEqual(result, expected) gcode = self.get_gcode( [command], - "--machine=A350 --toolhead=50W --spindle-percent --no-header", + "--machine=A350 --toolhead=50W_CNC --no-header", ) result = gcode.splitlines()[18] self.assertEqual(result, expected) gcode = self.get_gcode( [command], - "--machine=A350T --toolhead=50W --spindle-percent --no-header", + "--machine=A350T --toolhead=50W_CNC --no-header", ) result = gcode.splitlines()[18] self.assertEqual(result, expected) gcode = self.get_gcode( [command], - "--machine=A350T --toolhead=200W --no-header", + "--machine=A350T --toolhead=200W_CNC --no-header", ) result = gcode.splitlines()[18] self.assertEqual(result, expected) gcode = self.get_gcode( [command], - "--machine=Artisan --toolhead=200W --no-header", + "--machine=Artisan --no-header", ) result = gcode.splitlines()[18] self.assertEqual(result, expected) - def test_spindle(self): + def test_toolhead_selection(self): + """Test automatic selection of toolhead where appropriate""" + + # check succeeds + command = Path.Command("G0 X10 Y20 Z30") + expected = "G0 X10.000 Y20.000 Z30.000" + + gcode = self.get_gcode( + [command], + "--machine=Original --no-header", + ) + result = gcode.splitlines()[18] + self.assertEqual(result, expected) + self.assertEqual(self.post.values["TOOLHEAD_NAME"],"Original CNC module") + + gcode = self.get_gcode( + [command], + "--machine=A350 --no-header", + ) + self.assertFalse(isinstance(gcode,str)) + + gcode = self.get_gcode( + [command], + "--machine=A350T --no-header", + ) + self.assertFalse(isinstance(gcode,str)) + + # check succeed with artisan (which base is bigger) + gcode = self.get_gcode( + [command], + "--machine=Artisan --no-header --boundaries-check", + ) + result = gcode.splitlines()[18] + self.assertEqual(result, expected) + self.assertEqual(self.post.values["TOOLHEAD_NAME"],"200W CNC module") + + def test_spindle_percent_rpm_auto_select(self): + """Test automatic selection of spindle speed rpm vs percent""" + + command = Path.Command("M3 S2100") + + # test original toolhead + gcode = self.get_gcode( + [command], "--machine=Original --no-header" + ) + self.assertEqual(gcode.splitlines()[18], "M3 P30") + + command = Path.Command("M3 S3600") + + # test 50W toolhead + gcode = self.get_gcode( + [command], "--machine=A350 --toolhead=50W_CNC --no-header" + ) + self.assertEqual(gcode.splitlines()[18], "M3 P30") + + # test 200W toolhead + gcode = self.get_gcode( + [command], "--machine=A350 --toolhead=200W_CNC --no-header" + ) + self.assertEqual(gcode.splitlines()[18], "M3 S3600") + + # test 200W toolhead + gcode = self.get_gcode( + [command], "--machine=Artisan --no-header" + ) + self.assertEqual(gcode.splitlines()[18], "M3 S3600") + + def test_spindle_percent(self): """Test spindle speed conversion from RPM to percents""" command = Path.Command("M3 S3600") # test 50W toolhead gcode = self.get_gcode( - [command], "--machine=A350 --toolhead=50W --spindle-percent --no-header" + [command], "--machine=A350 --toolhead=50W_CNC --spindle-percent --no-header" ) self.assertEqual(gcode.splitlines()[18], "M3 P30") # test 200W toolhead gcode = self.get_gcode( - [command], "--machine=A350 --toolhead=200W --spindle-percent --no-header" + [command], "--machine=A350 --toolhead=200W_CNC --spindle-percent --no-header" ) self.assertEqual(gcode.splitlines()[18], "M3 P20") # test custom spindle speed extrema gcode = self.get_gcode( [command], - "--machine=A350 --toolhead=200W --spindle-percent --no-header --spindle-speeds=3000,4000", + "--machine=A350 --toolhead=200W_CNC --spindle-percent --no-header --spindle-speeds=3000,4000", ) self.assertEqual(gcode.splitlines()[18], "M3 P90") @@ -346,7 +413,7 @@ M5 command = Path.Command("(comment)") gcode = self.get_gcode( - [command], "--machine=A350 --toolhead=50W --spindle-percent --no-header" + [command], "--machine=A350 --toolhead=50W_CNC --spindle-percent --no-header" ) result = gcode.splitlines()[18] expected = ";comment" @@ -360,7 +427,7 @@ M5 gcode = self.get_gcode( [command], - "--machine=A350 --toolhead=50W --spindle-percent --no-header --boundaries-check", + "--machine=A350 --toolhead=50W_CNC --no-header --boundaries-check", ) self.assertTrue(self.post.check_boundaries(gcode.splitlines())) @@ -369,20 +436,20 @@ M5 c1 = Path.Command("G02 Y260") gcode = self.get_gcode( [c0, c1], - "--machine=A350 --toolhead=50W --spindle-percent --no-header --boundaries-check", + "--machine=A350 --toolhead=50W_CNC --no-header --boundaries-check", ) self.assertFalse(self.post.check_boundaries(gcode.splitlines())) # check succeed with artisan (which base is bigger) gcode = self.get_gcode( [c0, c1], - "--machine=Artisan --toolhead=50W --spindle-percent --no-header --boundaries-check", + "--machine=Artisan --no-header --boundaries-check", ) self.assertTrue(self.post.check_boundaries(gcode.splitlines())) # check fails with custom boundaries gcode = self.get_gcode( [c0, c1], - "--machine=A350 --toolhead=50W --spindle-percent --no-header --boundaries-check --boundaries='50,400,10'", + "--machine=A350 --toolhead=50W_CNC --no-header --boundaries-check --boundaries='50,400,10'", ) self.assertFalse(self.post.check_boundaries(gcode.splitlines())) diff --git a/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py b/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py index 31ad9c01ec..e8795ebabf 100644 --- a/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py +++ b/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py @@ -51,47 +51,79 @@ SNAPMAKER_MACHINES = dict( key="Original", name="Snapmaker Original", boundaries=dict(X=90, Y=90, Z=50), + compatible_toolheads={"Original_CNC"}, ), Original_Z_Extension=dict( key="Original_Z_Extension", name="Snapmaker Original with Z extension", boundaries=dict(X=90, Y=90, Z=146), + compatible_toolheads={"Original_CNC"}, ), A150=dict( key="A150", name="Snapmaker 2 A150", boundaries=dict(X=160, Y=160, Z=90), + compatible_toolheads={"50W_CNC"}, ), A250=dict( key="A250", name="Snapmaker 2 A250", boundaries=dict(X=230, Y=250, Z=180), + compatible_toolheads={"50W_CNC", "200W_CNC"}, ), A250T=dict( key="A250T", name="Snapmaker 2 A250T", boundaries=dict(X=230, Y=250, Z=180), + compatible_toolheads={"50W_CNC", "200W_CNC"}, ), A350=dict( key="A350", name="Snapmaker 2 A350", boundaries=dict(X=320, Y=350, Z=275), + compatible_toolheads={"50W_CNC", "200W_CNC"}, ), A350T=dict( key="A350T", name="Snapmaker 2 A350T", boundaries=dict(X=320, Y=350, Z=275), + compatible_toolheads={"50W_CNC", "200W_CNC"}, ), Artisan=dict( key="Artisan", name="Snapmaker Artisan", - boundaries=dict(X=400, Y=400, Z=400), + boundaries=dict(X=400, Y=413, Z=400), + compatible_toolheads={"200W_CNC"}, ), ) +# Could support other types of toolheads (laser, drag knife, 3DP, ...) in the future +# https://wiki.snapmaker.com/en/Snapmaker_Luban/manual/2_supported_gcode_references#m3m4-modified-cnclaser-on SNAPMAKER_TOOLHEADS = { - "50W": dict(name="50W CNC module", min=0, max=12000, percent=True), - "200W": dict(name="200W CNC module", min=8000, max=18000, percent=False), + "Original_CNC": dict( + key="Original_CNC", + name="Original CNC module", + speed_rpm=dict(min=0, max=7000), + boundaries_delta=dict(X=0, Y=0, Z=0), + has_percent=True, + has_speed_s=False, + ), + "50W_CNC": dict( + key="50W_CNC", + name="50W CNC module", + speed_rpm=dict(min=0, max=12000), + boundaries_delta=dict(X=0, Y=0, Z=0), + has_percent=True, + has_speed_s=False, + ), + "200W_CNC": dict( + key="200W_CNC", + name="200W CNC module", + speed_rpm=dict(min=0, max=18000), + boundaries_delta=dict(X=0, Y=-13, Z=0), + has_percent=True, + has_speed_s=True, + ), } @@ -223,16 +255,9 @@ class Snapmaker(Path.Post.Processor.PostProcessor): self.values["BOUNDARIES_CHECK"] = False self.values["MACHINES"] = SNAPMAKER_MACHINES self.values["TOOLHEADS"] = SNAPMAKER_TOOLHEADS - # default toolhead is 50W (the weakest one) - self.values["DEFAULT_TOOLHEAD"] = "50W" - self.values["TOOLHEAD_NAME"] = SNAPMAKER_TOOLHEADS[self.values["DEFAULT_TOOLHEAD"]]["name"] - self.values["SPINDLE_SPEEDS"] = dict( - min=SNAPMAKER_TOOLHEADS[self.values["DEFAULT_TOOLHEAD"]]["min"], - max=SNAPMAKER_TOOLHEADS[self.values["DEFAULT_TOOLHEAD"]]["max"], - ) - self.values["SPINDLE_PERCENT"] = SNAPMAKER_TOOLHEADS[self.values["DEFAULT_TOOLHEAD"]][ - "percent" - ] + self.values["TOOLHEAD_NAME"] = None + self.values["SPINDLE_SPEEDS"] = dict() + self.values["SPINDLE_PERCENT"] = None def snapmaker_init_argument_defaults(self) -> None: """Initialize which arguments (in a pair) are shown as the default argument.""" @@ -245,7 +270,6 @@ class Snapmaker(Path.Post.Processor.PostProcessor): self.argument_defaults["thumbnail"] = True self.argument_defaults["gui"] = True self.argument_defaults["boundaries-check"] = True - self.argument_defaults["spindle-percent"] = True def snapmaker_init_arguments_visible(self) -> None: """Initialize which argument pairs are visible in TOOLTIP_ARGS.""" @@ -349,14 +373,8 @@ class Snapmaker(Path.Post.Processor.PostProcessor): group.add_argument( "--spindle-percent", action="store_true", - default=argument_defaults["spindle-percent"], - help="use percent as toolhead spindle speed unit", - ) - group.add_argument( - "--spindle-rpm", - action="store_false", - dest="spindle_percent", - help="Use RPM as toolhead spindle speed unit", + default=None, + help="use percent as toolhead spindle speed unit (default: use RPM if supported by toolhead, otherwise percent)", ) group.add_argument( @@ -390,33 +408,63 @@ class Snapmaker(Path.Post.Processor.PostProcessor): # The deepcopy is necessary to avoid modifying the boundaries in the MACHINES dict. self.values["BOUNDARIES"] = copy.deepcopy(machine["boundaries"]) - if args.boundaries: # may override machine boundaries, which is expected - self.values["BOUNDARIES"] = args.boundaries - if args.toolhead: + if args.toolhead not in machine["compatible_toolheads"]: + FreeCAD.Console.PrintError( + f"Selected --toolhead={args.toolhead} is not compatible with machine {machine['name']}." + +f" Choose from [{machine['compatible_toolheads']}]\n") + flag = False + return (flag, args) toolhead = self.values["TOOLHEADS"][args.toolhead] - self.values["TOOLHEAD_NAME"] = toolhead["name"] + elif len(machine["compatible_toolheads"]) == 1: + toolhead_key = next(iter(machine["compatible_toolheads"])) + toolhead = self.values["TOOLHEADS"][toolhead_key] else: - FreeCAD.Console.PrintWarning( - f'No toolhead selected, using default ({self.values["TOOLHEAD_NAME"]}). ' - f"Consider adding --toolhead\n" + FreeCAD.Console.PrintError( + f"Machine {machine['name']} has multiple compatible toolheads:\n" + f"{machine['compatible_toolheads']}\n" + "Please add --toolhead argument.\n" ) - toolhead = self.values["TOOLHEADS"][self.values["DEFAULT_TOOLHEAD"]] + flag = False + return (flag, args) + self.values["TOOLHEAD_KEY"] = toolhead["key"] + self.values["TOOLHEAD_NAME"] = toolhead["name"] - self.values["SPINDLE_SPEEDS"] = {key: toolhead[key] for key in ("min", "max")} + self.values["SPINDLE_SPEEDS"] = toolhead["speed_rpm"] if args.spindle_speeds: # may override toolhead value, which is expected self.values["SPINDLE_SPEEDS"] = args.spindle_speeds if args.spindle_percent is not None: - if toolhead["percent"] is True: + if toolhead["has_percent"]: self.values["SPINDLE_PERCENT"] = True - if args.spindle_percent is False: - FreeCAD.Console.PrintWarning( - "Toolhead does not handle RPM spindle speed, using percents instead.\n" - ) else: - self.values["SPINDLE_PERCENT"] = args.spindle_percent + FreeCAD.Console.PrintError( + f"Requested spindle speed in percent, but toolhead {toolhead['name']}" + +" does not support speed as percent.\n" + ) + flag = False + return (flag, args) + else: + # Prefer speed S over percent P + self.values["SPINDLE_PERCENT"] = toolhead['has_percent'] and not toolhead['has_speed_s'] + if self.values["SPINDLE_PERCENT"]: + FreeCAD.Console.PrintWarning( + "Spindle speed will be controlled using using percentages.\n" + ) + else: + FreeCAD.Console.PrintWarning( + "Spindle speed will be controlled using using RPM.\n" + ) + + if args.boundaries: # may override machine boundaries, which is expected + self.values["BOUNDARIES"] = args.boundaries + self.values["MACHINE_NAME"] += " Boundaries overide=" + str(args.boundaries) + else: + # Update machine dimensions based on installed toolhead + for axis in toolhead["boundaries_delta"].keys(): + self.values["BOUNDARIES"][axis] += toolhead["boundaries_delta"][axis] + self.values["MACHINE_NAME"] += " " + toolhead["name"] self.values["THUMBNAIL"] = args.thumbnail self.values["ALLOW_GUI"] = args.gui @@ -539,9 +587,10 @@ class Snapmaker(Path.Post.Processor.PostProcessor): def convert_spindle(self, gcode: List[str]) -> List[str]: """convert spindle speed values from RPM to percent (%) (M3/M4 commands)""" if self.values["SPINDLE_PERCENT"] is False: - return + return gcode - # TODO: check if percentage covers range 0-max (most probable) or min-max (200W has a documented min speed) + # https://wiki.snapmaker.com/en/Snapmaker_Luban/manual/2_supported_gcode_references#m3m4-modified-cnclaser-on + # Speed as percentage in [0,100]% range for index, commandline in enumerate( gcode ): # .split(self.values["END_OF_LINE_CHARACTERS"]):