Package: release.debian.org Severity: normal Tags: patch X-Debbugs-CC: Tobias Frost <[email protected]> User: [email protected] Usertags: pu
I would like to fix a crash bug in the Fanuc post processor of FreeCAD in Trixie, reported as important BTS report #1117850. The issue has already been addressed upstream and in unstable and testing. This is the complete patch for my proposed update (also available in the salsa git repo as branch pere/debian/trixie. Note the 1100-cam-fanuc-post-fix.patch file is identical to the one already applied in unstable. I've tested the change on trixie, and the script work as it should there. A stable update to fix this is already approved by Tobias Frost in the FreeCAD packaging team, see <URL: https://bugs.debian.org/1117850 >. commit fcf0e49865baecf0cc89b0f2694a1b3d133e328f Author: Petter Reinholdtsen <[email protected]> Date: Sun Mar 8 07:30:15 2026 +0100 Trixie update to fix fanuc post processor. Patch fetched from upstream and already included in unstable and testing. Closes: #1117850 diff --git a/debian/changelog b/debian/changelog index 8ea9084f5..38c2dd519 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,10 @@ +freecad (1.0.0+dfsg-8+deb13u1) trixie; urgency=medium + + * Maintaner approvided upload. + * Get fanuc post processor working (Closes: #1117850). + + -- Petter Reinholdtsen <[email protected]> Sat, 07 Mar 2026 22:02:07 +0100 + freecad (1.0.0+dfsg-8) unstable; urgency=medium * Disable accidential added autopkgtest starttest, which was not ready. diff --git a/debian/gbp.conf b/debian/gbp.conf index 9f317df99..4e5f9a2a2 100644 --- a/debian/gbp.conf +++ b/debian/gbp.conf @@ -1,4 +1,4 @@ [DEFAULT] pristine-tar = True sign-tags = true -debian-branch = debian/unstable +debian-branch = debian/trixie diff --git a/debian/patches/1100-cam-fanuc-post-fix.patch b/debian/patches/1100-cam-fanuc-post-fix.patch new file mode 100644 index 000000000..7f9d497a1 --- /dev/null +++ b/debian/patches/1100-cam-fanuc-post-fix.patch @@ -0,0 +1,610 @@ +Description: fix fanuc postprocessor +Author: Petter Reinholdtsen <[email protected]> +Bug: https://github.com/FreeCAD/FreeCAD/issues/27814 +Bug-Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1117850 +Origin: https://github.com/FreeCAD/FreeCAD/pull/27964 +Applied-Upstream: https://github.com/FreeCAD/FreeCAD/commit/b0f4ad4a142e35c0e12c2ff241aaa7e3b23c5fa2 +Last-Update: 2026-03-02 + +commit 13c8babc1a63e1a656fb6559bff90cff58954375 +Author: Petter Reinholdtsen <[email protected]> +Date: Mon Mar 2 16:18:03 2026 +0100 + + CAM: Correct Fanuc post processor to work properly. + + Fixes crash problem, milling, drilling, tool changes, thread + tapping and get the post processor working as it should. + + This patch is tested on a Fanuc controller and sent upstream as + <URL: https://github.com/FreeCAD/FreeCAD/pull/27960 > + + Closes: #1117850 + +diff --git a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py +index 2edc7abaf..15af728a1 100644 +--- a/src/Mod/CAM/Path/Post/scripts/fanuc_post.py ++++ b/src/Mod/CAM/Path/Post/scripts/fanuc_post.py +@@ -1,3 +1,11 @@ ++# SPDX-License-Identifier: LGPL-2.1-or-later ++ ++""" ++ ++CAM post processor for CNC machines with a Fanuc controller. ++ ++""" ++ + # *************************************************************************** + # * Copyright (c) 2014 sliptonic <[email protected]> * + # * Copyright (c) 2021 shadowbane1000 <[email protected]> * +@@ -29,8 +37,9 @@ import argparse + import datetime + 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 = """ +@@ -46,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) +@@ -57,14 +76,19 @@ 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\nG90"', ++ 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 G90\nM2"', ++ 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)" +@@ -82,6 +106,11 @@ parser.add_argument( + action="store_true", + help="suppress tool length offset (G43) following tool changes", + ) ++parser.add_argument( ++ "--end-spindle-empty", ++ action="store_true", ++ help="place last tool in tool change carousel before postamble", ++) + + TOOLTIP_ARGS = parser.format_help() + +@@ -98,6 +127,8 @@ OUTPUT_DOUBLES = ( + COMMAND_SPACE = " " + LINENR = 100 # line number starting value + ++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 + UNIT_SPEED_FORMAT = "mm/min" +@@ -112,16 +143,8 @@ PRECISION = 3 + # rigid tapping. + tapSpeed = 0 + +-# Preamble text will appear at the beginning of the GCODE output file. +-PREAMBLE = """G17 G54 G40 G49 G80 G90 +-""" +- +-# Postamble text will appear following the last operation. +-POSTAMBLE = """M05 +-G17 G54 G90 G80 G40 +-M6 T0 +-M2 +-""" ++PREAMBLE = DEFAULT_PREAMBLE ++POSTAMBLE = DEFAULT_POSTAMBLE + + # Pre operation text will be inserted before every operation + PRE_OPERATION = """""" +@@ -130,15 +153,33 @@ 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 ++""" ++ ++# List of drill G codes where some parameters are required and their ++# required parameters. ++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 +@@ -146,35 +187,66 @@ def processArguments(argstring): + global UNIT_FORMAT + global MODAL + global USE_TLO ++ global END_SPINDLE_EMPTY + 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 +- print("Show editor = %d" % SHOW_EDITOR) +- PRECISION = args.precision ++ else: ++ SHOW_EDITOR = True ++ # print("Show editor = %s" % SHOW_EDITOR) # Commented to reduce test noise + if args.preamble is not None: +- PREAMBLE = args.preamble ++ PREAMBLE = args.preamble.replace("\\n", "\n") ++ else: ++ PREAMBLE = DEFAULT_PREAMBLE + if args.postamble is not None: +- POSTAMBLE = args.postamble ++ 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 ++ if args.end_spindle_empty: ++ END_SPINDLE_EMPTY = True ++ else: ++ END_SPINDLE_EMPTY = False + + except Exception: + return False +@@ -198,41 +270,56 @@ def export(objectslist, filename, argstring): + ) + return None + +- print("postprocessing...") ++ # print("postprocessing...") # Commented to reduce test noise + gcode = "" + ++ gcode += "%\n" ++ + # write header + if OUTPUT_HEADER: +- gcode += "%\n" +- gcode += ";\n" ++ # Get current version info ++ major = int(FreeCAD.ConfigGet("BuildVersionMajor")) ++ minor = int(FreeCAD.ConfigGet("BuildVersionMinor")) ++ ++ # 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 += ( +- 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 + if OUTPUT_COMMENTS: + gcode += linenumber() + "(BEGIN PREAMBLE)\n" +- for line in PREAMBLE.splitlines(False): ++ for line in PREAMBLE.splitlines(): + gcode += linenumber() + line + "\n" + gcode += linenumber() + UNITS + "\n" + + 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 hasattr(obj, "Active"): +- if not obj.Active: +- continue +- if hasattr(obj, "Base") and hasattr(obj.Base, "Active"): +- if not obj.Base.Active: +- continue ++ if not activeForOp(obj): ++ continue + + # do the pre_op + if OUTPUT_COMMENTS: +@@ -241,13 +328,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 = "None" +- if hasattr(obj, "CoolantMode") or hasattr(obj, "Base") and hasattr(obj.Base, "CoolantMode"): +- if hasattr(obj, "CoolantMode"): +- coolantMode = obj.CoolantMode +- else: +- coolantMode = obj.Base.CoolantMode ++ coolantMode = coolantModeForOp(obj) + + # turn coolant on if required + if OUTPUT_COMMENTS: +@@ -273,12 +373,18 @@ 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 += 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" +- for line in POSTAMBLE.splitlines(True): +- gcode += linenumber() + line +- gcode += "%\n" ++ for line in POSTAMBLE.splitlines(): ++ gcode += linenumber() + line + "\n" + + if FreeCAD.GuiUp and SHOW_EDITOR: + dia = PostUtils.GCodeEditorDialog() +@@ -291,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") +@@ -311,6 +417,8 @@ def linenumber(): + + def parse(pathobj): + global PRECISION ++ global DRILL_OPERATION ++ global DRILL_PARAM_REQ + global MODAL + global OUTPUT_DOUBLES + global UNIT_FORMAT +@@ -398,6 +506,7 @@ def parse(pathobj): + for index, c in enumerate(commands): + + outstring = [] ++ outsuffix = [] + command = c.Name + if index + 1 == len(commands): + nextcommand = "" +@@ -415,83 +524,100 @@ 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 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 pathobj.Tool.ToolType == "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 +- if command == "G81" or command == "G83": +- if ( +- hasattr(pathobj, "ToolController") +- and pathobj.ToolController.Tool.ToolType == "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, +- ) ++ # Handle thread tapping cycles. Uses rigid tapping. ++ # This only trigger when pathobj is a Operation. ++ 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(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 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(value.getValueAs(UNIT_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) + +@@ -504,7 +630,7 @@ def parse(pathobj): + if command == "G80" and lastcommand == nextcommand: + continue + +- if c.Name[0] == "(" and not OUTPUT_COMMENTS: # command is a comment ++ if c.Name.startswith("(") and not OUTPUT_COMMENTS: # command is a comment + continue + + # Now add the remaining parameters in order +@@ -540,7 +666,11 @@ def parse(pathobj): + if ( + (not OUTPUT_DOUBLES) + and (param in currLocation) +- and (currLocation[param] == c.Parameters[param]) ++ and currLocation[param] == c.Parameters[param] ++ and ( ++ command not in DRILL_OPERATION ++ or (command in DRILL_OPERATION and param not in DRILL_PARAM_REQ) ++ ) + ): + continue + else: +@@ -575,14 +705,14 @@ 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 + + # add height offset + if USE_TLO: +- tool_height = "\nG43 H" + str(int(c.Parameters["T"])) +- outstring.append(tool_height) ++ outsuffix.append("G91 G0 G43 G54 Z-[#[2000+#4120]] H#4120") ++ outsuffix.append("G90") + + if command == "message": + if OUTPUT_COMMENTS is False: +@@ -596,9 +726,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 + diff --git a/debian/patches/series b/debian/patches/series index 52ac756fa..0db094fce 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -11,3 +11,4 @@ 1091-freecad-spelling-others.patch 1110-GL_MULTISAMPLE.patch 2080-force-xcb-on-wayland.patch +1100-cam-fanuc-post-fix.patch -- Happy hacking Petter Reinholdtsen

