From 424f7d820c70d3cee816d3985fe67ae2a0854c68 Mon Sep 17 00:00:00 2001 From: jalapenopuzzle <8386278+jalapenopuzzle@users.noreply.github.com> Date: Sat, 29 Mar 2025 10:13:24 +1100 Subject: [PATCH 01/17] CAM: snapmaker fix broken license URL --- src/Mod/CAM/Path/Post/scripts/snapmaker_post.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py b/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py index 7606c18276..a9e206fca7 100644 --- a/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py +++ b/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py @@ -9,7 +9,7 @@ # * Any redistribution must include the specific provision above. * # * * # * You may obtain a copy of the Licence at: * -# * https://joinup.ec.europa.eu/software/page/eupl5 * +# * https://interoperable-europe.ec.europa.eu/collection/eupl/eupl-text-eupl-12 * # * * # * Unless required by applicable law or agreed to in writing, software * # * distributed under the Licence is distributed on an "AS IS" basis, * From 884fca0cc2e5f77e35fd99de74f979a560f3bd61 Mon Sep 17 00:00:00 2001 From: jalapenopuzzle <8386278+jalapenopuzzle@users.noreply.github.com> Date: Sat, 29 Mar 2025 10:53:45 +1100 Subject: [PATCH 02/17] CAM: snapmaker fix lint on formatted strings --- src/Mod/CAM/Path/Post/scripts/snapmaker_post.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py b/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py index a9e206fca7..added9858a 100644 --- a/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py +++ b/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py @@ -171,7 +171,7 @@ class Snapmaker(Path.Post.Processor.PostProcessor): "P", "O", ] - self.values["PREAMBLE"] = f"""G90\nG17""" + self.values["PREAMBLE"] = """G90\nG17""" self.values["PRE_OPERATION"] = """""" self.values["POST_OPERATION"] = """""" self.values["POSTAMBLE"] = """M400\nM5""" @@ -293,14 +293,14 @@ class Snapmaker(Path.Post.Processor.PostProcessor): "--machine", default=None, choices=self.values["MACHINES"].keys(), - help=f"Snapmaker machine", + help=f"Snapmaker machine. Choose from [{self.values['MACHINES'].keys()}].", ) group.add_argument( "--toolhead", default=None, choices=self.values["TOOLHEADS"].keys(), - help=f"Snapmaker toolhead", + help=f"Snapmaker toolhead. Choose from [{self.values['TOOLHEADS'].keys()}].", ) group.add_argument( @@ -375,7 +375,7 @@ class Snapmaker(Path.Post.Processor.PostProcessor): self.values["SPINDLE_PERCENT"] = True if args.spindle_percent is False: FreeCAD.Console.PrintWarning( - f"Toolhead does not handle RPM spindle speed, using percents instead.\n" + "Toolhead does not handle RPM spindle speed, using percents instead.\n" ) else: self.values["SPINDLE_PERCENT"] = args.spindle_percent From 322a2f7b3fd1bbe8d0d8ce0d961ca5c3f9798d52 Mon Sep 17 00:00:00 2001 From: jalapenopuzzle <8386278+jalapenopuzzle@users.noreply.github.com> Date: Sat, 29 Mar 2025 11:01:48 +1100 Subject: [PATCH 03/17] CAM: snapmaker fix lint on types --- .../CAM/Path/Post/scripts/snapmaker_post.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py b/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py index added9858a..49acf4c846 100644 --- a/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py +++ b/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py @@ -26,7 +26,7 @@ import os import pathlib import re import tempfile -from typing import Any +from typing import Any, List, Tuple import FreeCAD import Path @@ -339,7 +339,7 @@ class Snapmaker(Path.Post.Processor.PostProcessor): return parser - def snapmaker_process_arguments(self, filename: str = "-") -> (bool, str | argparse.Namespace): + def snapmaker_process_arguments(self, filename: str = "-") -> Tuple[bool, str | argparse.Namespace]: """Process any arguments to the postprocessor.""" (flag, args) = Path.Post.UtilsArguments.process_shared_arguments( self.values, self.parser, self._job.PostProcessorArgs, self.visible_parser, filename @@ -393,9 +393,9 @@ class Snapmaker(Path.Post.Processor.PostProcessor): return flag, args - def snapmaker_process_postables(self, filename: str = "-") -> [(str, str)]: + def snapmaker_process_postables(self, filename: str = "-") -> List[Tuple[str, str]]: """process job sections to gcode""" - sections: [(str, str)] = list() + sections: List[Tuple[str, str]] = list() postables = self._buildPostList() @@ -468,7 +468,7 @@ class Snapmaker(Path.Post.Processor.PostProcessor): return f"thumbnail: data:image/png;base64,{base64.b64encode(data).decode()}" - def output_header(self, gcode: [[]]): + def output_header(self, gcode: List[str]): """custom method derived from Path.Post.UtilsExport.output_header""" cam_file: str comment: str @@ -498,7 +498,7 @@ class Snapmaker(Path.Post.Processor.PostProcessor): add_comment(f"Output Time: {datetime.datetime.now()}") add_comment(self.get_thumbnail()) - def convert_spindle(self, gcode: [str]) -> [str]: + 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 @@ -518,7 +518,7 @@ class Snapmaker(Path.Post.Processor.PostProcessor): ) return gcode - def check_boundaries(self, gcode: [str]) -> bool: + def check_boundaries(self, gcode: List[str]) -> bool: """Check boundaries and return whether it succeeded""" status = True FreeCAD.Console.PrintLog("Boundaries check\n") @@ -553,10 +553,10 @@ class Snapmaker(Path.Post.Processor.PostProcessor): return status - def export_common(self, objects: list, filename: str | pathlib.Path) -> str: + def export_common(self, objects: List, filename: str | pathlib.Path) -> str: """custom method derived from Path.Post.UtilsExport.export_common""" final: str - gcode: [[]] = [] + gcode: List = [] result: bool for obj in objects: From ac80d6e4df7db2ef382049af8100d97f5c1c844d Mon Sep 17 00:00:00 2001 From: jalapenopuzzle <8386278+jalapenopuzzle@users.noreply.github.com> Date: Sat, 29 Mar 2025 17:58:17 +1100 Subject: [PATCH 04/17] CAM: snapmaker fix invalid escape sequences in regular expressions --- src/Mod/CAM/CAMTests/TestSnapmakerPost.py | 2 +- src/Mod/CAM/Path/Post/scripts/snapmaker_post.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Mod/CAM/CAMTests/TestSnapmakerPost.py b/src/Mod/CAM/CAMTests/TestSnapmakerPost.py index bfa832a438..8cfdae2669 100644 --- a/src/Mod/CAM/CAMTests/TestSnapmakerPost.py +++ b/src/Mod/CAM/CAMTests/TestSnapmakerPost.py @@ -78,7 +78,7 @@ class TestSnapmakerPost(PathTestUtils.PathTestBase): ;machine: Snapmaker 2 A350(T) ;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} +;Output Time: \\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{0,6} ;thumbnail: deactivated.""" expected_body = """\ diff --git a/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py b/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py index 49acf4c846..59dfb7b4ff 100644 --- a/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py +++ b/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py @@ -65,7 +65,7 @@ class CoordinatesAction(argparse.Action): def __call__(self, parser, namespace, values, option_string=None): match = re.match( - "^\s*(?P-?\d+\.?\d*),?\s*(?P-?\d+\.?\d*),?\s*(?P-?\d+\.?\d*)\s*$", values + r"^\s*(?P-?\d+\.?\d*),?\s*(?P-?\d+\.?\d*),?\s*(?P-?\d+\.?\d*)\s*$", values ) if match: # setattr(namespace, self.dest, 'G0 X{0} Y{1} Z{2}'.format(*match.groups())) @@ -79,7 +79,7 @@ class ExtremaAction(argparse.Action): """argparse Action to handle integer extrema (min,max)""" def __call__(self, parser, namespace, values, option_string=None): - if match := re.match("^ *(\d+),? *(\d+) *$", values): + if match := re.match(r"^ *(\d+),? *(\d+) *$", values): # setattr(namespace, self.dest, 'G0 X{0} Y{1} Z{2}'.format(*match.groups())) params = { key: int(value) @@ -507,7 +507,7 @@ class Snapmaker(Path.Post.Processor.PostProcessor): for index, commandline in enumerate( gcode ): # .split(self.values["END_OF_LINE_CHARACTERS"]): - if match := re.match("(?PM0?[34])\D.*(?PS\d+.?\d*)", commandline): + if match := re.match(r"(?PM0?[34])\D.*(?PS\d+.?\d*)", commandline): percent = ( float(match.group("spindle")[1:]) * 100 / self.values["SPINDLE_SPEEDS"]["max"] ) @@ -528,13 +528,13 @@ class Snapmaker(Path.Post.Processor.PostProcessor): relative = False for index, commandline in enumerate(gcode): - if re.match("G90(?:\D|$)", commandline): + if re.match(r"G90(?:\D|$)", commandline): relative = False - elif re.match("G91(?:\D|$)", commandline): + elif re.match(r"G91(?:\D|$)", commandline): relative = True - elif re.match("G0?[12](?:\D|$)", commandline): + elif re.match(r"G0?[12](?:\D|$)", commandline): for axis, value in re.findall( - "(?P[XYZ])(?P-?\d+\.?\d*)(?:\D|$)", commandline + r"(?P[XYZ])(?P-?\d+\.?\d*)(?:\D|$)", commandline ): if relative: position[axis] += float(value) From 1b4b36767db10777cbaff1dfef6bf50c5a7126f0 Mon Sep 17 00:00:00 2001 From: jalapenopuzzle <8386278+jalapenopuzzle@users.noreply.github.com> Date: Sun, 6 Apr 2025 18:27:57 +1000 Subject: [PATCH 05/17] CAM: snapmaker unit tests fix assertTrue() -> assertEqual() The test was NOT functioning as required. assert True does NOT compare the expected line with the generated GCode line. After fixing this, the expected output was updated so that the unit tests pass. --- src/Mod/CAM/CAMTests/TestSnapmakerPost.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Mod/CAM/CAMTests/TestSnapmakerPost.py b/src/Mod/CAM/CAMTests/TestSnapmakerPost.py index 8cfdae2669..a95a11a50f 100644 --- a/src/Mod/CAM/CAMTests/TestSnapmakerPost.py +++ b/src/Mod/CAM/CAMTests/TestSnapmakerPost.py @@ -76,7 +76,7 @@ class TestSnapmakerPost(PathTestUtils.PathTestBase): ;Header Start ;header_type: cnc ;machine: Snapmaker 2 A350(T) -;Post Processor: Snapmaker_post +;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} ;thumbnail: deactivated.""" @@ -117,7 +117,7 @@ M5 if exp.startswith(";Output Time:"): self.assertTrue(re.match(exp, line) is not None) else: - self.assertTrue(line, exp) + self.assertEqual(exp, line) # test body without header gcode = self.get_gcode([], "--machine=A350 --toolhead=50W --spindle-percent --no-header") From b738118718e36e1dee8b45ab4ae604a8eea6a115 Mon Sep 17 00:00:00 2001 From: jalapenopuzzle <8386278+jalapenopuzzle@users.noreply.github.com> Date: Tue, 8 Apr 2025 10:17:19 +1000 Subject: [PATCH 06/17] CAM: snapmaker unit tests fix type lint NOTE get_gcode() can return an argparse.Namespace if something goes wrong with parsing the arguments. This is contrary to the str specification. --- src/Mod/CAM/CAMTests/TestSnapmakerPost.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Mod/CAM/CAMTests/TestSnapmakerPost.py b/src/Mod/CAM/CAMTests/TestSnapmakerPost.py index a95a11a50f..a50782d77f 100644 --- a/src/Mod/CAM/CAMTests/TestSnapmakerPost.py +++ b/src/Mod/CAM/CAMTests/TestSnapmakerPost.py @@ -17,6 +17,7 @@ # * permissions and limitations under the Licence. * # *************************************************************************** import re +from typing import List import FreeCAD @@ -63,7 +64,7 @@ class TestSnapmakerPost(PathTestUtils.PathTestBase): """Unit test tear down""" pass - def get_gcode(self, ops: [str], arguments: str) -> str: + def get_gcode(self, ops: List[str], arguments: str) -> str: """Get postprocessed gcode from a list of operations and postprocessor arguments""" self.profile_op.Path = Path.Path(ops) self.job.PostProcessorArgs = "--no-show-editor --no-gui --no-thumbnail " + arguments From 42f40115a13ae78e7c5bf014480eb82abc6c72d3 Mon Sep 17 00:00:00 2001 From: jalapenopuzzle <8386278+jalapenopuzzle@users.noreply.github.com> Date: Sat, 29 Mar 2025 11:04:48 +1100 Subject: [PATCH 07/17] CAM: snapmaker use order {min,max} --- src/Mod/CAM/Path/Post/scripts/snapmaker_post.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py b/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py index 59dfb7b4ff..1c4b12fccd 100644 --- a/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py +++ b/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py @@ -540,11 +540,11 @@ class Snapmaker(Path.Post.Processor.PostProcessor): position[axis] += float(value) else: position[axis] = float(value) - extrema[axis][0] = max(extrema[axis][0], position[axis]) - extrema[axis][1] = min(extrema[axis][1], position[axis]) + extrema[axis][0] = min(extrema[axis][0], position[axis]) + extrema[axis][1] = max(extrema[axis][1], position[axis]) for axis in extrema.keys(): - if abs(extrema[axis][0] - extrema[axis][1]) > self.values["BOUNDARIES"][axis]: + if abs(extrema[axis][1] - extrema[axis][0]) > self.values["BOUNDARIES"][axis]: # gcode.insert(0, f';WARNING: Boundary check: job exceeds machine limit on {axis} axis{self.values["END_OF_LINE_CHARACTERS"]}') FreeCAD.Console.PrintWarning( f"Boundary check: job exceeds machine limit on {axis} axis\n" From c71bca3d49cdc26d75458b7c80660866f2b5dc28 Mon Sep 17 00:00:00 2001 From: jalapenopuzzle <8386278+jalapenopuzzle@users.noreply.github.com> Date: Sun, 6 Apr 2025 19:11:46 +1000 Subject: [PATCH 08/17] CAM: snapmaker machines separate boundaries --- .../CAM/Path/Post/scripts/snapmaker_post.py | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py b/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py index 1c4b12fccd..b4b9eb2713 100644 --- a/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py +++ b/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py @@ -46,12 +46,30 @@ else: Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) SNAPMAKER_MACHINES = dict( - original=dict(name="Snapmaker Original", X=90, Y=90, Z=50), - original_z_extension=dict(name="Snapmaker Original with Z extension", X=90, Y=90, Z=146), - a150=dict(name="A150", X=160, Y=160, Z=90), - **dict.fromkeys(("A250", "A250T"), dict(name="Snapmaker 2 A250(T)", X=230, Y=250, Z=180)), - **dict.fromkeys(("A350", "A350T"), dict(name="Snapmaker 2 A350(T)", X=320, Y=350, Z=275)), - artisan=dict(name="Snapmaker Artisan", X=400, Y=400, Z=400), + original=dict( + name="Snapmaker Original", + boundaries=dict(X=90, Y=90, Z=50), + ), + original_z_extension=dict( + name="Snapmaker Original with Z extension", + boundaries=dict(X=90, Y=90, Z=146), + ), + a150=dict( + name="A150", + boundaries=dict(X=160, Y=160, Z=90), + ), + **dict.fromkeys(("A250", "A250T"), dict( + name="Snapmaker 2 A250(T)", + boundaries=dict(X=230, Y=250, Z=180), + )), + **dict.fromkeys(("A350", "A350T"), dict( + name="Snapmaker 2 A350(T)", + boundaries=dict(X=320, Y=350, Z=275), + )), + artisan=dict( + name="Snapmaker Artisan", + boundaries=dict(X=400, Y=400, Z=400), + ), ) SNAPMAKER_TOOLHEADS = { @@ -350,7 +368,7 @@ class Snapmaker(Path.Post.Processor.PostProcessor): if args.machine: machine = self.values["MACHINES"][args.machine] self.values["MACHINE_NAME"] = machine["name"] - self.values["BOUNDARIES"] = {key: machine[key] for key in ("X", "Y", "Z")} + self.values["BOUNDARIES"] = machine["boundaries"] if args.boundaries: # may override machine boundaries, which is expected self.values["BOUNDARIES"] = args.boundaries From 94140ab2b167c84b6169a850d803ed8e8b98da08 Mon Sep 17 00:00:00 2001 From: jalapenopuzzle <8386278+jalapenopuzzle@users.noreply.github.com> Date: Sun, 6 Apr 2025 19:31:01 +1000 Subject: [PATCH 09/17] CAM: snapmaker machines separate T machines and capitalise names --- src/Mod/CAM/CAMTests/TestSnapmakerPost.py | 72 ++++++++++++++++++- .../CAM/Path/Post/scripts/snapmaker_post.py | 43 +++++++---- 2 files changed, 101 insertions(+), 14 deletions(-) diff --git a/src/Mod/CAM/CAMTests/TestSnapmakerPost.py b/src/Mod/CAM/CAMTests/TestSnapmakerPost.py index a50782d77f..eb28b107eb 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(T) +;machine: Snapmaker 2 A350 ;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} @@ -237,6 +237,74 @@ M5 gcode.splitlines()[22], "M3 P25" ) # no TLO on Snapmaker (G43 inserted after tool change) + def test_models(self): + """Test the various models.""" + command = Path.Command("G0 X10 Y20 Z30") + expected = "G0 X10.000 Y20.000 Z30.000" + + gcode = self.get_gcode( + [command], + "--machine=Original --toolhead=50W --spindle-percent --no-header", + ) + result = gcode.splitlines()[18] + self.assertEqual(result, expected) + + gcode = self.get_gcode( + [command], + "--machine=A150 --toolhead=50W --spindle-percent --no-header", + ) + result = gcode.splitlines()[18] + self.assertEqual(result, expected) + + gcode = self.get_gcode( + [command], + "--machine=A250 --toolhead=50W --spindle-percent --no-header", + ) + result = gcode.splitlines()[18] + self.assertEqual(result, expected) + + gcode = self.get_gcode( + [command], + "--machine=A250T --toolhead=50W --spindle-percent --no-header", + ) + result = gcode.splitlines()[18] + self.assertEqual(result, expected) + + gcode = self.get_gcode( + [command], + "--machine=A250T --toolhead=200W --no-header", + ) + result = gcode.splitlines()[18] + self.assertEqual(result, expected) + + gcode = self.get_gcode( + [command], + "--machine=A350 --toolhead=50W --spindle-percent --no-header", + ) + result = gcode.splitlines()[18] + self.assertEqual(result, expected) + + gcode = self.get_gcode( + [command], + "--machine=A350T --toolhead=50W --spindle-percent --no-header", + ) + result = gcode.splitlines()[18] + self.assertEqual(result, expected) + + gcode = self.get_gcode( + [command], + "--machine=A350T --toolhead=200W --no-header", + ) + result = gcode.splitlines()[18] + self.assertEqual(result, expected) + + gcode = self.get_gcode( + [command], + "--machine=Artisan --toolhead=200W --no-header", + ) + result = gcode.splitlines()[18] + self.assertEqual(result, expected) + def test_spindle(self): """Test spindle speed conversion from RPM to percents""" @@ -296,7 +364,7 @@ M5 # 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 --toolhead=50W --spindle-percent --no-header --boundaries-check", ) self.assertTrue(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 b4b9eb2713..d724861df1 100644 --- a/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py +++ b/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py @@ -21,6 +21,7 @@ import argparse import base64 +import copy import datetime import os import pathlib @@ -46,27 +47,43 @@ else: Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) SNAPMAKER_MACHINES = dict( - original=dict( + Original=dict( + key="Original", name="Snapmaker Original", boundaries=dict(X=90, Y=90, Z=50), ), - original_z_extension=dict( + Original_Z_Extension=dict( + key="Original_Z_Extension", name="Snapmaker Original with Z extension", boundaries=dict(X=90, Y=90, Z=146), ), - a150=dict( - name="A150", + A150=dict( + key="A150", + name="Snapmaker 2 A150", boundaries=dict(X=160, Y=160, Z=90), ), - **dict.fromkeys(("A250", "A250T"), dict( - name="Snapmaker 2 A250(T)", + A250=dict( + key="A250", + name="Snapmaker 2 A250", boundaries=dict(X=230, Y=250, Z=180), - )), - **dict.fromkeys(("A350", "A350T"), dict( - name="Snapmaker 2 A350(T)", + ), + A250T=dict( + key="A250T", + name="Snapmaker 2 A250T", + boundaries=dict(X=230, Y=250, Z=180), + ), + A350=dict( + key="A350", + name="Snapmaker 2 A350", boundaries=dict(X=320, Y=350, Z=275), - )), - artisan=dict( + ), + A350T=dict( + key="A350T", + name="Snapmaker 2 A350T", + boundaries=dict(X=320, Y=350, Z=275), + ), + Artisan=dict( + key="Artisan", name="Snapmaker Artisan", boundaries=dict(X=400, Y=400, Z=400), ), @@ -367,8 +384,10 @@ class Snapmaker(Path.Post.Processor.PostProcessor): if args.machine: machine = self.values["MACHINES"][args.machine] + self.values["MACHINE_KEY"] = machine["key"] self.values["MACHINE_NAME"] = machine["name"] - self.values["BOUNDARIES"] = machine["boundaries"] + # 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 From 699a25e243fa9d0ee71f541b432371e24b656d93 Mon Sep 17 00:00:00 2001 From: jalapenopuzzle <8386278+jalapenopuzzle@users.noreply.github.com> Date: Tue, 8 Apr 2025 10:29:44 +1000 Subject: [PATCH 10/17] CAM: snapmaker --machine is a required option --- src/Mod/CAM/CAMTests/TestSnapmakerPost.py | 14 +++++++++++++- src/Mod/CAM/Path/Post/scripts/snapmaker_post.py | 15 ++++++++------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/Mod/CAM/CAMTests/TestSnapmakerPost.py b/src/Mod/CAM/CAMTests/TestSnapmakerPost.py index eb28b107eb..117b509ee3 100644 --- a/src/Mod/CAM/CAMTests/TestSnapmakerPost.py +++ b/src/Mod/CAM/CAMTests/TestSnapmakerPost.py @@ -238,10 +238,22 @@ M5 ) # no TLO on Snapmaker (G43 inserted after tool change) def test_models(self): - """Test the various models.""" + """Test the various models, and also test models that don't exist cause an error.""" command = Path.Command("G0 X10 Y20 Z30") expected = "G0 X10.000 Y20.000 Z30.000" + with self.assertRaises(SystemExit): + self.get_gcode( + [command], + "--no-header", + ) + + with self.assertRaises(SystemExit): + gcode = self.get_gcode( + [command], + "--machine=robot --no-header", + ) + gcode = self.get_gcode( [command], "--machine=Original --toolhead=50W --spindle-percent --no-header", diff --git a/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py b/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py index d724861df1..31ad9c01ec 100644 --- a/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py +++ b/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py @@ -179,7 +179,7 @@ class Snapmaker(Path.Post.Processor.PostProcessor): self.values["END_OF_LINE_CHARACTERS"] = "\n" self.values["FINISH_LABEL"] = "End" self.values["LINE_INCREMENT"] = 1 - self.values["MACHINE_NAME"] = "Generic Snapmaker" + self.values["MACHINE_NAME"] = None self.values["MODAL"] = False self.values["OUTPUT_PATH_LABELS"] = True self.values["OUTPUT_HEADER"] = ( @@ -327,6 +327,7 @@ class Snapmaker(Path.Post.Processor.PostProcessor): group.add_argument( "--machine", default=None, + required=True, choices=self.values["MACHINES"].keys(), help=f"Snapmaker machine. Choose from [{self.values['MACHINES'].keys()}].", ) @@ -382,12 +383,12 @@ class Snapmaker(Path.Post.Processor.PostProcessor): if flag: # process extra arguments only if flag is True self._units = self.values["UNITS"] - if args.machine: - 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"]) + # --machine is a required "option" + 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"]) if args.boundaries: # may override machine boundaries, which is expected self.values["BOUNDARIES"] = args.boundaries From 5eaa6326a4a2d3887f41c4e50406b192a40776bd Mon Sep 17 00:00:00 2001 From: jalapenopuzzle <8386278+jalapenopuzzle@users.noreply.github.com> Date: Mon, 7 Apr 2025 00:03:57 +1000 Subject: [PATCH 11/17] CAM: snapmaker toolheads and spindle speeds * Removed generic snapmaker machine and toolhead configuration. The idea of running a CNC code that is not matched to the machine's abilities and work area seems dangerous. * --machine argument is required * --toolhead argument is required when the selected machine is compatible with more than one toolhead. When the selected machine only supports one toolhead, it is selected as the default. * --spindle-percent defaults according to the selected toolhead capabilities. If the toolhead can do RPM, then RPM is used, otherwise falls back to percent. This option now functions as an override. * fixed a bug in convert_spindle() when RPM is selected. The gcode was not returned. --- src/Mod/CAM/CAMTests/TestSnapmakerPost.py | 129 +++++++++++++----- .../CAM/Path/Post/scripts/snapmaker_post.py | 127 +++++++++++------ 2 files changed, 186 insertions(+), 70 deletions(-) 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"]): From 7e54513e5eef2bc4eea03249be3865a1f811a8f0 Mon Sep 17 00:00:00 2001 From: jalapenopuzzle <8386278+jalapenopuzzle@users.noreply.github.com> Date: Mon, 7 Apr 2025 09:42:51 +1000 Subject: [PATCH 12/17] CAM: snapmaker add linear module lead screw pitch --- src/Mod/CAM/Path/Post/scripts/snapmaker_post.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py b/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py index e8795ebabf..5b50f42219 100644 --- a/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py +++ b/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py @@ -52,48 +52,56 @@ SNAPMAKER_MACHINES = dict( name="Snapmaker Original", boundaries=dict(X=90, Y=90, Z=50), compatible_toolheads={"Original_CNC"}, + 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + 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"}, + lead=dict(X=40, Y=40, Z=8), # Linear module screw pitch (mm/turn) ), ) From 42636926ca52bb1499e2ed1bc6c93d97c21945f4 Mon Sep 17 00:00:00 2001 From: jalapenopuzzle <8386278+jalapenopuzzle@users.noreply.github.com> Date: Sat, 29 Mar 2025 12:30:03 +1100 Subject: [PATCH 13/17] CAM: snapmaker clean output_header() --- src/Mod/CAM/CAMTests/TestSnapmakerPost.py | 2 +- src/Mod/CAM/Path/Post/scripts/snapmaker_post.py | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/Mod/CAM/CAMTests/TestSnapmakerPost.py b/src/Mod/CAM/CAMTests/TestSnapmakerPost.py index 2186482bb9..ac0cb9f3e2 100644 --- a/src/Mod/CAM/CAMTests/TestSnapmakerPost.py +++ b/src/Mod/CAM/CAMTests/TestSnapmakerPost.py @@ -78,7 +78,7 @@ class TestSnapmakerPost(PathTestUtils.PathTestBase): ;header_type: cnc ;machine: Snapmaker 2 A350 50W CNC module ;Post Processor: snapmaker_post -;Cam File: boxtest.fcstd +;CAM File: boxtest.fcstd ;Output Time: \\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{0,6} ;thumbnail: deactivated.""" diff --git a/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py b/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py index 5b50f42219..613ffcbf01 100644 --- a/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py +++ b/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py @@ -565,8 +565,6 @@ class Snapmaker(Path.Post.Processor.PostProcessor): def output_header(self, gcode: List[str]): """custom method derived from Path.Post.UtilsExport.output_header""" cam_file: str - comment: str - nl: str = "\n" if not self.values["OUTPUT_HEADER"]: return @@ -580,15 +578,12 @@ class Snapmaker(Path.Post.Processor.PostProcessor): add_comment("Header Start") add_comment("header_type: cnc") add_comment(f'machine: {self.values["MACHINE_NAME"]}') - comment = Path.Post.UtilsParse.create_comment( - self.values, f'Post Processor: {self.values["POSTPROCESSOR_FILE_NAME"]}' - ) - gcode.append(f"{Path.Post.UtilsParse.linenumber(self.values)}{comment}{nl}") + add_comment(f'Post Processor: {self.values["POSTPROCESSOR_FILE_NAME"]}') if FreeCAD.ActiveDocument: cam_file = os.path.basename(FreeCAD.ActiveDocument.FileName) else: cam_file = "" - add_comment(f"Cam File: {cam_file}") + add_comment(f"CAM File: {cam_file}") add_comment(f"Output Time: {datetime.datetime.now()}") add_comment(self.get_thumbnail()) From 9298ad8ad0f37bdb7651ba5ac2919873e2020101 Mon Sep 17 00:00:00 2001 From: jalapenopuzzle <8386278+jalapenopuzzle@users.noreply.github.com> Date: Sat, 29 Mar 2025 13:10:37 +1100 Subject: [PATCH 14/17] CAM: snapmaker add --quick-swap and --bracing-kit options --- src/Mod/CAM/CAMTests/TestSnapmakerPost.py | 124 ++++++++++++++++++ .../CAM/Path/Post/scripts/snapmaker_post.py | 56 +++++++- 2 files changed, 179 insertions(+), 1 deletion(-) diff --git a/src/Mod/CAM/CAMTests/TestSnapmakerPost.py b/src/Mod/CAM/CAMTests/TestSnapmakerPost.py index ac0cb9f3e2..313b871387 100644 --- a/src/Mod/CAM/CAMTests/TestSnapmakerPost.py +++ b/src/Mod/CAM/CAMTests/TestSnapmakerPost.py @@ -16,6 +16,7 @@ # * implied. See the Licence for the specific language governing * # * permissions and limitations under the Licence. * # *************************************************************************** +import argparse import re from typing import List @@ -317,6 +318,129 @@ M5 result = gcode.splitlines()[18] self.assertEqual(result, expected) + def test_mod_kits(self): + """Test the various mod kits against various models.""" + + 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["MOD_KITS_INSTALLED"],[]) + self.assertEqual(self.post.values["BOUNDARIES"],dict(X=90, Y=90, Z=50)) + + gcode = self.get_gcode( + [command], + "--machine=Original --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=Original --bracing-kit --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", + ) + result = gcode.splitlines()[18] + self.assertEqual(result, expected) + self.assertEqual(self.post.values["MOD_KITS_INSTALLED"],[]) + self.assertEqual(self.post.values["BOUNDARIES"],dict(X=400, Y=400, Z=400)) + + gcode = self.get_gcode( + [command], + "--machine=Artisan --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 --bracing-kit --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=A150 --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=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)) + + gcode = self.get_gcode( + [command], + "--machine=A250 --toolhead=50W_CNC --bracing-kit --quick-swap --no-header", + ) + 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)) + + gcode = self.get_gcode( + [command], + "--machine=A250T --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=230, Y=235, Z=165)) + + gcode = self.get_gcode( + [command], + "--machine=A350 --toolhead=50W_CNC --bracing-kit --quick-swap --no-header", + ) + 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)) + + gcode = self.get_gcode( + [command], + "--machine=A350T --toolhead=50W_CNC --bracing-kit --quick-swap --no-header", + ) + 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)) + + gcode = self.get_gcode( + [command], + "--machine=A350T --toolhead=200W_CNC --bracing-kit --quick-swap --no-header", + ) + 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)) + def test_toolhead_selection(self): """Test automatic selection of toolhead where appropriate""" diff --git a/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py b/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py index 613ffcbf01..db0b4c941b 100644 --- a/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py +++ b/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py @@ -46,6 +46,15 @@ if DEBUG := False: else: Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) +def convert_option_to_attr(option_name): + # transforms argparse options into identifiers + if option_name.startswith('--'): + option_name = option_name[2:] + elif option_name.startswith('-'): + option_name = option_name[1:] + + return option_name.replace('-','_') + SNAPMAKER_MACHINES = dict( Original=dict( key="Original", @@ -105,6 +114,27 @@ SNAPMAKER_MACHINES = dict( ), ) +# These modifications were released to upgrade the Snapmaker 2.0 machines +# which started on Kickstarter. +SNAPMAKER_MOD_KITS = { + "QS": dict( + key="QS", + 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), + ), +} + # 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 = { @@ -262,6 +292,7 @@ class Snapmaker(Path.Post.Processor.PostProcessor): self.values["BOUNDARIES"] = None self.values["BOUNDARIES_CHECK"] = False self.values["MACHINES"] = SNAPMAKER_MACHINES + self.values["MOD_KITS_ALL"] = SNAPMAKER_MOD_KITS self.values["TOOLHEADS"] = SNAPMAKER_TOOLHEADS self.values["TOOLHEAD_NAME"] = None self.values["SPINDLE_SPEEDS"] = dict() @@ -353,7 +384,7 @@ class Snapmaker(Path.Post.Processor.PostProcessor): "--boundaries", action=CoordinatesAction, default=None, - help='Custom boundaries (e.g. "100, 200, 300"). Overrides --machine', + help='Custom boundaries (e.g. "100, 200, 300"). Overrides boundaries from --machine', ) group.add_argument( @@ -364,6 +395,14 @@ class Snapmaker(Path.Post.Processor.PostProcessor): help=f"Snapmaker machine. Choose from [{self.values['MACHINES'].keys()}].", ) + for key, value in SNAPMAKER_MOD_KITS.items(): + group.add_argument( + value["option_name"], + default=False, + action="store_true", + help=value["option_help_text"], + ) + group.add_argument( "--toolhead", default=None, @@ -465,10 +504,25 @@ class Snapmaker(Path.Post.Processor.PostProcessor): "Spindle speed will be controlled using using RPM.\n" ) + self.values["MOD_KITS_INSTALLED"] = [] 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 kits + 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) + 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] + # Update machine dimensions based on installed toolhead for axis in toolhead["boundaries_delta"].keys(): self.values["BOUNDARIES"][axis] += toolhead["boundaries_delta"][axis] From c7773b5d4d39ec6030e9d064766c36f39b873a16 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 10:15:00 +0000 Subject: [PATCH 15/17] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/Mod/CAM/CAMTests/TestSnapmakerPost.py | 92 ++++++++----------- .../CAM/Path/Post/scripts/snapmaker_post.py | 47 +++++----- 2 files changed, 64 insertions(+), 75 deletions(-) diff --git a/src/Mod/CAM/CAMTests/TestSnapmakerPost.py b/src/Mod/CAM/CAMTests/TestSnapmakerPost.py index 313b871387..8633d714d5 100644 --- a/src/Mod/CAM/CAMTests/TestSnapmakerPost.py +++ b/src/Mod/CAM/CAMTests/TestSnapmakerPost.py @@ -126,9 +126,7 @@ M5 self.assertEqual(gcode, expected_body) # test body without comments - gcode = self.get_gcode( - [], "--machine=A350 --toolhead=50W_CNC --no-header --no-comments" - ) + gcode = self.get_gcode([], "--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,9 +137,7 @@ M5 command = Path.Command("G0 X10 Y20 Z30") expected = "G0 X10.000 Y20.000 Z30.000" - gcode = self.get_gcode( - [command], "--machine=A350 --toolhead=50W_CNC --no-header" - ) + gcode = self.get_gcode([command], "--machine=A350 --toolhead=50W_CNC --no-header") result = gcode.splitlines()[18] self.assertEqual(result, expected) @@ -195,9 +191,7 @@ M5 # test inches conversion expected = "G0 X0.3937 Y0.7874 Z1.1811" - gcode = self.get_gcode( - [command], "--machine=A350 --toolhead=50W_CNC --no-header --inches" - ) + gcode = self.get_gcode([command], "--machine=A350 --toolhead=50W_CNC --no-header --inches") self.assertEqual(gcode.splitlines()[3], "G20") result = gcode.splitlines()[18] self.assertEqual(result, expected) @@ -230,9 +224,7 @@ M5 c0 = Path.Command("M6 T2") c1 = Path.Command("M3 S3000") - gcode = self.get_gcode( - [c0, c1], "--machine=A350 --toolhead=50W_CNC --no-header" - ) + gcode = self.get_gcode([c0, c1], "--machine=A350 --toolhead=50W_CNC --no-header") self.assertEqual(gcode.splitlines()[19:22], ["M5", "M76", "M6 T2"]) self.assertEqual( gcode.splitlines()[22], "M3 P25" @@ -330,8 +322,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)) + self.assertEqual(self.post.values["MOD_KITS_INSTALLED"], []) + self.assertEqual(self.post.values["BOUNDARIES"], dict(X=90, Y=90, Z=50)) gcode = self.get_gcode( [command], @@ -339,8 +331,8 @@ M5 ) # 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)) + self.assertTrue(isinstance(gcode, argparse.Namespace)) + self.assertFalse(isinstance(gcode, str)) gcode = self.get_gcode( [command], @@ -348,8 +340,8 @@ M5 ) # 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)) + self.assertTrue(isinstance(gcode, argparse.Namespace)) + self.assertFalse(isinstance(gcode, str)) gcode = self.get_gcode( [command], @@ -357,8 +349,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=400, Y=400, Z=400)) + self.assertEqual(self.post.values["MOD_KITS_INSTALLED"], []) + self.assertEqual(self.post.values["BOUNDARIES"], dict(X=400, Y=400, Z=400)) gcode = self.get_gcode( [command], @@ -366,8 +358,8 @@ M5 ) # 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)) + self.assertTrue(isinstance(gcode, argparse.Namespace)) + self.assertFalse(isinstance(gcode, str)) gcode = self.get_gcode( [command], @@ -375,8 +367,8 @@ M5 ) # 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)) + self.assertTrue(isinstance(gcode, argparse.Namespace)) + self.assertFalse(isinstance(gcode, str)) gcode = self.get_gcode( [command], @@ -384,8 +376,8 @@ 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)) + 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], @@ -393,8 +385,8 @@ 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=160, Y=145, Z=75)) + self.assertEqual(self.post.values["MOD_KITS_INSTALLED"], ["QS"]) + self.assertEqual(self.post.values["BOUNDARIES"], dict(X=160, Y=145, Z=75)) gcode = self.get_gcode( [command], @@ -402,8 +394,8 @@ 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["MOD_KITS_INSTALLED"], ["QS", "BK"]) + self.assertEqual(self.post.values["BOUNDARIES"], dict(X=230, Y=223, Z=159)) gcode = self.get_gcode( [command], @@ -411,8 +403,8 @@ 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["MOD_KITS_INSTALLED"], ["QS"]) + self.assertEqual(self.post.values["BOUNDARIES"], dict(X=230, Y=235, Z=165)) gcode = self.get_gcode( [command], @@ -420,8 +412,8 @@ 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["MOD_KITS_INSTALLED"], ["QS", "BK"]) + self.assertEqual(self.post.values["BOUNDARIES"], dict(X=320, Y=323, Z=254)) gcode = self.get_gcode( [command], @@ -429,8 +421,8 @@ 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["MOD_KITS_INSTALLED"], ["QS", "BK"]) + self.assertEqual(self.post.values["BOUNDARIES"], dict(X=320, Y=323, Z=254)) gcode = self.get_gcode( [command], @@ -438,8 +430,8 @@ 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["MOD_KITS_INSTALLED"], ["QS", "BK"]) + self.assertEqual(self.post.values["BOUNDARIES"], dict(X=320, Y=310, Z=254)) def test_toolhead_selection(self): """Test automatic selection of toolhead where appropriate""" @@ -454,19 +446,19 @@ M5 ) result = gcode.splitlines()[18] self.assertEqual(result, expected) - self.assertEqual(self.post.values["TOOLHEAD_NAME"],"Original CNC module") + self.assertEqual(self.post.values["TOOLHEAD_NAME"], "Original CNC module") gcode = self.get_gcode( [command], "--machine=A350 --no-header", ) - self.assertFalse(isinstance(gcode,str)) + self.assertFalse(isinstance(gcode, str)) gcode = self.get_gcode( [command], "--machine=A350T --no-header", ) - self.assertFalse(isinstance(gcode,str)) + self.assertFalse(isinstance(gcode, str)) # check succeed with artisan (which base is bigger) gcode = self.get_gcode( @@ -475,7 +467,7 @@ M5 ) result = gcode.splitlines()[18] self.assertEqual(result, expected) - self.assertEqual(self.post.values["TOOLHEAD_NAME"],"200W CNC module") + 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""" @@ -483,29 +475,21 @@ M5 command = Path.Command("M3 S2100") # test original toolhead - gcode = self.get_gcode( - [command], "--machine=Original --no-header" - ) + 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" - ) + 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" - ) + 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" - ) + gcode = self.get_gcode([command], "--machine=Artisan --no-header") self.assertEqual(gcode.splitlines()[18], "M3 S3600") def test_spindle_percent(self): diff --git a/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py b/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py index db0b4c941b..e06e0559fd 100644 --- a/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py +++ b/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py @@ -46,14 +46,16 @@ if DEBUG := False: else: Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) + def convert_option_to_attr(option_name): # transforms argparse options into identifiers - if option_name.startswith('--'): + if option_name.startswith("--"): option_name = option_name[2:] - elif option_name.startswith('-'): + elif option_name.startswith("-"): option_name = option_name[1:] - - return option_name.replace('-','_') + + return option_name.replace("-", "_") + SNAPMAKER_MACHINES = dict( Original=dict( @@ -61,56 +63,56 @@ SNAPMAKER_MACHINES = dict( name="Snapmaker Original", boundaries=dict(X=90, Y=90, Z=50), compatible_toolheads={"Original_CNC"}, - lead=dict(X=8, Y=8, Z=8), # Linear module screw pitch (mm/turn) + 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"}, - lead=dict(X=8, Y=8, Z=8), # Linear module screw pitch (mm/turn) + 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"}, - lead=dict(X=8, Y=8, Z=8), # Linear module screw pitch (mm/turn) + 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"}, - lead=dict(X=8, Y=8, Z=8), # Linear module screw pitch (mm/turn) + 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"}, - lead=dict(X=20, Y=20, Z=8), # Linear module screw pitch (mm/turn) + 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"}, - lead=dict(X=8, Y=8, Z=8), # Linear module screw pitch (mm/turn) + 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"}, - lead=dict(X=20, Y=20, Z=8), # Linear module screw pitch (mm/turn) + 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"}, - lead=dict(X=40, Y=40, Z=8), # Linear module screw pitch (mm/turn) + lead=dict(X=40, Y=40, Z=8), # Linear module screw pitch (mm/turn) ), ) @@ -122,7 +124,7 @@ 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"}, + compatible_machines={"A150", "A250", "A250T", "A350", "A350T"}, boundaries_delta=dict(X=0, Y=-15, Z=-15), ), "BK": dict( @@ -130,7 +132,7 @@ SNAPMAKER_MOD_KITS = { 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"}, + compatible_machines={"A150", "A250", "A250T", "A350", "A350T"}, boundaries_delta=dict(X=0, Y=-12, Z=-6), ), } @@ -440,7 +442,9 @@ class Snapmaker(Path.Post.Processor.PostProcessor): return parser - def snapmaker_process_arguments(self, filename: str = "-") -> Tuple[bool, str | argparse.Namespace]: + def snapmaker_process_arguments( + self, filename: str = "-" + ) -> Tuple[bool, str | argparse.Namespace]: """Process any arguments to the postprocessor.""" (flag, args) = Path.Post.UtilsArguments.process_shared_arguments( self.values, self.parser, self._job.PostProcessorArgs, self.visible_parser, filename @@ -459,7 +463,8 @@ class Snapmaker(Path.Post.Processor.PostProcessor): 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") + + f" Choose from [{machine['compatible_toolheads']}]\n" + ) flag = False return (flag, args) toolhead = self.values["TOOLHEADS"][args.toolhead] @@ -488,21 +493,21 @@ class Snapmaker(Path.Post.Processor.PostProcessor): else: FreeCAD.Console.PrintError( f"Requested spindle speed in percent, but toolhead {toolhead['name']}" - +" does not support speed as percent.\n" + + " 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'] + 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" - ) + FreeCAD.Console.PrintWarning("Spindle speed will be controlled using using RPM.\n") self.values["MOD_KITS_INSTALLED"] = [] if args.boundaries: # may override machine boundaries, which is expected 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 16/17] 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 From 3cd0a7d2fff9750c9bef7c2a5cefd8bb1d214b2c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 13 Apr 2025 23:21:28 +0000 Subject: [PATCH 17/17] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../CAM/Path/Post/scripts/snapmaker_post.py | 82 ++++++++++--------- 1 file changed, 45 insertions(+), 37 deletions(-) diff --git a/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py b/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py index 94ae74f24c..925f406bba 100644 --- a/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py +++ b/src/Mod/CAM/Path/Post/scripts/snapmaker_post.py @@ -63,7 +63,7 @@ SNAPMAKER_MACHINES = dict( name="Snapmaker Original", 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() ), + 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) ), @@ -72,7 +72,7 @@ SNAPMAKER_MACHINES = dict( name="Snapmaker Original with Z extension", 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() ), + 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) ), @@ -81,8 +81,8 @@ SNAPMAKER_MACHINES = dict( name="Snapmaker 2 A150", 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"} ), + 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) ), @@ -91,12 +91,12 @@ SNAPMAKER_MACHINES = dict( name="Snapmaker 2 A250", 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"} ), + 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) ), @@ -105,12 +105,12 @@ SNAPMAKER_MACHINES = dict( name="Snapmaker 2 A250T", 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"} ), + 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) ), @@ -119,12 +119,12 @@ SNAPMAKER_MACHINES = dict( name="Snapmaker 2 A350", 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"} ), + 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) ), @@ -133,12 +133,12 @@ SNAPMAKER_MACHINES = dict( name="Snapmaker 2 A350T", 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"} ), + 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) ), @@ -146,7 +146,7 @@ SNAPMAKER_MACHINES = dict( key="Artisan", name="Snapmaker Artisan", boundaries_table=[ - dict( boundaries=dict(X=400, Y=400, Z=400), toolhead="200W_CNC", mods=set() ), + 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) ), @@ -489,7 +489,7 @@ class Snapmaker(Path.Post.Processor.PostProcessor): self.values["MACHINE_KEY"] = machine["key"] self.values["MACHINE_NAME"] = machine["name"] - compatible_toolheads = { bt["toolhead"] for bt in machine["boundaries_table"] } + compatible_toolheads = {bt["toolhead"] for bt in machine["boundaries_table"]} if args.toolhead: if args.toolhead not in compatible_toolheads: @@ -546,28 +546,36 @@ class Snapmaker(Path.Post.Processor.PostProcessor): self.values["BOUNDARIES"] = args.boundaries self.values["MACHINE_NAME"] += " Boundaries overide=" + str(args.boundaries) else: - compatible_modkit_combos = [ bt["mods"] for bt in machine["boundaries_table"] if toolhead["key"] == bt["toolhead"] ] + 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"])): - configured_modkits.add( mod_kit["key"] ) + configured_modkits.add(mod_kit["key"]) self.values["MACHINE_NAME"] += " " + mod_kit["name"] self.values["MOD_KITS_INSTALLED"].append(mod_kit["key"]) - + 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}." + + 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 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_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.