Create Custom FreeCAD Path Post-Processors in Python

This guide explains how to build a custom post-processor for the FreeCAD CAM (formerly Path) Workbench using Python. You will learn where to store your post-processor script, how the core export function works, how to parse FreeCAD toolpath commands into machine-specific G-code, and how to format header, movement, and footer blocks for your specific CNC controller.

Locating the Post-Processor Directory

FreeCAD searches for post-processors in its internal application directory and your user macro directory. To ensure your custom script persists across FreeCAD updates, place your script in the user macro directory:

  • Linux: ~/.local/share/FreeCAD/Macro/ or ~/.FreeCAD/Macro/
  • Windows: %APPDATA%\FreeCAD\Macro\
  • macOS: ~/Library/Application Support/FreeCAD/Macro/

Post-processor file names must end with _post.py (for example, my_controller_post.py). Once saved in this directory with the proper suffix, FreeCAD will automatically register and display it in the CAM Job configuration dropdown.

Basic Script Structure and Metadata

A FreeCAD post-processor is a standard Python file containing metadata definitions and an export() entry point.

Create a new file named custom_cnc_post.py and start with the standard metadata header:

TOOLTIP = "Custom post-processor for specific CNC controller"
UNITS = "G21"  # Default to metric (G21) or imperial (G20)
CORNER_MAX = None
CORNER_MIN = None

import FreeCAD
import Path
from Path.Post.Command import Command

The TOOLTIP variable provides the description visible inside FreeCAD’s Job Edit dialog under the Output tab.

The export Function

The CAM workbench executes the export(objectslist, filename, argstring) function when you post-process a job. This function accepts three arguments:

  • objectslist: A list of FreeCAD document objects selected for post-processing (typically operations, tools, and compound paths).
  • filename: The absolute destination path of the output file.
  • argstring: Optional string arguments passed from the export dialogue.

Here is the fundamental implementation of the export function:

def export(objectslist, filename, argstring):
    out = open(filename, "w")
    
    # Write machine preamble / safety block
    out.write("%\n")
    out.write("G90 G17 G21\n") # Absolute mode, XY plane, Metric
    
    for obj in objectslist:
        if not hasattr(obj, "Path"):
            continue
        
        # Parse the Path object
        for command in obj.Path.Commands:
            gcode_line = parse_command(command)
            if gcode_line:
                out.write(gcode_line + "\n")
                
    # Write postamble / shutdown block
    out.write("M5\n")   # Spindle off
    out.write("M30\n")  # Program end
    out.write("%\n")
    out.close()
    
    return filename

Parsing Path Commands

FreeCAD stores toolpath data as a sequence of Path.Command objects. Each command contains:

  • command.Name: The movement or state instruction (e.g., G0, G1, G2, G3, M3, M5, message).
  • command.Parameters: A dictionary holding the axes coordinates and feed values (e.g., {'X': 10.0, 'Y': 5.5, 'F': 1200}).

Implement a command parsing function to translate these internal objects into properly formatted text:

def parse_command(command):
    name = command.Name
    params = command.Parameters
    
    # Ignore comments or internal FreeCAD markers
    if name in ["message", "comment"]:
        return f"({params.get('message', '')})"
    
    # Format coordinate values to 3 decimal places
    line = [name]
    for axis in ["X", "Y", "Z", "A", "B"]:
        if axis in params:
            line.append(f"{axis}{params[axis]:.3f}")
            
    # Format feed rate and spindle speed as integers
    if "F" in params:
        line.append(f"F{int(params['F'])}")
    if "S" in params:
        line.append(f"S{int(params['S'])}")
    if "T" in params:
        line.append(f"T{int(params['T'])}")
        
    # Return space-delimited G-code instruction
    return " ".join(line)

Handling Arc Commands (G2 and G3)

Circular interpolation commands (G2 for clockwise, G3 for counter-clockwise) require center offsets relative to the start point (I, J, K) or absolute radius (R).

Extend the parsing function to include arc offsets:

    if name in ["G2", "G3"]:
        for offset in ["I", "J", "K", "R"]:
            if offset in params:
                line.append(f"{offset}{params[offset]:.3f}")

Loading and Testing the Post-Processor

  1. Save your completed file as my_machine_post.py into your FreeCAD Macro folder.
  2. Restart FreeCAD or reload macros via the Python console using import my_machine_post.
  3. Open a document containing a CAM Job.
  4. Double-click the Job in the tree view to edit it, and navigate to the Output tab.
  5. In the Post Processor dropdown, select my_machine.
  6. Click the Post Process button on the CAM toolbar, select your output location, and verify that the output G-code matches your controller's formatting rules.