From 533e957f80b4fae2fcd64d0c0631665af9d3e254 Mon Sep 17 00:00:00 2001 From: Petter Reinholdtsen Date: Mon, 1 Dec 2025 05:23:53 +0100 Subject: [PATCH 01/15] CAM: Print "Show editor" status boolean as string, not integer This get rid of a Python style warning and make the output easier to understand. --- src/Mod/CAM/Path/Post/scripts/fanuc_post.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py index f16c27af16..5670fc7a89 100644 --- a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py +++ b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py @@ -161,7 +161,7 @@ def processArguments(argstring): OUTPUT_LINE_NUMBERS = True if args.no_show_editor: SHOW_EDITOR = False - print("Show editor = %d" % SHOW_EDITOR) + print("Show editor = %s" % SHOW_EDITOR) PRECISION = args.precision if args.preamble is not None: PREAMBLE = args.preamble.replace("\\n", "\n") From ef794c31bd85cd2d5a11df47b8a07e93e8982be3 Mon Sep 17 00:00:00 2001 From: Petter Reinholdtsen Date: Fri, 28 Nov 2025 00:53:38 +0100 Subject: [PATCH 02/15] CAM: Adjusted Fanuc post processing script to not inherit behaviour between calls Reset line number when using --line-numbers. Restore all default values when a command line argument is not used. This helps the test scripts ensure that the arguments passed during one test is the only one taking effect during this test. --- src/Mod/CAM/Path/Post/scripts/fanuc_post.py | 34 ++++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py index 5670fc7a89..27363acb2a 100644 --- a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py +++ b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py @@ -60,7 +60,7 @@ parser.add_argument( action="store_true", help="don't pop up editor before writing output", ) -parser.add_argument("--precision", default="3", help="number of digits of precision, default=3") +parser.add_argument("--precision", help="number of digits of precision, default=3 (mm) or 4 (in)") parser.add_argument( "--preamble", help='set commands to be issued before the first command, default="G17 G54 G40 G49 G80 G90\\n"', @@ -116,11 +116,11 @@ PRECISION = 3 tapSpeed = 0 # Preamble text will appear at the beginning of the GCODE output file. -PREAMBLE = """G17 G54 G40 G49 G80 G90 +DEFAULT_PREAMBLE = """G17 G54 G40 G49 G80 G90 """ # Postamble text will appear following the last operation. -POSTAMBLE = """M05 +DEFAULT_POSTAMBLE = """M05 G17 G54 G90 G80 G40 M6 T0 M2 @@ -150,34 +150,60 @@ def processArguments(argstring): global MODAL global USE_TLO global OUTPUT_DOUBLES + global LINENR try: args = parser.parse_args(shlex.split(argstring)) if args.no_header: OUTPUT_HEADER = False + else: + OUTPUT_HEADER = True if args.no_comments: OUTPUT_COMMENTS = False + else: + OUTPUT_COMMENTS = True if args.line_numbers: OUTPUT_LINE_NUMBERS = True + LINENR = 100 + else: + OUTPUT_LINE_NUMBERS = False if args.no_show_editor: SHOW_EDITOR = False + else: + SHOW_EDITOR = True print("Show editor = %s" % SHOW_EDITOR) - PRECISION = args.precision if args.preamble is not None: PREAMBLE = args.preamble.replace("\\n", "\n") + else: + PREAMBLE = DEFAULT_PREAMBLE if args.postamble is not None: POSTAMBLE = args.postamble.replace("\\n", "\n") + else: + POSTAMBLE = DEFAULT_POSTAMBLE if args.inches: UNITS = "G20" UNIT_SPEED_FORMAT = "in/min" UNIT_FORMAT = "in" PRECISION = 4 + else: + UNITS = "G21" + UNIT_SPEED_FORMAT = "mm/min" + UNIT_FORMAT = "mm" + PRECISION = 3 + if args.precision: + PRECISION = int(args.precision) if args.no_modal: MODAL = False + else: + MODAL = True if args.no_tlo: USE_TLO = False + else: + USE_TLO = True if args.no_axis_modal: OUTPUT_DOUBLES = True + else: + OUTPUT_DOUBLES = False except Exception: return False From 0cdf9abc4b3af866928960a07fc324c5bd67583d Mon Sep 17 00:00:00 2001 From: Petter Reinholdtsen Date: Thu, 27 Nov 2025 18:58:20 +0100 Subject: [PATCH 03/15] CAM: Adjusted Fanuc post processing script to always start with a percent The percent signal to the machine that a program follows, and is not part of the header but a required part when uploading programs into the machine. --- src/Mod/CAM/Path/Post/scripts/fanuc_post.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py index 27363acb2a..02b921a2a0 100644 --- a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py +++ b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py @@ -230,9 +230,10 @@ def export(objectslist, filename, argstring): print("postprocessing...") gcode = "" + gcode += "%\n" + # write header if OUTPUT_HEADER: - gcode += "%\n" gcode += ";\n" gcode += ( os.path.split(filename)[-1] From 87185c8135e6ff8a2b1cb7efda0576208a368def Mon Sep 17 00:00:00 2001 From: Petter Reinholdtsen Date: Wed, 26 Nov 2025 16:43:48 +0100 Subject: [PATCH 04/15] CAM: Made empty spindle at the end optional in Fanuc CAM post processing script Some Fanuc machines do not understand the 'M6 T0' instructions in the preamble. Move it out of the preamble and controlled by a new command line argument --no-end-spindle-empty for the machines where running to cause a "Tool Number Alarm" and the program to crash as tool zero is not a valid tool. Fixes: #25677 --- src/Mod/CAM/Path/Post/scripts/fanuc_post.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py index 02b921a2a0..070816eb56 100644 --- a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py +++ b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py @@ -67,7 +67,7 @@ parser.add_argument( ) parser.add_argument( "--postamble", - help='set commands to be issued after the last command, default="M05\\nG17 G54 G90 G80 G40\\nM6 T0\\nM2\\n"', + help='set commands to be issued after the last command, default="M05\\nG17 G54 G90 G80 G40\\nM2\\n"', ) parser.add_argument( "--inches", action="store_true", help="Convert output for US imperial mode (G20)" @@ -85,6 +85,11 @@ parser.add_argument( action="store_true", help="suppress tool length offset (G43) following tool changes", ) +parser.add_argument( + "--no-end-spindle-empty", + action="store_true", + help="suppress putting last tool in tool change carousel before postamble", +) TOOLTIP_ARGS = parser.format_help() @@ -101,6 +106,8 @@ OUTPUT_DOUBLES = ( COMMAND_SPACE = " " LINENR = 100 # line number starting value +END_SPINDLE_EMPTY = True + # These globals will be reflected in the Machine configuration of the project UNITS = "G21" # G21 for metric, G20 for us standard UNIT_SPEED_FORMAT = "mm/min" @@ -122,7 +129,6 @@ DEFAULT_PREAMBLE = """G17 G54 G40 G49 G80 G90 # Postamble text will appear following the last operation. DEFAULT_POSTAMBLE = """M05 G17 G54 G90 G80 G40 -M6 T0 M2 """ @@ -149,6 +155,7 @@ def processArguments(argstring): global UNIT_FORMAT global MODAL global USE_TLO + global END_SPINDLE_EMPTY global OUTPUT_DOUBLES global LINENR @@ -204,6 +211,10 @@ def processArguments(argstring): OUTPUT_DOUBLES = True else: OUTPUT_DOUBLES = False + if args.no_end_spindle_empty: + END_SPINDLE_EMPTY = False + else: + END_SPINDLE_EMPTY = True except Exception: return False @@ -294,6 +305,10 @@ def export(objectslist, filename, argstring): gcode += linenumber() + "(COOLANT OFF:" + coolantMode.upper() + ")\n" gcode += linenumber() + "M9" + "\n" + if END_SPINDLE_EMPTY: + if OUTPUT_COMMENTS: + gcode += "(BEGIN MAKING SPINDLE EMPTY)\n" + gcode += "M05\nM6 T0\n" # do the post_amble if OUTPUT_COMMENTS: gcode += "(BEGIN POSTAMBLE)\n" From 06fe4b37bac410d0c96bf3003eff4f6b8cf3fd5b Mon Sep 17 00:00:00 2001 From: Petter Reinholdtsen Date: Sun, 7 Dec 2025 17:33:23 +0100 Subject: [PATCH 05/15] CAM: Switch Fanuc post processor default for M6T0 to disable by default. --- src/Mod/CAM/Path/Post/scripts/fanuc_post.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py index 070816eb56..fdfda5e29d 100644 --- a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py +++ b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py @@ -86,9 +86,9 @@ parser.add_argument( help="suppress tool length offset (G43) following tool changes", ) parser.add_argument( - "--no-end-spindle-empty", + "--end-spindle-empty", action="store_true", - help="suppress putting last tool in tool change carousel before postamble", + help="place last tool in tool change carousel before postamble", ) TOOLTIP_ARGS = parser.format_help() @@ -106,7 +106,7 @@ OUTPUT_DOUBLES = ( COMMAND_SPACE = " " LINENR = 100 # line number starting value -END_SPINDLE_EMPTY = True +END_SPINDLE_EMPTY = False # These globals will be reflected in the Machine configuration of the project UNITS = "G21" # G21 for metric, G20 for us standard @@ -211,10 +211,10 @@ def processArguments(argstring): OUTPUT_DOUBLES = True else: OUTPUT_DOUBLES = False - if args.no_end_spindle_empty: - END_SPINDLE_EMPTY = False - else: + if args.end_spindle_empty: END_SPINDLE_EMPTY = True + else: + END_SPINDLE_EMPTY = False except Exception: return False @@ -308,7 +308,10 @@ def export(objectslist, filename, argstring): if END_SPINDLE_EMPTY: if OUTPUT_COMMENTS: gcode += "(BEGIN MAKING SPINDLE EMPTY)\n" - gcode += "M05\nM6 T0\n" + gcode += linenumber() + "M05\n" + for line in TOOL_CHANGE.splitlines(True): + gcode += linenumber() + line + gcode += linenumber() + "M6 T0\n" # do the post_amble if OUTPUT_COMMENTS: gcode += "(BEGIN POSTAMBLE)\n" From 3c5f39c0caf084bb88779cf105b54e09ea120a6a Mon Sep 17 00:00:00 2001 From: Petter Reinholdtsen Date: Sat, 29 Nov 2025 17:49:30 +0100 Subject: [PATCH 06/15] CAM: Corrected Fanuc post processor M3 handling The thread tapping implementation in the Fanuc post processor change behaviour of M3, G81 and G82 when the tool ShapeID matches "tap". but the code not not expect that the parse() method will be called with two different classes as arguments. Rewrite the M3 handling to handle ToolController arguments instead of crashing with an AttributeError. Issues: Fixes #14016 Fixes #25723 --- src/Mod/CAM/Path/Post/scripts/fanuc_post.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py index fdfda5e29d..97ed5b031c 100644 --- a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py +++ b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py @@ -454,17 +454,19 @@ def parse(pathobj): if command == "G0": continue - # if it's a tap, we rigid tap, so don't start the spindle yet... + # if tool a tap, we thread tap, so stop the spindle for now. + # This only trigger when pathobj is a ToolController. if command == "M03" or command == "M3": - if pathobj.Tool.ShapeID.lower() == "tap": + if hasattr(pathobj, "Tool") and pathobj.Tool.ShapeName.lower() == "tap": tapSpeed = int(pathobj.SpindleSpeed) continue - # convert drill cycles to tap cycles if tool is a tap + # Convert drill cycles to tap cycles if tool is a tap. + # This only trigger when pathobj is a Operation. if command == "G81" or command == "G83": if ( hasattr(pathobj, "ToolController") - and pathobj.ToolController.Tool.ShapeID.lower() == "tap" + and pathobj.ToolController.Tool.ShapeName.lower() == "tap" ): command = "G84" out += linenumber() + "G95\n" From bcb569977d3072ecbdc79cf3430fd48ea26447d9 Mon Sep 17 00:00:00 2001 From: Petter Reinholdtsen Date: Sat, 29 Nov 2025 21:04:59 +0100 Subject: [PATCH 07/15] CAM: Avoid adding trailing space to all M6 lines. The Fanuc post processor add a trailing space to all M6 lines. This make it problematic to write test for the generated output, when compiled with the automatic policy enforcer on github removing trialing space from the expected output. Avoid the problem by removing the trailing space from the generated output. --- src/Mod/CAM/Path/Post/scripts/fanuc_post.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py index 97ed5b031c..ab95d7fc4b 100644 --- a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py +++ b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py @@ -437,6 +437,7 @@ def parse(pathobj): for index, c in enumerate(commands): outstring = [] + outsuffix = [] command = c.Name if index + 1 == len(commands): nextcommand = "" @@ -622,8 +623,7 @@ def parse(pathobj): # add height offset if USE_TLO: - tool_height = "\nG43 H" + str(int(c.Parameters["T"])) - outstring.append(tool_height) + outsuffix.append("G43 H" + str(int(c.Parameters["T"])) if command == "message": if OUTPUT_COMMENTS is False: @@ -637,9 +637,11 @@ def parse(pathobj): outstring.insert(0, (linenumber())) # append the line to the final output - for w in outstring: - out += w.upper() + COMMAND_SPACE + out += COMMAND_SPACE.join(outstring).upper() out = out.strip() + "\n" + if len(outsuffix) >= 1: + for line in outsuffix: + out += linenumber() + line + "\n" return out From e4862572aecd5ca2878c3d199724a5cf3d317932 Mon Sep 17 00:00:00 2001 From: Petter Reinholdtsen Date: Mon, 1 Dec 2025 04:28:45 +0100 Subject: [PATCH 08/15] CAM: Avoid Z overtravel error on Fanuc tool changes Enabling tool height compensation will cause the axis to move up the length of the tool, which will cause a Z overtravel error when the tool change take place close to the top of the machine. To counter this move, ask the machine to move its commanded position down the length of the tool height, which in effect causes no upward movement after the tool change. The #4120 variable contain the current tool number, and #2000 - #20XX contain the tool heights. --- src/Mod/CAM/Path/Post/scripts/fanuc_post.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py index ab95d7fc4b..48b020c8a3 100644 --- a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py +++ b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py @@ -623,7 +623,8 @@ def parse(pathobj): # add height offset if USE_TLO: - outsuffix.append("G43 H" + str(int(c.Parameters["T"])) + outsuffix.append("G91 G0 G43 G54 Z-[#[2000+#4120]] H#4120") + outsuffix.append("G90") if command == "message": if OUTPUT_COMMENTS is False: From 4c18ac6c5480f741f0a2ad1594e8aa67a23b141f Mon Sep 17 00:00:00 2001 From: Petter Reinholdtsen Date: Mon, 1 Dec 2025 05:39:44 +0100 Subject: [PATCH 09/15] CAM: Provide correct and more relevant header from Fanuc post processor Fanuc only understand upper case letters, no point in providing the file name in lower case letters. The provided file name is always "-", so drop it completely. The first comment is presented in the Fanuc user interface, it should get more relevant content. Dropped useless semicolon. --- src/Mod/CAM/Path/Post/scripts/fanuc_post.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py index 48b020c8a3..ad10f3bf86 100644 --- a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py +++ b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py @@ -245,17 +245,16 @@ def export(objectslist, filename, argstring): # write header if OUTPUT_HEADER: - gcode += ";\n" + # Get current version info + major = int(FreeCAD.ConfigGet("BuildVersionMajor")) + minor = int(FreeCAD.ConfigGet("BuildVersionMinor")) + + # the filename variable always contain "-", so unable to + # provide more accurate information. + gcode += "(" + "FREECAD-FILENAME-GOES-HERE" + ", " + "JOB-NAME-GOES-HERE" + ")\n" gcode += ( - os.path.split(filename)[-1] - + " (" - + "FREECAD-FILENAME-GOES-HERE" - + ", " - + "JOB-NAME-GOES-HERE" - + ")\n" + linenumber() + "(POST PROCESSOR: FANUC USING FREECAD %d.%d" % (major, minor) + ")\n" ) - gcode += linenumber() + "(" + filename.upper() + ",EXPORTED BY FREECAD!)\n" - gcode += linenumber() + "(POST PROCESSOR: " + __name__.upper() + ")\n" gcode += linenumber() + "(OUTPUT TIME:" + str(now).upper() + ")\n" # Write the preamble From 8b500ca9a36c7e12e2f2f63161b06a669b237cdd Mon Sep 17 00:00:00 2001 From: Petter Reinholdtsen Date: Mon, 1 Dec 2025 06:29:51 +0100 Subject: [PATCH 10/15] CAM: Adjusted Fanuc post processor to use M05 consistently everywhere. --- src/Mod/CAM/Path/Post/scripts/fanuc_post.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py index ad10f3bf86..c32950c417 100644 --- a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py +++ b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py @@ -616,7 +616,7 @@ def parse(pathobj): # Check for Tool Change: if command == "M6": # stop the spindle - out += linenumber() + "M5\n" + out += linenumber() + "M05\n" for line in TOOL_CHANGE.splitlines(True): out += linenumber() + line From 34a53018624ffdddf0eecbcdbbec22141353a7bf Mon Sep 17 00:00:00 2001 From: Petter Reinholdtsen Date: Mon, 1 Dec 2025 10:23:40 +0100 Subject: [PATCH 11/15] CAM: Switched Fanuc post processor to end program with M30, not M2. The difference according to the documentation is that M30 will rewind the paper tape while M2 will not. The effect on a CNC from 1994 is that M30 turn on the indicator light marking that the program has completed, while M2 do not, and the light is wanted to know when the machine is done. --- src/Mod/CAM/Path/Post/scripts/fanuc_post.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py index c32950c417..83d0707090 100644 --- a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py +++ b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py @@ -67,7 +67,7 @@ parser.add_argument( ) parser.add_argument( "--postamble", - help='set commands to be issued after the last command, default="M05\\nG17 G54 G90 G80 G40\\nM2\\n"', + help='set commands to be issued after the last command, default="M05\\nG17 G54 G90 G80 G40\\nM30\\n"', ) parser.add_argument( "--inches", action="store_true", help="Convert output for US imperial mode (G20)" @@ -129,7 +129,7 @@ DEFAULT_PREAMBLE = """G17 G54 G40 G49 G80 G90 # Postamble text will appear following the last operation. DEFAULT_POSTAMBLE = """M05 G17 G54 G90 G80 G40 -M2 +M30 """ # Pre operation text will be inserted before every operation From 38c3eebd7ab81756b6d6d29039c375f0d08dbc9d Mon Sep 17 00:00:00 2001 From: Petter Reinholdtsen Date: Mon, 1 Dec 2025 08:22:15 +0100 Subject: [PATCH 12/15] CAM: Made Fanuc post processor compatible with FreeCAD 1.0. Several methods introduced 2025-05-04 are preferred, but the methods available in version 1.0 are used as fallback sources for active status and coolent enabled. --- src/Mod/CAM/Path/Post/scripts/fanuc_post.py | 34 +++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py index 83d0707090..d3f53ef8f3 100644 --- a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py +++ b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py @@ -266,8 +266,20 @@ def export(objectslist, filename, argstring): for obj in objectslist: + # to stay compatible with FreeCAD 1.0 + def activeForOp(obj): + # The activeForOp method is available since 2025-05-04 / + # commit 1e87d8e6681b755b9757f94b1201e50eb84b28a2 + if hasattr(PathUtil, "activeForOp"): + return PathUtil.activeForOp(obj) + if hasattr(obj, "Active"): + return obj.Active + if hasattr(obj, "Base") and hasattr(obj.Base, "Active"): + return obj.Base.Active + return True + # Skip inactive operations - if not PathUtil.activeForOp(obj): + if not activeForOp(obj): continue # do the pre_op @@ -277,8 +289,26 @@ def export(objectslist, filename, argstring): for line in PRE_OPERATION.splitlines(True): gcode += linenumber() + line + # to stay compatible with FreeCAD 1.0 + def coolantModeForOp(obj): + # The coolantModeForOp method is available since + # 2025-05-04 / commit + # 1e87d8e6681b755b9757f94b1201e50eb84b28a2 + if hasattr(PathUtil, "coolantModeForOp"): + return PathUtil.coolantModeForOp(obj) + if ( + hasattr(obj, "CoolantMode") + or hasattr(obj, "Base") + and hasattr(obj.Base, "CoolantMode") + ): + if hasattr(obj, "CoolantMode"): + return obj.CoolantMode + else: + return obj.Base.CoolantMode + return "None" + # get coolant mode - coolantMode = PathUtil.coolantModeForOp(obj) + coolantMode = coolantModeForOp(obj) # turn coolant on if required if OUTPUT_COMMENTS: From e6c95dbb9158dd48957b073446dd1784eba6ac52 Mon Sep 17 00:00:00 2001 From: Petter Reinholdtsen Date: Wed, 26 Nov 2025 22:54:31 +0100 Subject: [PATCH 13/15] CAM: Added test of Fanuc post processor Used TestMach3Mach4LegacyPost.py as the starting point with input from other test scripts too. The test demonstrate a fix for #25723, as the Fanuc post processor has not been not updated to the latest changes in the Path module. This is related to #24676, where thread tapping was added to LinuxCNC, but the equivalent code in the Fanuc postprocessor were not adjusted to cope, and #25677 where it was observed that the Fanuc postprocessor was broken in versions 0.20 and 1.0. Added MockTool class to provide ShapeName to fanuc_post.py, used to convert drill cycles to threading cycles if the tool has shapeid "tap". --- src/Mod/CAM/CAMTests/PostTestMocks.py | 6 + src/Mod/CAM/CAMTests/TestFanucPost.py | 289 ++++++++++++++++++++++++++ src/Mod/CAM/CMakeLists.txt | 1 + src/Mod/CAM/TestCAMApp.py | 1 + 4 files changed, 297 insertions(+) create mode 100644 src/Mod/CAM/CAMTests/TestFanucPost.py diff --git a/src/Mod/CAM/CAMTests/PostTestMocks.py b/src/Mod/CAM/CAMTests/PostTestMocks.py index 479d0e7080..c0b7506de0 100644 --- a/src/Mod/CAM/CAMTests/PostTestMocks.py +++ b/src/Mod/CAM/CAMTests/PostTestMocks.py @@ -28,6 +28,11 @@ without requiring disk I/O or loading actual FreeCAD documents. import Path +class MockTool: + def __init__(self): + self.ShapeName = "endmill" + + class MockToolController: """Mock ToolController for operations.""" @@ -38,6 +43,7 @@ class MockToolController: spindle_speed=1000, spindle_dir="Forward", ): + self.Tool = MockTool() self.ToolNumber = tool_number self.Label = label self.SpindleSpeed = spindle_speed diff --git a/src/Mod/CAM/CAMTests/TestFanucPost.py b/src/Mod/CAM/CAMTests/TestFanucPost.py new file mode 100644 index 0000000000..6ae30997ac --- /dev/null +++ b/src/Mod/CAM/CAMTests/TestFanucPost.py @@ -0,0 +1,289 @@ +# *************************************************************************** +# * Copyright (c) 2022 sliptonic * +# * Copyright (c) 2022 Larry Woestman * +# * Copyright (c) 2025 Petter Reinholdtsen * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program is distributed in the hope that it will be useful, * +# * but WITHOUT ANY WARRANTY; without even the implied warranty of * +# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +# * GNU Library General Public License for more details. * +# * * +# * You should have received a copy of the GNU Library General Public * +# * License along with this program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +import Path +from CAMTests import PathTestUtils +from CAMTests import PostTestMocks +from Path.Post.Processor import PostProcessorFactory + + +Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) +Path.Log.trackModule(Path.Log.thisModule()) + + +class TestFanucPost(PathTestUtils.PathTestBase): + @classmethod + def setUpClass(cls): + """setUpClass()... + This method is called upon instantiation of this test class. Add code + and objects here that are needed for the duration of the test() methods + in this class. In other words, set up the 'global' test environment + here; use the `setUp()` method to set up a 'local' test environment. + This method does not have access to the class `self` reference, but it + is able to call static methods within this same class. + """ + + @classmethod + def tearDownClass(cls): + """tearDownClass()... + This method is called prior to destruction of this test class. Add + code and objects here that cleanup the test environment after the + test() methods in this class have been executed. This method does + not have access to the class `self` reference. This method is able + to call static methods within this same class. + """ + + # Setup and tear down methods called before and after each unit test + def setUp(self): + """setUp()... + This method is called prior to each `test()` method. Add code and + objects here that are needed for multiple `test()` methods. + """ + + # Create mock job with default operation and tool controller + self.job, self.profile_op, self.tool_controller = ( + PostTestMocks.create_default_job_with_operation() + ) + + # Create postprocessor using the mock job + self.post = PostProcessorFactory.get_post_processor(self.job, "fanuc") + + # allow a full length "diff" if an error occurs + self.maxDiff = None + # reinitialize the postprocessor data structures between tests + self.post.reinitialize() + + def tearDown(self): + """tearDown()... + This method is called after each test() method. Add cleanup instructions here. + Such cleanup instructions will likely undo those in the setUp() method. + """ + + def test_empty_path(self): + """Test Output Generation. + Empty path. Produces only the preamble and postable. + """ + + self.profile_op.Path = Path.Path([]) + self.job.PostProcessorArgs = "--no-show-editor" + + # Test generating with header + # Header contains a time stamp that messes up unit testing. + # Only test length of result. + gcode = self.post.export()[0][1] + self.assertEqual(30, len(gcode.splitlines())) + # Test without header + expected = """% +(BEGIN PREAMBLE) +G17 G54 G40 G49 G80 G90 +G21 +(BEGIN OPERATION: TC: DEFAULT TOOL) +(MACHINE UNITS: MM/MIN) +M05 +M6 T1 +G91 G0 G43 G54 Z-[#[2000+#4120]] H#4120 +G90 +M3 S1000 +(FINISH OPERATION: TC: DEFAULT TOOL) +(BEGIN OPERATION: FIXTURE) +(MACHINE UNITS: MM/MIN) +G54 +(FINISH OPERATION: FIXTURE) +(BEGIN OPERATION: PROFILE) +(MACHINE UNITS: MM/MIN) +(FINISH OPERATION: PROFILE) +(BEGIN MAKING SPINDLE EMPTY) +M05 +M6 T0 +(BEGIN POSTAMBLE) +M05 +G17 G54 G90 G80 G40 +M30 +% +""" + + self.profile_op.Path = Path.Path([]) + self.job.PostProcessorArgs = ( + "--no-header --no-show-editor" + # "--no-header --no-comments --no-show-editor --precision=2" + ) + + gcode = self.post.export()[0][1] + self.assertEqual(gcode, expected) + + # test without comments + expected = """% +G17 G54 G40 G49 G80 G90 +G21 +M05 +M6 T1 +G91 G0 G43 G54 Z-[#[2000+#4120]] H#4120 +G90 +M3 S1000 +G54 +M05 +M6 T0 +M05 +G17 G54 G90 G80 G40 +M30 +% +""" + + self.profile_op.Path = Path.Path([]) + self.job.PostProcessorArgs = ( + "--no-header --no-comments --no-show-editor" + # "--no-header --no-comments --no-show-editor --precision=2" + ) + gcode = self.post.export()[0][1] + self.assertEqual(gcode, expected) + + def test_precision(self): + """Test command Generation. + Test Precision + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.profile_op.Path = Path.Path([c]) + self.job.PostProcessorArgs = "--no-header --no-show-editor" + gcode = self.post.export()[0][1] + result = gcode.splitlines()[18] + expected = "G0 X10.000 Y20.000 Z30.000" + self.assertEqual(result, expected) + + self.job.PostProcessorArgs = "--no-header --precision=2 --no-show-editor" + gcode = self.post.export()[0][1] + result = gcode.splitlines()[18] + expected = "G0 X10.00 Y20.00 Z30.00" + self.assertEqual(result, expected) + + def test_line_numbers(self): + """ + Test Line Numbers + """ + c = Path.Command("G0 X10 Y20 Z30") + + self.profile_op.Path = Path.Path([c]) + self.job.PostProcessorArgs = "--no-header --line-numbers --no-show-editor" + gcode = self.post.export()[0][1] + result = gcode.splitlines()[18] + expected = "N280 G0 X10.000 Y20.000 Z30.000" + self.assertEqual(result, expected) + + def test_pre_amble(self): + """ + Test Pre-amble + """ + + self.profile_op.Path = Path.Path([]) + self.job.PostProcessorArgs = ( + "--no-header --no-comments --preamble='G18 G55' --no-show-editor" + ) + gcode = self.post.export()[0][1] + result = gcode.splitlines()[1] + self.assertEqual(result, "G18 G55") + + def test_post_amble(self): + """ + Test Post-amble + """ + self.profile_op.Path = Path.Path([]) + self.job.PostProcessorArgs = ( + "--no-header --no-comments --postamble='G0 Z50\nM30' --no-show-editor" + ) + gcode = self.post.export()[0][1] + self.assertEqual(gcode.splitlines()[-3], "G0 Z50") + self.assertEqual(gcode.splitlines()[-2], "M30") + self.assertEqual(gcode.splitlines()[-1], "%") + + def test_inches(self): + """ + Test inches + """ + + c = Path.Command("G0 X10 Y20 Z30") + self.profile_op.Path = Path.Path([c]) + self.job.PostProcessorArgs = "--no-header --inches --no-show-editor" + gcode = self.post.export()[0][1] + self.assertEqual(gcode.splitlines()[3], "G20") + + result = gcode.splitlines()[18] + expected = "G0 X0.3937 Y0.7874 Z1.1811" + self.assertEqual(result, expected) + + self.job.PostProcessorArgs = "--no-header --inches --precision=2 --no-show-editor" + gcode = self.post.export()[0][1] + result = gcode.splitlines()[18] + expected = "G0 X0.39 Y0.79 Z1.18" + self.assertEqual(result, expected) + + def test_tool_change(self): + """ + Test tool change + """ + c = Path.Command("M6 T1") + c2 = Path.Command("M3 S3000") + self.profile_op.Path = Path.Path([c, c2]) + self.job.PostProcessorArgs = "--no-header --no-show-editor" + gcode = self.post.export()[0][1] + self.assertEqual(gcode.splitlines()[18], "M05") + self.assertEqual(gcode.splitlines()[19], "M6 T1") + self.assertEqual(gcode.splitlines()[20], "G91 G0 G43 G54 Z-[#[2000+#4120]] H#4120") + self.assertEqual(gcode.splitlines()[21], "G90") + self.assertEqual(gcode.splitlines()[22], "M3 S3000") + + # suppress TLO + self.job.PostProcessorArgs = "--no-header --no-tlo --no-show-editor" + gcode = self.post.export()[0][1] + self.assertEqual(gcode.splitlines()[18], "M3 S3000") + + def test_thread_tap(self): + """ + Test threading using drill cycle converted to tapping + """ + + self.tool_controller.Tool.ShapeName = "tap" + c = Path.Command("G0 X10 Y10") + c2 = Path.Command("G81 X10 Y10 Z-10 R20 F1 P1 Q1") + self.profile_op.Path = Path.Path([c, c2]) + self.job.PostProcessorArgs = "--no-header --no-show-editor" + gcode = self.post.export()[0][1] + self.assertEqual(gcode.splitlines()[17], "G0 X10.000 Y10.000") + self.assertEqual(gcode.splitlines()[18], "G95") + self.assertEqual(gcode.splitlines()[19], "M29 S1000") + self.assertEqual(gcode.splitlines()[20], "G84 Z-10.000 R20.000 F1.000 P1.000 Q1.000") + self.assertEqual(gcode.splitlines()[21], "G80") + self.assertEqual(gcode.splitlines()[22], "G94") + + def test_comment(self): + """ + Test comment + """ + + c = Path.Command("(comment)") + + self.profile_op.Path = Path.Path([c]) + self.job.PostProcessorArgs = "--no-header --no-show-editor" + gcode = self.post.export()[0][1] + result = gcode.splitlines()[18] + expected = "(COMMENT)" + self.assertEqual(result, expected) diff --git a/src/Mod/CAM/CMakeLists.txt b/src/Mod/CAM/CMakeLists.txt index 290945d182..d0ed7a1cb2 100644 --- a/src/Mod/CAM/CMakeLists.txt +++ b/src/Mod/CAM/CMakeLists.txt @@ -502,6 +502,7 @@ SET(Tests_SRCS CAMTests/TestCAMSanity.py CAMTests/TestCentroidPost.py CAMTests/TestCentroidLegacyPost.py + CAMTests/TestFanucPost.py CAMTests/TestGenericPost.py CAMTests/TestGrblPost.py CAMTests/TestGrblLegacyPost.py diff --git a/src/Mod/CAM/TestCAMApp.py b/src/Mod/CAM/TestCAMApp.py index 1b6bd06b32..abf4a26933 100644 --- a/src/Mod/CAM/TestCAMApp.py +++ b/src/Mod/CAM/TestCAMApp.py @@ -101,6 +101,7 @@ from CAMTests.TestPathVoronoi import TestPathVoronoi from CAMTests.TestGenericPost import TestGenericPost from CAMTests.TestLinuxCNCPost import TestLinuxCNCPost +from CAMTests.TestFanucPost import TestFanucPost from CAMTests.TestGrblPost import TestGrblPost from CAMTests.TestMassoG3Post import TestMassoG3Post from CAMTests.TestCentroidPost import TestCentroidPost From 8386f1394e4c88e56132e68888076c48c8f3a6ec Mon Sep 17 00:00:00 2001 From: Petter Reinholdtsen Date: Sun, 7 Dec 2025 16:04:12 +0100 Subject: [PATCH 14/15] CAM: Adjusted Fanuc post processor to move Z to tool change position before M6 A "G28 G91 Z0" is needed according to testing and page 195 (6-1-2 Procedure for ATC operation) in to get the spindle into the correct position for a tool change. --- src/Mod/CAM/CAMTests/TestFanucPost.py | 130 ++++++++++++++++---- src/Mod/CAM/Path/Post/scripts/fanuc_post.py | 4 +- 2 files changed, 108 insertions(+), 26 deletions(-) diff --git a/src/Mod/CAM/CAMTests/TestFanucPost.py b/src/Mod/CAM/CAMTests/TestFanucPost.py index 6ae30997ac..3ad8501185 100644 --- a/src/Mod/CAM/CAMTests/TestFanucPost.py +++ b/src/Mod/CAM/CAMTests/TestFanucPost.py @@ -91,7 +91,7 @@ class TestFanucPost(PathTestUtils.PathTestBase): # Header contains a time stamp that messes up unit testing. # Only test length of result. gcode = self.post.export()[0][1] - self.assertEqual(30, len(gcode.splitlines())) + self.assertEqual(28, len(gcode.splitlines())) # Test without header expected = """% (BEGIN PREAMBLE) @@ -100,6 +100,7 @@ G21 (BEGIN OPERATION: TC: DEFAULT TOOL) (MACHINE UNITS: MM/MIN) M05 +G28 G91 Z0 M6 T1 G91 G0 G43 G54 Z-[#[2000+#4120]] H#4120 G90 @@ -112,9 +113,6 @@ G54 (BEGIN OPERATION: PROFILE) (MACHINE UNITS: MM/MIN) (FINISH OPERATION: PROFILE) -(BEGIN MAKING SPINDLE EMPTY) -M05 -M6 T0 (BEGIN POSTAMBLE) M05 G17 G54 G90 G80 G40 @@ -136,14 +134,13 @@ M30 G17 G54 G40 G49 G80 G90 G21 M05 +G28 G91 Z0 M6 T1 G91 G0 G43 G54 Z-[#[2000+#4120]] H#4120 G90 M3 S1000 G54 M05 -M6 T0 -M05 G17 G54 G90 G80 G40 M30 % @@ -157,6 +154,88 @@ M30 gcode = self.post.export()[0][1] self.assertEqual(gcode, expected) + def test_empty_path_spindle_empty(self): + """Test Output Generation. + Empty path. Produces only the preamble and postable. + """ + + self.profile_op.Path = Path.Path([]) + self.job.PostProcessorArgs = "--no-show-editor --end-spindle-empty" + + # Test generating with header + # Header contains a time stamp that messes up unit testing. + # Only test length of result. + gcode = self.post.export()[0][1] + self.assertEqual(32, len(gcode.splitlines())) + # Test without header + expected = """% +(BEGIN PREAMBLE) +G17 G54 G40 G49 G80 G90 +G21 +(BEGIN OPERATION: TC: DEFAULT TOOL) +(MACHINE UNITS: MM/MIN) +M05 +G28 G91 Z0 +M6 T1 +G91 G0 G43 G54 Z-[#[2000+#4120]] H#4120 +G90 +M3 S1000 +(FINISH OPERATION: TC: DEFAULT TOOL) +(BEGIN OPERATION: FIXTURE) +(MACHINE UNITS: MM/MIN) +G54 +(FINISH OPERATION: FIXTURE) +(BEGIN OPERATION: PROFILE) +(MACHINE UNITS: MM/MIN) +(FINISH OPERATION: PROFILE) +(BEGIN MAKING SPINDLE EMPTY) +M05 +G28 G91 Z0 +M6 T0 +(BEGIN POSTAMBLE) +M05 +G17 G54 G90 G80 G40 +M30 +% +""" + + self.profile_op.Path = Path.Path([]) + self.job.PostProcessorArgs = ( + "--no-header --no-show-editor --end-spindle-empty" + # "--no-header --no-comments --no-show-editor --precision=2" + ) + + gcode = self.post.export()[0][1] + self.assertEqual(gcode, expected) + + # test without comments + expected = """% +G17 G54 G40 G49 G80 G90 +G21 +M05 +G28 G91 Z0 +M6 T1 +G91 G0 G43 G54 Z-[#[2000+#4120]] H#4120 +G90 +M3 S1000 +G54 +M05 +G28 G91 Z0 +M6 T0 +M05 +G17 G54 G90 G80 G40 +M30 +% +""" + + self.profile_op.Path = Path.Path([]) + self.job.PostProcessorArgs = ( + "--no-header --no-comments --no-show-editor --end-spindle-empty" + # "--no-header --no-comments --no-show-editor --precision=2" + ) + gcode = self.post.export()[0][1] + self.assertEqual(gcode, expected) + def test_precision(self): """Test command Generation. Test Precision @@ -166,13 +245,13 @@ M30 self.profile_op.Path = Path.Path([c]) self.job.PostProcessorArgs = "--no-header --no-show-editor" gcode = self.post.export()[0][1] - result = gcode.splitlines()[18] + result = gcode.splitlines()[19] expected = "G0 X10.000 Y20.000 Z30.000" self.assertEqual(result, expected) self.job.PostProcessorArgs = "--no-header --precision=2 --no-show-editor" gcode = self.post.export()[0][1] - result = gcode.splitlines()[18] + result = gcode.splitlines()[19] expected = "G0 X10.00 Y20.00 Z30.00" self.assertEqual(result, expected) @@ -185,8 +264,8 @@ M30 self.profile_op.Path = Path.Path([c]) self.job.PostProcessorArgs = "--no-header --line-numbers --no-show-editor" gcode = self.post.export()[0][1] - result = gcode.splitlines()[18] - expected = "N280 G0 X10.000 Y20.000 Z30.000" + result = gcode.splitlines()[19] + expected = "N290 G0 X10.000 Y20.000 Z30.000" self.assertEqual(result, expected) def test_pre_amble(self): @@ -226,13 +305,13 @@ M30 gcode = self.post.export()[0][1] self.assertEqual(gcode.splitlines()[3], "G20") - result = gcode.splitlines()[18] + result = gcode.splitlines()[19] expected = "G0 X0.3937 Y0.7874 Z1.1811" self.assertEqual(result, expected) self.job.PostProcessorArgs = "--no-header --inches --precision=2 --no-show-editor" gcode = self.post.export()[0][1] - result = gcode.splitlines()[18] + result = gcode.splitlines()[19] expected = "G0 X0.39 Y0.79 Z1.18" self.assertEqual(result, expected) @@ -245,16 +324,17 @@ M30 self.profile_op.Path = Path.Path([c, c2]) self.job.PostProcessorArgs = "--no-header --no-show-editor" gcode = self.post.export()[0][1] - self.assertEqual(gcode.splitlines()[18], "M05") - self.assertEqual(gcode.splitlines()[19], "M6 T1") - self.assertEqual(gcode.splitlines()[20], "G91 G0 G43 G54 Z-[#[2000+#4120]] H#4120") - self.assertEqual(gcode.splitlines()[21], "G90") - self.assertEqual(gcode.splitlines()[22], "M3 S3000") + self.assertEqual(gcode.splitlines()[19], "M05") + self.assertEqual(gcode.splitlines()[20], "G28 G91 Z0") + self.assertEqual(gcode.splitlines()[21], "M6 T1") + self.assertEqual(gcode.splitlines()[22], "G91 G0 G43 G54 Z-[#[2000+#4120]] H#4120") + self.assertEqual(gcode.splitlines()[23], "G90") + self.assertEqual(gcode.splitlines()[24], "M3 S3000") # suppress TLO self.job.PostProcessorArgs = "--no-header --no-tlo --no-show-editor" gcode = self.post.export()[0][1] - self.assertEqual(gcode.splitlines()[18], "M3 S3000") + self.assertEqual(gcode.splitlines()[20], "M3 S3000") def test_thread_tap(self): """ @@ -267,12 +347,12 @@ M30 self.profile_op.Path = Path.Path([c, c2]) self.job.PostProcessorArgs = "--no-header --no-show-editor" gcode = self.post.export()[0][1] - self.assertEqual(gcode.splitlines()[17], "G0 X10.000 Y10.000") - self.assertEqual(gcode.splitlines()[18], "G95") - self.assertEqual(gcode.splitlines()[19], "M29 S1000") - self.assertEqual(gcode.splitlines()[20], "G84 Z-10.000 R20.000 F1.000 P1.000 Q1.000") - self.assertEqual(gcode.splitlines()[21], "G80") - self.assertEqual(gcode.splitlines()[22], "G94") + self.assertEqual(gcode.splitlines()[18], "G0 X10.000 Y10.000") + self.assertEqual(gcode.splitlines()[19], "G95") + self.assertEqual(gcode.splitlines()[20], "M29 S1000") + self.assertEqual(gcode.splitlines()[21], "G84 Z-10.000 R20.000 F1.000 P1.000 Q1.000") + self.assertEqual(gcode.splitlines()[22], "G80") + self.assertEqual(gcode.splitlines()[23], "G94") def test_comment(self): """ @@ -284,6 +364,6 @@ M30 self.profile_op.Path = Path.Path([c]) self.job.PostProcessorArgs = "--no-header --no-show-editor" gcode = self.post.export()[0][1] - result = gcode.splitlines()[18] + result = gcode.splitlines()[19] expected = "(COMMENT)" self.assertEqual(result, expected) diff --git a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py index d3f53ef8f3..6a466f474c 100644 --- a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py +++ b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py @@ -139,7 +139,9 @@ PRE_OPERATION = """""" POST_OPERATION = """""" # Tool Change commands will be inserted before a tool change -TOOL_CHANGE = """""" +# Move to tool change Z position +TOOL_CHANGE = """G28 G91 Z0 +""" def processArguments(argstring): From 9c78ced00c67a6c2554d8fbfbeb84282a90d3363 Mon Sep 17 00:00:00 2001 From: Petter Reinholdtsen Date: Tue, 9 Dec 2025 00:02:34 +0100 Subject: [PATCH 15/15] CAM: Made Fanuc post processor compatible with FreeCAD 1.1. Use setPlainText() if available, otherwise use setText(). Workaround for a regression from #23862. --- src/Mod/CAM/Path/Post/scripts/fanuc_post.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py index 6a466f474c..399de11221 100644 --- a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py +++ b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py @@ -352,7 +352,13 @@ def export(objectslist, filename, argstring): if FreeCAD.GuiUp and SHOW_EDITOR: dia = PostUtils.GCodeEditorDialog() - dia.editor.setPlainText(gcode) + + # Workaround for 1.1 while we wait for + # https://github.com/FreeCAD/FreeCAD/pull/26008 to be merged. + if hasattr(dia.editor, "setPlainText"): + dia.editor.setPlainText(gcode) + else: + dia.editor.setText(gcode) result = dia.exec_() if result: final = dia.editor.toPlainText()