From 5f2792968bbd46b72444e754d997970ca49f8274 Mon Sep 17 00:00:00 2001 From: jalapenopuzzle <8386278+jalapenopuzzle@users.noreply.github.com> Date: Mon, 14 Apr 2025 00:26:57 +1000 Subject: [PATCH] CAM: Snapmaker use manufacturer's data table instead of calculating boundary offsets --- src/Mod/CAM/CAMTests/TestSnapmakerPost.py | 108 ++++++++++++---- .../CAM/Path/Post/scripts/snapmaker_post.py | 119 ++++++++++++------ 2 files changed, 167 insertions(+), 60 deletions(-) diff --git a/src/Mod/CAM/CAMTests/TestSnapmakerPost.py b/src/Mod/CAM/CAMTests/TestSnapmakerPost.py index 8633d714d5..a141271b8f 100644 --- a/src/Mod/CAM/CAMTests/TestSnapmakerPost.py +++ b/src/Mod/CAM/CAMTests/TestSnapmakerPost.py @@ -277,7 +277,7 @@ M5 gcode = self.get_gcode( [command], - "--machine=A250T --toolhead=200W_CNC --no-header", + "--machine=A250T --toolhead=200W_CNC --bracing-kit --no-header", ) result = gcode.splitlines()[18] self.assertEqual(result, expected) @@ -298,7 +298,7 @@ M5 gcode = self.get_gcode( [command], - "--machine=A350T --toolhead=200W_CNC --no-header", + "--machine=A350T --toolhead=200W_CNC --bracing-kit --no-header", ) result = gcode.splitlines()[18] self.assertEqual(result, expected) @@ -313,6 +313,11 @@ M5 def test_mod_kits(self): """Test the various mod kits against various models.""" + # Reference for boundaries with the bracing kit and quick swap kit combinations + # [1] https://support.snapmaker.com/hc/en-us/articles/20786910972311-FAQ-for-Bracing-Kit-for-Snapmaker-2-0-Linear-Modules#h_01HN4Z7S9WJE5BRT492WR0CKH1 + # Reference for quick swap kit + # [2] https://support.snapmaker.com/hc/en-us/articles/15320624494103-Pre-sale-FAQ-for-Quick-Swap-Kit + command = Path.Command("G0 X10 Y20 Z30") expected = "G0 X10.000 Y20.000 Z30.000" @@ -323,7 +328,8 @@ M5 result = gcode.splitlines()[18] self.assertEqual(result, expected) self.assertEqual(self.post.values["MOD_KITS_INSTALLED"], []) - self.assertEqual(self.post.values["BOUNDARIES"], dict(X=90, Y=90, Z=50)) + # https://forum.snapmaker.com/t/cnc-work-area-size/5178 + self.assertEqual(self.post.values["BOUNDARIES"], dict(X=125, Y=125, Z=50)) gcode = self.get_gcode( [command], @@ -343,6 +349,16 @@ M5 self.assertTrue(isinstance(gcode, argparse.Namespace)) self.assertFalse(isinstance(gcode, str)) + # This is incompatible according to [2] + gcode = self.get_gcode( + [command], + "--machine=A150 --quick-swap --no-header", + ) + # I don't understand why export returns the arguments + # if snapmaker_process_arguments fails. + self.assertTrue(isinstance(gcode, argparse.Namespace)) + self.assertFalse(isinstance(gcode, str)) + gcode = self.get_gcode( [command], "--machine=Artisan --no-header", @@ -370,6 +386,17 @@ M5 self.assertTrue(isinstance(gcode, argparse.Namespace)) self.assertFalse(isinstance(gcode, str)) + # This test case is covered in reference [1] + gcode = self.get_gcode( + [command], + "--machine=A150 --toolhead=50W_CNC --no-header", + ) + result = gcode.splitlines()[18] + self.assertEqual(result, expected) + self.assertEqual(self.post.values["MOD_KITS_INSTALLED"], []) + self.assertEqual(self.post.values["BOUNDARIES"], dict(X=145, Y=160, Z=90)) + + # This test case is covered in reference [1] gcode = self.get_gcode( [command], "--machine=A150 --toolhead=50W_CNC --bracing-kit --no-header", @@ -377,17 +404,9 @@ M5 result = gcode.splitlines()[18] self.assertEqual(result, expected) self.assertEqual(self.post.values["MOD_KITS_INSTALLED"], ["BK"]) - self.assertEqual(self.post.values["BOUNDARIES"], dict(X=160, Y=148, Z=84)) - - gcode = self.get_gcode( - [command], - "--machine=A150 --toolhead=50W_CNC --quick-swap --no-header", - ) - result = gcode.splitlines()[18] - self.assertEqual(result, expected) - self.assertEqual(self.post.values["MOD_KITS_INSTALLED"], ["QS"]) - self.assertEqual(self.post.values["BOUNDARIES"], dict(X=160, Y=145, Z=75)) + self.assertEqual(self.post.values["BOUNDARIES"], dict(X=145, Y=148, Z=90)) + # This test case is covered in reference [1] gcode = self.get_gcode( [command], "--machine=A250 --toolhead=50W_CNC --bracing-kit --quick-swap --no-header", @@ -395,8 +414,9 @@ M5 result = gcode.splitlines()[18] self.assertEqual(result, expected) self.assertEqual(self.post.values["MOD_KITS_INSTALLED"], ["QS", "BK"]) - self.assertEqual(self.post.values["BOUNDARIES"], dict(X=230, Y=223, Z=159)) + self.assertEqual(self.post.values["BOUNDARIES"], dict(X=230, Y=223, Z=180)) + # This test case is covered in reference [1] gcode = self.get_gcode( [command], "--machine=A250T --toolhead=50W_CNC --quick-swap --no-header", @@ -404,8 +424,39 @@ M5 result = gcode.splitlines()[18] self.assertEqual(result, expected) self.assertEqual(self.post.values["MOD_KITS_INSTALLED"], ["QS"]) - self.assertEqual(self.post.values["BOUNDARIES"], dict(X=230, Y=235, Z=165)) + self.assertEqual(self.post.values["BOUNDARIES"], dict(X=230, Y=235, Z=180)) + # This test case is covered in reference [1] + gcode = self.get_gcode( + [command], + "--machine=A250T --toolhead=200W_CNC --bracing-kit --no-header", + ) + result = gcode.splitlines()[18] + self.assertEqual(result, expected) + self.assertEqual(self.post.values["MOD_KITS_INSTALLED"], ["BK"]) + self.assertEqual(self.post.values["BOUNDARIES"], dict(X=230, Y=225, Z=180)) + + # This test case is covered in reference [1] + gcode = self.get_gcode( + [command], + "--machine=A350 --toolhead=50W_CNC --bracing-kit --no-header", + ) + result = gcode.splitlines()[18] + self.assertEqual(result, expected) + self.assertEqual(self.post.values["MOD_KITS_INSTALLED"], ["BK"]) + self.assertEqual(self.post.values["BOUNDARIES"], dict(X=320, Y=338, Z=275)) + + # This test case is covered in reference [1] + gcode = self.get_gcode( + [command], + "--machine=A350 --toolhead=50W_CNC --quick-swap --no-header", + ) + result = gcode.splitlines()[18] + self.assertEqual(result, expected) + self.assertEqual(self.post.values["MOD_KITS_INSTALLED"], ["QS"]) + self.assertEqual(self.post.values["BOUNDARIES"], dict(X=320, Y=335, Z=275)) + + # This test case is covered in reference [1] gcode = self.get_gcode( [command], "--machine=A350 --toolhead=50W_CNC --bracing-kit --quick-swap --no-header", @@ -413,8 +464,9 @@ M5 result = gcode.splitlines()[18] self.assertEqual(result, expected) self.assertEqual(self.post.values["MOD_KITS_INSTALLED"], ["QS", "BK"]) - self.assertEqual(self.post.values["BOUNDARIES"], dict(X=320, Y=323, Z=254)) + self.assertEqual(self.post.values["BOUNDARIES"], dict(X=320, Y=323, Z=275)) + # This test case is covered in reference [1] gcode = self.get_gcode( [command], "--machine=A350T --toolhead=50W_CNC --bracing-kit --quick-swap --no-header", @@ -422,8 +474,19 @@ M5 result = gcode.splitlines()[18] self.assertEqual(result, expected) self.assertEqual(self.post.values["MOD_KITS_INSTALLED"], ["QS", "BK"]) - self.assertEqual(self.post.values["BOUNDARIES"], dict(X=320, Y=323, Z=254)) + self.assertEqual(self.post.values["BOUNDARIES"], dict(X=320, Y=323, Z=275)) + # This test case is covered in reference [1] + gcode = self.get_gcode( + [command], + "--machine=A350T --toolhead=200W_CNC --bracing-kit --no-header", + ) + result = gcode.splitlines()[18] + self.assertEqual(result, expected) + self.assertEqual(self.post.values["MOD_KITS_INSTALLED"], ["BK"]) + self.assertEqual(self.post.values["BOUNDARIES"], dict(X=320, Y=325, Z=275)) + + # This test case is covered in reference [1] gcode = self.get_gcode( [command], "--machine=A350T --toolhead=200W_CNC --bracing-kit --quick-swap --no-header", @@ -431,7 +494,7 @@ M5 result = gcode.splitlines()[18] self.assertEqual(result, expected) self.assertEqual(self.post.values["MOD_KITS_INSTALLED"], ["QS", "BK"]) - self.assertEqual(self.post.values["BOUNDARIES"], dict(X=320, Y=310, Z=254)) + self.assertEqual(self.post.values["BOUNDARIES"], dict(X=320, Y=310, Z=275)) def test_toolhead_selection(self): """Test automatic selection of toolhead where appropriate""" @@ -485,7 +548,9 @@ M5 self.assertEqual(gcode.splitlines()[18], "M3 P30") # test 200W toolhead - gcode = self.get_gcode([command], "--machine=A350 --toolhead=200W_CNC --no-header") + gcode = self.get_gcode( + [command], "--machine=A350 --toolhead=200W_CNC --bracing-kit --no-header" + ) self.assertEqual(gcode.splitlines()[18], "M3 S3600") # test 200W toolhead @@ -505,14 +570,15 @@ M5 # test 200W toolhead gcode = self.get_gcode( - [command], "--machine=A350 --toolhead=200W_CNC --spindle-percent --no-header" + [command], + "--machine=A350 --toolhead=200W_CNC --bracing-kit --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_CNC --spindle-percent --no-header --spindle-speeds=3000,4000", + "--machine=A350 --toolhead=200W_CNC --bracing-kit --spindle-percent --no-header --spindle-speeds=3000,4000", ) self.assertEqual(gcode.splitlines()[18], "M3 P90") diff --git a/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py b/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py index e06e0559fd..94ae74f24c 100644 --- a/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py +++ b/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py @@ -61,57 +61,93 @@ SNAPMAKER_MACHINES = dict( Original=dict( key="Original", name="Snapmaker Original", - boundaries=dict(X=90, Y=90, Z=50), - compatible_toolheads={"Original_CNC"}, + boundaries_table=[ + # https://forum.snapmaker.com/t/cnc-work-area-size/5178 + dict( boundaries=dict(X=125, Y=125, Z=50), toolhead="Original_CNC", mods=set() ), + ], lead=dict(X=8, Y=8, Z=8), # Linear module screw pitch (mm/turn) ), 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"}, + boundaries_table=[ + # https://forum.snapmaker.com/t/cnc-work-area-size/5178 + dict( boundaries=dict(X=125, Y=125, Z=146), toolhead="Original_CNC", mods=set() ), + ], lead=dict(X=8, Y=8, Z=8), # Linear module screw pitch (mm/turn) ), A150=dict( key="A150", name="Snapmaker 2 A150", - boundaries=dict(X=160, Y=160, Z=90), - compatible_toolheads={"50W_CNC"}, + boundaries_table=[ + # [1] https://support.snapmaker.com/hc/en-us/articles/20786910972311-FAQ-for-Bracing-Kit-for-Snapmaker-2-0-Linear-Modules#h_01HN4Z7S9WJE5BRT492WR0CKH1 + dict( boundaries=dict(X=145, Y=160, Z=90), toolhead="50W_CNC", mods=set() ), + dict( boundaries=dict(X=145, Y=148, Z=90), toolhead="50W_CNC", mods={"BK"} ), + ], lead=dict(X=8, Y=8, Z=8), # Linear module screw pitch (mm/turn) ), A250=dict( key="A250", name="Snapmaker 2 A250", - boundaries=dict(X=230, Y=250, Z=180), - compatible_toolheads={"50W_CNC", "200W_CNC"}, + boundaries_table=[ + # [1] https://support.snapmaker.com/hc/en-us/articles/20786910972311-FAQ-for-Bracing-Kit-for-Snapmaker-2-0-Linear-Modules#h_01HN4Z7S9WJE5BRT492WR0CKH1 + dict( boundaries=dict(X=230, Y=250, Z=180), toolhead="50W_CNC", mods=set() ), + dict( boundaries=dict(X=230, Y=238, Z=180), toolhead="50W_CNC", mods={"BK"} ), + dict( boundaries=dict(X=230, Y=235, Z=180), toolhead="50W_CNC", mods={"QS"} ), + dict( boundaries=dict(X=230, Y=223, Z=180), toolhead="50W_CNC", mods={"BK","QS"} ), + dict( boundaries=dict(X=230, Y=225, Z=180), toolhead="200W_CNC", mods={"BK"} ), + dict( boundaries=dict(X=230, Y=210, Z=180), toolhead="200W_CNC", mods={"BK","QS"} ), + ], lead=dict(X=8, Y=8, Z=8), # Linear module screw pitch (mm/turn) ), A250T=dict( key="A250T", name="Snapmaker 2 A250T", - boundaries=dict(X=230, Y=250, Z=180), - compatible_toolheads={"50W_CNC", "200W_CNC"}, + boundaries_table=[ + # [1] https://support.snapmaker.com/hc/en-us/articles/20786910972311-FAQ-for-Bracing-Kit-for-Snapmaker-2-0-Linear-Modules#h_01HN4Z7S9WJE5BRT492WR0CKH1 + dict( boundaries=dict(X=230, Y=250, Z=180), toolhead="50W_CNC", mods=set() ), + dict( boundaries=dict(X=230, Y=238, Z=180), toolhead="50W_CNC", mods={"BK"} ), + dict( boundaries=dict(X=230, Y=235, Z=180), toolhead="50W_CNC", mods={"QS"} ), + dict( boundaries=dict(X=230, Y=223, Z=180), toolhead="50W_CNC", mods={"BK","QS"} ), + dict( boundaries=dict(X=230, Y=225, Z=180), toolhead="200W_CNC", mods={"BK"} ), + dict( boundaries=dict(X=230, Y=210, Z=180), toolhead="200W_CNC", mods={"BK","QS"} ), + ], lead=dict(X=20, Y=20, Z=8), # Linear module screw pitch (mm/turn) ), A350=dict( key="A350", name="Snapmaker 2 A350", - boundaries=dict(X=320, Y=350, Z=275), - compatible_toolheads={"50W_CNC", "200W_CNC"}, + boundaries_table=[ + # [1] https://support.snapmaker.com/hc/en-us/articles/20786910972311-FAQ-for-Bracing-Kit-for-Snapmaker-2-0-Linear-Modules#h_01HN4Z7S9WJE5BRT492WR0CKH1 + dict( boundaries=dict(X=320, Y=350, Z=275), toolhead="50W_CNC", mods=set() ), + dict( boundaries=dict(X=320, Y=338, Z=275), toolhead="50W_CNC", mods={"BK"} ), + dict( boundaries=dict(X=320, Y=335, Z=275), toolhead="50W_CNC", mods={"QS"} ), + dict( boundaries=dict(X=320, Y=323, Z=275), toolhead="50W_CNC", mods={"BK","QS"} ), + dict( boundaries=dict(X=320, Y=325, Z=275), toolhead="200W_CNC", mods={"BK"} ), + dict( boundaries=dict(X=320, Y=310, Z=275), toolhead="200W_CNC", mods={"BK","QS"} ), + ], lead=dict(X=8, Y=8, Z=8), # Linear module screw pitch (mm/turn) ), A350T=dict( key="A350T", name="Snapmaker 2 A350T", - boundaries=dict(X=320, Y=350, Z=275), - compatible_toolheads={"50W_CNC", "200W_CNC"}, + boundaries_table=[ + # [1] https://support.snapmaker.com/hc/en-us/articles/20786910972311-FAQ-for-Bracing-Kit-for-Snapmaker-2-0-Linear-Modules#h_01HN4Z7S9WJE5BRT492WR0CKH1 + dict( boundaries=dict(X=320, Y=350, Z=275), toolhead="50W_CNC", mods=set() ), + dict( boundaries=dict(X=320, Y=338, Z=275), toolhead="50W_CNC", mods={"BK"} ), + dict( boundaries=dict(X=320, Y=335, Z=275), toolhead="50W_CNC", mods={"QS"} ), + dict( boundaries=dict(X=320, Y=323, Z=275), toolhead="50W_CNC", mods={"BK","QS"} ), + dict( boundaries=dict(X=320, Y=325, Z=275), toolhead="200W_CNC", mods={"BK"} ), + dict( boundaries=dict(X=320, Y=310, Z=275), toolhead="200W_CNC", mods={"BK","QS"} ), + ], lead=dict(X=20, Y=20, Z=8), # Linear module screw pitch (mm/turn) ), Artisan=dict( key="Artisan", name="Snapmaker Artisan", - boundaries=dict(X=400, Y=413, Z=400), - compatible_toolheads={"200W_CNC"}, + boundaries_table=[ + dict( boundaries=dict(X=400, Y=400, Z=400), toolhead="200W_CNC", mods=set() ), + ], lead=dict(X=40, Y=40, Z=8), # Linear module screw pitch (mm/turn) ), ) @@ -124,16 +160,12 @@ SNAPMAKER_MOD_KITS = { name="Quick Swap Kit", option_name="--quick-swap", option_help_text="Indicates that the quick swap kit is installed. Only compatible with Snapmaker 2 machines.", - compatible_machines={"A150", "A250", "A250T", "A350", "A350T"}, - boundaries_delta=dict(X=0, Y=-15, Z=-15), ), "BK": dict( key="BK", name="Bracing Kit", option_name="--bracing-kit", option_help_text="Indicates that the bracing kit is installed. Only compatible with Snapmaker 2 machines.", - compatible_machines={"A150", "A250", "A250T", "A350", "A350T"}, - boundaries_delta=dict(X=0, Y=-12, Z=-6), ), } @@ -456,25 +488,25 @@ class Snapmaker(Path.Post.Processor.PostProcessor): machine = self.values["MACHINES"][args.machine] self.values["MACHINE_KEY"] = machine["key"] self.values["MACHINE_NAME"] = machine["name"] - # The deepcopy is necessary to avoid modifying the boundaries in the MACHINES dict. - self.values["BOUNDARIES"] = copy.deepcopy(machine["boundaries"]) + + compatible_toolheads = { bt["toolhead"] for bt in machine["boundaries_table"] } if args.toolhead: - if args.toolhead not in machine["compatible_toolheads"]: + if args.toolhead not in compatible_toolheads: FreeCAD.Console.PrintError( f"Selected --toolhead={args.toolhead} is not compatible with machine {machine['name']}." - + f" Choose from [{machine['compatible_toolheads']}]\n" + + f" Choose from [{compatible_toolheads}]\n" ) flag = False return (flag, args) toolhead = self.values["TOOLHEADS"][args.toolhead] - elif len(machine["compatible_toolheads"]) == 1: - toolhead_key = next(iter(machine["compatible_toolheads"])) + elif len(compatible_toolheads) == 1: + toolhead_key = next(iter(compatible_toolheads)) toolhead = self.values["TOOLHEADS"][toolhead_key] else: FreeCAD.Console.PrintError( f"Machine {machine['name']} has multiple compatible toolheads:\n" - f"{machine['compatible_toolheads']}\n" + f"{compatible_toolheads}\n" "Please add --toolhead argument.\n" ) flag = False @@ -514,23 +546,32 @@ class Snapmaker(Path.Post.Processor.PostProcessor): self.values["BOUNDARIES"] = args.boundaries self.values["MACHINE_NAME"] += " Boundaries overide=" + str(args.boundaries) else: - # Update machine dimensions based on installed kits + compatible_modkit_combos = [ bt["mods"] for bt in machine["boundaries_table"] if toolhead["key"] == bt["toolhead"] ] + configured_modkits = set() + + # Determine which mod kits are requested from the options for mod_kit in self.values["MOD_KITS_ALL"].values(): if getattr(args, convert_option_to_attr(mod_kit["option_name"])): - if self.values["MACHINE_KEY"] not in mod_kit["compatible_machines"]: - FreeCAD.Console.PrintError( - f"Machine {machine['name']} is not compatible with {mod_kit["option_name"]}.\n" - ) - flag = False - return (flag, args) + configured_modkits.add( mod_kit["key"] ) self.values["MACHINE_NAME"] += " " + mod_kit["name"] self.values["MOD_KITS_INSTALLED"].append(mod_kit["key"]) - for axis in mod_kit["boundaries_delta"].keys(): - self.values["BOUNDARIES"][axis] += mod_kit["boundaries_delta"][axis] + + if configured_modkits not in compatible_modkit_combos: + FreeCAD.Console.PrintError( + f"Machine {machine['name']} with toolhead {toolhead['name']}" + +f" is not compatible with modkit {configured_modkits if configured_modkits else None}.\n" + +f" Choose from {compatible_modkit_combos}." + ) + flag = False + return (flag, args) - # Update machine dimensions based on installed toolhead - for axis in toolhead["boundaries_delta"].keys(): - self.values["BOUNDARIES"][axis] += toolhead["boundaries_delta"][axis] + # Update machine dimensions based on installed toolhead and mod kits + boundaries_table_entry_l = [ bt for bt in machine["boundaries_table"] if bt["toolhead"] == toolhead["key"] and bt["mods"] == configured_modkits ] + assert(len(boundaries_table_entry_l) == 1) + boundaries_table_entry = boundaries_table_entry_l[0] + + # The deepcopy is necessary to avoid modifying the boundaries in the MACHINES dict. + self.values["BOUNDARIES"] = copy.deepcopy(boundaries_table_entry["boundaries"]) self.values["MACHINE_NAME"] += " " + toolhead["name"] self.values["THUMBNAIL"] = args.thumbnail