Writing a Machine-Based Post-Processor
Overview
introduced in 26.3: A machine-based Post-Processor (Post) is a Python class that inherits from Path.Post.Processor.PostProcessor. The PostProcessor class runs a fixed export sequence. The Machine of the Job configures that sequence. A subclass supplies only the behaviour that the Machine cannot express. Many controls need no subclass.
This page is the developer reference. It gives the class contract, the property schema, the export sequence and its hooks, and the tests. Machine Configuration explains how a Machine is edited and stored. Postprocessor Customization covers the script Posts that take arguments.
Architecture
Three layers share the work:
-
The Machine file (
.fcm) holds the physical description, the output format options and theprocessingoptions. The file is data only. -
The Post class holds the behaviour of one G-code dialect: command changes, parameter names, special cycles, and the lines around the file.
-
The
PostProcessorclass (Processor.py) does the order, the expansion, the modal suppression and the conversion. The first layer configures it. The second layer hooks into it.
Comment style, modal output, line numbers, precision, the preamble and the blocks before and after each item are configuration. A Post declares defaults and replaces a small number of hooks.
A file is a machine-based Post only when it sets the module constant POST_TYPE = "machine". These Posts appear in the Post list of the Machine Editor. Each other Post appears in the Processor field of the Job and takes its settings from arguments. A machine-based Post can have a script version with the _legacy_post.py suffix in the same folder.
FreeCAD includes these machine-based Posts: centroid, fanuc, generic, generic_sheet_cutting, heidenhain, linuxcnc, mach3_mach4, marlin, opensbp, smoothie. Post-Processor Genealogy shows the class of each Post.
When a subclass is necessary
Compare your control with plain G-code. When each difference is in this list, no code is necessary. Make a .fcm file that uses the generic Post.
-
Comment output, comment symbol and comment format
-
Modal output and suppression of repeated words
-
Line number format, start and increment
-
Precision for each class of value
-
Preamble, postamble and safety block
-
Blocks before and after each Job, Operation, tool change, Fixture change and rotary move
-
Tool length offset (G43) on or off
-
Arc split, rapid translation and drill cycle expansion
-
Early tool prep, and XY before Z after a tool change
-
File extension, the supported command list and the ignored command list
A subclass is necessary when the dialect is different:
-
A command has a different word (
G54becomesE01). -
A parameter has a different letter (
QbecomesKin a canned cycle). -
Arc centres are absolute and not incremental I and J.
-
The output has computed values, for example a dwell that the Spindle Speed scales.
-
The file needs marks at its start or its end.
-
The Post asks for input before the export, or sends the output to a remote endpoint.
-
The control needs its own Sanity Check rules.
Files and names
The factory finds the code by name. These rules apply:
-
File:
<name>_post.py, in one of the post search paths. The paths are thePath/Post/scripts/folder of the CAM module, the macro folder, the paths from , and the post folder that an add-on declares. -
Class: the file stem in title case.
mymachine_post.pymust contain the nameMymachine. Each part between underscores gets a capital:mach3_mach4_post.pygivesMach3_Mach4, andmasso_g3_post.pygivesMasso_G3. -
A module-level alias satisfies the class rule.
heidenhain_post.pydefinesHeidenhainPostand the aliasHeidenhain = HeidenhainPost. -
Module constant:
POST_TYPE = "machine". -
A Machine file refers to the stem:
"postprocessor": {"file_name": "mymachine"}.
A _post.py file with no such class loads as a script Post through the WrapperPost class. This is the compatibility route and not an error.
A minimal post
from typing import Any, Dict
from Path.Post.Processor import PostProcessor
import FreeCAD
translate = FreeCAD.Qt.translate
Values = Dict[str, Any]
POST_TYPE = "machine"
class Mymachine(PostProcessor):
@classmethod
def get_common_property_schema(cls):
props = super().get_common_property_schema()
for p in props:
if p["name"] == "file_extension":
p["default"] = "ngc"
elif p["name"] == "preamble":
p["default"] = "G17 G54 G40 G49 G80 G90"
elif p["name"] == "postamble":
p["default"] = "M05\nG17 G90 M2"
return props
@classmethod
def get_property_schema(cls):
return [] # no post-specific properties yet
def __init__(self, job):
super().__init__(
job=job,
tooltip=translate("CAM", "My machine post processor"),
tooltipargs=[],
units="Metric",
)
def init_values(self, values: Values) -> None:
super().init_values(values)
values["POSTPROCESSOR_FILE_NAME"] = __name__
This class is a complete Post. generic_post.py is the reference for this shape. linuxcnc_post.py shows a Post with its own properties and hooks.
The property schema
Each setting of a Post is a Post-Processor Property. A property is one schema entry, which is a plain dict. The interface builds its controls from the entries. The defaults come from the entries. The configuration bundle checks override keys against the entries. A new property is one dict in one list, with no interface code.
| Key | Required | Meaning |
|---|---|---|
|
yes |
The internal key. The Machine stores it in |
|
yes |
|
|
yes |
The label in the interface. Put it in |
|
yes |
The value when the Machine file and the overrides set none. |
|
yes |
The tooltip text. |
|
no |
The Property Scope: |
|
no |
The range for |
|
no |
The precision of the control for |
|
no |
For |
The scope constants are in Path.Post.Processor. The key "runtime": True is accepted as an alias for SCOPE_RUN. An entry with an unknown scope value gets Job scope, and the log shows a warning.
The types map to controls as follows: bool gives a checkbox, int and float give a spin box, choice gives a list, and text gives an editor with more than one line. Each other type gives a line edit.
Three class methods carry the schema:
-
get_common_property_schema():PostProcessordefines the properties that all Posts share. Override the method to change defaults. Edit the entries of the inherited list, as in the minimal example. Do not remove entries, because the export sequence reads them. -
get_property_schema(): the properties of your Post.PostProcessorreturns an empty list. -
get_full_property_schema(): the common properties and the properties of your Post. Do not override this method.
Two module functions read the scope:
-
property_scope(prop)returns the scope of one entry. -
properties_in_scope(schema, *scopes)returns the entries of a schema that have one of the given scopes, in schema order.
Common properties
Each machine-based Post has these properties.
| Name | Type | Scope | Default | Purpose |
|---|---|---|---|---|
|
string |
machine |
|
The extension of the output file, without the dot. |
|
bool |
machine |
false |
The Post can output G41 and G42. |
|
text |
machine |
the built-in list |
The commands that this Post accepts, one on each line. See Command filter and annotations. |
|
text |
machine |
empty |
The commands that this Post accepts and removes from the output, one on each line. |
|
text |
machine |
|
The drill cycles that the drill translation pass expands into G0 and G1 moves, one on each line. Empty when the control runs drill cycles. |
|
text |
machine |
empty |
Commands at the start of the program, after the header. |
|
text |
machine |
empty |
Commands at the end of the program. |
|
text |
machine |
empty |
Commands that reset the machine to a known safe condition, for example G40, G49, G80. |
|
text |
machine |
empty |
Commands before and after each Job. |
|
text |
machine |
empty |
Commands before and after a Fixture change. A Fixture is a G54 to G59.9 selection, not the workholding. |
|
text |
machine |
empty |
Commands before and after each Operation. |
|
text |
machine |
empty |
Commands before a tool change. |
|
text |
machine |
empty |
Commands immediately after the tool change (M6), before G43 and before the spindle start. |
|
text |
machine |
empty |
Commands immediately after the spindle start that follows a tool change. |
|
text |
machine |
empty |
Commands before the rotary axes move, and before the Post declares a tilted Work Plane when the control positions the axes. |
|
text |
machine |
empty |
Commands after the rotary axes moved, and after the Post declared and aligned a tilted Work Plane. |
|
bool |
machine |
true |
The plane command positions the rotary axes. When false, the program commands them with a rotary move before the plane declaration. |
|
string |
machine |
empty |
The lines that declare, align to and cancel a tilted Work Plane. Empty uses the form of the plane command. |
|
bool |
run |
true |
Permits interactive dialogs in the export. Set it to false for automation. |
|
text |
machine |
|
The order of the parameters on one line. |
|
bool |
machine |
true |
Output G43 after M6. |
|
bool |
job |
true |
False suppresses M6. |
|
int |
machine |
2 |
Decimals for axis words. |
|
int |
machine |
3 |
Decimals for the F word. |
|
int |
machine |
1 |
Decimals for the S word. |
|
bool |
machine |
false |
Output the F word on G0 lines. |
|
bool |
machine |
false |
G0 moves with the |
Machine describes each common property as the Machine Editor shows it.
Properties of your Post
Declare the properties in get_property_schema(). This example follows the generic_sheet_cutting Post.
import FreeCAD
from Path.Post.Processor import SCOPE_JOB, SCOPE_RUN
translate = FreeCAD.Qt.translate
@classmethod
def get_property_schema(cls):
return [
{
"name": "pierce_delay",
"scope": SCOPE_JOB,
"type": "int",
"label": translate("CAM", "Pierce Delay"),
"default": 1000,
"min": 0,
"max": 10000,
"help": translate("CAM", "Milliseconds to wait after the torch "
"ignites (M3) before moving"),
},
{
"name": "force_rapid_feeds",
"scope": SCOPE_RUN,
"type": "bool",
"label": translate("CAM", "Force Rapid Feeds"),
"default": False,
"help": translate("CAM", "Dry-run: run every move at rapid feed"),
},
]
The configuration bundle writes each property into self.values with an upper-case key. The code reads self.values["PIERCE_DELAY"] in a hook. Postprocessor Customization lists the properties of each included Post.
Where properties appear
The scope of a property sets the interface that edits it.
| Scope | Where the property is edited | Where the value is stored |
|---|---|---|
|
The Postprocessor tab of the Machine Editor only. The Post-Processing Dialog does not show the property. |
The |
|
The Postprocessor tab of the Machine Editor sets the default. The Postprocessor Properties group on the Options tab of the Post-Processing Dialog overrides the default for one run. |
The |
|
The Overview tab of the Post-Processing Dialog only. |
Not stored |
|
No interface shows the property. |
Not stored; the Post derives the value |
Use Machine scope for a fact about the machine or its control: an extension, a command block, a precision. Use Job scope for a default that one export can change: a delay, a blend mode. Use Run scope for a selection that applies to one export: a dry run, an output of marks only. Use Internal scope for an entry that the Post reads from the bundle and that is not a setting.
The scope is independent of the method that declares the property. A common property and a property of your Post obey the same rules.
Two related switches exist. The common property show_dialog permits the interactive dialogs that a Post shows in the export. The parameter Mod/CAM/EnableMachinePostprocessor (default true) hides the Postprocessor and Options tabs of the Machine Editor when it is false.
How values reach your code
At export, PostProcessor builds the configuration bundle. The bundle is one flat dict. PostProcessor merges the bundle from these sources, in this order:
-
machine.postprocessor_propertiesfrom the.fcmfile. -
The schema default for each key that the file does not set.
-
Values of the Job document (
job_author,comment,selected_fixtures). -
Overrides: the Job’s
PostProcessorPropertyOverridesJSON, or the values of the Post-Processing Dialog when the export starts there.
The export gives a warning for an override key that is not in the bundle, and ignores the key. PostProcessor writes the result to the Machine object and to self.values, with upper-case keys. _merge_machine_config() adds the values of the Machine’s output options, for example OUTPUT_COMMENTS, COMMENT_SYMBOL, AXIS_PRECISION and OUTPUT_LINE_NUMBERS. An explicit override has priority over the Machine model. Each later stage reads self.values. One export thus has one source of values.
_merge_machine_config() also calls _merge_toolhead_limits(). That method sets MIN_SPINDLE_SPEED and MAX_SPINDLE_SPEED from the min_rpm and max_rpm of the Toolhead. Each value is None in these conditions:
-
The Machine does not have exactly one Toolhead.
-
The Toolhead is not rotary.
-
The limit in the model is 0, which means not specified.
To force a value that the Machine must not change, override _merge_machine_config(). Call super()._merge_machine_config() first, then set the entries of self.values. The marlin Post sets OUTPUT_TOOL_CHANGE and USE_TLO to false in this method.
build_configuration_bundle() is a pure function of its inputs, which makes a unit test easy. apply_configuration_bundle() is the version with side effects that the export sequence calls.
The export2 sequence
export2() returns a list of (section_name, gcode) tuples, one for each output file. The sequence has these stages:
-
Pre-processing dialog.
pre_processing_dialog()runs unless the Post-Processing Dialog collected the input before. -
Postable list.
_buildPostList()changes the Job into an ordered list of postables: Fixtures, tool changes and Operations. The list obeys the Job’s Order Output By, Split Output and Fixtures properties and theearly_tool_prepoption of the Machine. Then_expand_postprocessor_commands(postables)runs. This hook gets the complete ordered plan before each expansion. -
Command expansion and block insertion. A fixed sequence of passes changes the postables. A postable holds a list of
Path.Commandobjects, or one block of text with the item typestr. -
Modal suppression and line numbers. These two passes run after all expansions and operate on
Path.Commandobjects. -
Command conversion. Each
Path.Commandbecomes one G-code line. No stage changes the text after this conversion. -
Output and remote post.
The expansion passes of stage 3 run in this order:
-
_expand_prefix: the header, the preamble and the safety block. -
_expand_pre_item: the blocks before each item. -
_expand_canned_cycles: the retract mode and the end of each group of canned cycles. -
_expand_translate_drill_cycles: the expansion of drill cycles into moves. -
_expand_split_arcs -
_expand_spindle_wait -
_expand_coolant_delay -
_expand_translate_rapids -
_expand_xy_before_z -
_expand_bcnc_commands -
_expand_tool_change: the commands after each M6. -
_suppress_tool_change: removes M6 whenTOOL_CHANGEis false. -
_expand_post_item: the blocks after each item. -
_expand_trailing_lines: thepost_jobblock and the postamble. -
_expand_rotary_move: the blocks around rotary moves. -
_expand_bcnc_postamble: always the last expansion.
Each pass reads its switch from self.values. A pass whose switch is off does nothing. The _expand_workplane_frames pass is also an expansion pass. It puts the Toolpath of each Operation into the coordinates that the Machine runs, as Work Planes and rotary axes describes.
Section names come from the structure of the postables. The Post-Processing Dialog changes them into file names through the output template of the Job.
Canned cycles and drill translation
_expand_canned_cycles passes each Toolpath that has drill cycles to PostUtils.cannedCycleTerminator(). The function writes G98 or G99 before each group of cycles and G80 after the group. Tapping cycles (G84, G74) are part of a group. The retract mode comes from the RetractMode annotation of the cycle. A cycle without the annotation uses the last literal G98 or G99 of the Toolpath, then G98. Each inserted command carries the annotations of its cycle. When M29 comes immediately before a tapping cycle, the retract word goes before the M29.
_expand_translate_drill_cycles runs after this pass, because the expansion needs the retract mode. The pass expands the cycles that drill_cycles_to_translate lists. The expansion needs a known X, Y and Z position and a retract mode before the cycle. If one is absent, the export stops with an error that names the missing parameters.
Tool change
_expand_tool_change inserts these items immediately after each M6, in this order:
-
The lines of
post_tool_change, without changes. -
G43 H<tool>, whenOUTPUT_TOOL_LENGTH_OFFSETis true.
When OUTPUT_TOOL_LENGTH_OFFSET is false, the pass replaces each G43 of the Toolpath with a comment. _expand_post_item adds the tool_return block after the Tool Controller item. The result has this shape:
(pretoolchange)
(TC: 3mm Endmill)
M6 T2
(posttoolchange)
G43 H2
(toolreturn)
When tool_change is false, the output has no M6 and no post_tool_change block. The tool_return block stays in the output.
Modal suppression and line numbers
_optimize_duplicates_doubles removes repeated words from the Path.Command objects:
-
When
OUTPUT_DUPLICATE_COMMANDSis false, a command with the same name as the command before it gets an empty name. -
When
OUTPUT_DOUBLESis false, the pass removes each X, Y, Z, U, V, W, A, B, C and F parameter whose value did not change.
The comparison stops at a barrier. These items are barriers: the start of a section, a tool change, a str postable, and a command with the modal_barrier annotation. A drill cycle keeps all its parameters and is also a barrier. Comments do not change. A conversion hook must accept a command with an empty name, or the Post must force OUTPUT_DUPLICATE_COMMANDS to true.
_add_line_numbers runs last, when OUTPUT_LINE_NUMBERS is true. The pass adds an N parameter to each command. The count starts again for each section, from LINE_NUMBER_START, with the step LINE_INCREMENT. Comments get no number. Each line of a str postable advances the count. The number is not the physical line number, because the conversion can add or remove lines.
_convert_move() writes the N parameter with LINE_NUMBER_PREFIX at the start of the line. A subclass that replaces the conversion must write the N parameter. _convert_move() returns None for a move or a dwell that has no parameter other than N.
For an arc, the conversion of PostProcessor reads the arc plane (G17, G18 or G19) from machine_state.Plane and does not write the center word along the plane normal. Postprocessor Customization gives the arc plane rule.
Work Planes and rotary axes
An Operation stores its Toolpath in the frame of its Work Plane. The Post puts the Toolpath into the form that the Rotation Strategy of the Machine names. Two class attributes declare what a Post can write:
-
ROTATION_STRATEGIES: the strategies that the Post can emit for an Operation on a tilted Work Plane. Default:("dwo", "twp"). -
PLANE_COMMAND: the Plane Command family that the Post writes for thetwpstrategy. Default:TiltedWorkPlane.PlaneCommand.G68_2, fromPath.Post.TiltedWorkPlane.
The export stops with a CAMValueError when an Operation is on a tilted Work Plane and one of these conditions is true:
-
The Machine has no rotary axes.
-
The Machine declares no Rotation Strategy.
-
The Machine declares the
post_transformstrategy. -
The Machine declares a Rotation Strategy that is not in
ROTATION_STRATEGIESof the Post.
The linuxcnc Post sets ROTATION_STRATEGIES to ("twp",). linuxcnc gives the lines that the linuxcnc Post writes for a tilted Work Plane.
The four twp_ properties change the lines of the twp strategy for a control with a different form. format_parameter() does not convert A, B and C values between millimetres and inches. Machine lists the conditions that the export examines.
Hooks
Override only the hooks that your dialect needs. Call super() for the remainder.
| Hook | Use |
|---|---|
|
Set entries in the values dict. Call |
|
Change how the output options of the Machine go into |
|
Add, remove or change commands after the postable list is complete and before the expansion. |
|
Change one expansion pass. The |
|
Return the list of |
|
Change the line number rule. The |
|
Change the conversion of one class of command into text. |
|
Change the format of canned cycles: parameter names, reference planes, the tapping feed. |
|
Change the conversion of the tapping F word. See Tapping feed. |
|
Change the finished output, for example to add end-of-file marks. Call |
|
Collect interactive input before the export. Return |
|
Add or remove a Sanity Check of the Post. See Sanity checks. |
|
The description that the Machine Editor shows. |
_edit_command_list(postables, edit_fn, all_postables=False) is a helper for a hook that edits commands. The helper calls edit_fn(section_name, item, command, section_state) for each command. edit_fn returns a flag and a list of commands. The flag -1 inserts the list before the command, 0 replaces the command, 1 inserts the list after it, and None changes nothing. section_state is a dict that is empty at the start of each section. With all_postables=True, the helper also calls edit_fn one time for each postable that has no Toolpath, with command set to None.
A hook reports an error with a class from Path.Post.CAMErrors: CAMError, CAMValueError, CAMAttributeError or CAMNotImplementedError. The keyword arguments job, operation, command and pp give the context of the error.
MachineState
A MachineState object (Path.Base.MachineState) follows the abstract machine. It holds the X, Y, Z, A, B and C position, F, S, T, the coolant, the active Fixture, and the drill return mode (G98 or G99). The F of G0 moves is held apart from F, as G0F.
A MachineState object also holds the arc plane (G17, G18 or G19) in its Plane attribute. Scripting describes the Plane attribute of MachineState and the addGcodeText() method.
self.machine_state is None unless a with self.use_machine_state(): block is active. The conversion stage opens one such block for each output section. The state thus starts new for each section.
The conversion calls machine_state.addCommand(cmd) before it calls the conversion hook for cmd. These rules follow:
-
machine_stategives the state after the command. -
machine_state.previousis a dict of the state before the command. -
machine_state.Xandmachine_state["X"]give the same value. A key that is not inMachineState.TrackedraisesKeyError.
The state becomes unknown, with each value of MachineState.Tracked set to None, after these events:
-
A tool change.
-
A
strpostable, because its text can contain G-code. -
A command with the
as-isannotation.
The Plane attribute of MachineState is not in MachineState.Tracked. setState(None) and updateState(None) do not change Plane. A tool change does not change Plane.
After a str postable and after a command with the as-is annotation, the conversion calls machine_state.setState(None). The conversion then calls machine_state.addGcodeText() with the text of the item. Plane keeps its value, or Plane gets the last G17, G18 or G19 word of the text.
An M3 or M4 without an S parameter sets S to None. setState(state) sets the keys of the dict and sets each other key to None. updateState(state) sets the keys of the dict and does not change the other keys. setState(None) sets each key to None. If your hook writes motion that the tracker did not see, update the state, because the next command reads it.
Command filter and annotations
The conversion examines each command in this order:
-
A command with the
as-isannotation gives the text of the annotation as its output line, without changes. -
A command whose name is in
ignored_commandsgives no output. -
Each other command must be in
supported_commands. If it is not, the export stops with an error.
The conversion compares the name of a command with each entry of the two lists as a full command name. Postprocessor Customization gives the rules of the two lists.
Three classes of command pass the third step when they are not in the list: a comment, a bare T word with its tool number, and a command with the allow_unsupported annotation. The lists in Constants.py supply the default of supported_commands: moves, drill cycles, probe commands, offsets, and the spindle and coolant M-codes. The default also has the set that Operations must not emit but Posts can: radius compensation, distance mode, units, return mode and cycle cancel.
The default of supported_commands also has the arc plane commands G17, G18 and G19. convert_command_to_gcode() sends G17, G18 and G19 to _convert_modal_command(), which writes the command name as one line. Override _convert_modal_command() to change this output.
A Path.Command carries annotations. Constants.py defines the keys.
| Constant | Key | Effect |
|---|---|---|
|
|
The value is the output line. The Custom Operation sets it for a line that must not change. |
|
|
The command passes the supported-command check. |
|
|
A G0 move that can become a G1 move. The value is the feed, as text that converts to a float. |
|
|
The modal suppression does not compare later commands with this command or with the commands before it. |
|
|
The G0 move must stay a separate move. The drill cycle expansion sets it on the retract moves between pecks. |
|
|
The Post added this G43 because |
_expand_translate_rapids runs when TRANSLATE_RAPID_MOVES or TRANSLATE_NO_ENGAGEMENT_FEED is true. With TRANSLATE_RAPID_MOVES, each G0 becomes G1. With TRANSLATE_NO_ENGAGEMENT_FEED, a command with the NoEngagementFeed annotation becomes G1 with the annotation value as its F parameter. The pass raises CAMAttributeError when the annotation value does not convert to a float.
Tapping feed
The F parameter of a tapping cycle from an Operation is the thread pitch. _convert_drill_cycle() calls _tapping_to_speed(command) before it formats the cycle. For a G84 or G74 command with the annotation operation set to tapping, the method returns a command whose F is the pitch multiplied by S and divided by 60. The method raises CAMAttributeError when the command has no S parameter. The method raises CAMValueError when S is not more than 0.
Override _tapping_to_speed() when the control uses a different rule. Override _convert_drill_cycle() when the control writes the pitch into its own cycle, as the linuxcnc and heidenhain Posts do.
Sanity checks
get_sanity_checks(job) returns the Squawks of the Post. They appear on the Warnings tab of the Post-Processing Dialog and in Quick Validate, with the generic checks of the Sanity Check.
get_sanity_checks() calls each method that sanity_check_methods() returns, in report order. Each method takes the Job and returns a list of Squawks. When a method raises an exception, the log shows a warning and the other methods run. The default list has the spindle speed check _sanity_spindle_speed and the Work Plane checks.
-
To change one check, override that method.
-
To add or remove a check, override
sanity_check_methods(). -
If you override
get_sanity_checks(), callsuper().get_sanity_checks(job), or the checks ofPostProcessordo not run.
Use self._create_squawk(type, note) to make a Squawk. The types are NOTE, TIP, WARNING and CAUTION. WARNING and CAUTION are critical.
def sanity_check_methods(self):
return super().sanity_check_methods() + [self._sanity_pierce_delay]
def _sanity_pierce_delay(self, job):
squawks = []
if self.values["PIERCE_DELAY"] < 300:
squawks.append(self._create_squawk(
"WARNING", "Pierce delay may be too short to pierce the material"))
return squawks
PostProcessor supplies these checks:
-
A
WARNINGSquawk for each Spindle Speed that an M3 or M4 commands outsideMIN_SPINDLE_SPEEDtoMAX_SPINDLE_SPEED. A speed of 0 is ignored. The check reads the commands of the Tool Controllers and the Operations, not the Tool Controller property. -
A
NOTESquawk when the Machine has more than one Toolhead, because the speeds were not examined. -
A
WARNINGSquawk when the rotary axes move between Operations andpre_rotary_moveis empty. -
A
WARNINGSquawk when a Work Plane names a Fixture that is not a Fixture word, or that is also in the Fixtures list of the Job.
PostProcessor also compares each Toolpath with the axis limits of the Machine. Write checks only for the rules of your control.
Tests
The Post tests are in Mod/CAM/CAMTests/. Run all of them with FreeCADCmd -t TestCAMApp, or one module with -t CAMTests.TestLinuxCNCPost. Use the mock pattern. PostTestMocks.create_default_job_with_operation() builds a stand-in for a Job without a document. The test then runs the export sequence directly.
import Path
import CAMTests.PathTestUtils as PathTestUtils
import CAMTests.PostTestMocks as PostTestMocks
from Path.Post.Processor import PostProcessorFactory
from Machine.models.machine import Machine
class TestMymachinePost(PathTestUtils.PathTestBase):
@classmethod
def setUpClass(cls):
cls.job, cls.op, cls.tc = PostTestMocks.create_default_job_with_operation()
cls.post = PostProcessorFactory.get_post_processor(cls.job, "mymachine")
def setUp(self):
self.post.reinitialize()
self.post._machine = Machine.create_3axis_config()
self.post._machine.output.output_header = False
self.post._machine.output.comments.enabled = False
def test_rapid(self):
self.op.Path = Path.Path([Path.Command("G0", {"X": 10.0, "Y": 5.0})])
gcode = self.post.export2()[0][1]
self.assertIn("G0 X10.000 Y5.000", gcode)
Set the header and the comments to off in setUp. The assertions then compare stable text. Test these items as a minimum:
-
An empty Toolpath, which gives the preamble and the postamble
-
The file extension
-
The precision
-
The comment format
-
The modal suppression
-
The drill cycles
-
Each dialect difference that your subclass exists for
Add the module to CAMTests/init.py. The test runner then finds it.
Distribution
For your own machine, put the file in the macro folder or in a folder from . Make sure that the Post is enabled under Post processors selection on that page.
An add-on distributes Posts when its package.xml declares them, adjacent to its Machine files. The FreeCAD/Machines add-on accepts the two.
A Post for FreeCAD itself goes into src/Mod/CAM/Path/Post/scripts/. Add an entry in the CMakeLists.txt of the CAM module and add tests. A .fcm Machine Template for the Post is recommended.
Reference implementations
| Post | Shows |
|---|---|
|
The minimal shape: |
|
Changed schema defaults, a |
|
A Post with many properties, for plasma, laser and waterjet machines: delays, the |
|
A dialect that is not G-code (Klartext). The Post replaces each conversion hook, forces values in |
|
A dialect that is not G-code (ShopBot OpenSBP), with probe support. |
|
Forced values in |
|
Posts for G-code controls. Compare each with its |