From 055e7156d30d21303729960cd2abdbbd8cf48354 Mon Sep 17 00:00:00 2001 From: Petter Reinholdtsen Date: Tue, 24 Feb 2026 22:48:41 +0100 Subject: [PATCH] CAM: Improved Fanuc support (crash, thread tapping, header, python warnings). The current post processor fail completely for any operation because the ShapeName attribute no longer exist. Changed code to look for attributes present in FreeCAD 1.1 and master branch. This fixes #27814. Rewrote thread tapping code to work with new FreeCAD tapping support. Switched thread tapping to use feed in distance per minute (G94) instead of earlier distance per revolution to avoid switching between mm/min and mm/rev for different operations. Adjusted G code output to include FreeCAD body and job information. The first comment in the G code is shown on the machine controller, and should contain useful information for operators to to identify jobs. Fetch the body and job label using findParentJob and insert it into the G code. For some reason the body and job information is not available when called from TestFanucPost.py, so handle case where it is undefined as earlier. This new use of findParentJob exposes error in mock code used in code tests. This is fixed in a different pull request to make this patch easily backportable to the 1.1 branch. Fixed issues with Fanuc post processor discovered by lint. Made sure global variables used are declared. This fixes issue updating the postamble and preamble introduced in ef794c31bd85cd2d5a11df47b8a07e93e8982be3 Added docstrings, adjusted import statements and wrapped long line to keep linter happy. Reformatted code with black for consistent formatting. Also reverted obsolete setText() workaround from commit 9c78ced00c67a6c2554d8fbfbeb84282a90d3363 now that https://github.com/FreeCAD/FreeCAD/pull/26008 is merged. Backport of PR #27960. --- src/Mod/CAM/Path/Post/scripts/fanuc_post.py | 217 ++++++++++++-------- 1 file changed, 132 insertions(+), 85 deletions(-) diff --git a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py index 15cb9e0faa..15af728a16 100644 --- a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py +++ b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py @@ -1,5 +1,11 @@ # SPDX-License-Identifier: LGPL-2.1-or-later +""" + +CAM post processor for CNC machines with a Fanuc controller. + +""" + # *************************************************************************** # * Copyright (c) 2014 sliptonic * # * Copyright (c) 2021 shadowbane1000 * @@ -33,7 +39,7 @@ import shlex import os.path import Path.Base.Util as PathUtil import Path.Post.Utils as PostUtils -import PathScripts.PathUtils as PathUtils +from PathScripts import PathUtils from builtins import open as pyopen TOOLTIP = """ @@ -49,6 +55,16 @@ import fanuc_post fanuc_post.export(object,"/path/to/file.ncc","") """ +# Preamble text will appear at the beginning of the GCODE output file. +DEFAULT_PREAMBLE = """G17 G54 G40 G49 G80 G90 G94 +""" + +# Postamble text will appear following the last operation. +DEFAULT_POSTAMBLE = """M05 +G17 G54 G90 G80 G40 +M30 +""" + now = datetime.datetime.now() parser = argparse.ArgumentParser(prog="fanuc", add_help=False) @@ -63,11 +79,16 @@ parser.add_argument( 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"', + help='set commands to be issued before the first command, default="' + + DEFAULT_PREAMBLE.replace("\n", "\\n") + + '"', ) parser.add_argument( "--postamble", - help='set commands to be issued after the last command, default="M05\\nG17 G54 G90 G80 G40\\nM30\\n"', + help="set commands to be issued after the last command, " + + 'default="' + + DEFAULT_POSTAMBLE.replace("\n", "\\n") + + '"', ) parser.add_argument( "--inches", action="store_true", help="Convert output for US imperial mode (G20)" @@ -122,15 +143,8 @@ PRECISION = 3 # rigid tapping. tapSpeed = 0 -# Preamble text will appear at the beginning of the GCODE output file. -DEFAULT_PREAMBLE = """G17 G54 G40 G49 G80 G90 -""" - -# Postamble text will appear following the last operation. -DEFAULT_POSTAMBLE = """M05 -G17 G54 G90 G80 G40 -M30 -""" +PREAMBLE = DEFAULT_PREAMBLE +POSTAMBLE = DEFAULT_POSTAMBLE # Pre operation text will be inserted before every operation PRE_OPERATION = """""" @@ -145,16 +159,27 @@ TOOL_CHANGE = """G28 G91 Z0 # List of drill G codes where some parameters are required and their # required parameters. -DRILL_OPERATION = ("G73", "G81", "G82", "G83", "G84", "G85") +DRILL_OPERATION = ("G73", "G81", "G82", "G83", "G85") DRILL_PARAM_REQ = ("L", "P", "Q", "R", "Z") +# The settings shared between methods +PREAMBLE = None +POSTAMBLE = None + def processArguments(argstring): + """ + Apply default values and command line arguments before + processing commands. + + """ global OUTPUT_HEADER global OUTPUT_COMMENTS global OUTPUT_LINE_NUMBERS global SHOW_EDITOR global PRECISION + global DEFAULT_PREAMBLE + global DEFAULT_POSTAMBLE global PREAMBLE global POSTAMBLE global UNITS @@ -185,7 +210,7 @@ def processArguments(argstring): SHOW_EDITOR = False else: SHOW_EDITOR = True - print("Show editor = %s" % SHOW_EDITOR) + # print("Show editor = %s" % SHOW_EDITOR) # Commented to reduce test noise if args.preamble is not None: PREAMBLE = args.preamble.replace("\\n", "\n") else: @@ -245,7 +270,7 @@ def export(objectslist, filename, argstring): ) return None - print("postprocessing...") + # print("postprocessing...") # Commented to reduce test noise gcode = "" gcode += "%\n" @@ -256,9 +281,16 @@ def export(objectslist, filename, argstring): 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" + # the filename variable always contain "-", use more relevant + # information + job = PathUtils.findParentJob(objectslist[0]) + if job: + body, job = job.FullName.split("#") + else: + # Workaround for the TestFanucPost code, where there is no + # job returned by findParentJob + body, job = ("FREECAD-FILENAME-GOES-HERE", "JOB-NAME-GOES-HERE") + gcode += "(" + body.upper() + ", " + job.upper() + ")\n" gcode += ( linenumber() + "(POST PROCESSOR: FANUC USING FREECAD %d.%d" % (major, minor) + ")\n" ) @@ -365,7 +397,7 @@ def export(objectslist, filename, argstring): else: final = gcode - print("done postprocessing.") + # print("done postprocessing.") # Commented to reduce test noise if not filename == "-": gfile = pyopen(filename, "w") @@ -492,85 +524,100 @@ def parse(pathobj): if command == "G0": continue - # if tool a tap, we thread tap, so stop the spindle for now. - # This only trigger when pathobj is a ToolController. + # If tool a tap, we will thread tap, so stop the spindle + # for now as there is no point in starting it to stop it + # in the G74/G84 operation after S29. This only trigger + # when pathobj is a ToolController. if command == "M03" or command == "M3": - if hasattr(pathobj, "Tool") and pathobj.Tool.ShapeName.lower() == "tap": + if ( + hasattr(pathobj, "Tool") + and getattr(pathobj.Tool, "ShapeType", "").lower() == "tap" + ): tapSpeed = int(pathobj.SpindleSpeed) continue - # Convert drill cycles to tap cycles if tool is a tap. + # Handle thread tapping cycles. Uses rigid tapping. # This only trigger when pathobj is a Operation. - if command == "G81" or command == "G83": - if ( - hasattr(pathobj, "ToolController") - and pathobj.ToolController.Tool.ShapeName.lower() == "tap" - ): - command = "G84" - out += linenumber() + "G95\n" - paramstring = "" - for param in ["X", "Y"]: - if param in c.Parameters: - if ( - (not OUTPUT_DOUBLES) - and (param in currLocation) - and (currLocation[param] == c.Parameters[param]) - ): - continue - else: - pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length) - paramstring += ( - " " - + param - + format( - float(pos.getValueAs(UNIT_FORMAT)), - precision_string, - ) - ) - if paramstring != "": - out += linenumber() + "G00" + paramstring + "\n" - - if "S" in c.Parameters: - tapSpeed = int(c.Parameters["S"]) - out += "M29 S" + str(tapSpeed) + "\n" - - for param in ["Z", "R"]: - if param in c.Parameters: - if ( - (not OUTPUT_DOUBLES) - and (param in currLocation) - and (currLocation[param] == c.Parameters[param]) - ): - continue - else: - pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length) - paramstring += ( - " " - + param - + format( - float(pos.getValueAs(UNIT_FORMAT)), - precision_string, - ) - ) - # in this mode, F is the distance per revolution of the thread (pitch) - # P is the dwell time in seconds at the bottom of the thread - # Q is the peck depth of the threading operation - for param in ["F", "P", "Q"]: - if param in c.Parameters: - value = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length) + if command == "G74" or command == "G84": + pitch_mm = float(c.Parameters["F"]) + # Convert pitch to inches if needed + if UNITS == "G20": # imperial + pitch = pitch_mm / 25.4 + else: + pitch = pitch_mm + paramstring = "" + for param in ["X", "Y"]: + if param in c.Parameters: + if ( + (not OUTPUT_DOUBLES) + and (param in currLocation) + and (currLocation[param] == c.Parameters[param]) + ): + continue + else: + pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length) paramstring += ( " " + param + format( - float(value.getValueAs(UNIT_FORMAT)), + float(pos.getValueAs(UNIT_FORMAT)), + precision_string, + ) + ) + if paramstring != "": + out += linenumber() + "G00" + paramstring + "\n" + + if "S" in c.Parameters: + tapSpeed = int(c.Parameters["S"]) + out += "M29 S" + str(tapSpeed) + "\n" + + for param in ["Z", "R"]: + if param in c.Parameters: + if ( + (not OUTPUT_DOUBLES) + and (param in currLocation) + and (currLocation[param] == c.Parameters[param]) + ): + continue + else: + pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length) + paramstring += ( + " " + + param + + format( + float(pos.getValueAs(UNIT_FORMAT)), precision_string, ) ) - out += linenumber() + "G84" + paramstring + "\n" - out += linenumber() + "G80\n" - out += linenumber() + "G94\n" - continue + # Calculate feed rate as distance per minute + if tapSpeed is not None: + feed_rate = pitch * tapSpeed + speed = Units.Quantity(feed_rate, UNIT_SPEED_FORMAT) + paramstring += " F" + format( + float(speed.getValueAs(UNIT_SPEED_FORMAT)), precision_string + ) + else: + # No spindle speed found, output pitch as F + paramstring += " F" + format(pitch, precision_string) + + # P is the dwell time in seconds at the bottom of the thread + # Q is the peck depth of the threading operation + for param in ["P", "Q"]: + if param in c.Parameters: + value = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length) + paramstring += ( + " " + + param + + format( + float(value.getValueAs(UNIT_FORMAT)), + precision_string, + ) + ) + + out += linenumber() + command + paramstring + "\n" + out += linenumber() + "G80\n" # End tapping cycle + continue outstring.append(command)