CAM: Renaming of postprocessors and cleanup (#24771)

* Post Processor cleanup and rename

remove unused files
Basic implementation of generic post
add blending mode support for linuxcnc
add mocking for postprocessor tests
Add tests for generic post
linuxcnc test only tests linuxcnc specific functionality

minor improvements
add arc splitting to refactored post processors
Refactor smoothie post
move posts to legacy.  Remove 'refactored'
lint cleanup
fixes #19417

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
sliptonic
2025-11-15 09:46:34 -06:00
committed by GitHub
parent f8afa5ac72
commit bb172e4a6b
48 changed files with 5086 additions and 5418 deletions

View File

@@ -0,0 +1,155 @@
# ***************************************************************************
# * Copyright (c) 2025 sliptonic <shopinthewoods@gmail.com> *
# * *
# * 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 *
# * *
# ***************************************************************************
"""Mock objects for postprocessor testing.
This module provides mock objects that simulate FreeCAD CAM job structure
without requiring disk I/O or loading actual FreeCAD documents.
"""
import Path
class MockToolController:
"""Mock ToolController for operations."""
def __init__(
self,
tool_number=1,
label="TC: Default Tool",
spindle_speed=1000,
spindle_dir="Forward",
):
self.ToolNumber = tool_number
self.Label = label
self.SpindleSpeed = spindle_speed
self.SpindleDir = spindle_dir
self.Name = f"TC{tool_number}"
# Create a simple path with tool change commands
self.Path = Path.Path()
self.Path.addCommands(
[Path.Command(f"M6 T{tool_number}"), Path.Command(f"M3 S{spindle_speed}")]
)
def InList(self):
return []
class MockOperation:
"""Mock Operation object for testing postprocessors."""
def __init__(self, name="Operation", label=None, tool_controller=None, active=True):
self.Name = name
self.Label = label or name
self.Active = active
self.ToolController = tool_controller
# Create an empty path by default
self.Path = Path.Path()
def InList(self):
"""Mock InList - operations belong to a job."""
return []
class MockStock:
"""Mock Stock object with BoundBox."""
def __init__(self, xmin=0.0, xmax=100.0, ymin=0.0, ymax=100.0, zmin=0.0, zmax=10.0):
self.Shape = type(
"obj",
(object,),
{
"BoundBox": type(
"obj",
(object,),
{
"XMin": xmin,
"XMax": xmax,
"YMin": ymin,
"YMax": ymax,
"ZMin": zmin,
"ZMax": zmax,
},
)()
},
)()
class MockSetupSheet:
"""Mock SetupSheet object."""
def __init__(self, clearance_height=5.0, safe_height=3.0):
self.ClearanceHeightOffset = type("obj", (object,), {"Value": clearance_height})()
self.SafeHeightOffset = type("obj", (object,), {"Value": safe_height})()
class MockJob:
"""Mock Job object for testing postprocessors."""
def __init__(self):
# Create mock Stock with BoundBox
self.Stock = MockStock()
# Create mock SetupSheet
self.SetupSheet = MockSetupSheet()
# Create mock Operations group
self.Operations = type("obj", (object,), {"Group": []})()
# Create mock Tools group
self.Tools = type("obj", (object,), {"Group": []})()
# Create mock Model group
self.Model = type("obj", (object,), {"Group": []})()
# Basic properties
self.Label = "MockJob"
self.PostProcessor = ""
self.PostProcessorArgs = ""
self.PostProcessorOutputFile = ""
self.Fixtures = ["G54"]
self.OrderOutputBy = "Tool"
self.SplitOutput = False
def InList(self):
"""Mock InList for fixture setup."""
return []
def create_default_job_with_operation():
"""Create a mock job with a default tool controller and operation.
This is a convenience function for common test scenarios.
Returns: (job, operation, tool_controller)
"""
job = MockJob()
# Create default tool controller
tc = MockToolController(tool_number=1, label="TC: Default Tool", spindle_speed=1000)
job.Tools.Group = [tc]
# Create default operation
op = MockOperation(name="Profile", label="Profile", tool_controller=tc)
job.Operations.Group = [op]
return job, op, tc

View File

@@ -0,0 +1,326 @@
# ***************************************************************************
# * Copyright (c) 2022 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2022 Larry Woestman <LarryWoestman2@gmail.com> *
# * *
# * 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 *
# * *
# ***************************************************************************
from importlib import reload
import FreeCAD
import Path
from CAMTests import PathTestUtils
from Path.Post.scripts import centroid_legacy_post as postprocessor
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
class TestCentroidLegacyPost(PathTestUtils.PathTestBase):
"""Test suite for the Centroid legacy postprocessor.
This class verifies the functionality of the centroid_legacy_post postprocessor,
which generates G-code for Centroid CNC controllers. Tests cover:
- Output generation with various options (header, comments, editor suppression)
- Command formatting and coordinate precision control
- Line numbering
- Preamble and postamble generation
- Unit conversion (metric/imperial)
- Modal command suppression
- Axis modal (coordinate suppression for unchanged axes)
- Tool change command generation with tool length offset (TLO)
- Comment formatting and conversion
The legacy postprocessor has limited configurability compared to newer
postprocessors, and some command-line options are tested to verify they
don't break the default behavior.
"""
@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.
"""
# Open existing FreeCAD document with test geometry
FreeCAD.newDocument("Unnamed")
@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.
"""
# Close geometry document without saving
FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name)
# 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.
"""
self.doc = FreeCAD.ActiveDocument
self.con = FreeCAD.Console
self.docobj = FreeCAD.ActiveDocument.addObject("Path::Feature", "testpath")
reload(
postprocessor
) # technical debt. This shouldn't be necessary but here to bypass a bug
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.
"""
FreeCAD.ActiveDocument.removeObject("testpath")
def test000(self):
"""Test Output Generation.
Empty path. Produces only the preamble and postable.
"""
self.docobj.Path = Path.Path([])
postables = [self.docobj]
# Test generating with header
# Header contains a time stamp that messes up unit testing.
# Only test length of result.
args = "--no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.assertEqual(len(gcode.splitlines()), 16)
# Test without header
expected = """G90 G80 G40 G49
;begin preamble
G53 G00 G17
G21
;begin operation
;end operation: testpath
;begin postamble
M5
M25
G49 H0
G90 G80 G40 G49
M99
"""
self.docobj.Path = Path.Path([])
postables = [self.docobj]
args = "--no-header --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.assertEqual(gcode, expected)
# test without comments
expected = """G90 G80 G40 G49
G53 G00 G17
G21
M5
M25
G49 H0
G90 G80 G40 G49
M99
"""
args = "--no-header --no-comments --no-show-editor"
# args = ("--no-header --no-comments --no-show-editor --precision=2")
gcode = postprocessor.export(postables, "-", args)
self.assertEqual(gcode, expected)
def test010(self):
"""Test command Generation.
Test Precision
"""
c = Path.Command("G0 X10 Y20 Z30")
self.docobj.Path = Path.Path([c])
postables = [self.docobj]
args = "--no-header --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[5]
expected = "G0 X10.0000 Y20.0000 Z30.0000"
self.assertEqual(result, expected)
args = "--no-header --axis-precision=2 --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[5]
expected = "G0 X10.00 Y20.00 Z30.00"
self.assertEqual(result, expected)
def test020(self):
"""
Test Line Numbers
"""
c = Path.Command("G0 X10 Y20 Z30")
self.docobj.Path = Path.Path([c])
postables = [self.docobj]
args = "--no-header --line-numbers --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[5]
expected = "N150 G0 X10.0000 Y20.0000 Z30.0000"
self.assertEqual(result, expected)
def test030(self):
"""
Test Pre-amble
"""
self.docobj.Path = Path.Path([])
postables = [self.docobj]
#
# The original centroid postprocessor does not have a
# --preamble option. We end up with the default preamble.
#
args = "--no-header --no-comments --preamble='G18 G55' --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[1]
self.assertEqual(result, "G53 G00 G17")
def test040(self):
"""
Test Post-amble
"""
self.docobj.Path = Path.Path([])
postables = [self.docobj]
#
# The original centroid postprocessor does not have a
# --postamble option. We end up with the default postamble.
#
args = "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.assertEqual(gcode.splitlines()[-1], "M99")
def test050(self):
"""
Test inches
"""
c = Path.Command("G0 X10 Y20 Z30")
self.docobj.Path = Path.Path([c])
postables = [self.docobj]
args = "--no-header --inches --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.assertEqual(gcode.splitlines()[3], "G20")
result = gcode.splitlines()[5]
expected = "G0 X0.3937 Y0.7874 Z1.1811"
self.assertEqual(result, expected)
args = "--no-header --inches --axis-precision=2 --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[5]
expected = "G0 X0.39 Y0.79 Z1.18"
self.assertEqual(result, expected)
def test060(self):
"""
Test test modal
Suppress the command name if the same as previous
"""
c = Path.Command("G0 X10 Y20 Z30")
c1 = Path.Command("G0 X10 Y30 Z30")
self.docobj.Path = Path.Path([c, c1])
postables = [self.docobj]
#
# The original centroid postprocessor does not have a
# --modal option. We end up with the original gcode.
#
args = "--no-header --modal --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[6]
expected = "G0 X10.0000 Y30.0000 Z30.0000"
self.assertEqual(result, expected)
def test070(self):
"""
Test axis modal
Suppress the axis coordinate if the same as previous
"""
c = Path.Command("G0 X10 Y20 Z30")
c1 = Path.Command("G0 X10 Y30 Z30")
self.docobj.Path = Path.Path([c, c1])
postables = [self.docobj]
#
# The original centroid postprocessor does not have an
# --axis-modal option. We end up with the original gcode.
#
args = "--no-header --axis-modal --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[6]
expected = "G0 X10.0000 Y30.0000 Z30.0000"
self.assertEqual(result, expected)
def test080(self):
"""
Test tool change
"""
c = Path.Command("M6 T2")
c2 = Path.Command("M3 S3000")
self.docobj.Path = Path.Path([c, c2])
postables = [self.docobj]
args = "--no-header --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.assertEqual(gcode.splitlines()[5], "G43 H2")
self.assertEqual(gcode.splitlines()[6], "M6 T2")
self.assertEqual(gcode.splitlines()[7], "M3 S3000")
# suppress TLO
#
# The original centroid postprocessor does not have an
# --no-tlo option. We end up with the original gcode.
#
args = "--no-header --no-tlo --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.assertEqual(gcode.splitlines()[6], "M3 S3000")
def test090(self):
"""
Test comment
"""
c = Path.Command("(comment)")
self.docobj.Path = Path.Path([c])
postables = [self.docobj]
args = "--no-header --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[5]
expected = ";comment"
self.assertEqual(result, expected)

View File

@@ -2,7 +2,7 @@
# ***************************************************************************
# * Copyright (c) 2022 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2022 Larry Woestman <LarryWoestman2@gmail.com> *
# * Copyright (c) 2022 - 2025 Larry Woestman <LarryWoestman2@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
@@ -22,13 +22,11 @@
# * *
# ***************************************************************************
from importlib import reload
import FreeCAD
import Path
import CAMTests.PathTestUtils as PathTestUtils
from Path.Post.scripts import centroid_post as postprocessor
from Path.Post.Processor import PostProcessorFactory
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
@@ -36,9 +34,12 @@ Path.Log.trackModule(Path.Log.thisModule())
class TestCentroidPost(PathTestUtils.PathTestBase):
"""Test the centroid_post.py postprocessor."""
@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
@@ -47,64 +48,109 @@ class TestCentroidPost(PathTestUtils.PathTestBase):
is able to call static methods within this same class.
"""
# Open existing FreeCAD document with test geometry
FreeCAD.newDocument("Unnamed")
FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "True")
cls.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/CAMTests/boxtest.fcstd")
cls.job = cls.doc.getObject("Job")
cls.post = PostProcessorFactory.get_post_processor(cls.job, "centroid")
# locate the operation named "Profile"
for op in cls.job.Operations.Group:
if op.Label == "Profile":
# remember the "Profile" operation
cls.profile_op = op
return
@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.
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.
"""
# Close geometry document without saving
FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name)
FreeCAD.closeDocument(cls.doc.Name)
FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "")
# 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.
"""
self.doc = FreeCAD.ActiveDocument
self.con = FreeCAD.Console
self.docobj = FreeCAD.ActiveDocument.addObject("Path::Feature", "testpath")
reload(
postprocessor
) # technical debt. This shouldn't be necessary but here to bypass a bug
# 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.
"""
FreeCAD.ActiveDocument.removeObject("testpath")
pass
def single_compare(self, path, expected, args, debug=False):
"""Perform a test with a single line of gcode comparison."""
nl = "\n"
self.job.PostProcessorArgs = args
# replace the original path (that came with the job and operation) with our path
self.profile_op.Path = Path.Path(path)
# the gcode is in the first section for this particular job and operation
gcode = self.post.export()[0][1]
if debug:
print(f"--------{nl}{gcode}--------{nl}")
# there are 4 lines of "other stuff" before the line we are interested in
self.assertEqual(gcode.splitlines()[4], expected)
def multi_compare(self, path, expected, args, debug=False):
"""Perform a test with multiple lines of gcode comparison."""
nl = "\n"
self.job.PostProcessorArgs = args
# replace the original path (that came with the job and operation) with our path
self.profile_op.Path = Path.Path(path)
# the gcode is in the first section for this particular job and operation
gcode = self.post.export()[0][1]
if debug:
print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(gcode, expected)
def test000(self):
"""Test Output Generation.
Empty path. Produces only the preamble and postable.
"""
nl = "\n"
self.docobj.Path = Path.Path([])
postables = [self.docobj]
self.profile_op.Path = Path.Path([])
# Test generating with header
# Header contains a time stamp that messes up unit testing.
# Only test length of result.
args = "--no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.assertTrue(len(gcode.splitlines()) == 16)
self.job.PostProcessorArgs = "--no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertTrue(len(gcode.splitlines()) == 25)
# Test without header
expected = """G90 G80 G40 G49
;begin preamble
;T1=TC__Default_Tool
;Begin preamble
G53 G00 G17
G21
;begin operation
;end operation: testpath
;begin postamble
;Begin operation
G54
;End operation
;Begin operation
;TC: Default Tool
;Begin toolchange
M6 T1
;End operation
;Begin operation
;End operation
;Begin postamble
M5
M25
G49 H0
@@ -112,17 +158,17 @@ G90 G80 G40 G49
M99
"""
self.docobj.Path = Path.Path([])
postables = [self.docobj]
args = "--no-header --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.job.PostProcessorArgs = "--no-header --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(gcode, expected)
# test without comments
expected = """G90 G80 G40 G49
G53 G00 G17
G21
G54
M6 T1
M5
M25
G49 H0
@@ -130,29 +176,33 @@ G90 G80 G40 G49
M99
"""
args = "--no-header --no-comments --no-show-editor"
# args = ("--no-header --no-comments --no-show-editor --precision=2")
gcode = postprocessor.export(postables, "-", args)
self.job.PostProcessorArgs = "--no-header --no-comments --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(gcode, expected)
def test010(self):
"""Test command Generation.
Test Precision
"""
nl = "\n"
c = Path.Command("G0 X10 Y20 Z30")
self.docobj.Path = Path.Path([c])
postables = [self.docobj]
self.profile_op.Path = Path.Path([c])
args = "--no-header --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[5]
self.job.PostProcessorArgs = "--no-header --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[14]
expected = "G0 X10.0000 Y20.0000 Z30.0000"
self.assertEqual(result, expected)
args = "--no-header --axis-precision=2 --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[5]
self.job.PostProcessorArgs = "--no-header --axis-precision=2 --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[14]
expected = "G0 X10.00 Y20.00 Z30.00"
self.assertEqual(result, expected)
@@ -160,68 +210,75 @@ M99
"""
Test Line Numbers
"""
nl = "\n"
c = Path.Command("G0 X10 Y20 Z30")
self.docobj.Path = Path.Path([c])
postables = [self.docobj]
self.profile_op.Path = Path.Path([c])
args = "--no-header --line-numbers --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[5]
expected = "N150 G0 X10.0000 Y20.0000 Z30.0000"
self.job.PostProcessorArgs = "--no-header --line-numbers --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[14]
expected = "N240 G0 X10.0000 Y20.0000 Z30.0000"
self.assertEqual(result, expected)
def test030(self):
"""
Test Pre-amble
"""
nl = "\n"
self.docobj.Path = Path.Path([])
postables = [self.docobj]
self.profile_op.Path = Path.Path([])
#
# The original centroid postprocessor does not have a
# --preamble option. We end up with the default preamble.
#
args = "--no-header --no-comments --preamble='G18 G55' --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.job.PostProcessorArgs = (
"--no-header --no-comments --preamble='G18 G55' --no-show-editor"
)
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[1]
self.assertEqual(result, "G53 G00 G17")
self.assertEqual(result, "G18 G55")
def test040(self):
"""
Test Post-amble
"""
self.docobj.Path = Path.Path([])
postables = [self.docobj]
#
# The original centroid postprocessor does not have a
# --postamble option. We end up with the default postamble.
#
args = "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.assertEqual(gcode.splitlines()[-1], "M99")
nl = "\n"
self.profile_op.Path = Path.Path([])
self.job.PostProcessorArgs = (
"--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor"
)
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[-2]
self.assertEqual(result, "G0 Z50")
self.assertEqual(gcode.splitlines()[-1], "M2")
def test050(self):
"""
Test inches
"""
nl = "\n"
c = Path.Command("G0 X10 Y20 Z30")
self.docobj.Path = Path.Path([c])
postables = [self.docobj]
args = "--no-header --inches --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.assertEqual(gcode.splitlines()[3], "G20")
self.profile_op.Path = Path.Path([c])
result = gcode.splitlines()[5]
self.job.PostProcessorArgs = "--no-header --inches --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(gcode.splitlines()[4], "G20")
result = gcode.splitlines()[14]
expected = "G0 X0.3937 Y0.7874 Z1.1811"
self.assertEqual(result, expected)
args = "--no-header --inches --axis-precision=2 --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[5]
self.job.PostProcessorArgs = "--no-header --inches --axis-precision=2 --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[14]
expected = "G0 X0.39 Y0.79 Z1.18"
self.assertEqual(result, expected)
@@ -230,20 +287,18 @@ M99
Test test modal
Suppress the command name if the same as previous
"""
nl = "\n"
c = Path.Command("G0 X10 Y20 Z30")
c1 = Path.Command("G0 X10 Y30 Z30")
self.docobj.Path = Path.Path([c, c1])
postables = [self.docobj]
self.profile_op.Path = Path.Path([c, c1])
#
# The original centroid postprocessor does not have a
# --modal option. We end up with the original gcode.
#
args = "--no-header --modal --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[6]
expected = "G0 X10.0000 Y30.0000 Z30.0000"
self.job.PostProcessorArgs = "--no-header --modal --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[15]
expected = "X10.0000 Y30.0000 Z30.0000"
self.assertEqual(result, expected)
def test070(self):
@@ -251,58 +306,56 @@ M99
Test axis modal
Suppress the axis coordinate if the same as previous
"""
nl = "\n"
c = Path.Command("G0 X10 Y20 Z30")
c1 = Path.Command("G0 X10 Y30 Z30")
self.docobj.Path = Path.Path([c, c1])
postables = [self.docobj]
self.profile_op.Path = Path.Path([c, c1])
#
# The original centroid postprocessor does not have an
# --axis-modal option. We end up with the original gcode.
#
args = "--no-header --axis-modal --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[6]
expected = "G0 X10.0000 Y30.0000 Z30.0000"
self.job.PostProcessorArgs = "--no-header --axis-modal --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[15]
expected = "G0 Y30.0000"
self.assertEqual(result, expected)
def test080(self):
"""
Test tool change
"""
nl = "\n"
c = Path.Command("M6 T2")
c2 = Path.Command("M3 S3000")
self.docobj.Path = Path.Path([c, c2])
postables = [self.docobj]
args = "--no-header --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.assertEqual(gcode.splitlines()[5], "G43 H2")
self.assertEqual(gcode.splitlines()[6], "M6 T2")
self.assertEqual(gcode.splitlines()[7], "M3 S3000")
self.profile_op.Path = Path.Path([c, c2])
self.job.PostProcessorArgs = "--no-header --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(gcode.splitlines()[15], "M6 T2")
self.assertEqual(gcode.splitlines()[16], "M3 S3000")
# suppress TLO
#
# The original centroid postprocessor does not have an
# --no-tlo option. We end up with the original gcode.
#
args = "--no-header --no-tlo --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.assertEqual(gcode.splitlines()[6], "M3 S3000")
self.job.PostProcessorArgs = "--no-header --no-tlo --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(gcode.splitlines()[16], "M3 S3000")
def test090(self):
"""
Test comment
"""
nl = "\n"
c = Path.Command("(comment)")
self.docobj.Path = Path.Path([c])
postables = [self.docobj]
self.profile_op.Path = Path.Path([c])
args = "--no-header --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[5]
self.job.PostProcessorArgs = "--no-header --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[14]
expected = ";comment"
self.assertEqual(result, expected)

View File

@@ -34,8 +34,8 @@ Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
class TestRefactoredTestDressupPost(PathTestUtils.PathTestBase):
"""Test the refactored_test_post.py postprocessor command line arguments."""
class TestDressupPost(PathTestUtils.PathTestBase):
"""Test the test_post.py postprocessor command line arguments."""
@classmethod
def setUpClass(cls):
@@ -51,7 +51,7 @@ class TestRefactoredTestDressupPost(PathTestUtils.PathTestBase):
FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "True")
cls.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/CAMTests/dressuptest.FCStd")
cls.job = cls.doc.getObject("Job")
cls.post = PostProcessorFactory.get_post_processor(cls.job, "refactored_test")
cls.post = PostProcessorFactory.get_post_processor(cls.job, "test")
# there are 4 operations in dressuptest.FCStd
# every operation uses a different ToolController

View File

@@ -22,18 +22,12 @@
# * *
# ***************************************************************************
# ***************************************************************************
# * Note: TestRefactoredMassoG3Post.py is a modified clone of this file *
# * any changes to this file should be applied to the other *
# * *
# * *
# ***************************************************************************
import FreeCAD
import Path
import CAMTests.PathTestUtils as PathTestUtils
import CAMTests.PostTestMocks as PostTestMocks
from Path.Post.Processor import PostProcessorFactory
@@ -41,8 +35,8 @@ Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
class TestRefactoredLinuxCNCPost(PathTestUtils.PathTestBase):
"""Test the refactored_linuxcnc_post.py postprocessor."""
class TestGenericPost(PathTestUtils.PathTestBase):
"""Test the generic postprocessor."""
@classmethod
def setUpClass(cls):
@@ -56,16 +50,13 @@ class TestRefactoredLinuxCNCPost(PathTestUtils.PathTestBase):
is able to call static methods within this same class.
"""
FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "True")
cls.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/CAMTests/boxtest.fcstd")
cls.job = cls.doc.getObject("Job")
cls.post = PostProcessorFactory.get_post_processor(cls.job, "refactored_linuxcnc")
# locate the operation named "Profile"
for op in cls.job.Operations.Group:
if op.Label == "Profile":
# remember the "Profile" operation
cls.profile_op = op
return
# Create mock job with default operation and tool controller
cls.job, cls.profile_op, cls.tool_controller = (
PostTestMocks.create_default_job_with_operation()
)
# Create postprocessor using the mock job
cls.post = PostProcessorFactory.get_post_processor(cls.job, "generic")
@classmethod
def tearDownClass(cls):
@@ -77,8 +68,8 @@ class TestRefactoredLinuxCNCPost(PathTestUtils.PathTestBase):
have access to the class `self` reference. This method
is able to call static methods within this same class.
"""
FreeCAD.closeDocument(cls.doc.Name)
FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "")
# No cleanup needed for mock objects
pass
# Setup and tear down methods called before and after each unit test
@@ -114,32 +105,29 @@ class TestRefactoredLinuxCNCPost(PathTestUtils.PathTestBase):
# Only test length of result.
self.job.PostProcessorArgs = "--no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertTrue(len(gcode.splitlines()) == 26)
# print(f"--------{nl}Actual line count: {len(gcode.splitlines())}{nl}{gcode}--------{nl}")
self.assertTrue(len(gcode.splitlines()) == 23)
# Test without header
expected = """(Begin preamble)
G17 G54 G40 G49 G80 G90
G90
G21
(Begin operation: Fixture)
(Machine units: mm/min)
G54
(Finish operation: Fixture)
(Begin operation: TC: Default Tool)
(Machine units: mm/min)
(TC: Default Tool)
(Begin toolchange)
M5
M6 T1
G43 H1
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 postamble)
M05
G17 G54 G90 G80 G40
M2
"""
self.profile_op.Path = Path.Path([])
@@ -151,15 +139,13 @@ M2
self.assertEqual(gcode, expected)
# test without comments
expected = """G17 G54 G40 G49 G80 G90
expected = """G90
G21
G54
M5
M6 T1
G43 H1
M05
G17 G54 G90 G80 G40
M2
M3 S1000
G54
"""
# args = ("--no-header --no-comments --no-show-editor --precision=2")
@@ -222,8 +208,9 @@ M2
)
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[0]
self.assertEqual(result, "G18 G55")
lines = gcode.splitlines()
# Preamble should be in the output
self.assertIn("G18 G55", gcode)
def test040(self):
"""

View File

@@ -0,0 +1,254 @@
# ***************************************************************************
# * Copyright (c) 2022 sliptonic <shopinthewoods@gmail.com> *
# * *
# * 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 FreeCAD
import Path
import CAMTests.PathTestUtils as PathTestUtils
from importlib import reload
from Path.Post.scripts import grbl_legacy_post as postprocessor
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
class TestGrblLegacyPost(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.
"""
# Open existing FreeCAD document with test geometry
FreeCAD.newDocument("Unnamed")
@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.
"""
# Close geometry document without saving
FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name)
# 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.
"""
self.doc = FreeCAD.ActiveDocument
self.con = FreeCAD.Console
self.docobj = FreeCAD.ActiveDocument.addObject("Path::Feature", "testpath")
reload(
postprocessor
) # technical debt. This shouldn't be necessary but here to bypass a bug
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.
"""
FreeCAD.ActiveDocument.removeObject("testpath")
def test000(self):
"""Test Output Generation.
Empty path. Produces only the preamble and postable.
"""
self.docobj.Path = Path.Path([])
postables = [self.docobj]
# Test generating with header
# Header contains a time stamp that messes up unit testing. Only test
# length of result.
args = "--no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.assertEqual(len(gcode.splitlines()), 13)
# Test without header
expected = """(Begin preamble)
G17 G90
G21
(Begin operation: testpath)
(Path: testpath)
(Finish operation: testpath)
(Begin postamble)
M5
G17 G90
M2
"""
self.docobj.Path = Path.Path([])
postables = [self.docobj]
args = "--no-header --no-show-editor"
# args = ("--no-header --no-comments --no-show-editor --precision=2")
gcode = postprocessor.export(postables, "-", args)
self.assertEqual(gcode, expected)
# test without comments
expected = """G17 G90
G21
M5
G17 G90
M2
"""
args = "--no-header --no-comments --no-show-editor"
# args = ("--no-header --no-comments --no-show-editor --precision=2")
gcode = postprocessor.export(postables, "-", args)
self.assertEqual(gcode, expected)
def test010(self):
"""Test command Generation.
Test Precision
Test imperial / inches
"""
c = Path.Command("G0 X10 Y20 Z30")
self.docobj.Path = Path.Path([c])
postables = [self.docobj]
args = "--no-header --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[5]
expected = "G0 X10.000 Y20.000 Z30.000"
self.assertEqual(result, expected)
args = "--no-header --precision=2 --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[5]
expected = "G0 X10.00 Y20.00 Z30.00"
self.assertEqual(result, expected)
def test020(self):
"""
Test Line Numbers
"""
c = Path.Command("G0 X10 Y20 Z30")
self.docobj.Path = Path.Path([c])
postables = [self.docobj]
args = "--no-header --line-numbers --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[5]
expected = "N150 G0 X10.000 Y20.000 Z30.000"
self.assertEqual(result, expected)
def test030(self):
"""
Test Pre-amble
"""
self.docobj.Path = Path.Path([])
postables = [self.docobj]
args = "--no-header --no-comments --preamble='G18 G55\n' --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[0]
self.assertEqual(result, "G18 G55")
def test040(self):
"""
Test Post-amble
"""
self.docobj.Path = Path.Path([])
postables = [self.docobj]
args = "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[-2]
self.assertEqual(result, "G0 Z50")
self.assertEqual(gcode.splitlines()[-1], "M2")
def test050(self):
"""
Test inches
"""
c = Path.Command("G0 X10 Y20 Z30")
self.docobj.Path = Path.Path([c])
postables = [self.docobj]
args = "--no-header --inches --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.assertEqual(gcode.splitlines()[2], "G20")
result = gcode.splitlines()[5]
expected = "G0 X0.3937 Y0.7874 Z1.1811"
self.assertEqual(result, expected)
# Technical debt. The following test fails. Precision not working
# with imperial units.
# args = ("--no-header --inches --precision=2")
# gcode = postprocessor.export(postables, "-", args)
# result = gcode.splitlines()[5]
# expected = "G0 X0.39 Y0.78 Z1.18 "
# self.assertEqual(result, expected)
def test080(self):
"""
Test tool change
"""
c = Path.Command("M6 T2")
c2 = Path.Command("M3 S3000")
self.docobj.Path = Path.Path([c, c2])
postables = [self.docobj]
args = "--no-header --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.assertEqual(gcode.splitlines()[6], "( M6 T2 )")
self.assertEqual(gcode.splitlines()[7], "M3 S3000")
# suppress TLO
#
# The grbl postprocessor does not have a --no-tlo option.
#
# args = "--no-header --no-tlo --no-show-editor"
# gcode = postprocessor.export(postables, "-", args)
# self.assertEqual(gcode.splitlines()[7], "M3 S3000 ")
def test090(self):
"""
Test comment
"""
c = Path.Command("(comment)")
self.docobj.Path = Path.Path([c])
postables = [self.docobj]
args = "--no-header --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[5]
expected = "(comment)"
self.assertEqual(result, expected)

View File

@@ -2,6 +2,7 @@
# ***************************************************************************
# * Copyright (c) 2022 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2022 - 2025 Larry Woestman <LarryWoestman2@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
@@ -25,8 +26,7 @@ import FreeCAD
import Path
import CAMTests.PathTestUtils as PathTestUtils
from importlib import reload
from Path.Post.scripts import grbl_post as postprocessor
from Path.Post.Processor import PostProcessorFactory
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
@@ -34,9 +34,12 @@ Path.Log.trackModule(Path.Log.thisModule())
class TestGrblPost(PathTestUtils.PathTestBase):
"""Test the grbl_post.py postprocessor."""
@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
@@ -45,109 +48,134 @@ class TestGrblPost(PathTestUtils.PathTestBase):
is able to call static methods within this same class.
"""
# Open existing FreeCAD document with test geometry
FreeCAD.newDocument("Unnamed")
FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "True")
cls.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/CAMTests/boxtest.fcstd")
cls.job = cls.doc.getObject("Job")
cls.post = PostProcessorFactory.get_post_processor(cls.job, "grbl")
# locate the operation named "Profile"
for op in cls.job.Operations.Group:
if op.Label == "Profile":
# remember the "Profile" operation
cls.profile_op = op
return
@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.
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.
"""
# Close geometry document without saving
FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name)
FreeCAD.closeDocument(cls.doc.Name)
FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "")
# 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.
"""
self.doc = FreeCAD.ActiveDocument
self.con = FreeCAD.Console
self.docobj = FreeCAD.ActiveDocument.addObject("Path::Feature", "testpath")
reload(
postprocessor
) # technical debt. This shouldn't be necessary but here to bypass a bug
# 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.
"""
FreeCAD.ActiveDocument.removeObject("testpath")
pass
def test000(self):
"""Test Output Generation.
Empty path. Produces only the preamble and postable.
"""
nl = "\n"
self.docobj.Path = Path.Path([])
postables = [self.docobj]
self.profile_op.Path = Path.Path([])
# Test generating with header
# Header contains a time stamp that messes up unit testing. Only test
# length of result.
args = "--no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.assertTrue(len(gcode.splitlines()) == 13)
# Header contains a time stamp that messes up unit testing.
# Only test length of result.
self.job.PostProcessorArgs = "--no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertTrue(len(gcode.splitlines()) == 24)
# Test without header
expected = """(Begin preamble)
G17 G90
G21
(Begin operation: testpath)
(Path: testpath)
(Finish operation: testpath)
(Begin operation: Fixture)
(Path: Fixture)
G54
(Finish operation: Fixture)
(Begin operation: TC: Default Tool)
(Path: TC: Default Tool)
(TC: Default Tool)
(Begin toolchange)
(M6 T1)
(Finish operation: TC: Default Tool)
(Begin operation: Profile)
(Path: Profile)
(Finish operation: Profile)
(Begin postamble)
M5
G17 G90
M2
"""
self.docobj.Path = Path.Path([])
postables = [self.docobj]
self.profile_op.Path = Path.Path([])
args = "--no-header --no-show-editor"
# args = ("--no-header --no-comments --no-show-editor --precision=2")
gcode = postprocessor.export(postables, "-", args)
self.job.PostProcessorArgs = "--no-header --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(gcode, expected)
# test without comments
expected = """G17 G90
G21
G54
M5
G17 G90
M2
"""
args = "--no-header --no-comments --no-show-editor"
# args = ("--no-header --no-comments --no-show-editor --precision=2")
gcode = postprocessor.export(postables, "-", args)
self.job.PostProcessorArgs = "--no-header --no-comments --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(gcode, expected)
def test010(self):
"""Test command Generation.
Test Precision
Test imperial / inches
"""
nl = "\n"
c = Path.Command("G0 X10 Y20 Z30")
self.docobj.Path = Path.Path([c])
postables = [self.docobj]
self.profile_op.Path = Path.Path([c])
args = "--no-header --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[5]
self.job.PostProcessorArgs = "--no-header --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[15]
expected = "G0 X10.000 Y20.000 Z30.000"
self.assertEqual(result, expected)
args = "--no-header --precision=2 --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[5]
self.job.PostProcessorArgs = "--no-header --precision=2 --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[15]
expected = "G0 X10.00 Y20.00 Z30.00"
self.assertEqual(result, expected)
@@ -155,27 +183,32 @@ M2
"""
Test Line Numbers
"""
nl = "\n"
c = Path.Command("G0 X10 Y20 Z30")
self.docobj.Path = Path.Path([c])
postables = [self.docobj]
self.profile_op.Path = Path.Path([c])
args = "--no-header --line-numbers --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[5]
expected = "N150 G0 X10.000 Y20.000 Z30.000"
self.job.PostProcessorArgs = "--no-header --line-numbers --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[15]
expected = "N250 G0 X10.000 Y20.000 Z30.000"
self.assertEqual(result, expected)
def test030(self):
"""
Test Pre-amble
"""
nl = "\n"
self.docobj.Path = Path.Path([])
postables = [self.docobj]
self.profile_op.Path = Path.Path([])
args = "--no-header --no-comments --preamble='G18 G55\n' --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.job.PostProcessorArgs = (
"--no-header --no-comments --preamble='G18 G55' --no-show-editor"
)
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[0]
self.assertEqual(result, "G18 G55")
@@ -183,10 +216,15 @@ M2
"""
Test Post-amble
"""
self.docobj.Path = Path.Path([])
postables = [self.docobj]
args = "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
nl = "\n"
self.profile_op.Path = Path.Path([])
self.job.PostProcessorArgs = (
"--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor"
)
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[-2]
self.assertEqual(result, "G0 Z50")
self.assertEqual(gcode.splitlines()[-1], "M2")
@@ -195,102 +233,121 @@ M2
"""
Test inches
"""
nl = "\n"
c = Path.Command("G0 X10 Y20 Z30")
self.docobj.Path = Path.Path([c])
postables = [self.docobj]
args = "--no-header --inches --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.profile_op.Path = Path.Path([c])
self.job.PostProcessorArgs = "--no-header --inches --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(gcode.splitlines()[2], "G20")
result = gcode.splitlines()[5]
result = gcode.splitlines()[15]
expected = "G0 X0.3937 Y0.7874 Z1.1811"
self.assertEqual(result, expected)
# Technical debt. The following test fails. Precision not working
# with imperial units.
# args = ("--no-header --inches --precision=2")
# gcode = postprocessor.export(postables, "-", args)
# result = gcode.splitlines()[5]
# expected = "G0 X0.39 Y0.78 Z1.18 "
# self.assertEqual(result, expected)
self.job.PostProcessorArgs = "--no-header --inches --precision=2 --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[15]
expected = "G0 X0.39 Y0.79 Z1.18"
self.assertEqual(result, expected)
def test060(self):
"""
Test test modal
Suppress the command name if the same as previous
"""
nl = "\n"
c = Path.Command("G0 X10 Y20 Z30")
c1 = Path.Command("G0 X10 Y30 Z30")
self.docobj.Path = Path.Path([c, c1])
postables = [self.docobj]
self.profile_op.Path = Path.Path([c, c1])
#
# The grbl postprocessor does not have a --modal option.
#
# args = "--no-header --modal --no-show-editor"
# gcode = postprocessor.export(postables, "-", args)
# result = gcode.splitlines()[6]
# expected = "X10.000 Y30.000 Z30.000 "
# self.assertEqual(result, expected)
self.job.PostProcessorArgs = "--no-header --modal --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[16]
expected = "X10.000 Y30.000 Z30.000"
self.assertEqual(result, expected)
def test070(self):
"""
Test axis modal
Suppress the axis coordinate if the same as previous
"""
nl = "\n"
c = Path.Command("G0 X10 Y20 Z30")
c1 = Path.Command("G0 X10 Y30 Z30")
self.docobj.Path = Path.Path([c, c1])
postables = [self.docobj]
self.profile_op.Path = Path.Path([c, c1])
#
# The grbl postprocessor does not have a --axis-modal option.
#
# args = "--no-header --axis-modal --no-show-editor"
# gcode = postprocessor.export(postables, "-", args)
# result = gcode.splitlines()[6]
# expected = "G0 Y30.000 "
# self.assertEqual(result, expected)
self.job.PostProcessorArgs = "--no-header --axis-modal --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[16]
expected = "G0 Y30.000"
self.assertEqual(result, expected)
def test080(self):
"""
Test tool change
"""
nl = "\n"
c = Path.Command("M6 T2")
c2 = Path.Command("M3 S3000")
self.docobj.Path = Path.Path([c, c2])
postables = [self.docobj]
args = "--no-header --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.assertEqual(gcode.splitlines()[6], "( M6 T2 )")
self.assertEqual(gcode.splitlines()[7], "M3 S3000")
self.profile_op.Path = Path.Path([c, c2])
self.job.PostProcessorArgs = "--no-header --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(gcode.splitlines()[16], "(M6 T2)")
self.assertEqual(gcode.splitlines()[17], "M3 S3000")
# suppress TLO
#
# The grbl postprocessor does not have a --no-tlo option.
#
# args = "--no-header --no-tlo --no-show-editor"
# gcode = postprocessor.export(postables, "-", args)
# self.assertEqual(gcode.splitlines()[7], "M3 S3000 ")
self.job.PostProcessorArgs = "--no-header --no-tlo --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(gcode.splitlines()[17], "M3 S3000")
def test090(self):
"""
Test comment
"""
nl = "\n"
c = Path.Command("(comment)")
self.docobj.Path = Path.Path([c])
postables = [self.docobj]
self.profile_op.Path = Path.Path([c])
args = "--no-header --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[5]
self.job.PostProcessorArgs = "--no-header --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[15]
expected = "(comment)"
self.assertEqual(result, expected)
def test100(self):
"""
Test if coolant is enabled.
"""
nl = "\n"
c = Path.Command("M7")
c1 = Path.Command("M8")
c2 = Path.Command("M9")
self.profile_op.Path = Path.Path([c, c1, c2])
self.job.PostProcessorArgs = "--no-header --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(gcode.splitlines()[15], "M7")
self.assertEqual(gcode.splitlines()[16], "M8")
self.assertEqual(gcode.splitlines()[17], "M9")

View File

@@ -0,0 +1,420 @@
# ***************************************************************************
# * Copyright (c) 2022 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2023 Larry Woestman <LarryWoestman2@gmail.com> *
# * *
# * 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 FreeCAD
import Path
import CAMTests.PathTestUtils as PathTestUtils
from importlib import reload
from Path.Post.scripts import linuxcnc_legacy_post as postprocessor
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
class TestLinuxCNCLegacyPost(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.
"""
# Open existing FreeCAD document with test geometry
FreeCAD.newDocument("Unnamed")
@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.
"""
# Close geometry document without saving
FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name)
# 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.
"""
self.doc = FreeCAD.ActiveDocument
self.con = FreeCAD.Console
self.docobj = FreeCAD.ActiveDocument.addObject("Path::Feature", "testpath")
reload(
postprocessor
) # technical debt. This shouldn't be necessary but here to bypass a bug
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.
"""
FreeCAD.ActiveDocument.removeObject("testpath")
def compare_sixth_line(self, path_string, expected, args, debug=False):
"""Perform a test with a single comparison to the sixth line of the output."""
nl = "\n"
if path_string:
self.docobj.Path = Path.Path([Path.Command(path_string)])
else:
self.docobj.Path = Path.Path([])
postables = [self.docobj]
gcode = postprocessor.export(postables, "-", args)
if debug:
print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(gcode.splitlines()[5], expected)
def test000(self):
"""Test Output Generation.
Empty path. Produces only the preamble and postable.
"""
self.docobj.Path = Path.Path([])
postables = [self.docobj]
# Test generating with header
# Header contains a time stamp that messes up unit testing.
# Only test length of result.
args = "--no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.assertTrue(len(gcode.splitlines()) == 13)
# Test without header
expected = """(begin preamble)
G17 G54 G40 G49 G80 G90
G21
(begin operation: testpath)
(machine units: mm/min)
(finish operation: testpath)
(begin postamble)
M05
G17 G54 G90 G80 G40
M2
"""
self.docobj.Path = Path.Path([])
postables = [self.docobj]
args = "--no-header --no-show-editor"
# args = ("--no-header --no-comments --no-show-editor --precision=2")
gcode = postprocessor.export(postables, "-", args)
self.assertEqual(gcode, expected)
# test without comments
expected = """G17 G54 G40 G49 G80 G90
G21
M05
G17 G54 G90 G80 G40
M2
"""
args = "--no-header --no-comments --no-show-editor"
# args = ("--no-header --no-comments --no-show-editor --precision=2")
gcode = postprocessor.export(postables, "-", args)
self.assertEqual(gcode, expected)
def test010(self):
"""Test command Generation.
Test Precision
Test imperial / inches
"""
self.compare_sixth_line(
"G0 X10 Y20 Z30", "G0 X10.000 Y20.000 Z30.000 ", "--no-header --no-show-editor"
)
self.compare_sixth_line(
"G0 X10 Y20 Z30",
"G0 X10.00 Y20.00 Z30.00 ",
"--no-header --precision=2 --no-show-editor",
)
def test020(self):
"""
Test Line Numbers
"""
self.compare_sixth_line(
"G0 X10 Y20 Z30",
"N160 G0 X10.000 Y20.000 Z30.000 ",
"--no-header --line-numbers --no-show-editor",
)
def test030(self):
"""
Test Pre-amble
"""
self.docobj.Path = Path.Path([])
postables = [self.docobj]
args = "--no-header --no-comments --preamble='G18 G55' --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[0]
self.assertEqual(result, "G18 G55")
def test040(self):
"""
Test Post-amble
"""
self.docobj.Path = Path.Path([])
postables = [self.docobj]
args = "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[-2]
self.assertEqual(result, "G0 Z50")
self.assertEqual(gcode.splitlines()[-1], "M2")
def test050(self):
"""
Test inches
"""
c = Path.Command("G0 X10 Y20 Z30")
self.docobj.Path = Path.Path([c])
postables = [self.docobj]
args = "--no-header --inches --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.assertEqual(gcode.splitlines()[2], "G20")
result = gcode.splitlines()[5]
expected = "G0 X0.3937 Y0.7874 Z1.1811 "
self.assertEqual(result, expected)
# Technical debt. The following test fails. Precision not working
# with imperial units.
# args = ("--no-header --inches --precision=2")
# gcode = postprocessor.export(postables, "-", args)
# result = gcode.splitlines()[5]
# expected = "G0 X0.39 Y0.78 Z1.18 "
# self.assertEqual(result, expected)
def test060(self):
"""
Test test modal
Suppress the command name if the same as previous
"""
c = Path.Command("G0 X10 Y20 Z30")
c1 = Path.Command("G0 X10 Y30 Z30")
self.docobj.Path = Path.Path([c, c1])
postables = [self.docobj]
args = "--no-header --modal --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[6]
expected = "X10.000 Y30.000 Z30.000 "
self.assertEqual(result, expected)
def test070(self):
"""
Test axis modal
Suppress the axis coordinate if the same as previous
"""
c = Path.Command("G0 X10 Y20 Z30")
c1 = Path.Command("G0 X10 Y30 Z30")
self.docobj.Path = Path.Path([c, c1])
postables = [self.docobj]
args = "--no-header --axis-modal --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[6]
expected = "G0 Y30.000 "
self.assertEqual(result, expected)
def test080(self):
"""
Test tool change
"""
c = Path.Command("M6 T2")
c2 = Path.Command("M3 S3000")
self.docobj.Path = Path.Path([c, c2])
postables = [self.docobj]
args = "--no-header --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.assertEqual(gcode.splitlines()[5], "M5")
self.assertEqual(gcode.splitlines()[6], "M6 T2 ")
self.assertEqual(gcode.splitlines()[7], "G43 H2 ")
self.assertEqual(gcode.splitlines()[8], "M3 S3000 ")
# suppress TLO
args = "--no-header --no-tlo --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.assertEqual(gcode.splitlines()[7], "M3 S3000 ")
def test090(self):
"""
Test comment
"""
self.compare_sixth_line("(comment)", "(comment) ", "--no-header --no-show-editor")
def test100(self):
"""Test A, B, & C axis output for values between 0 and 90 degrees"""
self.compare_sixth_line(
"G1 X10 Y20 Z30 A40 B50 C60",
"G1 X10.000 Y20.000 Z30.000 A40.000 B50.000 C60.000 ",
"--no-header --no-show-editor",
)
self.compare_sixth_line(
"G1 X10 Y20 Z30 A40 B50 C60",
"G1 X0.3937 Y0.7874 Z1.1811 A40.0000 B50.0000 C60.0000 ",
"--no-header --inches --no-show-editor",
)
def test110(self):
"""Test A, B, & C axis output for 89 degrees"""
self.compare_sixth_line(
"G1 X10 Y20 Z30 A89 B89 C89",
"G1 X10.000 Y20.000 Z30.000 A89.000 B89.000 C89.000 ",
"--no-header --no-show-editor",
)
self.compare_sixth_line(
"G1 X10 Y20 Z30 A89 B89 C89",
"G1 X0.3937 Y0.7874 Z1.1811 A89.0000 B89.0000 C89.0000 ",
"--no-header --inches --no-show-editor",
)
def test120(self):
"""Test A, B, & C axis output for 90 degrees"""
self.compare_sixth_line(
"G1 X10 Y20 Z30 A90 B90 C90",
"G1 X10.000 Y20.000 Z30.000 A90.000 B90.000 C90.000 ",
"--no-header --no-show-editor",
)
self.compare_sixth_line(
"G1 X10 Y20 Z30 A90 B90 C90",
"G1 X0.3937 Y0.7874 Z1.1811 A90.0000 B90.0000 C90.0000 ",
"--no-header --inches --no-show-editor",
)
def test130(self):
"""Test A, B, & C axis output for 91 degrees"""
self.compare_sixth_line(
"G1 X10 Y20 Z30 A91 B91 C91",
"G1 X10.000 Y20.000 Z30.000 A91.000 B91.000 C91.000 ",
"--no-header --no-show-editor",
)
self.compare_sixth_line(
"G1 X10 Y20 Z30 A91 B91 C91",
"G1 X0.3937 Y0.7874 Z1.1811 A91.0000 B91.0000 C91.0000 ",
"--no-header --inches --no-show-editor",
)
def test140(self):
"""Test A, B, & C axis output for values between 90 and 180 degrees"""
self.compare_sixth_line(
"G1 X10 Y20 Z30 A100 B110 C120",
"G1 X10.000 Y20.000 Z30.000 A100.000 B110.000 C120.000 ",
"--no-header --no-show-editor",
)
self.compare_sixth_line(
"G1 X10 Y20 Z30 A100 B110 C120",
"G1 X0.3937 Y0.7874 Z1.1811 A100.0000 B110.0000 C120.0000 ",
"--no-header --inches --no-show-editor",
)
def test150(self):
"""Test A, B, & C axis output for values between 180 and 360 degrees"""
self.compare_sixth_line(
"G1 X10 Y20 Z30 A240 B250 C260",
"G1 X10.000 Y20.000 Z30.000 A240.000 B250.000 C260.000 ",
"--no-header --no-show-editor",
)
self.compare_sixth_line(
"G1 X10 Y20 Z30 A240 B250 C260",
"G1 X0.3937 Y0.7874 Z1.1811 A240.0000 B250.0000 C260.0000 ",
"--no-header --inches --no-show-editor",
)
def test160(self):
"""Test A, B, & C axis output for values greater than 360 degrees"""
self.compare_sixth_line(
"G1 X10 Y20 Z30 A440 B450 C460",
"G1 X10.000 Y20.000 Z30.000 A440.000 B450.000 C460.000 ",
"--no-header --no-show-editor",
)
self.compare_sixth_line(
"G1 X10 Y20 Z30 A440 B450 C460",
"G1 X0.3937 Y0.7874 Z1.1811 A440.0000 B450.0000 C460.0000 ",
"--no-header --inches --no-show-editor",
)
def test170(self):
"""Test A, B, & C axis output for values between 0 and -90 degrees"""
self.compare_sixth_line(
"G1 X10 Y20 Z30 A-40 B-50 C-60",
"G1 X10.000 Y20.000 Z30.000 A-40.000 B-50.000 C-60.000 ",
"--no-header --no-show-editor",
)
self.compare_sixth_line(
"G1 X10 Y20 Z30 A-40 B-50 C-60",
"G1 X0.3937 Y0.7874 Z1.1811 A-40.0000 B-50.0000 C-60.0000 ",
"--no-header --inches --no-show-editor",
)
def test180(self):
"""Test A, B, & C axis output for values between -90 and -180 degrees"""
self.compare_sixth_line(
"G1 X10 Y20 Z30 A-100 B-110 C-120",
"G1 X10.000 Y20.000 Z30.000 A-100.000 B-110.000 C-120.000 ",
"--no-header --no-show-editor",
)
self.compare_sixth_line(
"G1 X10 Y20 Z30 A-100 B-110 C-120",
"G1 X0.3937 Y0.7874 Z1.1811 A-100.0000 B-110.0000 C-120.0000 ",
"--no-header --inches --no-show-editor",
)
def test190(self):
"""Test A, B, & C axis output for values between -180 and -360 degrees"""
self.compare_sixth_line(
"G1 X10 Y20 Z30 A-240 B-250 C-260",
"G1 X10.000 Y20.000 Z30.000 A-240.000 B-250.000 C-260.000 ",
"--no-header --no-show-editor",
)
self.compare_sixth_line(
"G1 X10 Y20 Z30 A-240 B-250 C-260",
"G1 X0.3937 Y0.7874 Z1.1811 A-240.0000 B-250.0000 C-260.0000 ",
"--no-header --inches --no-show-editor",
)
def test200(self):
"""Test A, B, & C axis output for values below -360 degrees"""
self.compare_sixth_line(
"G1 X10 Y20 Z30 A-440 B-450 C-460",
"G1 X10.000 Y20.000 Z30.000 A-440.000 B-450.000 C-460.000 ",
"--no-header --no-show-editor",
)
self.compare_sixth_line(
"G1 X10 Y20 Z30 A-440 B-450 C-460",
"G1 X0.3937 Y0.7874 Z1.1811 A-440.0000 B-450.0000 C-460.0000 ",
"--no-header --inches --no-show-editor",
)

View File

@@ -2,7 +2,7 @@
# ***************************************************************************
# * Copyright (c) 2022 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2023 Larry Woestman <LarryWoestman2@gmail.com> *
# * Copyright (c) 2022 - 2025 Larry Woestman <LarryWoestman2@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
@@ -26,17 +26,25 @@ import FreeCAD
import Path
import CAMTests.PathTestUtils as PathTestUtils
from importlib import reload
from Path.Post.scripts import linuxcnc_post as postprocessor
import CAMTests.PostTestMocks as PostTestMocks
from Path.Post.Processor import PostProcessorFactory
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
class TestLinuxCNCPost(PathTestUtils.PathTestBase):
"""Test LinuxCNC-specific features of the inuxcnc_post.py postprocessor.
This test suite focuses on LinuxCNC-specific functionality such as path blending modes.
Generic postprocessor functionality is tested in TestGenericPost.
"""
@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
@@ -45,378 +53,138 @@ class TestLinuxCNCPost(PathTestUtils.PathTestBase):
is able to call static methods within this same class.
"""
# Open existing FreeCAD document with test geometry
FreeCAD.newDocument("Unnamed")
# Create mock job with default operation and tool controller
cls.job, cls.profile_op, cls.tool_controller = (
PostTestMocks.create_default_job_with_operation()
)
# Create postprocessor using the mock job
cls.post = PostProcessorFactory.get_post_processor(cls.job, "linuxcnc")
@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.
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.
"""
# Close geometry document without saving
FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name)
# No cleanup needed for mock objects
pass
# 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.
"""
self.doc = FreeCAD.ActiveDocument
self.con = FreeCAD.Console
self.docobj = FreeCAD.ActiveDocument.addObject("Path::Feature", "testpath")
reload(
postprocessor
) # technical debt. This shouldn't be necessary but here to bypass a bug
# 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.
"""
FreeCAD.ActiveDocument.removeObject("testpath")
pass
def compare_sixth_line(self, path_string, expected, args, debug=False):
"""Perform a test with a single comparison to the sixth line of the output."""
nl = "\n"
if path_string:
self.docobj.Path = Path.Path([Path.Command(path_string)])
else:
self.docobj.Path = Path.Path([])
postables = [self.docobj]
gcode = postprocessor.export(postables, "-", args)
if debug:
print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(gcode.splitlines()[5], expected)
def test000(self):
"""Test Output Generation.
Empty path. Produces only the preamble and postable.
"""
self.docobj.Path = Path.Path([])
postables = [self.docobj]
# Test generating with header
# Header contains a time stamp that messes up unit testing.
# Only test length of result.
args = "--no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.assertTrue(len(gcode.splitlines()) == 13)
# Test without header
expected = """(begin preamble)
G17 G54 G40 G49 G80 G90
G21
(begin operation: testpath)
(machine units: mm/min)
(finish operation: testpath)
(begin postamble)
M05
G17 G54 G90 G80 G40
M2
"""
self.docobj.Path = Path.Path([])
postables = [self.docobj]
args = "--no-header --no-show-editor"
# args = ("--no-header --no-comments --no-show-editor --precision=2")
gcode = postprocessor.export(postables, "-", args)
self.assertEqual(gcode, expected)
# test without comments
expected = """G17 G54 G40 G49 G80 G90
G21
M05
G17 G54 G90 G80 G40
M2
"""
args = "--no-header --no-comments --no-show-editor"
# args = ("--no-header --no-comments --no-show-editor --precision=2")
gcode = postprocessor.export(postables, "-", args)
self.assertEqual(gcode, expected)
def test010(self):
"""Test command Generation.
Test Precision
Test imperial / inches
"""
self.compare_sixth_line(
"G0 X10 Y20 Z30", "G0 X10.000 Y20.000 Z30.000 ", "--no-header --no-show-editor"
def test_blend_mode_exact_path(self):
"""Test EXACT_PATH blend mode outputs G61."""
self.profile_op.Path = Path.Path([])
self.job.PostProcessorArgs = (
"--no-header --no-comments --blend-mode EXACT_PATH --no-show-editor"
)
self.compare_sixth_line(
"G0 X10 Y20 Z30",
"G0 X10.00 Y20.00 Z30.00 ",
"--no-header --precision=2 --no-show-editor",
gcode = self.post.export()[0][1]
# G61 should be in the preamble
self.assertIn("G61", gcode)
# Should not have G64
self.assertNotIn("G64", gcode)
# Should not have G61.1
self.assertNotIn("G61.1", gcode)
def test_blend_mode_exact_stop(self):
"""Test EXACT_STOP blend mode outputs G61.1."""
self.profile_op.Path = Path.Path([])
self.job.PostProcessorArgs = (
"--no-header --no-comments --blend-mode EXACT_STOP --no-show-editor"
)
gcode = self.post.export()[0][1]
def test020(self):
"""
Test Line Numbers
"""
self.compare_sixth_line(
"G0 X10 Y20 Z30",
"N160 G0 X10.000 Y20.000 Z30.000 ",
"--no-header --line-numbers --no-show-editor",
# G61.1 should be in the preamble
self.assertIn("G61.1", gcode)
# Should not have G64
self.assertNotIn("G64", gcode)
def test_blend_mode_blend_default(self):
"""Test BLEND mode with default tolerance (0) outputs G64."""
self.profile_op.Path = Path.Path([])
self.job.PostProcessorArgs = "--no-header --no-comments --blend-mode BLEND --no-show-editor"
gcode = self.post.export()[0][1]
# G64 should be in the preamble (without P parameter)
lines = gcode.splitlines()
has_g64 = any("G64" in line and "P" not in line for line in lines)
self.assertTrue(has_g64, "Expected G64 without P parameter")
def test_blend_mode_blend_with_tolerance(self):
"""Test BLEND mode with tolerance outputs G64 P<tolerance>."""
self.profile_op.Path = Path.Path([])
self.job.PostProcessorArgs = (
"--no-header --no-comments --blend-mode BLEND --blend-tolerance 0.05 --no-show-editor"
)
gcode = self.post.export()[0][1]
def test030(self):
"""
Test Pre-amble
"""
# G64 P0.05 should be in the preamble
self.assertIn("G64 P0.0500", gcode)
self.docobj.Path = Path.Path([])
postables = [self.docobj]
args = "--no-header --no-comments --preamble='G18 G55' --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[0]
self.assertEqual(result, "G18 G55")
def test040(self):
"""
Test Post-amble
"""
self.docobj.Path = Path.Path([])
postables = [self.docobj]
args = "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[-2]
self.assertEqual(result, "G0 Z50")
self.assertEqual(gcode.splitlines()[-1], "M2")
def test050(self):
"""
Test inches
"""
c = Path.Command("G0 X10 Y20 Z30")
self.docobj.Path = Path.Path([c])
postables = [self.docobj]
args = "--no-header --inches --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.assertEqual(gcode.splitlines()[2], "G20")
result = gcode.splitlines()[5]
expected = "G0 X0.3937 Y0.7874 Z1.1811 "
self.assertEqual(result, expected)
# Technical debt. The following test fails. Precision not working
# with imperial units.
# args = ("--no-header --inches --precision=2")
# gcode = postprocessor.export(postables, "-", args)
# result = gcode.splitlines()[5]
# expected = "G0 X0.39 Y0.78 Z1.18 "
# self.assertEqual(result, expected)
def test060(self):
"""
Test test modal
Suppress the command name if the same as previous
"""
c = Path.Command("G0 X10 Y20 Z30")
c1 = Path.Command("G0 X10 Y30 Z30")
self.docobj.Path = Path.Path([c, c1])
postables = [self.docobj]
args = "--no-header --modal --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[6]
expected = "X10.000 Y30.000 Z30.000 "
self.assertEqual(result, expected)
def test070(self):
"""
Test axis modal
Suppress the axis coordinate if the same as previous
"""
c = Path.Command("G0 X10 Y20 Z30")
c1 = Path.Command("G0 X10 Y30 Z30")
self.docobj.Path = Path.Path([c, c1])
postables = [self.docobj]
args = "--no-header --axis-modal --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[6]
expected = "G0 Y30.000 "
self.assertEqual(result, expected)
def test080(self):
"""
Test tool change
"""
c = Path.Command("M6 T2")
c2 = Path.Command("M3 S3000")
self.docobj.Path = Path.Path([c, c2])
postables = [self.docobj]
args = "--no-header --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.assertEqual(gcode.splitlines()[5], "M5")
self.assertEqual(gcode.splitlines()[6], "M6 T2 ")
self.assertEqual(gcode.splitlines()[7], "G43 H2 ")
self.assertEqual(gcode.splitlines()[8], "M3 S3000 ")
# suppress TLO
args = "--no-header --no-tlo --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.assertEqual(gcode.splitlines()[7], "M3 S3000 ")
def test090(self):
"""
Test comment
"""
self.compare_sixth_line("(comment)", "(comment) ", "--no-header --no-show-editor")
def test100(self):
"""Test A, B, & C axis output for values between 0 and 90 degrees"""
self.compare_sixth_line(
"G1 X10 Y20 Z30 A40 B50 C60",
"G1 X10.000 Y20.000 Z30.000 A40.000 B50.000 C60.000 ",
"--no-header --no-show-editor",
)
self.compare_sixth_line(
"G1 X10 Y20 Z30 A40 B50 C60",
"G1 X0.3937 Y0.7874 Z1.1811 A40.0000 B50.0000 C60.0000 ",
"--no-header --inches --no-show-editor",
def test_blend_mode_blend_with_custom_tolerance(self):
"""Test BLEND mode with custom tolerance value."""
self.profile_op.Path = Path.Path([])
self.job.PostProcessorArgs = (
"--no-header --no-comments --blend-mode BLEND --blend-tolerance 0.02 --no-show-editor"
)
gcode = self.post.export()[0][1]
def test110(self):
"""Test A, B, & C axis output for 89 degrees"""
self.compare_sixth_line(
"G1 X10 Y20 Z30 A89 B89 C89",
"G1 X10.000 Y20.000 Z30.000 A89.000 B89.000 C89.000 ",
"--no-header --no-show-editor",
)
self.compare_sixth_line(
"G1 X10 Y20 Z30 A89 B89 C89",
"G1 X0.3937 Y0.7874 Z1.1811 A89.0000 B89.0000 C89.0000 ",
"--no-header --inches --no-show-editor",
)
# G64 P0.02 should be in the preamble
self.assertIn("G64 P0.0200", gcode)
def test120(self):
"""Test A, B, & C axis output for 90 degrees"""
self.compare_sixth_line(
"G1 X10 Y20 Z30 A90 B90 C90",
"G1 X10.000 Y20.000 Z30.000 A90.000 B90.000 C90.000 ",
"--no-header --no-show-editor",
)
self.compare_sixth_line(
"G1 X10 Y20 Z30 A90 B90 C90",
"G1 X0.3937 Y0.7874 Z1.1811 A90.0000 B90.0000 C90.0000 ",
"--no-header --inches --no-show-editor",
def test_blend_mode_in_preamble_position(self):
"""Test that blend mode command appears in correct position in preamble."""
self.profile_op.Path = Path.Path([])
self.job.PostProcessorArgs = (
"--no-header --no-comments --blend-mode BLEND --blend-tolerance 0.1 --no-show-editor"
)
gcode = self.post.export()[0][1]
lines = gcode.splitlines()
def test130(self):
"""Test A, B, & C axis output for 91 degrees"""
self.compare_sixth_line(
"G1 X10 Y20 Z30 A91 B91 C91",
"G1 X10.000 Y20.000 Z30.000 A91.000 B91.000 C91.000 ",
"--no-header --no-show-editor",
)
self.compare_sixth_line(
"G1 X10 Y20 Z30 A91 B91 C91",
"G1 X0.3937 Y0.7874 Z1.1811 A91.0000 B91.0000 C91.0000 ",
"--no-header --inches --no-show-editor",
)
# Find G64 P line
g64_line_idx = None
for i, line in enumerate(lines):
if "G64 P" in line:
g64_line_idx = i
break
def test140(self):
"""Test A, B, & C axis output for values between 90 and 180 degrees"""
self.compare_sixth_line(
"G1 X10 Y20 Z30 A100 B110 C120",
"G1 X10.000 Y20.000 Z30.000 A100.000 B110.000 C120.000 ",
"--no-header --no-show-editor",
)
self.compare_sixth_line(
"G1 X10 Y20 Z30 A100 B110 C120",
"G1 X0.3937 Y0.7874 Z1.1811 A100.0000 B110.0000 C120.0000 ",
"--no-header --inches --no-show-editor",
)
self.assertIsNotNone(g64_line_idx, "G64 P command not found")
# Should be early in output (within first few lines of preamble)
self.assertLess(g64_line_idx, 5, "G64 command should be in preamble")
def test150(self):
"""Test A, B, & C axis output for values between 180 and 360 degrees"""
self.compare_sixth_line(
"G1 X10 Y20 Z30 A240 B250 C260",
"G1 X10.000 Y20.000 Z30.000 A240.000 B250.000 C260.000 ",
"--no-header --no-show-editor",
)
self.compare_sixth_line(
"G1 X10 Y20 Z30 A240 B250 C260",
"G1 X0.3937 Y0.7874 Z1.1811 A240.0000 B250.0000 C260.0000 ",
"--no-header --inches --no-show-editor",
def test_blend_tolerance_zero_equals_no_tolerance(self):
"""Test that blend tolerance of 0 outputs G64 without P parameter."""
self.profile_op.Path = Path.Path([])
self.job.PostProcessorArgs = (
"--no-header --no-comments --blend-mode BLEND --blend-tolerance 0 --no-show-editor"
)
gcode = self.post.export()[0][1]
def test160(self):
"""Test A, B, & C axis output for values greater than 360 degrees"""
self.compare_sixth_line(
"G1 X10 Y20 Z30 A440 B450 C460",
"G1 X10.000 Y20.000 Z30.000 A440.000 B450.000 C460.000 ",
"--no-header --no-show-editor",
)
self.compare_sixth_line(
"G1 X10 Y20 Z30 A440 B450 C460",
"G1 X0.3937 Y0.7874 Z1.1811 A440.0000 B450.0000 C460.0000 ",
"--no-header --inches --no-show-editor",
)
def test170(self):
"""Test A, B, & C axis output for values between 0 and -90 degrees"""
self.compare_sixth_line(
"G1 X10 Y20 Z30 A-40 B-50 C-60",
"G1 X10.000 Y20.000 Z30.000 A-40.000 B-50.000 C-60.000 ",
"--no-header --no-show-editor",
)
self.compare_sixth_line(
"G1 X10 Y20 Z30 A-40 B-50 C-60",
"G1 X0.3937 Y0.7874 Z1.1811 A-40.0000 B-50.0000 C-60.0000 ",
"--no-header --inches --no-show-editor",
)
def test180(self):
"""Test A, B, & C axis output for values between -90 and -180 degrees"""
self.compare_sixth_line(
"G1 X10 Y20 Z30 A-100 B-110 C-120",
"G1 X10.000 Y20.000 Z30.000 A-100.000 B-110.000 C-120.000 ",
"--no-header --no-show-editor",
)
self.compare_sixth_line(
"G1 X10 Y20 Z30 A-100 B-110 C-120",
"G1 X0.3937 Y0.7874 Z1.1811 A-100.0000 B-110.0000 C-120.0000 ",
"--no-header --inches --no-show-editor",
)
def test190(self):
"""Test A, B, & C axis output for values between -180 and -360 degrees"""
self.compare_sixth_line(
"G1 X10 Y20 Z30 A-240 B-250 C-260",
"G1 X10.000 Y20.000 Z30.000 A-240.000 B-250.000 C-260.000 ",
"--no-header --no-show-editor",
)
self.compare_sixth_line(
"G1 X10 Y20 Z30 A-240 B-250 C-260",
"G1 X0.3937 Y0.7874 Z1.1811 A-240.0000 B-250.0000 C-260.0000 ",
"--no-header --inches --no-show-editor",
)
def test200(self):
"""Test A, B, & C axis output for values below -360 degrees"""
self.compare_sixth_line(
"G1 X10 Y20 Z30 A-440 B-450 C-460",
"G1 X10.000 Y20.000 Z30.000 A-440.000 B-450.000 C-460.000 ",
"--no-header --no-show-editor",
)
self.compare_sixth_line(
"G1 X10 Y20 Z30 A-440 B-450 C-460",
"G1 X0.3937 Y0.7874 Z1.1811 A-440.0000 B-450.0000 C-460.0000 ",
"--no-header --inches --no-show-editor",
)
# Should have G64 without P
lines = gcode.splitlines()
has_g64_without_p = any("G64" in line and "P" not in line for line in lines)
self.assertTrue(has_g64_without_p, "Expected G64 without P parameter when tolerance is 0")

View File

@@ -0,0 +1,288 @@
# ***************************************************************************
# * Copyright (c) 2022 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2022 Larry Woestman <LarryWoestman2@gmail.com> *
# * *
# * 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 *
# * *
# ***************************************************************************
from importlib import reload
import FreeCAD
import Path
import CAMTests.PathTestUtils as PathTestUtils
from Path.Post.scripts import mach3_mach4_legacy_post as postprocessor
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
class TestMach3Mach4LegacyPost(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.
"""
# Open existing FreeCAD document with test geometry
FreeCAD.newDocument("Unnamed")
@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.
"""
# Close geometry document without saving
FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name)
# 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.
"""
self.doc = FreeCAD.ActiveDocument
self.con = FreeCAD.Console
self.docobj = FreeCAD.ActiveDocument.addObject("Path::Feature", "testpath")
reload(
postprocessor
) # technical debt. This shouldn't be necessary but here to bypass a bug
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.
"""
FreeCAD.ActiveDocument.removeObject("testpath")
def test000(self):
"""Test Output Generation.
Empty path. Produces only the preamble and postable.
"""
self.docobj.Path = Path.Path([])
postables = [self.docobj]
# Test generating with header
# Header contains a time stamp that messes up unit testing.
# Only test length of result.
args = "--no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.assertTrue(len(gcode.splitlines()) == 13)
# Test without header
expected = """(begin preamble)
G17 G54 G40 G49 G80 G90
G21
(begin operation: testpath)
(machine: mach3_4, mm/min)
(finish operation: testpath)
(begin postamble)
M05
G17 G54 G90 G80 G40
M2
"""
self.docobj.Path = Path.Path([])
postables = [self.docobj]
args = "--no-header --no-show-editor"
# args = ("--no-header --no-comments --no-show-editor --precision=2")
gcode = postprocessor.export(postables, "-", args)
self.assertEqual(gcode, expected)
# test without comments
expected = """G17 G54 G40 G49 G80 G90
G21
M05
G17 G54 G90 G80 G40
M2
"""
args = "--no-header --no-comments --no-show-editor"
# args = ("--no-header --no-comments --no-show-editor --precision=2")
gcode = postprocessor.export(postables, "-", args)
self.assertEqual(gcode, expected)
def test010(self):
"""Test command Generation.
Test Precision
"""
c = Path.Command("G0 X10 Y20 Z30")
self.docobj.Path = Path.Path([c])
postables = [self.docobj]
args = "--no-header --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[5]
expected = "G0 X10.000 Y20.000 Z30.000"
self.assertEqual(result, expected)
args = "--no-header --precision=2 --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[5]
expected = "G0 X10.00 Y20.00 Z30.00"
self.assertEqual(result, expected)
def test020(self):
"""
Test Line Numbers
"""
c = Path.Command("G0 X10 Y20 Z30")
self.docobj.Path = Path.Path([c])
postables = [self.docobj]
args = "--no-header --line-numbers --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[5]
expected = "N160 G0 X10.000 Y20.000 Z30.000"
self.assertEqual(result, expected)
def test030(self):
"""
Test Pre-amble
"""
self.docobj.Path = Path.Path([])
postables = [self.docobj]
args = "--no-header --no-comments --preamble='G18 G55' --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[0]
self.assertEqual(result, "G18 G55")
def test040(self):
"""
Test Post-amble
"""
self.docobj.Path = Path.Path([])
postables = [self.docobj]
args = "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[-2]
self.assertEqual(result, "G0 Z50")
self.assertEqual(gcode.splitlines()[-1], "M2")
def test050(self):
"""
Test inches
"""
c = Path.Command("G0 X10 Y20 Z30")
self.docobj.Path = Path.Path([c])
postables = [self.docobj]
args = "--no-header --inches --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.assertEqual(gcode.splitlines()[2], "G20")
result = gcode.splitlines()[5]
expected = "G0 X0.3937 Y0.7874 Z1.1811"
self.assertEqual(result, expected)
# Technical debt. The following test fails. Precision not working
# with imperial units.
# args = ("--no-header --inches --precision=2 --no-show-editor")
# gcode = postprocessor.export(postables, "-", args)
# result = gcode.splitlines()[5]
# expected = "G0 X0.39 Y0.79 Z1.18"
# self.assertEqual(result, expected)
def test060(self):
"""
Test test modal
Suppress the command name if the same as previous
"""
c = Path.Command("G0 X10 Y20 Z30")
c1 = Path.Command("G0 X10 Y30 Z30")
self.docobj.Path = Path.Path([c, c1])
postables = [self.docobj]
args = "--no-header --modal --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[6]
expected = "X10.000 Y30.000 Z30.000"
self.assertEqual(result, expected)
def test070(self):
"""
Test axis modal
Suppress the axis coordinate if the same as previous
"""
c = Path.Command("G0 X10 Y20 Z30")
c1 = Path.Command("G0 X10 Y30 Z30")
self.docobj.Path = Path.Path([c, c1])
postables = [self.docobj]
args = "--no-header --axis-modal --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[6]
expected = "G0 Y30.000"
self.assertEqual(result, expected)
def test080(self):
"""
Test tool change
"""
c = Path.Command("M6 T2")
c2 = Path.Command("M3 S3000")
self.docobj.Path = Path.Path([c, c2])
postables = [self.docobj]
args = "--no-header --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.assertEqual(gcode.splitlines()[5], "M5")
self.assertEqual(gcode.splitlines()[6], "M6 T2 ")
self.assertEqual(gcode.splitlines()[7], "G43 H2")
self.assertEqual(gcode.splitlines()[8], "M3 S3000")
# suppress TLO
args = "--no-header --no-tlo --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.assertEqual(gcode.splitlines()[7], "M3 S3000")
def test090(self):
"""
Test comment
"""
c = Path.Command("(comment)")
self.docobj.Path = Path.Path([c])
postables = [self.docobj]
args = "--no-header --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[5]
expected = "(comment)"
self.assertEqual(result, expected)

View File

@@ -2,7 +2,7 @@
# ***************************************************************************
# * Copyright (c) 2022 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2022 Larry Woestman <LarryWoestman2@gmail.com> *
# * Copyright (c) 2022 - 2025 Larry Woestman <LarryWoestman2@gmail.com> *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
@@ -22,13 +22,11 @@
# * *
# ***************************************************************************
from importlib import reload
import FreeCAD
import Path
import CAMTests.PathTestUtils as PathTestUtils
from Path.Post.scripts import mach3_mach4_post as postprocessor
from Path.Post.Processor import PostProcessorFactory
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
@@ -36,9 +34,12 @@ Path.Log.trackModule(Path.Log.thisModule())
class TestMach3Mach4Post(PathTestUtils.PathTestBase):
"""Test the mach3_mach4_post.py postprocessor."""
@classmethod
def setUpClass(cls):
def setUpClass(cls) -> None:
"""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
@@ -47,108 +48,162 @@ class TestMach3Mach4Post(PathTestUtils.PathTestBase):
is able to call static methods within this same class.
"""
# Open existing FreeCAD document with test geometry
FreeCAD.newDocument("Unnamed")
FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "True")
cls.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/CAMTests/boxtest.fcstd")
cls.job = cls.doc.getObject("Job")
cls.post = PostProcessorFactory.get_post_processor(cls.job, "mach3_mach4")
# locate the operation named "Profile"
for op in cls.job.Operations.Group:
if op.Label == "Profile":
# remember the "Profile" operation
cls.profile_op = op
return
@classmethod
def tearDownClass(cls):
def tearDownClass(cls) -> None:
"""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.
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.
"""
# Close geometry document without saving
FreeCAD.closeDocument(FreeCAD.ActiveDocument.Name)
FreeCAD.closeDocument(cls.doc.Name)
FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "")
# Setup and tear down methods called before and after each unit test
def setUp(self):
def setUp(self) -> None:
"""setUp()...
This method is called prior to each `test()` method. Add code and
objects here that are needed for multiple `test()` methods.
"""
self.doc = FreeCAD.ActiveDocument
self.con = FreeCAD.Console
self.docobj = FreeCAD.ActiveDocument.addObject("Path::Feature", "testpath")
reload(
postprocessor
) # technical debt. This shouldn't be necessary but here to bypass a bug
# 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):
def tearDown(self) -> None:
"""tearDown()...
This method is called after each test() method. Add cleanup instructions here.
Such cleanup instructions will likely undo those in the setUp() method.
"""
FreeCAD.ActiveDocument.removeObject("testpath")
pass
def test000(self):
def single_compare(self, path, expected, args, debug=False):
"""Perform a test with a single line of gcode comparison."""
nl = "\n"
self.job.PostProcessorArgs = args
# replace the original path (that came with the job and operation) with our path
self.profile_op.Path = Path.Path(path)
# the gcode is in the first section for this particular job and operation
gcode = self.post.export()[0][1]
if debug:
print(f"--------{nl}{gcode}--------{nl}")
# there are 4 lines of "other stuff" before the line we are interested in
self.assertEqual(gcode.splitlines()[4], expected)
def multi_compare(self, path, expected, args, debug=False):
"""Perform a test with multiple lines of gcode comparison."""
nl = "\n"
self.job.PostProcessorArgs = args
# replace the original path (that came with the job and operation) with our path
self.profile_op.Path = Path.Path(path)
# the gcode is in the first section for this particular job and operation
gcode = self.post.export()[0][1]
if debug:
print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(gcode, expected)
def test000(self) -> None:
"""Test Output Generation.
Empty path. Produces only the preamble and postable.
"""
nl = "\n"
self.docobj.Path = Path.Path([])
postables = [self.docobj]
self.profile_op.Path = Path.Path([])
# Test generating with header
# Header contains a time stamp that messes up unit testing.
# Only test length of result.
args = "--no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.assertTrue(len(gcode.splitlines()) == 13)
self.job.PostProcessorArgs = "--no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertTrue(len(gcode.splitlines()) == 26)
# Test without header
expected = """(begin preamble)
expected = """(Begin preamble)
G17 G54 G40 G49 G80 G90
G21
(begin operation: testpath)
(machine: mach3_4, mm/min)
(finish operation: testpath)
(begin postamble)
(Begin operation: Fixture)
(Machine: mach3_4, mm/min)
G54
(Finish operation: Fixture)
(Begin operation: TC: Default Tool)
(Machine: mach3_4, mm/min)
(TC: Default Tool)
(Begin toolchange)
M5
M6 T1
G43 H1
(Finish operation: TC: Default Tool)
(Begin operation: Profile)
(Machine: mach3_4, mm/min)
(Finish operation: Profile)
(Begin postamble)
M05
G17 G54 G90 G80 G40
M2
"""
self.docobj.Path = Path.Path([])
postables = [self.docobj]
args = "--no-header --no-show-editor"
# args = ("--no-header --no-comments --no-show-editor --precision=2")
gcode = postprocessor.export(postables, "-", args)
self.job.PostProcessorArgs = "--no-header --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(gcode, expected)
# test without comments
expected = """G17 G54 G40 G49 G80 G90
G21
G54
M5
M6 T1
G43 H1
M05
G17 G54 G90 G80 G40
M2
"""
args = "--no-header --no-comments --no-show-editor"
# args = ("--no-header --no-comments --no-show-editor --precision=2")
gcode = postprocessor.export(postables, "-", args)
self.job.PostProcessorArgs = "--no-header --no-comments --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(gcode, expected)
def test010(self):
"""Test command Generation.
Test Precision
"""
nl = "\n"
c = Path.Command("G0 X10 Y20 Z30")
self.docobj.Path = Path.Path([c])
postables = [self.docobj]
self.profile_op.Path = Path.Path([c])
args = "--no-header --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[5]
self.job.PostProcessorArgs = "--no-header --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[17]
expected = "G0 X10.000 Y20.000 Z30.000"
self.assertEqual(result, expected)
args = "--no-header --precision=2 --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[5]
self.job.PostProcessorArgs = "--no-header --precision=2 --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[17]
expected = "G0 X10.00 Y20.00 Z30.00"
self.assertEqual(result, expected)
@@ -156,27 +211,32 @@ M2
"""
Test Line Numbers
"""
nl = "\n"
c = Path.Command("G0 X10 Y20 Z30")
self.docobj.Path = Path.Path([c])
postables = [self.docobj]
self.profile_op.Path = Path.Path([c])
args = "--no-header --line-numbers --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[5]
expected = "N160 G0 X10.000 Y20.000 Z30.000"
self.job.PostProcessorArgs = "--no-header --line-numbers --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[17]
expected = "N270 G0 X10.000 Y20.000 Z30.000"
self.assertEqual(result, expected)
def test030(self):
"""
Test Pre-amble
"""
nl = "\n"
self.docobj.Path = Path.Path([])
postables = [self.docobj]
self.profile_op.Path = Path.Path([])
args = "--no-header --no-comments --preamble='G18 G55' --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.job.PostProcessorArgs = (
"--no-header --no-comments --preamble='G18 G55' --no-show-editor"
)
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[0]
self.assertEqual(result, "G18 G55")
@@ -184,10 +244,15 @@ M2
"""
Test Post-amble
"""
self.docobj.Path = Path.Path([])
postables = [self.docobj]
args = "--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
nl = "\n"
self.profile_op.Path = Path.Path([])
self.job.PostProcessorArgs = (
"--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor"
)
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[-2]
self.assertEqual(result, "G0 Z50")
self.assertEqual(gcode.splitlines()[-1], "M2")
@@ -196,42 +261,44 @@ M2
"""
Test inches
"""
nl = "\n"
c = Path.Command("G0 X10 Y20 Z30")
self.docobj.Path = Path.Path([c])
postables = [self.docobj]
args = "--no-header --inches --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.profile_op.Path = Path.Path([c])
self.job.PostProcessorArgs = "--no-header --inches --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(gcode.splitlines()[2], "G20")
result = gcode.splitlines()[5]
result = gcode.splitlines()[17]
expected = "G0 X0.3937 Y0.7874 Z1.1811"
self.assertEqual(result, expected)
# Technical debt. The following test fails. Precision not working
# with imperial units.
# args = ("--no-header --inches --precision=2 --no-show-editor")
# gcode = postprocessor.export(postables, "-", args)
# result = gcode.splitlines()[5]
# expected = "G0 X0.39 Y0.79 Z1.18"
# self.assertEqual(result, expected)
self.job.PostProcessorArgs = "--no-header --inches --precision=2 --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[17]
expected = "G0 X0.39 Y0.79 Z1.18"
self.assertEqual(result, expected)
def test060(self):
"""
Test test modal
Suppress the command name if the same as previous
"""
nl = "\n"
c = Path.Command("G0 X10 Y20 Z30")
c1 = Path.Command("G0 X10 Y30 Z30")
self.docobj.Path = Path.Path([c, c1])
postables = [self.docobj]
self.profile_op.Path = Path.Path([c, c1])
args = "--no-header --modal --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[6]
self.job.PostProcessorArgs = "--no-header --modal --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[18]
expected = "X10.000 Y30.000 Z30.000"
self.assertEqual(result, expected)
@@ -240,15 +307,17 @@ M2
Test axis modal
Suppress the axis coordinate if the same as previous
"""
nl = "\n"
c = Path.Command("G0 X10 Y20 Z30")
c1 = Path.Command("G0 X10 Y30 Z30")
self.docobj.Path = Path.Path([c, c1])
postables = [self.docobj]
self.profile_op.Path = Path.Path([c, c1])
args = "--no-header --axis-modal --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[6]
self.job.PostProcessorArgs = "--no-header --axis-modal --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[18]
expected = "G0 Y30.000"
self.assertEqual(result, expected)
@@ -256,35 +325,41 @@ M2
"""
Test tool change
"""
nl = "\n"
c = Path.Command("M6 T2")
c2 = Path.Command("M3 S3000")
self.docobj.Path = Path.Path([c, c2])
postables = [self.docobj]
args = "--no-header --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.assertEqual(gcode.splitlines()[5], "M5")
self.assertEqual(gcode.splitlines()[6], "M6 T2 ")
self.assertEqual(gcode.splitlines()[7], "G43 H2")
self.assertEqual(gcode.splitlines()[8], "M3 S3000")
self.profile_op.Path = Path.Path([c, c2])
self.job.PostProcessorArgs = "--no-header --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
split_gcode = gcode.splitlines()
self.assertEqual(split_gcode[18], "M5")
self.assertEqual(split_gcode[19], "M6 T2")
self.assertEqual(split_gcode[20], "G43 H2")
self.assertEqual(split_gcode[21], "M3 S3000")
# suppress TLO
args = "--no-header --no-tlo --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.assertEqual(gcode.splitlines()[7], "M3 S3000")
self.job.PostProcessorArgs = "--no-header --no-tlo --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(gcode.splitlines()[19], "M3 S3000")
def test090(self):
"""
Test comment
"""
nl = "\n"
c = Path.Command("(comment)")
self.docobj.Path = Path.Path([c])
postables = [self.docobj]
self.profile_op.Path = Path.Path([c])
args = "--no-header --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
result = gcode.splitlines()[5]
self.job.PostProcessorArgs = "--no-header --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[17]
expected = "(comment)"
self.assertEqual(result, expected)

View File

@@ -43,7 +43,7 @@ Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
class TestRefactoredMassoG3Post(PathTestUtils.PathTestBase):
class TestMassoG3Post(PathTestUtils.PathTestBase):
@classmethod
def setUpClass(cls):
"""setUpClass()...
@@ -58,7 +58,7 @@ class TestRefactoredMassoG3Post(PathTestUtils.PathTestBase):
FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "True")
cls.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/CAMTests/boxtest.fcstd")
cls.job = cls.doc.getObject("Job")
cls.post = PostProcessorFactory.get_post_processor(cls.job, "refactored_masso_g3")
cls.post = PostProcessorFactory.get_post_processor(cls.job, "masso_g3")
# locate the operation named "Profile"
for op in cls.job.Operations.Group:
if op.Label == "Profile":
@@ -109,7 +109,6 @@ class TestRefactoredMassoG3Post(PathTestUtils.PathTestBase):
# Only test length of result.
self.job.PostProcessorArgs = "--no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertTrue(len(gcode.splitlines()) == 26)
# Test without header

View File

@@ -470,14 +470,14 @@ class TestPostProcessorFactory(unittest.TestCase):
def test030(self):
# test wrapping of old school postprocessor scripts
post = PostProcessorFactory.get_post_processor(self.job, "linuxcnc")
post = PostProcessorFactory.get_post_processor(self.job, "linuxcnc_legacy")
self.assertTrue(post is not None)
self.assertTrue(hasattr(post, "_buildPostList"))
def test040(self):
"""Test that the __name__ of the postprocessor is correct."""
post = PostProcessorFactory.get_post_processor(self.job, "linuxcnc")
self.assertEqual(post.script_module.__name__, "linuxcnc_post")
post = PostProcessorFactory.get_post_processor(self.job, "linuxcnc_legacy")
self.assertEqual(post.script_module.__name__, "linuxcnc_legacy_post")
class TestPathPostUtils(unittest.TestCase):

View File

@@ -33,8 +33,8 @@ Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
class TestRefactoredTestPostGCodes(PathTestUtils.PathTestBase):
"""Test the refactored_test_post.py postprocessor G codes."""
class TestPostGCodes(PathTestUtils.PathTestBase):
"""Test the test_post.py postprocessor G codes."""
@classmethod
def setUpClass(cls):
@@ -50,7 +50,7 @@ class TestRefactoredTestPostGCodes(PathTestUtils.PathTestBase):
FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "True")
cls.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/CAMTests/boxtest.fcstd")
cls.job = cls.doc.getObject("Job")
cls.post = PostProcessorFactory.get_post_processor(cls.job, "refactored_test")
cls.post = PostProcessorFactory.get_post_processor(cls.job, "test")
# locate the operation named "Profile"
for op in cls.job.Operations.Group:
if op.Label == "Profile":

View File

@@ -33,8 +33,8 @@ Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
class TestRefactoredTestPostMCodes(PathTestUtils.PathTestBase):
"""Test the refactored_test_post.py postprocessor."""
class TestPostMCodes(PathTestUtils.PathTestBase):
"""Test the test_post.py postprocessor M codes."""
@classmethod
def setUpClass(cls):
@@ -50,7 +50,7 @@ class TestRefactoredTestPostMCodes(PathTestUtils.PathTestBase):
FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "True")
cls.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/CAMTests/boxtest.fcstd")
cls.job = cls.doc.getObject("Job")
cls.post = PostProcessorFactory.get_post_processor(cls.job, "refactored_test")
cls.post = PostProcessorFactory.get_post_processor(cls.job, "test")
# locate the operation named "Profile"
for op in cls.job.Operations.Group:
if op.Label == "Profile":

View File

@@ -1,361 +0,0 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# ***************************************************************************
# * Copyright (c) 2022 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2022 - 2025 Larry Woestman <LarryWoestman2@gmail.com> *
# * *
# * 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 FreeCAD
import Path
import CAMTests.PathTestUtils as PathTestUtils
from Path.Post.Processor import PostProcessorFactory
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
class TestRefactoredCentroidPost(PathTestUtils.PathTestBase):
"""Test the refactored_centroid_post.py postprocessor."""
@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.
"""
FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "True")
cls.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/CAMTests/boxtest.fcstd")
cls.job = cls.doc.getObject("Job")
cls.post = PostProcessorFactory.get_post_processor(cls.job, "refactored_centroid")
# locate the operation named "Profile"
for op in cls.job.Operations.Group:
if op.Label == "Profile":
# remember the "Profile" operation
cls.profile_op = op
return
@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.
"""
FreeCAD.closeDocument(cls.doc.Name)
FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "")
# 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.
"""
# 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.
"""
pass
def single_compare(self, path, expected, args, debug=False):
"""Perform a test with a single line of gcode comparison."""
nl = "\n"
self.job.PostProcessorArgs = args
# replace the original path (that came with the job and operation) with our path
self.profile_op.Path = Path.Path(path)
# the gcode is in the first section for this particular job and operation
gcode = self.post.export()[0][1]
if debug:
print(f"--------{nl}{gcode}--------{nl}")
# there are 4 lines of "other stuff" before the line we are interested in
self.assertEqual(gcode.splitlines()[4], expected)
def multi_compare(self, path, expected, args, debug=False):
"""Perform a test with multiple lines of gcode comparison."""
nl = "\n"
self.job.PostProcessorArgs = args
# replace the original path (that came with the job and operation) with our path
self.profile_op.Path = Path.Path(path)
# the gcode is in the first section for this particular job and operation
gcode = self.post.export()[0][1]
if debug:
print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(gcode, expected)
def test000(self):
"""Test Output Generation.
Empty path. Produces only the preamble and postable.
"""
nl = "\n"
self.profile_op.Path = Path.Path([])
# Test generating with header
# Header contains a time stamp that messes up unit testing.
# Only test length of result.
self.job.PostProcessorArgs = "--no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertTrue(len(gcode.splitlines()) == 25)
# Test without header
expected = """G90 G80 G40 G49
;T1=TC__Default_Tool
;Begin preamble
G53 G00 G17
G21
;Begin operation
G54
;End operation
;Begin operation
;TC: Default Tool
;Begin toolchange
M6 T1
;End operation
;Begin operation
;End operation
;Begin postamble
M5
M25
G49 H0
G90 G80 G40 G49
M99
"""
self.job.PostProcessorArgs = "--no-header --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(gcode, expected)
# test without comments
expected = """G90 G80 G40 G49
G53 G00 G17
G21
G54
M6 T1
M5
M25
G49 H0
G90 G80 G40 G49
M99
"""
# args = ("--no-header --no-comments --no-show-editor --precision=2")
self.job.PostProcessorArgs = "--no-header --no-comments --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(gcode, expected)
def test010(self):
"""Test command Generation.
Test Precision
"""
nl = "\n"
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]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[14]
expected = "G0 X10.0000 Y20.0000 Z30.0000"
self.assertEqual(result, expected)
self.job.PostProcessorArgs = "--no-header --axis-precision=2 --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[14]
expected = "G0 X10.00 Y20.00 Z30.00"
self.assertEqual(result, expected)
def test020(self):
"""
Test Line Numbers
"""
nl = "\n"
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]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[14]
expected = "N240 G0 X10.0000 Y20.0000 Z30.0000"
self.assertEqual(result, expected)
def test030(self):
"""
Test Pre-amble
"""
nl = "\n"
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]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[1]
self.assertEqual(result, "G18 G55")
def test040(self):
"""
Test Post-amble
"""
nl = "\n"
self.profile_op.Path = Path.Path([])
self.job.PostProcessorArgs = (
"--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor"
)
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[-2]
self.assertEqual(result, "G0 Z50")
self.assertEqual(gcode.splitlines()[-1], "M2")
def test050(self):
"""
Test inches
"""
nl = "\n"
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]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(gcode.splitlines()[4], "G20")
result = gcode.splitlines()[14]
expected = "G0 X0.3937 Y0.7874 Z1.1811"
self.assertEqual(result, expected)
self.job.PostProcessorArgs = "--no-header --inches --axis-precision=2 --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[14]
expected = "G0 X0.39 Y0.79 Z1.18"
self.assertEqual(result, expected)
def test060(self):
"""
Test test modal
Suppress the command name if the same as previous
"""
nl = "\n"
c = Path.Command("G0 X10 Y20 Z30")
c1 = Path.Command("G0 X10 Y30 Z30")
self.profile_op.Path = Path.Path([c, c1])
self.job.PostProcessorArgs = "--no-header --modal --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[15]
expected = "X10.0000 Y30.0000 Z30.0000"
self.assertEqual(result, expected)
def test070(self):
"""
Test axis modal
Suppress the axis coordinate if the same as previous
"""
nl = "\n"
c = Path.Command("G0 X10 Y20 Z30")
c1 = Path.Command("G0 X10 Y30 Z30")
self.profile_op.Path = Path.Path([c, c1])
self.job.PostProcessorArgs = "--no-header --axis-modal --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[15]
expected = "G0 Y30.0000"
self.assertEqual(result, expected)
def test080(self):
"""
Test tool change
"""
nl = "\n"
c = Path.Command("M6 T2")
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]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(gcode.splitlines()[15], "M6 T2")
self.assertEqual(gcode.splitlines()[16], "M3 S3000")
# suppress TLO
self.job.PostProcessorArgs = "--no-header --no-tlo --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(gcode.splitlines()[16], "M3 S3000")
def test090(self):
"""
Test comment
"""
nl = "\n"
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]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[14]
expected = ";comment"
self.assertEqual(result, expected)

View File

@@ -1,353 +0,0 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# ***************************************************************************
# * Copyright (c) 2022 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2022 - 2025 Larry Woestman <LarryWoestman2@gmail.com> *
# * *
# * 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 FreeCAD
import Path
import CAMTests.PathTestUtils as PathTestUtils
from Path.Post.Processor import PostProcessorFactory
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
class TestRefactoredGrblPost(PathTestUtils.PathTestBase):
"""Test the refactored_grbl_post.py postprocessor."""
@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.
"""
FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "True")
cls.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/CAMTests/boxtest.fcstd")
cls.job = cls.doc.getObject("Job")
cls.post = PostProcessorFactory.get_post_processor(cls.job, "refactored_grbl")
# locate the operation named "Profile"
for op in cls.job.Operations.Group:
if op.Label == "Profile":
# remember the "Profile" operation
cls.profile_op = op
return
@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.
"""
FreeCAD.closeDocument(cls.doc.Name)
FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "")
# 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.
"""
# 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.
"""
pass
def test000(self):
"""Test Output Generation.
Empty path. Produces only the preamble and postable.
"""
nl = "\n"
self.profile_op.Path = Path.Path([])
# Test generating with header
# Header contains a time stamp that messes up unit testing.
# Only test length of result.
self.job.PostProcessorArgs = "--no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertTrue(len(gcode.splitlines()) == 24)
# Test without header
expected = """(Begin preamble)
G17 G90
G21
(Begin operation: Fixture)
(Path: Fixture)
G54
(Finish operation: Fixture)
(Begin operation: TC: Default Tool)
(Path: TC: Default Tool)
(TC: Default Tool)
(Begin toolchange)
(M6 T1)
(Finish operation: TC: Default Tool)
(Begin operation: Profile)
(Path: Profile)
(Finish operation: Profile)
(Begin postamble)
M5
G17 G90
M2
"""
self.profile_op.Path = Path.Path([])
# args = ("--no-header --no-comments --no-show-editor --precision=2")
self.job.PostProcessorArgs = "--no-header --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(gcode, expected)
# test without comments
expected = """G17 G90
G21
G54
M5
G17 G90
M2
"""
# args = ("--no-header --no-comments --no-show-editor --precision=2")
self.job.PostProcessorArgs = "--no-header --no-comments --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(gcode, expected)
def test010(self):
"""Test command Generation.
Test Precision
"""
nl = "\n"
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]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[15]
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]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[15]
expected = "G0 X10.00 Y20.00 Z30.00"
self.assertEqual(result, expected)
def test020(self):
"""
Test Line Numbers
"""
nl = "\n"
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]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[15]
expected = "N250 G0 X10.000 Y20.000 Z30.000"
self.assertEqual(result, expected)
def test030(self):
"""
Test Pre-amble
"""
nl = "\n"
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]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[0]
self.assertEqual(result, "G18 G55")
def test040(self):
"""
Test Post-amble
"""
nl = "\n"
self.profile_op.Path = Path.Path([])
self.job.PostProcessorArgs = (
"--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor"
)
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[-2]
self.assertEqual(result, "G0 Z50")
self.assertEqual(gcode.splitlines()[-1], "M2")
def test050(self):
"""
Test inches
"""
nl = "\n"
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]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(gcode.splitlines()[2], "G20")
result = gcode.splitlines()[15]
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]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[15]
expected = "G0 X0.39 Y0.79 Z1.18"
self.assertEqual(result, expected)
def test060(self):
"""
Test test modal
Suppress the command name if the same as previous
"""
nl = "\n"
c = Path.Command("G0 X10 Y20 Z30")
c1 = Path.Command("G0 X10 Y30 Z30")
self.profile_op.Path = Path.Path([c, c1])
self.job.PostProcessorArgs = "--no-header --modal --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[16]
expected = "X10.000 Y30.000 Z30.000"
self.assertEqual(result, expected)
def test070(self):
"""
Test axis modal
Suppress the axis coordinate if the same as previous
"""
nl = "\n"
c = Path.Command("G0 X10 Y20 Z30")
c1 = Path.Command("G0 X10 Y30 Z30")
self.profile_op.Path = Path.Path([c, c1])
self.job.PostProcessorArgs = "--no-header --axis-modal --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[16]
expected = "G0 Y30.000"
self.assertEqual(result, expected)
def test080(self):
"""
Test tool change
"""
nl = "\n"
c = Path.Command("M6 T2")
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]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(gcode.splitlines()[16], "(M6 T2)")
self.assertEqual(gcode.splitlines()[17], "M3 S3000")
# suppress TLO
self.job.PostProcessorArgs = "--no-header --no-tlo --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(gcode.splitlines()[17], "M3 S3000")
def test090(self):
"""
Test comment
"""
nl = "\n"
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]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[15]
expected = "(comment)"
self.assertEqual(result, expected)
def test100(self):
"""
Test if coolant is enabled.
"""
nl = "\n"
c = Path.Command("M7")
c1 = Path.Command("M8")
c2 = Path.Command("M9")
self.profile_op.Path = Path.Path([c, c1, c2])
self.job.PostProcessorArgs = "--no-header --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(gcode.splitlines()[15], "M7")
self.assertEqual(gcode.splitlines()[16], "M8")
self.assertEqual(gcode.splitlines()[17], "M9")

View File

@@ -1,365 +0,0 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# ***************************************************************************
# * Copyright (c) 2022 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2022 - 2025 Larry Woestman <LarryWoestman2@gmail.com> *
# * *
# * 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 FreeCAD
import Path
import CAMTests.PathTestUtils as PathTestUtils
from Path.Post.Processor import PostProcessorFactory
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
class TestRefactoredMach3Mach4Post(PathTestUtils.PathTestBase):
"""Test the refactored_mach3_mach4_post.py postprocessor."""
@classmethod
def setUpClass(cls) -> None:
"""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.
"""
FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "True")
cls.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/CAMTests/boxtest.fcstd")
cls.job = cls.doc.getObject("Job")
cls.post = PostProcessorFactory.get_post_processor(cls.job, "refactored_mach3_mach4")
# locate the operation named "Profile"
for op in cls.job.Operations.Group:
if op.Label == "Profile":
# remember the "Profile" operation
cls.profile_op = op
return
@classmethod
def tearDownClass(cls) -> None:
"""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.
"""
FreeCAD.closeDocument(cls.doc.Name)
FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "")
# Setup and tear down methods called before and after each unit test
def setUp(self) -> None:
"""setUp()...
This method is called prior to each `test()` method. Add code and
objects here that are needed for multiple `test()` methods.
"""
# 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) -> None:
"""tearDown()...
This method is called after each test() method. Add cleanup instructions here.
Such cleanup instructions will likely undo those in the setUp() method.
"""
pass
def single_compare(self, path, expected, args, debug=False):
"""Perform a test with a single line of gcode comparison."""
nl = "\n"
self.job.PostProcessorArgs = args
# replace the original path (that came with the job and operation) with our path
self.profile_op.Path = Path.Path(path)
# the gcode is in the first section for this particular job and operation
gcode = self.post.export()[0][1]
if debug:
print(f"--------{nl}{gcode}--------{nl}")
# there are 4 lines of "other stuff" before the line we are interested in
self.assertEqual(gcode.splitlines()[4], expected)
def multi_compare(self, path, expected, args, debug=False):
"""Perform a test with multiple lines of gcode comparison."""
nl = "\n"
self.job.PostProcessorArgs = args
# replace the original path (that came with the job and operation) with our path
self.profile_op.Path = Path.Path(path)
# the gcode is in the first section for this particular job and operation
gcode = self.post.export()[0][1]
if debug:
print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(gcode, expected)
def test000(self) -> None:
"""Test Output Generation.
Empty path. Produces only the preamble and postable.
"""
nl = "\n"
self.profile_op.Path = Path.Path([])
# Test generating with header
# Header contains a time stamp that messes up unit testing.
# Only test length of result.
self.job.PostProcessorArgs = "--no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertTrue(len(gcode.splitlines()) == 26)
# Test without header
expected = """(Begin preamble)
G17 G54 G40 G49 G80 G90
G21
(Begin operation: Fixture)
(Machine: mach3_4, mm/min)
G54
(Finish operation: Fixture)
(Begin operation: TC: Default Tool)
(Machine: mach3_4, mm/min)
(TC: Default Tool)
(Begin toolchange)
M5
M6 T1
G43 H1
(Finish operation: TC: Default Tool)
(Begin operation: Profile)
(Machine: mach3_4, mm/min)
(Finish operation: Profile)
(Begin postamble)
M05
G17 G54 G90 G80 G40
M2
"""
# args = ("--no-header --no-comments --no-show-editor --precision=2")
self.job.PostProcessorArgs = "--no-header --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(gcode, expected)
# test without comments
expected = """G17 G54 G40 G49 G80 G90
G21
G54
M5
M6 T1
G43 H1
M05
G17 G54 G90 G80 G40
M2
"""
# args = ("--no-header --no-comments --no-show-editor --precision=2")
self.job.PostProcessorArgs = "--no-header --no-comments --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(gcode, expected)
def test010(self):
"""Test command Generation.
Test Precision
"""
nl = "\n"
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]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[17]
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]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[17]
expected = "G0 X10.00 Y20.00 Z30.00"
self.assertEqual(result, expected)
def test020(self):
"""
Test Line Numbers
"""
nl = "\n"
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]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[17]
expected = "N270 G0 X10.000 Y20.000 Z30.000"
self.assertEqual(result, expected)
def test030(self):
"""
Test Pre-amble
"""
nl = "\n"
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]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[0]
self.assertEqual(result, "G18 G55")
def test040(self):
"""
Test Post-amble
"""
nl = "\n"
self.profile_op.Path = Path.Path([])
self.job.PostProcessorArgs = (
"--no-header --no-comments --postamble='G0 Z50\nM2' --no-show-editor"
)
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[-2]
self.assertEqual(result, "G0 Z50")
self.assertEqual(gcode.splitlines()[-1], "M2")
def test050(self):
"""
Test inches
"""
nl = "\n"
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]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(gcode.splitlines()[2], "G20")
result = gcode.splitlines()[17]
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]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[17]
expected = "G0 X0.39 Y0.79 Z1.18"
self.assertEqual(result, expected)
def test060(self):
"""
Test test modal
Suppress the command name if the same as previous
"""
nl = "\n"
c = Path.Command("G0 X10 Y20 Z30")
c1 = Path.Command("G0 X10 Y30 Z30")
self.profile_op.Path = Path.Path([c, c1])
self.job.PostProcessorArgs = "--no-header --modal --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[18]
expected = "X10.000 Y30.000 Z30.000"
self.assertEqual(result, expected)
def test070(self):
"""
Test axis modal
Suppress the axis coordinate if the same as previous
"""
nl = "\n"
c = Path.Command("G0 X10 Y20 Z30")
c1 = Path.Command("G0 X10 Y30 Z30")
self.profile_op.Path = Path.Path([c, c1])
self.job.PostProcessorArgs = "--no-header --axis-modal --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[18]
expected = "G0 Y30.000"
self.assertEqual(result, expected)
def test080(self):
"""
Test tool change
"""
nl = "\n"
c = Path.Command("M6 T2")
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]
# print(f"--------{nl}{gcode}--------{nl}")
split_gcode = gcode.splitlines()
self.assertEqual(split_gcode[18], "M5")
self.assertEqual(split_gcode[19], "M6 T2")
self.assertEqual(split_gcode[20], "G43 H2")
self.assertEqual(split_gcode[21], "M3 S3000")
# suppress TLO
self.job.PostProcessorArgs = "--no-header --no-tlo --no-show-editor"
gcode = self.post.export()[0][1]
# print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(gcode.splitlines()[19], "M3 S3000")
def test090(self):
"""
Test comment
"""
nl = "\n"
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]
# print(f"--------{nl}{gcode}--------{nl}")
result = gcode.splitlines()[17]
expected = "(comment)"
self.assertEqual(result, expected)

View File

@@ -39,8 +39,8 @@ Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
class TestRefactoredTestPost(PathTestUtils.PathTestBase):
"""Test the refactored_test_post.py postprocessor command line arguments."""
class TestTestPost(PathTestUtils.PathTestBase):
"""Test the test_post.py postprocessor command line arguments."""
@classmethod
def setUpClass(cls):
@@ -56,7 +56,7 @@ class TestRefactoredTestPost(PathTestUtils.PathTestBase):
FreeCAD.ConfigSet("SuppressRecomputeRequiredDialog", "True")
cls.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/CAMTests/boxtest.fcstd")
cls.job = cls.doc.getObject("Job")
cls.post = PostProcessorFactory.get_post_processor(cls.job, "refactored_test")
cls.post = PostProcessorFactory.get_post_processor(cls.job, "test")
# locate the operation named "Profile"
for op in cls.job.Operations.Group:
if op.Label == "Profile":
@@ -944,7 +944,7 @@ G54
# print(f"--------{nl}{gcode}--------{nl}")
split_gcode = gcode.splitlines()
self.assertEqual(split_gcode[0], "(Exported by FreeCAD)")
self.assertEqual(split_gcode[1], "(Post Processor: refactored_test_post)")
self.assertEqual(split_gcode[1], "(Post Processor: test_post)")
self.assertEqual(split_gcode[2], "(Cam File: boxtest.fcstd)")
# The header contains a time stamp that messes up unit testing.
# Only test the length of the line that contains the time.
@@ -992,7 +992,7 @@ G54
split_gcode = gcode.splitlines()
# print(f"--------{nl}{gcode}--------{nl}")
self.assertEqual(split_gcode[0], "(Exported by FreeCAD)")
self.assertEqual(split_gcode[1], "(Post Processor: refactored_test_post)")
self.assertEqual(split_gcode[1], "(Post Processor: test_post)")
self.assertEqual(split_gcode[2], "(Cam File: boxtest.fcstd)")
# The header contains a time stamp that messes up unit testing.
# Only test the length of the line that contains the time.
@@ -1278,6 +1278,8 @@ G0 Z8.000
e.g. --return-to=0,0,0 (default is do not move)
--show-editor Pop up editor before writing output (default)
--no-show-editor Don't pop up editor before writing output
--split-arcs Convert G2/G3 arc commands to discrete G1 line segments
--no-split-arcs Output G2/G3 arc commands as-is (default)
--tlo Output tool length offset (G43) following tool changes
(default)
--no-tlo Suppress tool length offset (G43) following tool

Binary file not shown.

View File

@@ -1,69 +0,0 @@
G90 G80 G40 G49
G53 G00 G17
G20
;Default_Tool
M6 T2
M3 S0
;Contour
;Uncompensated Tool Path
G0 Z15.00
G90
G17
G0 Z15.00
G0 X10.00 Y10.00
G0 Z10.00
G1 X10.00 Y10.00 Z9.00
G1 X10.00 Y0.00 Z9.00
G1 X0.00 Y0.00 Z9.00
G1 X0.00 Y10.00 Z9.00
G1 X10.00 Y10.00 Z9.00
G1 X10.00 Y10.00 Z8.00
G1 X10.00 Y0.00 Z8.00
G1 X0.00 Y0.00 Z8.00
G1 X0.00 Y10.00 Z8.00
G1 X10.00 Y10.00 Z8.00
G1 X10.00 Y10.00 Z7.00
G1 X10.00 Y0.00 Z7.00
G1 X0.00 Y0.00 Z7.00
G1 X0.00 Y10.00 Z7.00
G1 X10.00 Y10.00 Z7.00
G1 X10.00 Y10.00 Z6.00
G1 X10.00 Y0.00 Z6.00
G1 X0.00 Y0.00 Z6.00
G1 X0.00 Y10.00 Z6.00
G1 X10.00 Y10.00 Z6.00
G1 X10.00 Y10.00 Z5.00
G1 X10.00 Y0.00 Z5.00
G1 X0.00 Y0.00 Z5.00
G1 X0.00 Y10.00 Z5.00
G1 X10.00 Y10.00 Z5.00
G1 X10.00 Y10.00 Z4.00
G1 X10.00 Y0.00 Z4.00
G1 X0.00 Y0.00 Z4.00
G1 X0.00 Y10.00 Z4.00
G1 X10.00 Y10.00 Z4.00
G1 X10.00 Y10.00 Z3.00
G1 X10.00 Y0.00 Z3.00
G1 X0.00 Y0.00 Z3.00
G1 X0.00 Y10.00 Z3.00
G1 X10.00 Y10.00 Z3.00
G1 X10.00 Y10.00 Z2.00
G1 X10.00 Y0.00 Z2.00
G1 X0.00 Y0.00 Z2.00
G1 X0.00 Y10.00 Z2.00
G1 X10.00 Y10.00 Z2.00
G1 X10.00 Y10.00 Z1.00
G1 X10.00 Y0.00 Z1.00
G1 X0.00 Y0.00 Z1.00
G1 X0.00 Y10.00 Z1.00
G1 X10.00 Y10.00 Z1.00
G1 X10.00 Y10.00 Z0.00
G1 X10.00 Y0.00 Z0.00
G1 X0.00 Y0.00 Z0.00
G1 X0.00 Y10.00 Z0.00
G1 X10.00 Y10.00 Z0.00
G0 Z15.00
M5 M25
G49 H0
G90 G80 G40 G49
M99

View File

@@ -302,34 +302,33 @@ SET(PathPythonPost_SRCS
SET(PathPythonPostScripts_SRCS
Path/Post/scripts/__init__.py
Path/Post/scripts/centroid_post.py
Path/Post/scripts/comparams_post.py
Path/Post/scripts/centroid_legacy_post.py
Path/Post/scripts/dxf_post.py
Path/Post/scripts/dynapath_post.py
Path/Post/scripts/dynapath_4060_post.py
Path/Post/scripts/estlcam_post.py
Path/Post/scripts/example_pre.py
Path/Post/scripts/fablin_post.py
Path/Post/scripts/fanuc_post.py
Path/Post/scripts/fangling_post.py
Path/Post/scripts/gcode_pre.py
Path/Post/scripts/generic_post.py
Path/Post/scripts/grbl_post.py
Path/Post/scripts/grbl_legacy_post.py
Path/Post/scripts/heidenhain_post.py
Path/Post/scripts/jtech_post.py
Path/Post/scripts/KineticNCBeamicon2_post.py
Path/Post/scripts/linuxcnc_post.py
Path/Post/scripts/linuxcnc_legacy_post.py
Path/Post/scripts/mach3_mach4_post.py
Path/Post/scripts/mach3_mach4_legacy_post.py
Path/Post/scripts/masso_g3_post.py
Path/Post/scripts/marlin_post.py
Path/Post/scripts/nccad_post.py
Path/Post/scripts/opensbp_post.py
Path/Post/scripts/opensbp_pre.py
Path/Post/scripts/philips_post.py
Path/Post/scripts/refactored_centroid_post.py
Path/Post/scripts/refactored_grbl_post.py
Path/Post/scripts/refactored_linuxcnc_post.py
Path/Post/scripts/refactored_mach3_mach4_post.py
Path/Post/scripts/refactored_masso_g3_post.py
Path/Post/scripts/refactored_test_post.py
Path/Post/scripts/grbl_post.py
Path/Post/scripts/grbl_legacy_post.py
Path/Post/scripts/test_post.py
Path/Post/scripts/rml_post.py
Path/Post/scripts/rrf_post.py
Path/Post/scripts/slic3r_pre.py
@@ -481,23 +480,26 @@ SET(Tools_Shape_SRCS
SET(Tests_SRCS
CAMTests/__init__.py
CAMTests/boxtest.fcstd
CAMTests/boxtest1.fcstd
CAMTests/dressuptest.FCStd
CAMTests/Drilling_1.FCStd
CAMTests/drill_test1.FCStd
CAMTests/FilePathTestUtils.py
CAMTests/PathTestUtils.py
CAMTests/PostTestMocks.py
CAMTests/test_adaptive.fcstd
CAMTests/test_profile.fcstd
CAMTests/test_centroid_00.ngc
CAMTests/test_filenaming.fcstd
CAMTests/test_geomop.fcstd
CAMTests/test_holes00.fcstd
CAMTests/TestCAMSanity.py
CAMTests/TestCentroidPost.py
CAMTests/TestCentroidLegacyPost.py
CAMTests/TestGenericPost.py
CAMTests/TestGrblPost.py
CAMTests/TestGrblLegacyPost.py
CAMTests/TestLinuxCNCPost.py
CAMTests/TestLinuxCNCLegacyPost.py
CAMTests/TestMach3Mach4Post.py
CAMTests/TestMach3Mach4LegacyPost.py
CAMTests/TestMassoG3Post.py
CAMTests/TestPathAdaptive.py
CAMTests/TestPathCommandAnnotations.py
CAMTests/TestPathCore.py
@@ -550,15 +552,12 @@ SET(Tests_SRCS
CAMTests/TestPathUtil.py
CAMTests/TestPathVcarve.py
CAMTests/TestPathVoronoi.py
CAMTests/TestRefactoredCentroidPost.py
CAMTests/TestRefactoredGrblPost.py
CAMTests/TestRefactoredLinuxCNCPost.py
CAMTests/TestRefactoredMach3Mach4Post.py
CAMTests/TestRefactoredMassoG3Post.py
CAMTests/TestRefactoredTestDressupPost.py
CAMTests/TestRefactoredTestPost.py
CAMTests/TestRefactoredTestPostGCodes.py
CAMTests/TestRefactoredTestPostMCodes.py
CAMTests/TestGrblLegacyPost.py
CAMTests/TestLinuxCNCLegacyPost.py
CAMTests/TestDressupPost.py
CAMTests/TestTestPost.py
CAMTests/TestPostGCodes.py
CAMTests/TestPostMCodes.py
CAMTests/TestSnapmakerPost.py
CAMTests/Tools/Bit/test-path-tool-bit-bit-00.fctb
CAMTests/Tools/Library/test-path-tool-bit-library-00.fctl

View File

@@ -335,31 +335,40 @@ def fcoms(string, commentsym):
return comment
def splitArcs(path):
def splitArcs(path, deflection=None):
"""Filter a path object and replace all G2/G3 moves with discrete G1 moves.
Returns a Path object.
"""
prefGrp = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/CAM")
deflection = prefGrp.GetFloat("LibAreaCurveAccuarcy", 0.01)
Args:
path: Path.Path object to process
deflection: Curve deflection tolerance (default: from preferences)
results = []
Returns:
Path.Path object with arcs replaced by G1 segments.
"""
if not isinstance(path, Path.Path):
raise TypeError("path must be a Path object")
if deflection is None:
prefGrp = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/CAM")
deflection = prefGrp.GetFloat("LibAreaCurveAccuracy", 0.01)
results = []
machine = MachineState()
for command in path.Commands:
if command.Name not in Path.Geom.CmdMoveArc:
machine.addCommand(command)
results.append(command)
continue
else:
# Discretize arc into line segments
edge = Path.Geom.edgeForCmd(command, machine.getPosition())
pts = edge.discretize(Deflection=deflection)
edge = Path.Geom.edgeForCmd(command, machine.getPosition())
pts = edge.discretize(Deflection=deflection)
edges = [Part.makeLine(v1, v2) for v1, v2 in zip(pts, pts[1:])]
for edge in edges:
results.extend(Path.Geom.cmdsForEdge(edge))
# Convert points directly to G1 commands
feed_params = {"F": command.Parameters["F"]} if "F" in command.Parameters else {}
for pt in pts[1:]: # Skip first point (already at that position)
params = {"X": pt.x, "Y": pt.y, "Z": pt.z}
params.update(feed_params)
results.append(Path.Command("G1", params))
machine.addCommand(command)

View File

@@ -87,6 +87,7 @@ def init_argument_defaults(argument_defaults: Dict[str, bool]) -> None:
argument_defaults["output_path_labels"] = False
argument_defaults["output_visible_arguments"] = False
argument_defaults["show-editor"] = True
argument_defaults["split_arcs"] = False
argument_defaults["tlo"] = True
argument_defaults["tool_change"] = True
argument_defaults["translate_drill"] = False
@@ -123,6 +124,7 @@ def init_arguments_visible(arguments_visible: Dict[str, bool]) -> None:
arguments_visible["precision"] = True
arguments_visible["return-to"] = False
arguments_visible["show-editor"] = True
arguments_visible["split_arcs"] = True
arguments_visible["tlo"] = True
arguments_visible["tool_change"] = False
arguments_visible["translate_drill"] = False
@@ -436,6 +438,15 @@ def init_shared_arguments(
"Don't pop up editor before writing output",
arguments_visible["show-editor"],
)
add_flag_type_arguments(
shared,
argument_defaults["split_arcs"],
"--split-arcs",
"--no-split-arcs",
"Convert G2/G3 arc commands to discrete G1 line segments",
"Output G2/G3 arc commands as-is",
arguments_visible["split_arcs"],
)
add_flag_type_arguments(
shared,
argument_defaults["tlo"],
@@ -703,6 +714,10 @@ def init_shared_values(values: Values) -> None:
#
values["SHOW_EDITOR"] = True
#
# If True then G2/G3 arc commands will be converted to discrete G1 line segments.
#
values["SPLIT_ARCS"] = False
#
# If True then the current machine units are output just before the PRE_OPERATION.
#
values["SHOW_MACHINE_UNITS"] = True
@@ -906,6 +921,10 @@ def process_shared_arguments(
values["SHOW_EDITOR"] = True
if args.no_show_editor:
values["SHOW_EDITOR"] = False
if args.split_arcs:
values["SPLIT_ARCS"] = True
if args.no_split_arcs:
values["SPLIT_ARCS"] = False
if args.tlo:
values["USE_TLO"] = True
if args.no_tlo:

View File

@@ -37,6 +37,7 @@ import FreeCAD
from FreeCAD import Units
import Path
import Path.Post.Utils as PostUtils
# Define some types that are used throughout this file
CommandLine = List[str]
@@ -704,7 +705,12 @@ def parse_a_path(values: Values, gcode: Gcode, pathobj) -> None:
)
adaptive_op_variables = determine_adaptive_op(values, pathobj)
for c in pathobj.Path.Commands:
# Apply arc splitting if requested
path_to_process = pathobj.Path
if values["SPLIT_ARCS"]:
path_to_process = PostUtils.splitArcs(path_to_process)
for c in path_to_process.Commands:
command = c.Name
command_line = []

View File

@@ -0,0 +1,347 @@
# ***************************************************************************
# * Copyright (c) 2015 Dan Falck <ddfalck@gmail.com> *
# * Copyright (c) 2020 Schildkroet *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
# * 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. *
# * *
# * FreeCAD 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 Lesser General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with FreeCAD; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
import os
import FreeCAD
from FreeCAD import Units
import Path.Post.Utils as PostUtils
from PathScripts import PathUtils
import datetime
import Path
from builtins import open as pyopen
TOOLTIP = """
This is a postprocessor file for the Path workbench. It is used to
take a pseudo-G-code fragment outputted by a Path object, and output
real G-code suitable for a centroid 3 axis mill. This postprocessor, once placed
in the appropriate Path/Tool folder, can be used directly from inside
FreeCAD, via the GUI importer or via python scripts with:
import centroid_legacy_post
centroid_legacy_post.export(object,"/path/to/file.ncc","")
"""
TOOLTIP_ARGS = """
Arguments for centroid:
--header,--no-header ... output headers (--header)
--comments,--no-comments ... output comments (--comments)
--line-numbers,--no-line-numbers ... prefix with line numbers (--no-lin-numbers)
--show-editor, --no-show-editor ... pop up editor before writing output(--show-editor)
--feed-precision=1 ... number of digits of precision for feed rate. Default=1
--axis-precision=4 ... number of digits of precision for axis moves. Default=4
--inches ... Convert output for US imperial mode (G20)
--no-tlo ... Suppress tool length offset (G43) following tool changes
"""
# These globals set common customization preferences
OUTPUT_COMMENTS = True
OUTPUT_HEADER = True
OUTPUT_LINE_NUMBERS = False
if FreeCAD.GuiUp:
SHOW_EDITOR = True
else:
SHOW_EDITOR = False
MODAL = False # if true commands are suppressed if the same as previous line.
USE_TLO = True # if true G43 will be output following tool changes
COMMAND_SPACE = " "
LINENR = 100 # line number starting value
# These globals will be reflected in the Machine configuration of the project
UNITS = "G21" # G21 for metric, G20 for us standard
UNIT_FORMAT = "mm"
UNIT_SPEED_FORMAT = "mm/min"
MACHINE_NAME = "Centroid"
CORNER_MIN = {"x": -609.6, "y": -152.4, "z": 0} # use metric for internal units
CORNER_MAX = {"x": 609.6, "y": 152.4, "z": 304.8} # use metric for internal units
AXIS_PRECISION = 4
FEED_PRECISION = 1
SPINDLE_DECIMALS = 0
COMMENT = ";"
# gCode header with information about CAD-software, post-processor
# and date/time
if FreeCAD.ActiveDocument:
cam_file = os.path.basename(FreeCAD.ActiveDocument.FileName)
else:
cam_file = "<None>"
HEADER = """;Exported by FreeCAD
;Post Processor: {}
;CAM file: {}
;Output Time: {}
""".format(
__name__, cam_file, str(datetime.datetime.now())
)
# Preamble text will appear at the beginning of the GCODE output file.
PREAMBLE = """G53 G00 G17
"""
# Postamble text will appear following the last operation.
POSTAMBLE = """M99
"""
TOOLRETURN = """M5
M25
G49 H0
""" # spindle off,height offset canceled,spindle retracted (M25 is a centroid command to retract spindle)
ZAXISRETURN = """G91 G28 X0 Z0
G90
"""
SAFETYBLOCK = """G90 G80 G40 G49
"""
# Pre operation text will be inserted before every operation
PRE_OPERATION = """"""
# Post operation text will be inserted after every operation
POST_OPERATION = """"""
# Tool Change commands will be inserted before a tool change
TOOL_CHANGE = """"""
def processArguments(argstring):
global OUTPUT_HEADER
global OUTPUT_COMMENTS
global OUTPUT_LINE_NUMBERS
global SHOW_EDITOR
global AXIS_PRECISION
global FEED_PRECISION
global UNIT_SPEED_FORMAT
global UNIT_FORMAT
global UNITS
global USE_TLO
for arg in argstring.split():
if arg == "--header":
OUTPUT_HEADER = True
elif arg == "--no-header":
OUTPUT_HEADER = False
elif arg == "--comments":
OUTPUT_COMMENTS = True
elif arg == "--no-comments":
OUTPUT_COMMENTS = False
elif arg == "--line-numbers":
OUTPUT_LINE_NUMBERS = True
elif arg == "--no-line-numbers":
OUTPUT_LINE_NUMBERS = False
elif arg == "--show-editor":
SHOW_EDITOR = True
elif arg == "--no-show-editor":
SHOW_EDITOR = False
elif arg.split("=")[0] == "--axis-precision":
AXIS_PRECISION = arg.split("=")[1]
elif arg.split("=")[0] == "--feed-precision":
FEED_PRECISION = arg.split("=")[1]
elif arg == "--inches":
UNITS = "G20"
UNIT_SPEED_FORMAT = "in/min"
UNIT_FORMAT = "in"
elif arg == "--no-tlo":
USE_TLO = False
def export(objectslist, filename, argstring):
processArguments(argstring)
for i in objectslist:
print(i.Name)
global UNITS
global UNIT_FORMAT
global UNIT_SPEED_FORMAT
print("postprocessing...")
gcode = ""
# write header
if OUTPUT_HEADER:
gcode += HEADER
gcode += SAFETYBLOCK
# Write the preamble
if OUTPUT_COMMENTS:
for item in objectslist:
if hasattr(item, "Proxy") and isinstance(
item.Proxy, Path.Tool.Controller.ToolController
):
gcode += ";T{}={}\n".format(item.ToolNumber, item.Name)
gcode += linenumber() + ";begin preamble\n"
for line in PREAMBLE.splitlines():
gcode += linenumber() + line + "\n"
gcode += linenumber() + UNITS + "\n"
for obj in objectslist:
# do the pre_op
if OUTPUT_COMMENTS:
gcode += linenumber() + ";begin operation\n"
for line in PRE_OPERATION.splitlines(True):
gcode += linenumber() + line
gcode += parse(obj)
# do the post_op
if OUTPUT_COMMENTS:
gcode += linenumber() + ";end operation: %s\n" % obj.Label
for line in POST_OPERATION.splitlines(True):
gcode += linenumber() + line
# do the post_amble
if OUTPUT_COMMENTS:
gcode += ";begin postamble\n"
for line in TOOLRETURN.splitlines(True):
gcode += linenumber() + line
for line in SAFETYBLOCK.splitlines(True):
gcode += linenumber() + line
for line in POSTAMBLE.splitlines():
gcode += linenumber() + line + "\n"
if SHOW_EDITOR:
dia = PostUtils.GCodeEditorDialog()
dia.editor.setText(gcode)
result = dia.exec_()
if result:
final = dia.editor.toPlainText()
else:
final = gcode
else:
final = gcode
print("done postprocessing.")
if not filename == "-":
gfile = pyopen(filename, "w")
gfile.write(final)
gfile.close()
return final
def linenumber():
global LINENR
if OUTPUT_LINE_NUMBERS is True:
LINENR += 10
return "N" + str(LINENR) + " "
return ""
def parse(pathobj):
out = ""
lastcommand = None
axis_precision_string = "." + str(AXIS_PRECISION) + "f"
feed_precision_string = "." + str(FEED_PRECISION) + "f"
# the order of parameters
# centroid doesn't want K properties on XY plane Arcs need work.
params = ["X", "Y", "Z", "A", "B", "I", "J", "F", "S", "T", "Q", "R", "L", "H"]
if hasattr(pathobj, "Group"): # We have a compound or project.
# if OUTPUT_COMMENTS:
# out += linenumber() + "(compound: " + pathobj.Label + ")\n"
for p in pathobj.Group:
out += parse(p)
return out
else: # parsing simple path
# groups might contain non-path things like stock.
if not hasattr(pathobj, "Path"):
return out
for c in PathUtils.getPathWithPlacement(pathobj).Commands:
commandlist = [] # list of elements in the command, code and params.
command = c.Name # command M or G code or comment string
if command.startswith("("):
command = PostUtils.fcoms(command, COMMENT)
commandlist.append(command)
if MODAL is True:
if command == lastcommand:
commandlist.pop(0)
# Now add the remaining parameters in order
for param in params:
if param in c.Parameters:
if param == "F":
if c.Name not in [
"G0",
"G00",
]: # centroid doesn't use rapid speeds
speed = Units.Quantity(c.Parameters["F"], FreeCAD.Units.Velocity)
commandlist.append(
param
+ format(
float(speed.getValueAs(UNIT_SPEED_FORMAT)),
feed_precision_string,
)
)
elif param == "H":
commandlist.append(param + str(int(c.Parameters["H"])))
elif param == "S":
commandlist.append(
param + PostUtils.fmt(c.Parameters["S"], SPINDLE_DECIMALS, "G21")
)
elif param == "T":
commandlist.append(param + str(int(c.Parameters["T"])))
else:
pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length)
commandlist.append(
param
+ format(
float(pos.getValueAs(UNIT_FORMAT)),
axis_precision_string,
)
)
outstr = str(commandlist)
outstr = outstr.replace("[", "")
outstr = outstr.replace("]", "")
outstr = outstr.replace("'", "")
outstr = outstr.replace(",", "")
# store the latest command
lastcommand = command
# Check for Tool Change:
if command == "M6":
for line in TOOL_CHANGE.splitlines(True):
out += linenumber() + line
if USE_TLO:
out += linenumber() + "G43 H" + str(int(c.Parameters["T"])) + "\n"
# prepend a line number and append a newline
if len(commandlist) >= 1:
if OUTPUT_LINE_NUMBERS:
commandlist.insert(0, (linenumber()))
# append the line to the final output
for w in commandlist:
out += w + COMMAND_SPACE
out = out.strip() + "\n"
return out

View File

@@ -1,8 +1,9 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# ***************************************************************************
# * Copyright (c) 2015 Dan Falck <ddfalck@gmail.com> *
# * Copyright (c) 2020 Schildkroet *
# * Copyright (c) 2014 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2022 - 2025 Larry Woestman <LarryWoestman2@gmail.com> *
# * Copyright (c) 2024 Ondsel <development@ondsel.com> *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
@@ -24,340 +25,172 @@
# * *
# ***************************************************************************
import os
import FreeCAD
from FreeCAD import Units
import Path.Post.Utils as PostUtils
import PathScripts.PathUtils as PathUtils
import datetime
from typing import Any, Dict
from Path.Post.Processor import PostProcessor
import Path
from builtins import open as pyopen
import FreeCAD
TOOLTIP = """
This is a postprocessor file for the Path workbench. It is used to
take a pseudo-G-code fragment outputted by a Path object, and output
real G-code suitable for a centroid 3 axis mill. This postprocessor, once placed
in the appropriate Path/Tool folder, can be used directly from inside
FreeCAD, via the GUI importer or via python scripts with:
translate = FreeCAD.Qt.translate
import centroid_post
centroid_post.export(object,"/path/to/file.ncc","")
"""
TOOLTIP_ARGS = """
Arguments for centroid:
--header,--no-header ... output headers (--header)
--comments,--no-comments ... output comments (--comments)
--line-numbers,--no-line-numbers ... prefix with line numbers (--no-lin-numbers)
--show-editor, --no-show-editor ... pop up editor before writing output(--show-editor)
--feed-precision=1 ... number of digits of precision for feed rate. Default=1
--axis-precision=4 ... number of digits of precision for axis moves. Default=4
--inches ... Convert output for US imperial mode (G20)
--no-tlo ... Suppress tool length offset (G43) following tool changes
"""
# These globals set common customization preferences
OUTPUT_COMMENTS = True
OUTPUT_HEADER = True
OUTPUT_LINE_NUMBERS = False
if FreeCAD.GuiUp:
SHOW_EDITOR = True
DEBUG = False
if DEBUG:
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
else:
SHOW_EDITOR = False
MODAL = False # if true commands are suppressed if the same as previous line.
USE_TLO = True # if true G43 will be output following tool changes
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
COMMAND_SPACE = " "
LINENR = 100 # line number starting value
#
# Define some types that are used throughout this file.
#
Values = Dict[str, Any]
Visible = Dict[str, bool]
# These globals will be reflected in the Machine configuration of the project
UNITS = "G21" # G21 for metric, G20 for us standard
UNIT_FORMAT = "mm"
UNIT_SPEED_FORMAT = "mm/min"
MACHINE_NAME = "Centroid"
CORNER_MIN = {"x": -609.6, "y": -152.4, "z": 0} # use metric for internal units
CORNER_MAX = {"x": 609.6, "y": 152.4, "z": 304.8} # use metric for internal units
AXIS_PRECISION = 4
FEED_PRECISION = 1
SPINDLE_DECIMALS = 0
COMMENT = ";"
class Centroid(PostProcessor):
"""The Centroid post processor class."""
# gCode header with information about CAD-software, post-processor
# and date/time
if FreeCAD.ActiveDocument:
cam_file = os.path.basename(FreeCAD.ActiveDocument.FileName)
else:
cam_file = "<None>"
def __init__(
self,
job,
tooltip=translate("CAM", "Centroid post processor"),
tooltipargs=[""],
units="Metric",
) -> None:
super().__init__(
job=job,
tooltip=tooltip,
tooltipargs=tooltipargs,
units=units,
)
Path.Log.debug("Centroid post processor initialized.")
HEADER = """;Exported by FreeCAD
;Post Processor: {}
;CAM file: {}
;Output Time: {}
""".format(
__name__, cam_file, str(datetime.datetime.now())
)
# Preamble text will appear at the beginning of the GCODE output file.
PREAMBLE = """G53 G00 G17
"""
# Postamble text will appear following the last operation.
POSTAMBLE = """M99
"""
TOOLRETURN = """M5
def init_values(self, values: Values) -> None:
"""Initialize values that are used throughout the postprocessor."""
#
super().init_values(values)
#
# Set any values here that need to override the default values set
# in the parent routine.
#
# Use 4 digits for axis precision by default.
#
values["AXIS_PRECISION"] = 4
values["DEFAULT_AXIS_PRECISION"] = 4
values["DEFAULT_INCH_AXIS_PRECISION"] = 4
#
# Use ";" as the comment symbol
#
values["COMMENT_SYMBOL"] = ";"
#
# Use 1 digit for feed precision by default.
#
values["FEED_PRECISION"] = 1
values["DEFAULT_FEED_PRECISION"] = 1
values["DEFAULT_INCH_FEED_PRECISION"] = 1
#
# This value usually shows up in the post_op comment as "Finish operation:".
# Change it to "End" to produce "End operation:".
#
values["FINISH_LABEL"] = "End"
#
# If this value is True, then a list of tool numbers
# with their labels are output just before the preamble.
#
values["LIST_TOOLS_IN_PREAMBLE"] = True
#
# Used in the argparser code as the "name" of the postprocessor program.
# This would normally show up in the usage message in the TOOLTIP_ARGS.
#
values["MACHINE_NAME"] = "Centroid"
#
# This list controls the order of parameters in a line during output.
# centroid doesn't want K properties on XY plane; Arcs need work.
#
values["PARAMETER_ORDER"] = [
"X",
"Y",
"Z",
"A",
"B",
"I",
"J",
"F",
"S",
"T",
"Q",
"R",
"L",
"H",
]
#
# Any commands in this value will be output as the last commands
# in the G-code file.
#
values["POSTAMBLE"] = """M99"""
values["POSTPROCESSOR_FILE_NAME"] = __name__
#
# Any commands in this value will be output after the header and
# safety block at the beginning of the G-code file.
#
values["PREAMBLE"] = """G53 G00 G17"""
#
# Output any messages.
#
values["REMOVE_MESSAGES"] = False
#
# Any commands in this value are output after the header but before the preamble,
# then again after the TOOLRETURN but before the POSTAMBLE.
#
values["SAFETYBLOCK"] = """G90 G80 G40 G49"""
#
# Do not show the current machine units just before the PRE_OPERATION.
#
values["SHOW_MACHINE_UNITS"] = False
#
# Do not show the current operation label just before the PRE_OPERATION.
#
values["SHOW_OPERATION_LABELS"] = False
#
# Do not output an M5 command to stop the spindle for tool changes.
#
values["STOP_SPINDLE_FOR_TOOL_CHANGE"] = False
#
# spindle off, height offset canceled, spindle retracted
# (M25 is a centroid command to retract spindle)
#
values[
"TOOLRETURN"
] = """M5
M25
G49 H0
""" # spindle off,height offset canceled,spindle retracted (M25 is a centroid command to retract spindle)
G49 H0"""
#
# Default to not outputting a G43 following tool changes
#
values["USE_TLO"] = False
#
# This was in the original centroid postprocessor file
# but does not appear to be used anywhere.
#
# ZAXISRETURN = """G91 G28 X0 Z0 G90"""
#
ZAXISRETURN = """G91 G28 X0 Z0
G90
"""
def init_arguments_visible(self, arguments_visible: Visible) -> None:
"""Initialize which argument pairs are visible in TOOLTIP_ARGS."""
super().init_arguments_visible(arguments_visible)
#
# Modify the visibility of any arguments from the defaults here.
#
arguments_visible["axis-modal"] = False
arguments_visible["precision"] = False
arguments_visible["tlo"] = False
SAFETYBLOCK = """G90 G80 G40 G49
"""
# Pre operation text will be inserted before every operation
PRE_OPERATION = """"""
# Post operation text will be inserted after every operation
POST_OPERATION = """"""
# Tool Change commands will be inserted before a tool change
TOOL_CHANGE = """"""
def processArguments(argstring):
global OUTPUT_HEADER
global OUTPUT_COMMENTS
global OUTPUT_LINE_NUMBERS
global SHOW_EDITOR
global AXIS_PRECISION
global FEED_PRECISION
global UNIT_SPEED_FORMAT
global UNIT_FORMAT
global UNITS
global USE_TLO
for arg in argstring.split():
if arg == "--header":
OUTPUT_HEADER = True
elif arg == "--no-header":
OUTPUT_HEADER = False
elif arg == "--comments":
OUTPUT_COMMENTS = True
elif arg == "--no-comments":
OUTPUT_COMMENTS = False
elif arg == "--line-numbers":
OUTPUT_LINE_NUMBERS = True
elif arg == "--no-line-numbers":
OUTPUT_LINE_NUMBERS = False
elif arg == "--show-editor":
SHOW_EDITOR = True
elif arg == "--no-show-editor":
SHOW_EDITOR = False
elif arg.split("=")[0] == "--axis-precision":
AXIS_PRECISION = arg.split("=")[1]
elif arg.split("=")[0] == "--feed-precision":
FEED_PRECISION = arg.split("=")[1]
elif arg == "--inches":
UNITS = "G20"
UNIT_SPEED_FORMAT = "in/min"
UNIT_FORMAT = "in"
elif arg == "--no-tlo":
USE_TLO = False
def export(objectslist, filename, argstring):
processArguments(argstring)
for i in objectslist:
print(i.Name)
global UNITS
global UNIT_FORMAT
global UNIT_SPEED_FORMAT
print("postprocessing...")
gcode = ""
# write header
if OUTPUT_HEADER:
gcode += HEADER
gcode += SAFETYBLOCK
# Write the preamble
if OUTPUT_COMMENTS:
for item in objectslist:
if hasattr(item, "Proxy") and isinstance(
item.Proxy, Path.Tool.Controller.ToolController
):
gcode += ";T{}={}\n".format(item.ToolNumber, item.Name)
gcode += linenumber() + ";begin preamble\n"
for line in PREAMBLE.splitlines():
gcode += linenumber() + line + "\n"
gcode += linenumber() + UNITS + "\n"
for obj in objectslist:
# do the pre_op
if OUTPUT_COMMENTS:
gcode += linenumber() + ";begin operation\n"
for line in PRE_OPERATION.splitlines(True):
gcode += linenumber() + line
gcode += parse(obj)
# do the post_op
if OUTPUT_COMMENTS:
gcode += linenumber() + ";end operation: %s\n" % obj.Label
for line in POST_OPERATION.splitlines(True):
gcode += linenumber() + line
# do the post_amble
if OUTPUT_COMMENTS:
gcode += ";begin postamble\n"
for line in TOOLRETURN.splitlines(True):
gcode += linenumber() + line
for line in SAFETYBLOCK.splitlines(True):
gcode += linenumber() + line
for line in POSTAMBLE.splitlines():
gcode += linenumber() + line + "\n"
if SHOW_EDITOR:
dia = PostUtils.GCodeEditorDialog()
dia.editor.setText(gcode)
result = dia.exec_()
if result:
final = dia.editor.toPlainText()
else:
final = gcode
else:
final = gcode
print("done postprocessing.")
if not filename == "-":
gfile = pyopen(filename, "w")
gfile.write(final)
gfile.close()
return final
def linenumber():
global LINENR
if OUTPUT_LINE_NUMBERS is True:
LINENR += 10
return "N" + str(LINENR) + " "
return ""
def parse(pathobj):
out = ""
lastcommand = None
axis_precision_string = "." + str(AXIS_PRECISION) + "f"
feed_precision_string = "." + str(FEED_PRECISION) + "f"
# params = ['X','Y','Z','A','B','I','J','K','F','S'] #This list control
# the order of parameters
# centroid doesn't want K properties on XY plane Arcs need work.
params = ["X", "Y", "Z", "A", "B", "I", "J", "F", "S", "T", "Q", "R", "L", "H"]
if hasattr(pathobj, "Group"): # We have a compound or project.
# if OUTPUT_COMMENTS:
# out += linenumber() + "(compound: " + pathobj.Label + ")\n"
for p in pathobj.Group:
out += parse(p)
return out
else: # parsing simple path
# groups might contain non-path things like stock.
if not hasattr(pathobj, "Path"):
return out
# if OUTPUT_COMMENTS:
# out += linenumber() + "(" + pathobj.Label + ")\n"
for c in PathUtils.getPathWithPlacement(pathobj).Commands:
commandlist = [] # list of elements in the command, code and params.
command = c.Name # command M or G code or comment string
if command.startswith("("):
command = PostUtils.fcoms(command, COMMENT)
commandlist.append(command)
# if modal: only print the command if it is not the same as the
# last one
if MODAL is True:
if command == lastcommand:
commandlist.pop(0)
# Now add the remaining parameters in order
for param in params:
if param in c.Parameters:
if param == "F":
if c.Name not in [
"G0",
"G00",
]: # centroid doesn't use rapid speeds
speed = Units.Quantity(c.Parameters["F"], FreeCAD.Units.Velocity)
commandlist.append(
param
+ format(
float(speed.getValueAs(UNIT_SPEED_FORMAT)),
feed_precision_string,
)
)
elif param == "H":
commandlist.append(param + str(int(c.Parameters["H"])))
elif param == "S":
commandlist.append(
param + PostUtils.fmt(c.Parameters["S"], SPINDLE_DECIMALS, "G21")
)
elif param == "T":
commandlist.append(param + str(int(c.Parameters["T"])))
else:
pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length)
commandlist.append(
param
+ format(
float(pos.getValueAs(UNIT_FORMAT)),
axis_precision_string,
)
)
outstr = str(commandlist)
outstr = outstr.replace("[", "")
outstr = outstr.replace("]", "")
outstr = outstr.replace("'", "")
outstr = outstr.replace(",", "")
# store the latest command
lastcommand = command
# Check for Tool Change:
if command == "M6":
# if OUTPUT_COMMENTS:
# out += linenumber() + "(begin toolchange)\n"
for line in TOOL_CHANGE.splitlines(True):
out += linenumber() + line
if USE_TLO:
out += linenumber() + "G43 H" + str(int(c.Parameters["T"])) + "\n"
# if command == "message":
# if OUTPUT_COMMENTS is False:
# out = []
# else:
# commandlist.pop(0) # remove the command
# prepend a line number and append a newline
if len(commandlist) >= 1:
if OUTPUT_LINE_NUMBERS:
commandlist.insert(0, (linenumber()))
# append the line to the final output
for w in commandlist:
out += w + COMMAND_SPACE
out = out.strip() + "\n"
return out
@property
def tooltip(self):
tooltip: str = """
This is a postprocessor file for the CAM workbench.
It is used to take a pseudo-gcode fragment from a CAM object
and output 'real' GCode suitable for a centroid 3 axis mill.
"""
return tooltip

View File

@@ -1,116 +0,0 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# ***************************************************************************
# * Copyright (c) 2015 Dan Falck <ddfalck@gmail.com> *
# * *
# * 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 FreeCAD
import Path
import Path.Post.Utils as PostUtils
import PathScripts.PathUtils as PathUtils
TOOLTIP = (
"""Example Post, using Path.Commands instead of Path.toGCode strings for Path G-code output."""
)
SHOW_EDITOR = True
def fmt(num):
fnum = ""
fnum += "%.3f" % (num)
return fnum
def ffmt(num):
fnum = ""
fnum += "%.1f" % (num)
return fnum
class saveVals(object):
"""save command info for modal output"""
def __init__(self, command):
self.com = command.Name
self.params = command.Parameters
def retVals(self):
return self.com, self.params
def lineout(command, oldvals, modal):
line = ""
if modal and (oldvals.com == command.Name):
line += ""
else:
line += str(command.Name)
if command.Name == "M6":
line += "T" + str(int(command.Parameters["T"]))
if command.Name == "M3":
line += "S" + str(ffmt(command.Parameters["S"]))
if command.Name == "M4":
line += "S" + str(ffmt(command.Parameters["S"]))
if "X" in command.Parameters:
line += "X" + str(fmt(command.Parameters["X"]))
if "Y" in command.Parameters:
line += "Y" + str(fmt(command.Parameters["Y"]))
if "Z" in command.Parameters:
line += "Z" + str(fmt(command.Parameters["Z"]))
if "I" in command.Parameters:
line += "I" + str(fmt(command.Parameters["I"]))
if "J" in command.Parameters:
line += "J" + str(fmt(command.Parameters["J"]))
if "F" in command.Parameters:
line += "F" + str(ffmt(command.Parameters["F"]))
return line
def export(obj, filename, argstring):
modal = True
gcode = ""
safetyblock1 = "G90G40G49\n"
gcode += safetyblock1
units = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Units")
if units.GetInt("UserSchema") == 0:
firstcommand = Path.Command("G21") # metric mode
else:
firstcommand = Path.Command("G20") # inch mode
oldvals = saveVals(firstcommand) # save first command for modal use
fp = obj[0]
gcode += firstcommand.Name
if hasattr(fp, "Path"):
for c in PathUtils.getPathWithPlacement(fp).Commands:
gcode += lineout(c, oldvals, modal) + "\n"
oldvals = saveVals(c)
gcode += "M2\n"
gfile = open(filename, "w")
gfile.write(gcode)
gfile.close()
else:
FreeCAD.Console.PrintError("Select a path object and try again\n")
if SHOW_EDITOR:
FreeCAD.Console.PrintMessage("Editor Activated\n")
dia = PostUtils.GCodeEditorDialog()
dia.editor.setText(gcode)
dia.exec_()

View File

@@ -1,98 +0,0 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# ***************************************************************************
# * Copyright (c) 2014 sliptonic <shopinthewoods@gmail.com> *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
# * 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. *
# * *
# * FreeCAD 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 Lesser General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with FreeCAD; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
import datetime
import Path.Post.Utils as PostUtils
import PathScripts.PathUtils as PathUtils
from builtins import open as pyopen
TOOLTIP = """
Dumper is an extremely simple postprocessor file for the CAM workbench. It is used
to dump the command list from one or more Path objects for simple inspection. This post
doesn't do any manipulation of the path and doesn't write anything to disk. It just
shows the dialog so you can see it. Useful for debugging, but not much else.
"""
now = datetime.datetime.now()
SHOW_EDITOR = True
def export(objectslist, filename, argstring):
"called when freecad exports a list of objects"
output = """(This output produced with the dump post processor)
(Dump is useful for inspecting the raw commands in your paths)
(but is not useful for driving machines.)
(Consider setting a default postprocessor in your project or )
(exporting your paths using a specific post that matches your machine)
"""
for obj in objectslist:
if not hasattr(obj, "Path"):
print(
"the object " + obj.Name + " is not a path. Please select only path and Compounds."
)
return
print("postprocessing...")
output += parse(obj)
if SHOW_EDITOR:
dia = PostUtils.GCodeEditorDialog()
dia.editor.setText(output)
result = dia.exec_()
if result:
final = dia.editor.toPlainText()
else:
final = output
else:
final = output
print("done postprocessing.")
return final
def parse(pathobj):
out = ""
if hasattr(pathobj, "Group"): # We have a compound or project.
out += "(Group: " + pathobj.Label + ")\n"
for p in pathobj.Group:
out += parse(p)
return out
else: # parsing simple path
if not hasattr(pathobj, "Path"): # groups might contain non-path things like stock.
return out
out += "(Path: " + pathobj.Label + ")\n"
for c in PathUtils.getPathWithPlacement(pathobj).Commands:
out += str(c) + "\n"
return out
# print(__name__ + " gcode postprocessor loaded.")

View File

@@ -1,100 +0,0 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# ***************************************************************************
# * Copyright (c) 2014 Yorik van Havre <yorik@uncreated.net> *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
# * 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. *
# * *
# * FreeCAD 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 Lesser General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with FreeCAD; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
import datetime
from builtins import open as pyopen
TOOLTIP = """
This is an example postprocessor file for the Path workbench. It is used
to save a list of FreeCAD Path objects to a file.
Read the Path Workbench documentation to know how to convert Path objects
to GCode.
"""
now = datetime.datetime.now()
def export(objectslist, filename, argstring):
"called when freecad exports a list of objects"
if len(objectslist) > 1:
print("This script is unable to write more than one Path object")
return
obj = objectslist[0]
if not hasattr(obj, "Path"):
print("the given object is not a path")
gcode = obj.Path.toGCode()
gcode = parse(gcode)
gfile = pyopen(filename, "w")
gfile.write(gcode)
gfile.close()
def parse(inputstring):
"parse(inputstring): returns a parsed output string"
print("postprocessing...")
output = ""
# write some stuff first
output += "N10 ;time:" + str(now) + "\n"
output += "N20 G17 G20 G80 G40 G90\n"
output += "N30 (Exported by FreeCAD)\n"
linenr = 100
lastcommand = None
# treat the input line by line
lines = inputstring.split("\n")
for line in lines:
# split the G/M command from the arguments
if " " in line:
command, args = line.split(" ", 1)
else:
# no space found, which means there are no arguments
command = line
args = ""
# add a line number
output += "N" + str(linenr) + " "
# only print the command if it is not the same as the last one
if command != lastcommand:
output += command + " "
output += args + "\n"
# increment the line number
linenr += 10
# store the latest command
lastcommand = command
# write some more stuff at the end
output += "N" + str(linenr) + " M05\n"
output += "N" + str(linenr + 10) + " M25\n"
output += "N" + str(linenr + 20) + " G00 X-1.0 Y1.0\n"
output += "N" + str(linenr + 30) + " G17 G80 G40 G90\n"
output += "N" + str(linenr + 40) + " M99\n"
print("done postprocessing.")
return output
# print(__name__ + " gcode postprocessor loaded.")

View File

@@ -1,115 +0,0 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# ***************************************************************************
# * Copyright (c) 2014 Yorik van Havre <yorik@uncreated.net> *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
# * 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. *
# * *
# * FreeCAD 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 Lesser General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with FreeCAD; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
"""
This is an example preprocessor file for the Path workbench. Its aim is to
open a gcode file, parse its contents, and create the appropriate objects
in FreeCAD. This preprocessor will not add imported gcode to an existing
job. For a more useful preprocessor, look at the gcode_pre.py file
Read the Path Workbench documentation to know how to create Path objects
from GCode.
"""
import FreeCAD
import Path
import os
from builtins import open as pyopen
# LEVEL = Path.Log.Level.DEBUG
LEVEL = Path.Log.Level.INFO
Path.Log.setLevel(LEVEL, Path.Log.thisModule())
if LEVEL == Path.Log.Level.DEBUG:
Path.Log.trackModule(Path.Log.thisModule())
def open(filename):
"called when freecad opens a file."
Path.Log.track(filename)
docname = os.path.splitext(os.path.basename(filename))[0]
doc = FreeCAD.newDocument(docname)
insert(filename, doc.Name)
def insert(filename, docname):
"called when freecad imports a file"
Path.Log.track(filename)
gfile = pyopen(filename)
gcode = gfile.read()
gfile.close()
gcode = parse(gcode)
doc = FreeCAD.getDocument(docname)
obj = doc.addObject("Path::Feature", "Path")
path = Path.Path(gcode)
obj.Path = path
def parse(inputstring):
"parse(inputstring): returns a parsed output string"
print("preprocessing...")
Path.Log.track(inputstring)
# split the input by line
lines = inputstring.split("\n")
output = []
lastcommand = None
for lin in lines:
# remove any leftover trailing and preceding spaces
lin = lin.strip()
if not lin:
# discard empty lines
continue
if lin[0].upper() in ["N"]:
# remove line numbers
lin = lin.split(" ", 1)
if len(lin) >= 1:
lin = lin[1].strip()
else:
continue
if lin[0] in ["(", "%", "#", ";"]:
# discard comment and other non strictly gcode lines
continue
if lin[0].upper() in ["G", "M"]:
# found a G or M command: we store it
output.append(Path.Command(str(lin)))
last = lin[0].upper()
for c in lin[1:]:
if not c.isdigit():
break
else:
last += c
lastcommand = last
elif lastcommand:
# no G or M command: we repeat the last one
output.append(Path.Command(str(lastcommand + " " + lin)))
print("done preprocessing.")
return output
print(__name__ + " gcode preprocessor loaded.")

View File

@@ -21,7 +21,7 @@
# * *
# ***************************************************************************
import os
from typing import Any, Dict
from Path.Post.Processor import PostProcessor
import Path
import FreeCAD
@@ -30,57 +30,65 @@ Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
translate = FreeCAD.Qt.translate
debug = True
debug = False
if debug:
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
else:
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
Values = Dict[str, Any]
class Generic(PostProcessor):
def __init__(self, job):
super().__init__(
job,
tooltip=translate("CAM", "Generic post processor"),
tooltipargs=["arg1", "arg2"],
units="kg",
tooltipargs=[],
units="Metric",
)
Path.Log.debug("Generic post processor initialized")
def export(self):
Path.Log.debug("Exporting the job")
def init_values(self, values: Values) -> None:
"""Initialize values that are used throughout the postprocessor."""
#
super().init_values(values)
values["POSTPROCESSOR_FILE_NAME"] = __name__
values["MACHINE_NAME"] = "Generic"
postables = self._buildPostList()
Path.Log.debug(f"postables count: {len(postables)}")
g_code_sections = []
for idx, section in enumerate(postables):
partname, sublist = section
# here is where the sections are converted to gcode.
g_code_sections.append((idx, partname))
return g_code_sections
# Set any values here that need to override the default values set
# in the parent routine.
#
# Any commands in this value will be output after the header and
# safety block at the beginning of the G-code file.
#
values["PREAMBLE"] = """"""
#
# Any commands in this value will be output as the last commands
# in the G-code file.
#
values["POSTAMBLE"] = """"""
@property
def tooltip(self):
tooltip = """
This is a generic post processor.
It doesn't do anything yet because we haven't immplemented it.
Implementing it would be a good idea
It exposes functionality of the base post processor.
"""
return tooltip
@property
def tooltipArgs(self):
argtooltip = """
--arg1: This is the first argument
--arg2: This is the second argument
argtooltip = super().tooltipArgs
"""
# One could add additional arguments here.
# argtooltip += """
# --arg1: This is the first argument
# --arg2: This is the second argument
# """
return argtooltip
@property

View File

@@ -0,0 +1,717 @@
# ***************************************************************************
# * Copyright (c) 2014 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2018, 2019 Gauthier Briere *
# * Copyright (c) 2019, 2020 Schildkroet *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
# * 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. *
# * *
# * FreeCAD 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 Lesser General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with FreeCAD; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
import FreeCAD
from FreeCAD import Units
import Path
import Path.Base.Util as PathUtil
import Path.Post.Utils as PostUtils
import PathScripts.PathUtils as PathUtils
import argparse
import datetime
import shlex
import re
from builtins import open as pyopen
TOOLTIP = """
Generate g-code from a Path that is compatible with the grbl controller.
import grbl_legacy_post
grbl_legacy_post.export(object, "/path/to/file.ncc")
"""
# ***************************************************************************
# * Globals set customization preferences
# ***************************************************************************
# Default values for command line arguments:
OUTPUT_COMMENTS = True # default output of comments in output gCode file
OUTPUT_HEADER = True # default output header in output gCode file
OUTPUT_LINE_NUMBERS = False # default doesn't output line numbers in output gCode file
OUTPUT_BCNC = False # default doesn't add bCNC operation block headers in output gCode file
SHOW_EDITOR = True # default show the resulting file dialog output in GUI
PRECISION = 3 # Default precision for metric (see http://linuxcnc.org/docs/2.7/html/gcode/overview.html#_g_code_best_practices)
TRANSLATE_DRILL_CYCLES = True # If true, G81, G82 & G83 are translated in G0/G1 moves
PREAMBLE = """G17 G90
""" # default preamble text will appear at the beginning of the gCode output file.
POSTAMBLE = """M5
G17 G90
M2
""" # default postamble text will appear following the last operation.
SPINDLE_WAIT = 0 # no waiting after M3 / M4 by default
RETURN_TO = None # no movements after end of program
# Customisation with no command line argument
MODAL = False # if true commands are suppressed if the same as previous line.
LINENR = 100 # line number starting value
LINEINCR = 10 # line number increment
OUTPUT_TOOL_CHANGE = (
False # default don't output M6 tool changes (comment it) as grbl currently does not handle it
)
DRILL_RETRACT_MODE = (
"G98" # Default value of drill retractations (CURRENT_Z) other possible value is G99
)
MOTION_MODE = "G90" # G90 for absolute moves, G91 for relative
UNITS = "G21" # G21 for metric, G20 for us standard
UNIT_FORMAT = "mm"
UNIT_SPEED_FORMAT = "mm/min"
PRE_OPERATION = """""" # Pre operation text will be inserted before every operation
POST_OPERATION = """""" # Post operation text will be inserted after every operation
TOOL_CHANGE = """""" # Tool Change commands will be inserted before a tool change
# ***************************************************************************
# * End of customization
# ***************************************************************************
# Parser arguments list & definition
parser = argparse.ArgumentParser(prog="grbl", add_help=False)
parser.add_argument("--comments", action="store_true", help="output comment (default)")
parser.add_argument("--no-comments", action="store_true", help="suppress comment output")
parser.add_argument("--header", action="store_true", help="output headers (default)")
parser.add_argument("--no-header", action="store_true", help="suppress header output")
parser.add_argument("--line-numbers", action="store_true", help="prefix with line numbers")
parser.add_argument(
"--no-line-numbers",
action="store_true",
help="don't prefix with line numbers (default)",
)
parser.add_argument(
"--show-editor",
action="store_true",
help="pop up editor before writing output (default)",
)
parser.add_argument(
"--no-show-editor",
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(
"--translate_drill",
action="store_true",
help="translate drill cycles G81, G82 & G83 in G0/G1 movements",
)
parser.add_argument(
"--no-translate_drill",
action="store_true",
help="don't translate drill cycles G81, G82 & G83 in G0/G1 movements (default)",
)
parser.add_argument(
"--preamble",
help='set commands to be issued before the first command, default="G17 G90\\n"',
)
parser.add_argument(
"--postamble",
help='set commands to be issued after the last command, default="M5\\nG17 G90\\nM2\\n"',
)
parser.add_argument(
"--inches", action="store_true", help="Convert output for US imperial mode (G20)"
)
parser.add_argument("--tool-change", action="store_true", help="Insert M6 for all tool changes")
parser.add_argument(
"--wait-for-spindle",
type=int,
default=0,
help="Wait for spindle to reach desired speed after M3 / M4, default=0",
)
parser.add_argument(
"--return-to",
default="",
help="Move to the specified coordinates at the end, e.g. --return-to=0,0",
)
parser.add_argument(
"--bcnc",
action="store_true",
help="Add Job operations as bCNC block headers. Consider suppressing existing comments: Add argument --no-comments",
)
parser.add_argument(
"--no-bcnc", action="store_true", help="suppress bCNC block header output (default)"
)
TOOLTIP_ARGS = parser.format_help()
# ***************************************************************************
# * Internal global variables
# ***************************************************************************
MOTION_COMMANDS = [
"G0",
"G00",
"G1",
"G01",
"G2",
"G02",
"G3",
"G03",
] # Motion gCode commands definition
RAPID_MOVES = ["G0", "G00"] # Rapid moves gCode commands definition
SUPPRESS_COMMANDS = [] # These commands are ignored by commenting them out
COMMAND_SPACE = " "
# Global variables storing current position
CURRENT_X = 0
CURRENT_Y = 0
CURRENT_Z = 0
def processArguments(argstring):
global OUTPUT_HEADER
global OUTPUT_COMMENTS
global OUTPUT_LINE_NUMBERS
global SHOW_EDITOR
global PRECISION
global PREAMBLE
global POSTAMBLE
global UNITS
global UNIT_SPEED_FORMAT
global UNIT_FORMAT
global TRANSLATE_DRILL_CYCLES
global OUTPUT_TOOL_CHANGE
global SPINDLE_WAIT
global RETURN_TO
global OUTPUT_BCNC
try:
args = parser.parse_args(shlex.split(argstring))
if args.no_header:
OUTPUT_HEADER = False
if args.header:
OUTPUT_HEADER = True
if args.no_comments:
OUTPUT_COMMENTS = False
if args.comments:
OUTPUT_COMMENTS = True
if args.no_line_numbers:
OUTPUT_LINE_NUMBERS = False
if args.line_numbers:
OUTPUT_LINE_NUMBERS = True
if args.no_show_editor:
SHOW_EDITOR = False
if args.show_editor:
SHOW_EDITOR = True
PRECISION = args.precision
if args.preamble is not None:
PREAMBLE = args.preamble.replace("\\n", "\n")
if args.postamble is not None:
POSTAMBLE = args.postamble.replace("\\n", "\n")
if args.no_translate_drill:
TRANSLATE_DRILL_CYCLES = False
if args.translate_drill:
TRANSLATE_DRILL_CYCLES = True
if args.inches:
UNITS = "G20"
UNIT_SPEED_FORMAT = "in/min"
UNIT_FORMAT = "in"
PRECISION = 4
if args.tool_change:
OUTPUT_TOOL_CHANGE = True
if args.wait_for_spindle > 0:
SPINDLE_WAIT = args.wait_for_spindle
if args.return_to != "":
RETURN_TO = [int(v) for v in args.return_to.split(",")]
if len(RETURN_TO) != 2:
RETURN_TO = None
print("--return-to coordinates must be specified as <x>,<y>, ignoring")
if args.bcnc:
OUTPUT_BCNC = True
if args.no_bcnc:
OUTPUT_BCNC = False
except Exception as e:
return False
return True
# For debug...
def dump(obj):
for attr in dir(obj):
print("obj.%s = %s" % (attr, getattr(obj, attr)))
def export(objectslist, filename, argstring):
if not processArguments(argstring):
return None
global UNITS
global UNIT_FORMAT
global UNIT_SPEED_FORMAT
global MOTION_MODE
global SUPPRESS_COMMANDS
print("Post Processor: " + __name__ + " postprocessing...")
gcode = ""
# write header
if OUTPUT_HEADER:
gcode += linenumber() + "(Exported by FreeCAD)\n"
gcode += linenumber() + "(Post Processor: " + __name__ + ")\n"
gcode += linenumber() + "(Output Time:" + str(datetime.datetime.now()) + ")\n"
# Check canned cycles for drilling
if TRANSLATE_DRILL_CYCLES:
if len(SUPPRESS_COMMANDS) == 0:
SUPPRESS_COMMANDS = ["G99", "G98", "G80"]
else:
SUPPRESS_COMMANDS += ["G99", "G98", "G80"]
# Write the preamble
if OUTPUT_COMMENTS:
gcode += linenumber() + "(Begin preamble)\n"
for line in PREAMBLE.splitlines():
gcode += linenumber() + line + "\n"
# verify if PREAMBLE have changed MOTION_MODE or UNITS
if "G90" in PREAMBLE:
MOTION_MODE = "G90"
elif "G91" in PREAMBLE:
MOTION_MODE = "G91"
else:
gcode += linenumber() + MOTION_MODE + "\n"
if "G21" in PREAMBLE:
UNITS = "G21"
UNIT_FORMAT = "mm"
UNIT_SPEED_FORMAT = "mm/min"
elif "G20" in PREAMBLE:
UNITS = "G20"
UNIT_FORMAT = "in"
UNIT_SPEED_FORMAT = "in/min"
else:
gcode += linenumber() + UNITS + "\n"
for obj in objectslist:
# Debug...
# print("\n" + "*"*70)
# dump(obj)
# print("*"*70 + "\n")
if not hasattr(obj, "Path"):
print(
"The object " + obj.Name + " is not a path. Please select only path and Compounds."
)
return
# Skip inactive operations
if not PathUtil.activeForOp(obj):
continue
# do the pre_op
if OUTPUT_BCNC:
gcode += linenumber() + "(Block-name: " + obj.Label + ")\n"
gcode += linenumber() + "(Block-expand: 0)\n"
gcode += linenumber() + "(Block-enable: 1)\n"
if OUTPUT_COMMENTS:
gcode += linenumber() + "(Begin operation: " + obj.Label + ")\n"
for line in PRE_OPERATION.splitlines(True):
gcode += linenumber() + line
# get coolant mode
coolantMode = PathUtil.coolantModeForOp(obj)
# turn coolant on if required
if OUTPUT_COMMENTS:
if not coolantMode == "None":
gcode += linenumber() + "(Coolant On:" + coolantMode + ")\n"
if coolantMode == "Flood":
gcode += linenumber() + "M8" + "\n"
if coolantMode == "Mist":
gcode += linenumber() + "M7" + "\n"
# Parse the op
gcode += parse(obj)
# do the post_op
if OUTPUT_COMMENTS:
gcode += linenumber() + "(Finish operation: " + obj.Label + ")\n"
for line in POST_OPERATION.splitlines(True):
gcode += linenumber() + line
# turn coolant off if required
if not coolantMode == "None":
if OUTPUT_COMMENTS:
gcode += linenumber() + "(Coolant Off:" + coolantMode + ")\n"
gcode += linenumber() + "M9" + "\n"
if RETURN_TO:
gcode += linenumber() + "G0 X%s Y%s\n" % tuple(RETURN_TO)
# do the post_amble
if OUTPUT_BCNC:
gcode += linenumber() + "(Block-name: post_amble)\n"
gcode += linenumber() + "(Block-expand: 0)\n"
gcode += linenumber() + "(Block-enable: 1)\n"
if OUTPUT_COMMENTS:
gcode += linenumber() + "(Begin postamble)\n"
for line in POSTAMBLE.splitlines():
gcode += linenumber() + line + "\n"
# show the gCode result dialog
if FreeCAD.GuiUp and SHOW_EDITOR:
dia = PostUtils.GCodeEditorDialog()
dia.editor.setText(gcode)
result = dia.exec_()
if result:
final = dia.editor.toPlainText()
else:
final = gcode
else:
final = gcode
print("Done postprocessing.")
# write the file
if filename != "-":
with pyopen(filename, "w") as gfile:
gfile.write(final)
return final
def linenumber():
if not OUTPUT_LINE_NUMBERS:
return ""
global LINENR
global LINEINCR
s = "N" + str(LINENR) + " "
LINENR += LINEINCR
return s
def format_outstring(strTable):
global COMMAND_SPACE
# construct the line for the final output
s = ""
for w in strTable:
s += w + COMMAND_SPACE
s = s.strip()
return s
def parse(pathobj):
global DRILL_RETRACT_MODE
global MOTION_MODE
global CURRENT_X
global CURRENT_Y
global CURRENT_Z
out = ""
lastcommand = None
precision_string = "." + str(PRECISION) + "f"
params = [
"X",
"Y",
"Z",
"A",
"B",
"C",
"U",
"V",
"W",
"I",
"J",
"K",
"F",
"S",
"T",
"Q",
"R",
"L",
"P",
]
if hasattr(pathobj, "Group"): # We have a compound or project.
if OUTPUT_COMMENTS:
out += linenumber() + "(Compound: " + pathobj.Label + ")\n"
for p in pathobj.Group:
out += parse(p)
return out
else: # parsing simple path
if not hasattr(pathobj, "Path"): # groups might contain non-path things like stock.
return out
if OUTPUT_COMMENTS:
out += linenumber() + "(Path: " + pathobj.Label + ")\n"
for c in PathUtils.getPathWithPlacement(pathobj).Commands:
outstring = []
command = c.Name
outstring.append(command)
# if modal: only print the command if it is not the same as the last one
if MODAL:
if command == lastcommand:
outstring.pop(0)
# Now add the remaining parameters in order
for param in params:
if param in c.Parameters:
if param == "F":
if command not in RAPID_MOVES:
speed = Units.Quantity(c.Parameters["F"], FreeCAD.Units.Velocity)
if speed.getValueAs(UNIT_SPEED_FORMAT) > 0.0:
outstring.append(
param
+ format(
float(speed.getValueAs(UNIT_SPEED_FORMAT)),
precision_string,
)
)
elif param in ["T", "H", "S"]:
outstring.append(param + str(int(c.Parameters[param])))
elif param in ["D", "P", "L"]:
outstring.append(param + str(c.Parameters[param]))
elif param in ["A", "B", "C"]:
outstring.append(param + format(c.Parameters[param], precision_string))
else: # [X, Y, Z, U, V, W, I, J, K, R, Q] (Conversion eventuelle mm/inches)
pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length)
outstring.append(
param + format(float(pos.getValueAs(UNIT_FORMAT)), precision_string)
)
# store the latest command
lastcommand = command
# Memorizes the current position for calculating the related movements and the withdrawal plan
if command in MOTION_COMMANDS:
if "X" in c.Parameters:
CURRENT_X = Units.Quantity(c.Parameters["X"], FreeCAD.Units.Length)
if "Y" in c.Parameters:
CURRENT_Y = Units.Quantity(c.Parameters["Y"], FreeCAD.Units.Length)
if "Z" in c.Parameters:
CURRENT_Z = Units.Quantity(c.Parameters["Z"], FreeCAD.Units.Length)
if command in ("G98", "G99"):
DRILL_RETRACT_MODE = command
if command in ("G90", "G91"):
MOTION_MODE = command
if TRANSLATE_DRILL_CYCLES:
if command in ("G81", "G82", "G83"):
out += drill_translate(outstring, command, c.Parameters)
# Erase the line we just translated
outstring = []
if SPINDLE_WAIT > 0:
if command in ("M3", "M03", "M4", "M04"):
out += linenumber() + format_outstring(outstring) + "\n"
out += linenumber() + format_outstring(["G4", "P%s" % SPINDLE_WAIT]) + "\n"
outstring = []
# Check for Tool Change:
if command in ("M6", "M06"):
if OUTPUT_COMMENTS:
out += linenumber() + "(Begin toolchange)\n"
if not OUTPUT_TOOL_CHANGE:
outstring.insert(0, "(")
outstring.append(")")
else:
for line in TOOL_CHANGE.splitlines(True):
out += linenumber() + line
if command == "message":
if OUTPUT_COMMENTS is False:
out = []
else:
outstring.pop(0) # remove the command
if command in SUPPRESS_COMMANDS:
outstring.insert(0, "(")
outstring.append(")")
# prepend a line number and append a newline
if len(outstring) >= 1:
out += linenumber() + format_outstring(outstring) + "\n"
# Check for comments containing machine-specific commands to pass literally to the controller
m = re.match(r"^\(MC_RUN_COMMAND: ([^)]+)\)$", command)
if m:
raw_command = m.group(1)
out += linenumber() + raw_command + "\n"
return out
def drill_translate(outstring, cmd, params):
global DRILL_RETRACT_MODE
global MOTION_MODE
global CURRENT_X
global CURRENT_Y
global CURRENT_Z
global UNITS
global UNIT_FORMAT
global UNIT_SPEED_FORMAT
strFormat = "." + str(PRECISION) + "f"
trBuff = ""
if OUTPUT_COMMENTS: # Comment the original command
outstring[0] = "(" + outstring[0]
outstring[-1] = outstring[-1] + ")"
trBuff += linenumber() + format_outstring(outstring) + "\n"
# cycle conversion
# currently only cycles in XY are provided (G17)
# other plains ZX (G18) and YZ (G19) are not dealt with : Z drilling only.
param_X = Units.Quantity(params["X"], FreeCAD.Units.Length)
param_Y = Units.Quantity(params["Y"], FreeCAD.Units.Length)
param_Z = Units.Quantity(params["Z"], FreeCAD.Units.Length)
param_R = Units.Quantity(params["R"], FreeCAD.Units.Length)
# R less than Z is error
if param_R < param_Z:
trBuff += linenumber() + "(drill cycle error: R less than Z )\n"
return trBuff
if MOTION_MODE == "G91": # G91 relative movements, (not generated by CAM WB drilling)
param_X += CURRENT_X
param_Y += CURRENT_Y
param_Z += CURRENT_Z
param_R += param_Z
# NIST-RS274
# 3.5.20 Set Canned Cycle Return Level — G98 and G99
# When the spindle retracts during canned cycles, there is a choice of how far it retracts: (1) retract
# perpendicular to the selected plane to the position indicated by the R word, or (2) retract
# perpendicular to the selected plane to the position that axis was in just before the canned cycle
# started (unless that position is lower than the position indicated by the R word, in which case use
# the R word position).
# To use option (1), program G99. To use option (2), program G98. Remember that the R word has
# different meanings in absolute distance mode and incremental distance mode.
# """
if DRILL_RETRACT_MODE == "G99":
clear_Z = param_R
if DRILL_RETRACT_MODE == "G98" and CURRENT_Z >= param_R:
clear_Z = CURRENT_Z
else:
clear_Z = param_R
strG0_clear_Z = "G0 Z" + format(float(clear_Z.getValueAs(UNIT_FORMAT)), strFormat) + "\n"
strG0_param_R = "G0 Z" + format(float(param_R.getValueAs(UNIT_FORMAT)), strFormat) + "\n"
# get the other parameters
drill_feedrate = Units.Quantity(params["F"], FreeCAD.Units.Velocity)
strF_Feedrate = " F" + format(float(drill_feedrate.getValueAs(UNIT_SPEED_FORMAT)), ".2f") + "\n"
if cmd == "G83":
drill_Step = Units.Quantity(params["Q"], FreeCAD.Units.Length)
a_bit = (
drill_Step * 0.05
) # NIST 3.5.16.4 G83 Cycle: "current hole bottom, backed off a bit."
elif cmd == "G82":
drill_DwellTime = params["P"]
# wrap this block to ensure machine MOTION_MODE is restored in case of error
try:
if MOTION_MODE == "G91":
trBuff += linenumber() + "G90\n" # force absolute coordinates during cycles
# NIST-RS274
# 3.5.16.1 Preliminary and In-Between Motion
# At the very beginning of the execution of any of the canned cycles, with the XY-plane selected, if
# the current Z position is below the R position, the Z-axis is traversed to the R position. This
# happens only once, regardless of the value of L.
# In addition, at the beginning of the first cycle and each repeat, the following one or two moves are
# made:
# 1. a straight traverse parallel to the XY-plane to the given XY-position,
# 2. a straight traverse of the Z-axis only to the R position, if it is not already at the R position.
if CURRENT_Z < param_R:
trBuff += linenumber() + strG0_param_R
trBuff += (
linenumber()
+ "G0 X"
+ format(float(param_X.getValueAs(UNIT_FORMAT)), strFormat)
+ " Y"
+ format(float(param_Y.getValueAs(UNIT_FORMAT)), strFormat)
+ "\n"
)
if CURRENT_Z > param_R:
trBuff += linenumber() + strG0_param_R
last_Stop_Z = param_R
# drill moves
if cmd in ("G81", "G82"):
trBuff += (
linenumber()
+ "G1 Z"
+ format(float(param_Z.getValueAs(UNIT_FORMAT)), strFormat)
+ strF_Feedrate
)
# pause where applicable
if cmd == "G82":
trBuff += linenumber() + "G4 P" + str(drill_DwellTime) + "\n"
trBuff += linenumber() + strG0_clear_Z
else: # 'G83'
if params["Q"] != 0:
while 1:
if last_Stop_Z != clear_Z:
clearance_depth = (
last_Stop_Z + a_bit
) # rapid move to just short of last drilling depth
trBuff += (
linenumber()
+ "G0 Z"
+ format(
float(clearance_depth.getValueAs(UNIT_FORMAT)),
strFormat,
)
+ "\n"
)
next_Stop_Z = last_Stop_Z - drill_Step
if next_Stop_Z > param_Z:
trBuff += (
linenumber()
+ "G1 Z"
+ format(float(next_Stop_Z.getValueAs(UNIT_FORMAT)), strFormat)
+ strF_Feedrate
)
trBuff += linenumber() + strG0_clear_Z
last_Stop_Z = next_Stop_Z
else:
trBuff += (
linenumber()
+ "G1 Z"
+ format(float(param_Z.getValueAs(UNIT_FORMAT)), strFormat)
+ strF_Feedrate
)
trBuff += linenumber() + strG0_clear_Z
break
except Exception as e:
pass
if MOTION_MODE == "G91":
trBuff += linenumber() + "G91" # Restore if changed
return trBuff
# print(__name__ + ": GCode postprocessor loaded.")

View File

@@ -2,8 +2,8 @@
# ***************************************************************************
# * Copyright (c) 2014 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2018, 2019 Gauthier Briere *
# * Copyright (c) 2019, 2020 Schildkroet *
# * Copyright (c) 2022 - 2025 Larry Woestman <LarryWoestman2@gmail.com> *
# * Copyright (c) 2024 Ondsel <development@ondsel.com> *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
@@ -25,695 +25,165 @@
# * *
# ***************************************************************************
import FreeCAD
from FreeCAD import Units
import Path
import Path.Base.Util as PathUtil
import Path.Post.Utils as PostUtils
import PathScripts.PathUtils as PathUtils
import argparse
import datetime
import shlex
import re
from builtins import open as pyopen
TOOLTIP = """
Generate g-code from a Path that is compatible with the grbl controller.
import grbl_post
grbl_post.export(object, "/path/to/file.ncc")
"""
# ***************************************************************************
# * Globals set customization preferences
# ***************************************************************************
# Default values for command line arguments:
OUTPUT_COMMENTS = True # default output of comments in output gCode file
OUTPUT_HEADER = True # default output header in output gCode file
OUTPUT_LINE_NUMBERS = False # default doesn't output line numbers in output gCode file
OUTPUT_BCNC = False # default doesn't add bCNC operation block headers in output gCode file
SHOW_EDITOR = True # default show the resulting file dialog output in GUI
PRECISION = 3 # Default precision for metric (see http://linuxcnc.org/docs/2.7/html/gcode/overview.html#_g_code_best_practices)
TRANSLATE_DRILL_CYCLES = True # If true, G81, G82 & G83 are translated in G0/G1 moves
PREAMBLE = """G17 G90
""" # default preamble text will appear at the beginning of the gCode output file.
POSTAMBLE = """M5
G17 G90
M2
""" # default postamble text will appear following the last operation.
SPINDLE_WAIT = 0 # no waiting after M3 / M4 by default
RETURN_TO = None # no movements after end of program
# Customisation with no command line argument
MODAL = False # if true commands are suppressed if the same as previous line.
LINENR = 100 # line number starting value
LINEINCR = 10 # line number increment
OUTPUT_TOOL_CHANGE = (
False # default don't output M6 tool changes (comment it) as grbl currently does not handle it
)
DRILL_RETRACT_MODE = (
"G98" # Default value of drill retractations (CURRENT_Z) other possible value is G99
)
MOTION_MODE = "G90" # G90 for absolute moves, G91 for relative
UNITS = "G21" # G21 for metric, G20 for us standard
UNIT_FORMAT = "mm"
UNIT_SPEED_FORMAT = "mm/min"
PRE_OPERATION = """""" # Pre operation text will be inserted before every operation
POST_OPERATION = """""" # Post operation text will be inserted after every operation
TOOL_CHANGE = """""" # Tool Change commands will be inserted before a tool change
# ***************************************************************************
# * End of customization
# ***************************************************************************
# Parser arguments list & definition
parser = argparse.ArgumentParser(prog="grbl", add_help=False)
parser.add_argument("--comments", action="store_true", help="output comment (default)")
parser.add_argument("--no-comments", action="store_true", help="suppress comment output")
parser.add_argument("--header", action="store_true", help="output headers (default)")
parser.add_argument("--no-header", action="store_true", help="suppress header output")
parser.add_argument("--line-numbers", action="store_true", help="prefix with line numbers")
parser.add_argument(
"--no-line-numbers",
action="store_true",
help="don't prefix with line numbers (default)",
)
parser.add_argument(
"--show-editor",
action="store_true",
help="pop up editor before writing output (default)",
)
parser.add_argument(
"--no-show-editor",
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(
"--translate_drill",
action="store_true",
help="translate drill cycles G81, G82 & G83 in G0/G1 movements",
)
parser.add_argument(
"--no-translate_drill",
action="store_true",
help="don't translate drill cycles G81, G82 & G83 in G0/G1 movements (default)",
)
parser.add_argument(
"--preamble",
help='set commands to be issued before the first command, default="G17 G90\\n"',
)
parser.add_argument(
"--postamble",
help='set commands to be issued after the last command, default="M5\\nG17 G90\\nM2\\n"',
)
parser.add_argument(
"--inches", action="store_true", help="Convert output for US imperial mode (G20)"
)
parser.add_argument("--tool-change", action="store_true", help="Insert M6 for all tool changes")
parser.add_argument(
"--wait-for-spindle",
type=int,
default=0,
help="Wait for spindle to reach desired speed after M3 / M4, default=0",
)
parser.add_argument(
"--return-to",
default="",
help="Move to the specified coordinates at the end, e.g. --return-to=0,0",
)
parser.add_argument(
"--bcnc",
action="store_true",
help="Add Job operations as bCNC block headers. Consider suppressing existing comments: Add argument --no-comments",
)
parser.add_argument(
"--no-bcnc", action="store_true", help="suppress bCNC block header output (default)"
)
TOOLTIP_ARGS = parser.format_help()
# ***************************************************************************
# * Internal global variables
# ***************************************************************************
MOTION_COMMANDS = [
"G0",
"G00",
"G1",
"G01",
"G2",
"G02",
"G3",
"G03",
] # Motion gCode commands definition
RAPID_MOVES = ["G0", "G00"] # Rapid moves gCode commands definition
SUPPRESS_COMMANDS = [] # These commands are ignored by commenting them out
COMMAND_SPACE = " "
# Global variables storing current position
CURRENT_X = 0
CURRENT_Y = 0
CURRENT_Z = 0
def processArguments(argstring):
global OUTPUT_HEADER
global OUTPUT_COMMENTS
global OUTPUT_LINE_NUMBERS
global SHOW_EDITOR
global PRECISION
global PREAMBLE
global POSTAMBLE
global UNITS
global UNIT_SPEED_FORMAT
global UNIT_FORMAT
global TRANSLATE_DRILL_CYCLES
global OUTPUT_TOOL_CHANGE
global SPINDLE_WAIT
global RETURN_TO
global OUTPUT_BCNC
try:
args = parser.parse_args(shlex.split(argstring))
if args.no_header:
OUTPUT_HEADER = False
if args.header:
OUTPUT_HEADER = True
if args.no_comments:
OUTPUT_COMMENTS = False
if args.comments:
OUTPUT_COMMENTS = True
if args.no_line_numbers:
OUTPUT_LINE_NUMBERS = False
if args.line_numbers:
OUTPUT_LINE_NUMBERS = True
if args.no_show_editor:
SHOW_EDITOR = False
if args.show_editor:
SHOW_EDITOR = True
PRECISION = args.precision
if args.preamble is not None:
PREAMBLE = args.preamble.replace("\\n", "\n")
if args.postamble is not None:
POSTAMBLE = args.postamble.replace("\\n", "\n")
if args.no_translate_drill:
TRANSLATE_DRILL_CYCLES = False
if args.translate_drill:
TRANSLATE_DRILL_CYCLES = True
if args.inches:
UNITS = "G20"
UNIT_SPEED_FORMAT = "in/min"
UNIT_FORMAT = "in"
PRECISION = 4
if args.tool_change:
OUTPUT_TOOL_CHANGE = True
if args.wait_for_spindle > 0:
SPINDLE_WAIT = args.wait_for_spindle
if args.return_to != "":
RETURN_TO = [int(v) for v in args.return_to.split(",")]
if len(RETURN_TO) != 2:
RETURN_TO = None
print("--return-to coordinates must be specified as <x>,<y>, ignoring")
if args.bcnc:
OUTPUT_BCNC = True
if args.no_bcnc:
OUTPUT_BCNC = False
except Exception as e:
return False
return True
# For debug...
def dump(obj):
for attr in dir(obj):
print("obj.%s = %s" % (attr, getattr(obj, attr)))
def export(objectslist, filename, argstring):
if not processArguments(argstring):
return None
global UNITS
global UNIT_FORMAT
global UNIT_SPEED_FORMAT
global MOTION_MODE
global SUPPRESS_COMMANDS
print("Post Processor: " + __name__ + " postprocessing...")
gcode = ""
# write header
if OUTPUT_HEADER:
gcode += linenumber() + "(Exported by FreeCAD)\n"
gcode += linenumber() + "(Post Processor: " + __name__ + ")\n"
gcode += linenumber() + "(Output Time:" + str(datetime.datetime.now()) + ")\n"
# Check canned cycles for drilling
if TRANSLATE_DRILL_CYCLES:
if len(SUPPRESS_COMMANDS) == 0:
SUPPRESS_COMMANDS = ["G99", "G98", "G80"]
else:
SUPPRESS_COMMANDS += ["G99", "G98", "G80"]
# Write the preamble
if OUTPUT_COMMENTS:
gcode += linenumber() + "(Begin preamble)\n"
for line in PREAMBLE.splitlines():
gcode += linenumber() + line + "\n"
# verify if PREAMBLE have changed MOTION_MODE or UNITS
if "G90" in PREAMBLE:
MOTION_MODE = "G90"
elif "G91" in PREAMBLE:
MOTION_MODE = "G91"
else:
gcode += linenumber() + MOTION_MODE + "\n"
if "G21" in PREAMBLE:
UNITS = "G21"
UNIT_FORMAT = "mm"
UNIT_SPEED_FORMAT = "mm/min"
elif "G20" in PREAMBLE:
UNITS = "G20"
UNIT_FORMAT = "in"
UNIT_SPEED_FORMAT = "in/min"
else:
gcode += linenumber() + UNITS + "\n"
for obj in objectslist:
# Debug...
# print("\n" + "*"*70)
# dump(obj)
# print("*"*70 + "\n")
if not hasattr(obj, "Path"):
print(
"The object " + obj.Name + " is not a path. Please select only path and Compounds."
)
return
# Skip inactive operations
if not PathUtil.activeForOp(obj):
continue
# do the pre_op
if OUTPUT_BCNC:
gcode += linenumber() + "(Block-name: " + obj.Label + ")\n"
gcode += linenumber() + "(Block-expand: 0)\n"
gcode += linenumber() + "(Block-enable: 1)\n"
if OUTPUT_COMMENTS:
gcode += linenumber() + "(Begin operation: " + obj.Label + ")\n"
for line in PRE_OPERATION.splitlines(True):
gcode += linenumber() + line
# get coolant mode
coolantMode = PathUtil.coolantModeForOp(obj)
# turn coolant on if required
if OUTPUT_COMMENTS:
if not coolantMode == "None":
gcode += linenumber() + "(Coolant On:" + coolantMode + ")\n"
if coolantMode == "Flood":
gcode += linenumber() + "M8" + "\n"
if coolantMode == "Mist":
gcode += linenumber() + "M7" + "\n"
# Parse the op
gcode += parse(obj)
# do the post_op
if OUTPUT_COMMENTS:
gcode += linenumber() + "(Finish operation: " + obj.Label + ")\n"
for line in POST_OPERATION.splitlines(True):
gcode += linenumber() + line
# turn coolant off if required
if not coolantMode == "None":
if OUTPUT_COMMENTS:
gcode += linenumber() + "(Coolant Off:" + coolantMode + ")\n"
gcode += linenumber() + "M9" + "\n"
if RETURN_TO:
gcode += linenumber() + "G0 X%s Y%s\n" % tuple(RETURN_TO)
# do the post_amble
if OUTPUT_BCNC:
gcode += linenumber() + "(Block-name: post_amble)\n"
gcode += linenumber() + "(Block-expand: 0)\n"
gcode += linenumber() + "(Block-enable: 1)\n"
if OUTPUT_COMMENTS:
gcode += linenumber() + "(Begin postamble)\n"
for line in POSTAMBLE.splitlines():
gcode += linenumber() + line + "\n"
# show the gCode result dialog
if FreeCAD.GuiUp and SHOW_EDITOR:
dia = PostUtils.GCodeEditorDialog()
dia.editor.setText(gcode)
result = dia.exec_()
if result:
final = dia.editor.toPlainText()
else:
final = gcode
else:
final = gcode
print("Done postprocessing.")
# write the file
if filename != "-":
with pyopen(filename, "w") as gfile:
gfile.write(final)
return final
def linenumber():
if not OUTPUT_LINE_NUMBERS:
return ""
global LINENR
global LINEINCR
s = "N" + str(LINENR) + " "
LINENR += LINEINCR
return s
def format_outstring(strTable):
global COMMAND_SPACE
# construct the line for the final output
s = ""
for w in strTable:
s += w + COMMAND_SPACE
s = s.strip()
return s
def parse(pathobj):
global DRILL_RETRACT_MODE
global MOTION_MODE
global CURRENT_X
global CURRENT_Y
global CURRENT_Z
out = ""
lastcommand = None
precision_string = "." + str(PRECISION) + "f"
params = [
"X",
"Y",
"Z",
"A",
"B",
"C",
"U",
"V",
"W",
"I",
"J",
"K",
"F",
"S",
"T",
"Q",
"R",
"L",
"P",
]
if hasattr(pathobj, "Group"): # We have a compound or project.
if OUTPUT_COMMENTS:
out += linenumber() + "(Compound: " + pathobj.Label + ")\n"
for p in pathobj.Group:
out += parse(p)
return out
else: # parsing simple path
if not hasattr(pathobj, "Path"): # groups might contain non-path things like stock.
return out
if OUTPUT_COMMENTS:
out += linenumber() + "(Path: " + pathobj.Label + ")\n"
for c in PathUtils.getPathWithPlacement(pathobj).Commands:
outstring = []
command = c.Name
outstring.append(command)
# if modal: only print the command if it is not the same as the last one
if MODAL:
if command == lastcommand:
outstring.pop(0)
# Now add the remaining parameters in order
for param in params:
if param in c.Parameters:
if param == "F":
if command not in RAPID_MOVES:
speed = Units.Quantity(c.Parameters["F"], FreeCAD.Units.Velocity)
if speed.getValueAs(UNIT_SPEED_FORMAT) > 0.0:
outstring.append(
param
+ format(
float(speed.getValueAs(UNIT_SPEED_FORMAT)),
precision_string,
)
)
elif param in ["T", "H", "S"]:
outstring.append(param + str(int(c.Parameters[param])))
elif param in ["D", "P", "L"]:
outstring.append(param + str(c.Parameters[param]))
elif param in ["A", "B", "C"]:
outstring.append(param + format(c.Parameters[param], precision_string))
else: # [X, Y, Z, U, V, W, I, J, K, R, Q] (Conversion eventuelle mm/inches)
pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length)
outstring.append(
param + format(float(pos.getValueAs(UNIT_FORMAT)), precision_string)
)
# store the latest command
lastcommand = command
# Memorizes the current position for calculating the related movements and the withdrawal plan
if command in MOTION_COMMANDS:
if "X" in c.Parameters:
CURRENT_X = Units.Quantity(c.Parameters["X"], FreeCAD.Units.Length)
if "Y" in c.Parameters:
CURRENT_Y = Units.Quantity(c.Parameters["Y"], FreeCAD.Units.Length)
if "Z" in c.Parameters:
CURRENT_Z = Units.Quantity(c.Parameters["Z"], FreeCAD.Units.Length)
if command in ("G98", "G99"):
DRILL_RETRACT_MODE = command
if command in ("G90", "G91"):
MOTION_MODE = command
if TRANSLATE_DRILL_CYCLES:
if command in ("G81", "G82", "G83"):
out += drill_translate(outstring, command, c.Parameters)
# Erase the line we just translated
outstring = []
if SPINDLE_WAIT > 0:
if command in ("M3", "M03", "M4", "M04"):
out += linenumber() + format_outstring(outstring) + "\n"
out += linenumber() + format_outstring(["G4", "P%s" % SPINDLE_WAIT]) + "\n"
outstring = []
# Check for Tool Change:
if command in ("M6", "M06"):
if OUTPUT_COMMENTS:
out += linenumber() + "(Begin toolchange)\n"
if not OUTPUT_TOOL_CHANGE:
outstring.insert(0, "(")
outstring.append(")")
else:
for line in TOOL_CHANGE.splitlines(True):
out += linenumber() + line
if command == "message":
if OUTPUT_COMMENTS is False:
out = []
else:
outstring.pop(0) # remove the command
if command in SUPPRESS_COMMANDS:
outstring.insert(0, "(")
outstring.append(")")
# prepend a line number and append a newline
if len(outstring) >= 1:
out += linenumber() + format_outstring(outstring) + "\n"
# Check for comments containing machine-specific commands to pass literally to the controller
m = re.match(r"^\(MC_RUN_COMMAND: ([^)]+)\)$", command)
if m:
raw_command = m.group(1)
out += linenumber() + raw_command + "\n"
return out
def drill_translate(outstring, cmd, params):
global DRILL_RETRACT_MODE
global MOTION_MODE
global CURRENT_X
global CURRENT_Y
global CURRENT_Z
global UNITS
global UNIT_FORMAT
global UNIT_SPEED_FORMAT
strFormat = "." + str(PRECISION) + "f"
trBuff = ""
if OUTPUT_COMMENTS: # Comment the original command
outstring[0] = "(" + outstring[0]
outstring[-1] = outstring[-1] + ")"
trBuff += linenumber() + format_outstring(outstring) + "\n"
# cycle conversion
# currently only cycles in XY are provided (G17)
# other plains ZX (G18) and YZ (G19) are not dealt with : Z drilling only.
param_X = Units.Quantity(params["X"], FreeCAD.Units.Length)
param_Y = Units.Quantity(params["Y"], FreeCAD.Units.Length)
param_Z = Units.Quantity(params["Z"], FreeCAD.Units.Length)
param_R = Units.Quantity(params["R"], FreeCAD.Units.Length)
# R less than Z is error
if param_R < param_Z:
trBuff += linenumber() + "(drill cycle error: R less than Z )\n"
return trBuff
if MOTION_MODE == "G91": # G91 relative movements, (not generated by CAM WB drilling)
param_X += CURRENT_X
param_Y += CURRENT_Y
param_Z += CURRENT_Z
param_R += param_Z
# NIST-RS274
# 3.5.20 Set Canned Cycle Return Level — G98 and G99
# When the spindle retracts during canned cycles, there is a choice of how far it retracts: (1) retract
# perpendicular to the selected plane to the position indicated by the R word, or (2) retract
# perpendicular to the selected plane to the position that axis was in just before the canned cycle
# started (unless that position is lower than the position indicated by the R word, in which case use
# the R word position).
# To use option (1), program G99. To use option (2), program G98. Remember that the R word has
# different meanings in absolute distance mode and incremental distance mode.
# """
if DRILL_RETRACT_MODE == "G99":
clear_Z = param_R
if DRILL_RETRACT_MODE == "G98" and CURRENT_Z >= param_R:
clear_Z = CURRENT_Z
else:
clear_Z = param_R
strG0_clear_Z = "G0 Z" + format(float(clear_Z.getValueAs(UNIT_FORMAT)), strFormat) + "\n"
strG0_param_R = "G0 Z" + format(float(param_R.getValueAs(UNIT_FORMAT)), strFormat) + "\n"
# get the other parameters
drill_feedrate = Units.Quantity(params["F"], FreeCAD.Units.Velocity)
strF_Feedrate = " F" + format(float(drill_feedrate.getValueAs(UNIT_SPEED_FORMAT)), ".2f") + "\n"
if cmd == "G83":
drill_Step = Units.Quantity(params["Q"], FreeCAD.Units.Length)
a_bit = (
drill_Step * 0.05
) # NIST 3.5.16.4 G83 Cycle: "current hole bottom, backed off a bit."
elif cmd == "G82":
drill_DwellTime = params["P"]
# wrap this block to ensure machine MOTION_MODE is restored in case of error
try:
if MOTION_MODE == "G91":
trBuff += linenumber() + "G90\n" # force absolute coordinates during cycles
# NIST-RS274
# 3.5.16.1 Preliminary and In-Between Motion
# At the very beginning of the execution of any of the canned cycles, with the XY-plane selected, if
# the current Z position is below the R position, the Z-axis is traversed to the R position. This
# happens only once, regardless of the value of L.
# In addition, at the beginning of the first cycle and each repeat, the following one or two moves are
# made:
# 1. a straight traverse parallel to the XY-plane to the given XY-position,
# 2. a straight traverse of the Z-axis only to the R position, if it is not already at the R position.
if CURRENT_Z < param_R:
trBuff += linenumber() + strG0_param_R
trBuff += (
linenumber()
+ "G0 X"
+ format(float(param_X.getValueAs(UNIT_FORMAT)), strFormat)
+ " Y"
+ format(float(param_Y.getValueAs(UNIT_FORMAT)), strFormat)
+ "\n"
from typing import Any, Dict
from Path.Post.Processor import PostProcessor
import Path
import FreeCAD
translate = FreeCAD.Qt.translate
DEBUG = False
if DEBUG:
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
else:
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
#
# Define some types that are used throughout this file.
#
Defaults = Dict[str, bool]
Values = Dict[str, Any]
Visible = Dict[str, bool]
class Grbl(PostProcessor):
"""The Grbl post processor class."""
def __init__(
self,
job,
tooltip=translate("CAM", "Grbl post processor"),
tooltipargs=[""],
units="Metric",
) -> None:
super().__init__(
job=job,
tooltip=tooltip,
tooltipargs=tooltipargs,
units=units,
)
if CURRENT_Z > param_R:
trBuff += linenumber() + strG0_param_R
Path.Log.debug("Grbl post processor initialized.")
last_Stop_Z = param_R
def init_values(self, values: Values) -> None:
"""Initialize values that are used throughout the postprocessor."""
#
super().init_values(values)
#
# Set any values here that need to override the default values set
# in the parent routine.
#
values["ENABLE_COOLANT"] = True
#
# If this is set to True, then commands that are placed in
# comments that look like (MC_RUN_COMMAND: blah) will be output.
#
values["ENABLE_MACHINE_SPECIFIC_COMMANDS"] = True
#
# Used in the argparser code as the "name" of the postprocessor program.
# This would normally show up in the usage message in the TOOLTIP_ARGS.
#
values["MACHINE_NAME"] = "Grbl"
#
# Default to outputting Path labels at the beginning of each Path.
#
values["OUTPUT_PATH_LABELS"] = True
#
# Default to not outputting M6 tool changes (comment it) as grbl
# currently does not handle it.
#
values["OUTPUT_TOOL_CHANGE"] = False
#
# The order of the parameters.
# Arcs may only work on the XY plane (this needs to be verified).
#
values["PARAMETER_ORDER"] = [
"X",
"Y",
"Z",
"A",
"B",
"C",
"U",
"V",
"W",
"I",
"J",
"F",
"S",
"T",
"Q",
"R",
"L",
"P",
]
#
# Any commands in this value will be output as the last commands in the G-code file.
#
values[
"POSTAMBLE"
] = """M5
G17 G90
M2"""
values["POSTPROCESSOR_FILE_NAME"] = __name__
#
# Any commands in this value will be output after the header and
# safety block at the beginning of the G-code file.
#
values["PREAMBLE"] = """G17 G90"""
#
# Do not show the current machine units just before the PRE_OPERATION.
#
values["SHOW_MACHINE_UNITS"] = False
#
# Default to not outputting a G43 following tool changes
#
values["USE_TLO"] = False
# drill moves
if cmd in ("G81", "G82"):
trBuff += (
linenumber()
+ "G1 Z"
+ format(float(param_Z.getValueAs(UNIT_FORMAT)), strFormat)
+ strF_Feedrate
)
# pause where applicable
if cmd == "G82":
trBuff += linenumber() + "G4 P" + str(drill_DwellTime) + "\n"
trBuff += linenumber() + strG0_clear_Z
else: # 'G83'
if params["Q"] != 0:
while 1:
if last_Stop_Z != clear_Z:
clearance_depth = (
last_Stop_Z + a_bit
) # rapid move to just short of last drilling depth
trBuff += (
linenumber()
+ "G0 Z"
+ format(
float(clearance_depth.getValueAs(UNIT_FORMAT)),
strFormat,
)
+ "\n"
)
next_Stop_Z = last_Stop_Z - drill_Step
if next_Stop_Z > param_Z:
trBuff += (
linenumber()
+ "G1 Z"
+ format(float(next_Stop_Z.getValueAs(UNIT_FORMAT)), strFormat)
+ strF_Feedrate
)
trBuff += linenumber() + strG0_clear_Z
last_Stop_Z = next_Stop_Z
else:
trBuff += (
linenumber()
+ "G1 Z"
+ format(float(param_Z.getValueAs(UNIT_FORMAT)), strFormat)
+ strF_Feedrate
)
trBuff += linenumber() + strG0_clear_Z
break
def init_argument_defaults(self, argument_defaults: Defaults) -> None:
"""Initialize which arguments (in a pair) are shown as the default argument."""
super().init_argument_defaults(argument_defaults)
#
# Modify which argument to show as the default in flag-type arguments here.
# If the value is True, the first argument will be shown as the default.
# If the value is False, the second argument will be shown as the default.
#
# For example, if you want to show Metric mode as the default, use:
# argument_defaults["metric_inch"] = True
#
# If you want to show that "Don't pop up editor for writing output" is
# the default, use:
# argument_defaults["show-editor"] = False.
#
# Note: You also need to modify the corresponding entries in the "values" hash
# to actually make the default value(s) change to match.
#
argument_defaults["tlo"] = False
argument_defaults["tool_change"] = False
except Exception as e:
pass
def init_arguments_visible(self, arguments_visible: Visible) -> None:
"""Initialize which argument pairs are visible in TOOLTIP_ARGS."""
super().init_arguments_visible(arguments_visible)
#
# Modify the visibility of any arguments from the defaults here.
#
arguments_visible["bcnc"] = True
arguments_visible["axis-modal"] = False
arguments_visible["return-to"] = True
arguments_visible["tlo"] = False
arguments_visible["tool_change"] = True
arguments_visible["translate_drill"] = True
arguments_visible["wait-for-spindle"] = True
if MOTION_MODE == "G91":
trBuff += linenumber() + "G91" # Restore if changed
return trBuff
# print(__name__ + ": GCode postprocessor loaded.")
@property
def tooltip(self):
tooltip: str = """
This is a postprocessor file for the CAM workbench.
It is used to take a pseudo-gcode fragment from a CAM object
and output 'real' GCode suitable for a Grbl 3 axis mill.
"""
return tooltip

View File

@@ -0,0 +1,461 @@
# ***************************************************************************
# * Copyright (c) 2014 sliptonic <shopinthewoods@gmail.com> *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
# * 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. *
# * *
# * FreeCAD 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 Lesser General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with FreeCAD; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
import FreeCAD
from FreeCAD import Units
import Path
import argparse
import datetime
import shlex
import Path.Base.Util as PathUtil
import Path.Post.Utils as PostUtils
import PathScripts.PathUtils as PathUtils
from builtins import open as pyopen
TOOLTIP = """
This is a postprocessor file for the Path workbench. It is used to
take a pseudo-G-code fragment outputted by a Path object, and output
real G-code suitable for a linuxcnc 3 axis mill. This postprocessor, once placed
in the appropriate PathScripts folder, can be used directly from inside
FreeCAD, via the GUI importer or via python scripts with:
import linuxcnc_legacy_post
linuxcnc_legacy_post.export(object,"/path/to/file.ncc","")
"""
now = datetime.datetime.now()
parser = argparse.ArgumentParser(prog="linuxcnc", add_help=False)
parser.add_argument("--no-header", action="store_true", help="suppress header output")
parser.add_argument("--no-comments", action="store_true", help="suppress comment output")
parser.add_argument("--line-numbers", action="store_true", help="prefix with line numbers")
parser.add_argument(
"--no-show-editor",
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(
"--preamble",
help='set commands to be issued before the first command, default="G17 G54 G40 G49 G80 G90\\n"',
)
parser.add_argument(
"--postamble",
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)"
)
parser.add_argument(
"--modal",
action="store_true",
help="Output the Same G-command Name USE NonModal Mode",
)
parser.add_argument("--axis-modal", action="store_true", help="Output the Same Axis Value Mode")
parser.add_argument(
"--no-tlo",
action="store_true",
help="suppress tool length offset (G43) following tool changes",
)
TOOLTIP_ARGS = parser.format_help()
# These globals set common customization preferences
OUTPUT_COMMENTS = True
OUTPUT_HEADER = True
OUTPUT_LINE_NUMBERS = False
SHOW_EDITOR = True
MODAL = False # if true commands are suppressed if the same as previous line.
USE_TLO = True # if true G43 will be output following tool changes
OUTPUT_DOUBLES = True # if false duplicate axis values are suppressed if the same as previous line.
COMMAND_SPACE = " "
LINENR = 100 # line number starting value
# 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"
UNIT_FORMAT = "mm"
MACHINE_NAME = "LinuxCNC"
CORNER_MIN = {"x": 0, "y": 0, "z": 0}
CORNER_MAX = {"x": 500, "y": 300, "z": 300}
PRECISION = 3
# 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
M2
"""
# Pre operation text will be inserted before every operation
PRE_OPERATION = """"""
# Post operation text will be inserted after every operation
POST_OPERATION = """"""
# Tool Change commands will be inserted before a tool change
TOOL_CHANGE = """"""
def processArguments(argstring):
global OUTPUT_HEADER
global OUTPUT_COMMENTS
global OUTPUT_LINE_NUMBERS
global SHOW_EDITOR
global PRECISION
global PREAMBLE
global POSTAMBLE
global UNITS
global UNIT_SPEED_FORMAT
global UNIT_FORMAT
global MODAL
global USE_TLO
global OUTPUT_DOUBLES
try:
args = parser.parse_args(shlex.split(argstring))
if args.no_header:
OUTPUT_HEADER = False
if args.no_comments:
OUTPUT_COMMENTS = False
if args.line_numbers:
OUTPUT_LINE_NUMBERS = True
if args.no_show_editor:
SHOW_EDITOR = False
print("Show editor = %d" % SHOW_EDITOR)
PRECISION = args.precision
if args.preamble is not None:
PREAMBLE = args.preamble.replace("\\n", "\n")
if args.postamble is not None:
POSTAMBLE = args.postamble.replace("\\n", "\n")
if args.inches:
UNITS = "G20"
UNIT_SPEED_FORMAT = "in/min"
UNIT_FORMAT = "in"
PRECISION = 4
if args.modal:
MODAL = True
if args.no_tlo:
USE_TLO = False
if args.axis_modal:
print("here")
OUTPUT_DOUBLES = False
except Exception:
return False
return True
def export(objectslist, filename, argstring):
if not processArguments(argstring):
return None
global UNITS
global UNIT_FORMAT
global UNIT_SPEED_FORMAT
for obj in objectslist:
if not hasattr(obj, "Path"):
print(
"the object " + obj.Name + " is not a path. Please select only path and Compounds."
)
return None
print("postprocessing...")
gcode = ""
# write header
if OUTPUT_HEADER:
gcode += linenumber() + "(Exported by FreeCAD)\n"
gcode += linenumber() + "(Post Processor: " + __name__ + ")\n"
gcode += linenumber() + "(Output Time:" + str(now) + ")\n"
# Write the preamble
if OUTPUT_COMMENTS:
gcode += linenumber() + "(begin preamble)\n"
for line in PREAMBLE.splitlines():
gcode += linenumber() + line + "\n"
gcode += linenumber() + UNITS + "\n"
for obj in objectslist:
# Skip inactive operations
if not PathUtil.activeForOp(obj):
continue
# do the pre_op
if OUTPUT_COMMENTS:
gcode += linenumber() + "(begin operation: %s)\n" % obj.Label
gcode += linenumber() + "(machine units: %s)\n" % (UNIT_SPEED_FORMAT)
for line in PRE_OPERATION.splitlines(True):
gcode += linenumber() + line
# get coolant mode
coolantMode = PathUtil.coolantModeForOp(obj)
# turn coolant on if required
if OUTPUT_COMMENTS:
if not coolantMode == "None":
gcode += linenumber() + "(Coolant On:" + coolantMode + ")\n"
if coolantMode == "Flood":
gcode += linenumber() + "M8" + "\n"
if coolantMode == "Mist":
gcode += linenumber() + "M7" + "\n"
# process the operation gcode
gcode += parse(obj)
# do the post_op
if OUTPUT_COMMENTS:
gcode += linenumber() + "(finish operation: %s)\n" % obj.Label
for line in POST_OPERATION.splitlines(True):
gcode += linenumber() + line
# turn coolant off if required
if not coolantMode == "None":
if OUTPUT_COMMENTS:
gcode += linenumber() + "(Coolant Off:" + coolantMode + ")\n"
gcode += linenumber() + "M9" + "\n"
# do the post_amble
if OUTPUT_COMMENTS:
gcode += "(begin postamble)\n"
for line in POSTAMBLE.splitlines():
gcode += linenumber() + line + "\n"
if FreeCAD.GuiUp and SHOW_EDITOR:
final = gcode
if len(gcode) > 100000:
print("Skipping editor since output is greater than 100kb")
else:
dia = PostUtils.GCodeEditorDialog()
dia.editor.setText(gcode)
result = dia.exec_()
if result:
final = dia.editor.toPlainText()
else:
final = gcode
print("done postprocessing.")
if not filename == "-":
gfile = pyopen(filename, "w")
gfile.write(final)
gfile.close()
return final
def linenumber():
global LINENR
if OUTPUT_LINE_NUMBERS is True:
LINENR += 10
return "N" + str(LINENR) + " "
return ""
def parse(pathobj):
global PRECISION
global MODAL
global OUTPUT_DOUBLES
global UNIT_FORMAT
global UNIT_SPEED_FORMAT
out = ""
lastcommand = None
precision_string = "." + str(PRECISION) + "f"
currLocation = {} # keep track for no doubles
# the order of parameters
# linuxcnc doesn't want K properties on XY plane Arcs need work.
params = [
"X",
"Y",
"Z",
"A",
"B",
"C",
"I",
"J",
"F",
"S",
"T",
"Q",
"R",
"L",
"H",
"D",
"P",
]
firstmove = Path.Command("G0", {"X": -1, "Y": -1, "Z": -1, "F": 0.0})
currLocation.update(firstmove.Parameters) # set First location Parameters
if hasattr(pathobj, "Group"): # We have a compound or project.
# if OUTPUT_COMMENTS:
# out += linenumber() + "(compound: " + pathobj.Label + ")\n"
for p in pathobj.Group:
out += parse(p)
return out
else: # parsing simple path
# groups might contain non-path things like stock.
if not hasattr(pathobj, "Path"):
return out
# if OUTPUT_COMMENTS:
# out += linenumber() + "(" + pathobj.Label + ")\n"
# The following "for" statement was fairly recently added
# but seems to be using the A, B, and C parameters in ways
# that don't appear to be compatible with how the PATH code
# uses the A, B, and C parameters. I have reverted the
# change here until we can figure out what it going on.
#
# for c in PathUtils.getPathWithPlacement(pathobj).Commands:
for c in pathobj.Path.Commands:
outstring = []
command = c.Name
outstring.append(command)
# if modal: suppress the command if it is the same as the last one
if MODAL is True:
if command == lastcommand:
outstring.pop(0)
if c.Name.startswith("(") and not OUTPUT_COMMENTS: # command is a comment
continue
# Handle G84/G74 tapping cycles
if command in ("G84", "G74") and "F" in c.Parameters:
pitch_mm = float(c.Parameters["F"])
c.Parameters.pop("F") # Remove F from output, we'll handle it
# Get spindle speed (from S param or last known value)
spindle_speed = None
if "S" in c.Parameters:
spindle_speed = float(c.Parameters["S"])
c.Parameters.pop("S")
# Convert pitch to inches if needed
if UNITS == "G20": # imperial
pitch = pitch_mm / 25.4
else:
pitch = pitch_mm
# Calculate feed rate
if spindle_speed is not None:
feed_rate = pitch * spindle_speed
speed = Units.Quantity(feed_rate, UNIT_SPEED_FORMAT)
outstring.append(
"F" + format(float(speed.getValueAs(UNIT_SPEED_FORMAT)), precision_string)
)
else:
# No spindle speed found, output pitch as F
outstring.append("F" + format(pitch, precision_string))
# Now add the remaining parameters in order
for param in params:
if param in c.Parameters:
if param == "F" and (
currLocation[param] != c.Parameters[param] or OUTPUT_DOUBLES
):
if c.Name not in [
"G0",
"G00",
]: # linuxcnc doesn't use rapid speeds
speed = Units.Quantity(c.Parameters["F"], FreeCAD.Units.Velocity)
if speed.getValueAs(UNIT_SPEED_FORMAT) > 0.0:
outstring.append(
param
+ format(
float(speed.getValueAs(UNIT_SPEED_FORMAT)),
precision_string,
)
)
else:
continue
elif param == "T":
outstring.append(param + str(int(c.Parameters["T"])))
elif param == "H":
outstring.append(param + str(int(c.Parameters["H"])))
elif param == "D":
outstring.append(param + str(int(c.Parameters["D"])))
elif param == "S":
outstring.append(param + str(int(c.Parameters["S"])))
else:
if (
(not OUTPUT_DOUBLES)
and (param in currLocation)
and (currLocation[param] == c.Parameters[param])
):
continue
else:
if param in ("A", "B", "C"):
outstring.append(
param + format(float(c.Parameters[param]), precision_string)
)
else:
pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length)
outstring.append(
param
+ format(float(pos.getValueAs(UNIT_FORMAT)), precision_string)
)
# store the latest command
lastcommand = command
currLocation.update(c.Parameters)
# Check for Tool Change:
if command == "M6":
# stop the spindle
out += linenumber() + "M5\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)
if command == "message":
if OUTPUT_COMMENTS is False:
out = []
else:
outstring.pop(0) # remove the command
# prepend a line number and append a newline
if len(outstring) >= 1:
if OUTPUT_LINE_NUMBERS:
outstring.insert(0, (linenumber()))
# append the line to the final output
for w in outstring:
out += w + COMMAND_SPACE
out += "\n"
return out
# print(__name__ + " gcode postprocessor loaded.")

View File

@@ -2,6 +2,9 @@
# ***************************************************************************
# * Copyright (c) 2014 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2022 - 2025 Larry Woestman <LarryWoestman2@gmail.com> *
# * Copyright (c) 2024 Ondsel <development@ondsel.com> *
# * Copyright (c) 2024 Carl Slater <CandLWorkshopLLC@gmail.com> *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
@@ -23,517 +26,182 @@
# * *
# ***************************************************************************
import FreeCAD
from FreeCAD import Units
from typing import Any, Dict
from Path.Post.Processor import PostProcessor
import Path
import argparse
import datetime
import shlex
import Path.Base.Util as PathUtil
import Path.Post.Utils as PostUtils
import PathScripts.PathUtils as PathUtils
from builtins import open as pyopen
import FreeCAD
TOOLTIP = """
This is a postprocessor file for the Path workbench. It is used to
take a pseudo-G-code fragment outputted by a Path object, and output
real G-code suitable for a linuxcnc 3 axis mill. This postprocessor, once placed
in the appropriate PathScripts folder, can be used directly from inside
FreeCAD, via the GUI importer or via python scripts with:
translate = FreeCAD.Qt.translate
import linuxcnc_post
linuxcnc_post.export(object,"/path/to/file.ncc","")
"""
DEBUG = False
if DEBUG:
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
else:
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
now = datetime.datetime.now()
parser = argparse.ArgumentParser(prog="linuxcnc", add_help=False)
parser.add_argument("--no-header", action="store_true", help="suppress header output")
parser.add_argument("--no-comments", action="store_true", help="suppress comment output")
parser.add_argument("--line-numbers", action="store_true", help="prefix with line numbers")
parser.add_argument(
"--no-show-editor",
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(
"--preamble",
help='set commands to be issued before the first command, default="G17 G54 G40 G49 G80 G90\\n"',
)
parser.add_argument(
"--postamble",
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)"
)
parser.add_argument(
"--modal",
action="store_true",
help="Output the Same G-command Name USE NonModal Mode",
)
parser.add_argument("--axis-modal", action="store_true", help="Output the Same Axis Value Mode")
parser.add_argument(
"--no-tlo",
action="store_true",
help="suppress tool length offset (G43) following tool changes",
)
parser.add_argument("--rigid-tap", action="store_true", help="Enable G33.1 rigid tapping cycle")
TOOLTIP_ARGS = parser.format_help()
# These globals set common customization preferences
OUTPUT_COMMENTS = True
OUTPUT_HEADER = True
OUTPUT_LINE_NUMBERS = False
SHOW_EDITOR = True
MODAL = False # if true commands are suppressed if the same as previous line.
USE_TLO = True # if true G43 will be output following tool changes
OUTPUT_DOUBLES = True # if false duplicate axis values are suppressed if the same as previous line.
COMMAND_SPACE = " "
LINENR = 100 # line number starting value
# 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"
UNIT_FORMAT = "mm"
MACHINE_NAME = "LinuxCNC"
CORNER_MIN = {"x": 0, "y": 0, "z": 0}
CORNER_MAX = {"x": 500, "y": 300, "z": 300}
PRECISION = 3
RIGID_TAP = False
# 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
M2
"""
# Pre operation text will be inserted before every operation
PRE_OPERATION = """"""
# Post operation text will be inserted after every operation
POST_OPERATION = """"""
# Tool Change commands will be inserted before a tool change
TOOL_CHANGE = """"""
#
# Define some types that are used throughout this file.
#
Values = Dict[str, Any]
def processArguments(argstring):
global OUTPUT_HEADER
global OUTPUT_COMMENTS
global OUTPUT_LINE_NUMBERS
global SHOW_EDITOR
global PRECISION
global PREAMBLE
global POSTAMBLE
global UNITS
global UNIT_SPEED_FORMAT
global UNIT_FORMAT
global MODAL
global USE_TLO
global OUTPUT_DOUBLES
global RIGID_TAP
class Linuxcnc(PostProcessor):
"""
The LinuxCNC post processor class.
LinuxCNC supports various trajectory control methods (path blending) as
described at https://linuxcnc.org/docs/2.4/html/common_User_Concepts.html#r1_1_2
try:
args = parser.parse_args(shlex.split(argstring))
if args.no_header:
OUTPUT_HEADER = False
if args.no_comments:
OUTPUT_COMMENTS = False
if args.line_numbers:
OUTPUT_LINE_NUMBERS = True
if args.no_show_editor:
SHOW_EDITOR = False
print("Show editor = %d" % SHOW_EDITOR)
PRECISION = args.precision
if args.preamble is not None:
PREAMBLE = args.preamble.replace("\\n", "\n")
if args.postamble is not None:
POSTAMBLE = args.postamble.replace("\\n", "\n")
if args.inches:
UNITS = "G20"
UNIT_SPEED_FORMAT = "in/min"
UNIT_FORMAT = "in"
PRECISION = 4
if args.modal:
MODAL = True
if args.no_tlo:
USE_TLO = False
if args.axis_modal:
print("here")
OUTPUT_DOUBLES = False
if args.rigid_tap:
RIGID_TAP = True
except Exception:
return False
return True
This post processor implements the following trajectory control methods:
- Exact Path (G61)
- Exact Stop (G64)
- Blend (G61.1)
def export(objectslist, filename, argstring):
if not processArguments(argstring):
return None
global UNITS
global UNIT_FORMAT
global UNIT_SPEED_FORMAT
"""
for obj in objectslist:
if not hasattr(obj, "Path"):
print(
"the object " + obj.Name + " is not a path. Please select only path and Compounds."
)
return None
def __init__(
self,
job,
tooltip=translate("CAM", "LinuxCNC post processor"),
tooltipargs=["blend-mode", "blend-tolerance"],
units="Metric",
) -> None:
super().__init__(
job=job,
tooltip=tooltip,
tooltipargs=tooltipargs,
units=units,
)
Path.Log.debug("LinuxCNC post processor initialized.")
print("postprocessing...")
gcode = ""
# write header
if OUTPUT_HEADER:
gcode += linenumber() + "(Exported by FreeCAD)\n"
gcode += linenumber() + "(Post Processor: " + __name__ + ")\n"
gcode += linenumber() + "(Output Time:" + str(now) + ")\n"
# Write the preamble
if OUTPUT_COMMENTS:
gcode += linenumber() + "(begin preamble)\n"
for line in PREAMBLE.splitlines():
gcode += linenumber() + line + "\n"
gcode += linenumber() + UNITS + "\n"
for obj in objectslist:
# Skip inactive operations
if not PathUtil.activeForOp(obj):
continue
# do the pre_op
if OUTPUT_COMMENTS:
gcode += linenumber() + "(begin operation: %s)\n" % obj.Label
gcode += linenumber() + "(machine units: %s)\n" % (UNIT_SPEED_FORMAT)
for line in PRE_OPERATION.splitlines(True):
gcode += linenumber() + line
# get coolant mode
coolantMode = PathUtil.coolantModeForOp(obj)
# turn coolant on if required
if OUTPUT_COMMENTS:
if not coolantMode == "None":
gcode += linenumber() + "(Coolant On:" + coolantMode + ")\n"
if coolantMode == "Flood":
gcode += linenumber() + "M8" + "\n"
if coolantMode == "Mist":
gcode += linenumber() + "M7" + "\n"
# process the operation gcode
gcode += parse(obj)
# do the post_op
if OUTPUT_COMMENTS:
gcode += linenumber() + "(finish operation: %s)\n" % obj.Label
for line in POST_OPERATION.splitlines(True):
gcode += linenumber() + line
# turn coolant off if required
if not coolantMode == "None":
if OUTPUT_COMMENTS:
gcode += linenumber() + "(Coolant Off:" + coolantMode + ")\n"
gcode += linenumber() + "M9" + "\n"
# do the post_amble
if OUTPUT_COMMENTS:
gcode += "(begin postamble)\n"
for line in POSTAMBLE.splitlines():
gcode += linenumber() + line + "\n"
if FreeCAD.GuiUp and SHOW_EDITOR:
final = gcode
if len(gcode) > 100000:
print("Skipping editor since output is greater than 100kb")
else:
dia = PostUtils.GCodeEditorDialog()
dia.editor.setText(gcode)
result = dia.exec_()
if result:
final = dia.editor.toPlainText()
else:
final = gcode
print("done postprocessing.")
if not filename == "-":
gfile = pyopen(filename, "w")
gfile.write(final)
gfile.close()
return final
def linenumber():
global LINENR
if OUTPUT_LINE_NUMBERS is True:
LINENR += 10
return "N" + str(LINENR) + " "
return ""
def parse(pathobj):
global PRECISION
global MODAL
global OUTPUT_DOUBLES
global UNIT_FORMAT
global UNIT_SPEED_FORMAT
out = ""
lastcommand = None
precision_string = "." + str(PRECISION) + "f"
currLocation = {} # keep track for no doubles
# the order of parameters
# linuxcnc doesn't want K properties on XY plane Arcs need work.
params = [
"X",
"Y",
"Z",
"A",
"B",
"C",
"I",
"J",
"F",
"S",
"T",
"Q",
"R",
"L",
"H",
"D",
"P",
]
firstmove = Path.Command("G0", {"X": -1, "Y": -1, "Z": -1, "F": 0.0})
currLocation.update(firstmove.Parameters) # set First location Parameters
if hasattr(pathobj, "Group"): # We have a compound or project.
# if OUTPUT_COMMENTS:
# out += linenumber() + "(compound: " + pathobj.Label + ")\n"
for p in pathobj.Group:
out += parse(p)
return out
else: # parsing simple path
# groups might contain non-path things like stock.
if not hasattr(pathobj, "Path"):
return out
# if OUTPUT_COMMENTS:
# out += linenumber() + "(" + pathobj.Label + ")\n"
# The following "for" statement was fairly recently added
# but seems to be using the A, B, and C parameters in ways
# that don't appear to be compatible with how the PATH code
# uses the A, B, and C parameters. I have reverted the
# change here until we can figure out what it going on.
def init_values(self, values: Values) -> None:
"""Initialize values that are used throughout the postprocessor."""
#
# for c in PathUtils.getPathWithPlacement(pathobj).Commands:
for c in pathobj.Path.Commands:
outstring = []
command = c.Name
outstring.append(command)
super().init_values(values)
#
# Set any values here that need to override the default values set
# in the parent routine.
#
values["ENABLE_COOLANT"] = True
#
# The order of parameters.
#
# linuxcnc doesn't want K properties on XY plane; Arcs need work.
#
values["PARAMETER_ORDER"] = [
"X",
"Y",
"Z",
"A",
"B",
"C",
"I",
"J",
"F",
"S",
"T",
"Q",
"R",
"L",
"H",
"D",
"P",
]
#
# Used in the argparser code as the "name" of the postprocessor program.
#
values["MACHINE_NAME"] = "LinuxCNC"
#
# Any commands in this value will be output as the last commands
# in the G-code file.
#
values[
"POSTAMBLE"
] = """M05
G17 G54 G90 G80 G40
M2"""
values["POSTPROCESSOR_FILE_NAME"] = __name__
#
# Path blending mode configuration (LinuxCNC-specific)
#
values["BLEND_MODE"] = "BLEND" # Options: EXACT_PATH, EXACT_STOP, BLEND
values["BLEND_TOLERANCE"] = 0.0 # P value for BLEND mode (0 = G64, >0 = G64 P-)
#
# Any commands in this value will be output after the header and
# safety block at the beginning of the G-code file.
#
values["PREAMBLE"] = """G17 G54 G40 G49 G80 G90 """
# if modal: suppress the command if it is the same as the last one
if MODAL is True:
if command == lastcommand:
outstring.pop(0)
def init_arguments(self, values, argument_defaults, arguments_visible):
"""Initialize command-line arguments, including LinuxCNC-specific options."""
parser = super().init_arguments(values, argument_defaults, arguments_visible)
if c.Name.startswith("(") and not OUTPUT_COMMENTS: # command is a comment
continue
# Add LinuxCNC-specific argument group
linuxcnc_group = parser.add_argument_group("LinuxCNC-specific arguments")
# Check for G80, G98, G99 with rigid tapping and annotation
if (
command in ("G80", "G98", "G99")
and RIGID_TAP
and hasattr(c, "Annotations")
and c.Annotations.get("operation") == "tapping"
):
continue # Skip this command
linuxcnc_group.add_argument(
"--blend-mode",
choices=["EXACT_PATH", "EXACT_STOP", "BLEND"],
default="BLEND",
help="Path blending mode: EXACT_PATH (G61), EXACT_STOP (G61.1), "
"BLEND (G64/G64 P-) (default: BLEND)",
)
# Handle G84/G74 tapping cycles
if command in ("G84", "G74") and "F" in c.Parameters:
pitch_mm = float(c.Parameters["F"])
c.Parameters.pop("F") # Remove F from output, we'll handle it
linuxcnc_group.add_argument(
"--blend-tolerance",
type=float,
default=0.0,
help="Tolerance for BLEND mode (P value): 0 = no tolerance (G64), "
">0 = tolerance (G64 P-), in current units (default: 0.0)",
)
return parser
# Get spindle speed (from S param or last known value)
spindle_speed = None
if "S" in c.Parameters:
spindle_speed = float(c.Parameters["S"])
c.Parameters.pop("S")
def process_arguments(self):
"""Process arguments and update values, including blend mode handling."""
flag, args = super().process_arguments()
# Convert pitch to inches if needed
if UNITS == "G20": # imperial
pitch = pitch_mm / 25.4
else:
pitch = pitch_mm
if flag and args:
# Update blend mode values from parsed arguments
if hasattr(args, "blend_mode"):
self.values["BLEND_MODE"] = args.blend_mode
if hasattr(args, "blend_tolerance"):
self.values["BLEND_TOLERANCE"] = args.blend_tolerance
# Rigid tapping logic
if RIGID_TAP:
# Output initial tapping command
outstring[0] = "G33.1"
outstring.append("K" + format(pitch, precision_string))
# Update PREAMBLE with blend command
blend_cmd = self._get_blend_command()
self.values[
"PREAMBLE"
] = f"""G17 G54 G40 G49 G80 G90
{blend_cmd}"""
if "Z" in c.Parameters:
outstring.append("Z" + format(float(c.Parameters["Z"]), precision_string))
return flag, args
# Output the tapping line
if len(outstring) >= 1:
if OUTPUT_LINE_NUMBERS:
outstring.insert(0, (linenumber()))
for w in outstring:
out += w + COMMAND_SPACE
out += "\n"
def _get_blend_command(self) -> str:
"""Generate the path blending G-code command based on current settings."""
mode = self.values.get("BLEND_MODE", "BLEND")
if "P" in c.Parameters:
# Issue spindle stop
out += linenumber() + "M5\n"
# Issue dwell with P value
out += linenumber() + f"G04 P{c.Parameters['P']}\n"
if mode == "EXACT_PATH":
return "G61"
elif mode == "EXACT_STOP":
return "G61.1"
else: # BLEND
tolerance = self.values.get("BLEND_TOLERANCE", 0.0)
if tolerance > 0:
return f"G64 P{tolerance:.4f}"
else:
return "G64"
# Now handle reverse out and spindle restore
if command == "G84":
# Reverse spindle (M4) with spindle speed
out += linenumber() + "M4\n"
# Repeat tapping command to reverse out, use R for Z
reverse_z = c.Parameters.get("R")
if reverse_z is not None:
pos = Units.Quantity(reverse_z, FreeCAD.Units.Length)
reverse_z = float(pos.getValueAs(UNIT_FORMAT))
out += (
linenumber()
+ f"G33.1 K{format(pitch, precision_string)} Z{format(float(reverse_z), precision_string)}\n"
)
else:
out += linenumber() + f"G33.1 K{format(pitch, precision_string)}\n"
# Restore original spindle direction (M3) with spindle speed
out += linenumber() + "M3\n"
elif command == "G74":
# Forward spindle (M3) with spindle speed
out += linenumber() + "M3\n"
# Repeat tapping command to reverse out, use R for Z
reverse_z = c.Parameters.get("R")
if reverse_z is not None:
pos = Units.Quantity(reverse_z, FreeCAD.Units.Length)
reverse_z = float(pos.getValueAs(UNIT_FORMAT))
out += (
linenumber()
+ f"G33.1 K{format(pitch, precision_string)} Z{format(float(reverse_z), precision_string)}\n"
)
else:
out += linenumber() + f"G33.1 K{format(pitch, precision_string)}\n"
# Restore original spindle direction (M4) with spindle speed
out += linenumber() + "M4\n"
# tooltipArgs is inherited from base class and automatically includes
# all arguments from init_arguments() via parser.format_help()
continue # Skip the rest of the parameter output for this command
else:
# Calculate feed rate
if spindle_speed is not None:
feed_rate = pitch * spindle_speed
speed = Units.Quantity(feed_rate, UNIT_SPEED_FORMAT)
outstring.append(
"F"
+ format(float(speed.getValueAs(UNIT_SPEED_FORMAT)), precision_string)
)
else:
# No spindle speed found, output pitch as F
outstring.append("F" + format(pitch, precision_string))
# Now add the remaining parameters in order
for param in params:
if param in c.Parameters:
if param == "F" and (
currLocation[param] != c.Parameters[param] or OUTPUT_DOUBLES
):
if c.Name not in [
"G0",
"G00",
]: # linuxcnc doesn't use rapid speeds
speed = Units.Quantity(c.Parameters["F"], FreeCAD.Units.Velocity)
if speed.getValueAs(UNIT_SPEED_FORMAT) > 0.0:
outstring.append(
param
+ format(
float(speed.getValueAs(UNIT_SPEED_FORMAT)),
precision_string,
)
)
else:
continue
elif param == "T":
outstring.append(param + str(int(c.Parameters["T"])))
elif param == "H":
outstring.append(param + str(int(c.Parameters["H"])))
elif param == "D":
outstring.append(param + str(int(c.Parameters["D"])))
elif param == "S":
outstring.append(param + str(int(c.Parameters["S"])))
else:
if (
(not OUTPUT_DOUBLES)
and (param in currLocation)
and (currLocation[param] == c.Parameters[param])
):
continue
else:
if param in ("A", "B", "C"):
outstring.append(
param + format(float(c.Parameters[param]), precision_string)
)
else:
pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length)
outstring.append(
param
+ format(float(pos.getValueAs(UNIT_FORMAT)), precision_string)
)
# store the latest command
lastcommand = command
currLocation.update(c.Parameters)
# Check for Tool Change:
if command == "M6":
# stop the spindle
out += linenumber() + "M5\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)
if command == "message":
if OUTPUT_COMMENTS is False:
out = []
else:
outstring.pop(0) # remove the command
# prepend a line number and append a newline
if len(outstring) >= 1:
if OUTPUT_LINE_NUMBERS:
outstring.insert(0, (linenumber()))
# append the line to the final output
for w in outstring:
out += w + COMMAND_SPACE
out += "\n"
return out
# print(__name__ + " gcode postprocessor loaded.")
@property
def tooltip(self):
tooltip: str = """
This is a postprocessor file for the CAM workbench.
It is used to take a pseudo-gcode fragment from a CAM object
and output 'real' GCode suitable for a linuxcnc 3 axis mill.
"""
return tooltip

View File

@@ -0,0 +1,481 @@
# ***************************************************************************
# * Copyright (c) 2014 sliptonic <shopinthewoods@gmail.com> *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
# * 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. *
# * *
# * FreeCAD 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 Lesser General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with FreeCAD; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************/
import FreeCAD
from FreeCAD import Units
import Path
import argparse
import datetime
import shlex
import Path.Base.Util as PathUtil
import Path.Post.Utils as PostUtils
import PathScripts.PathUtils as PathUtils
from builtins import open as pyopen
TOOLTIP = """
This is a postprocessor file for the Path workbench. It is used to
take a pseudo-G-code fragment outputted by a Path object, and output
real G-code suitable for a mach3_4 3 axis mill. This postprocessor, once placed
in the appropriate PathScripts folder, can be used directly from inside
FreeCAD, via the GUI importer or via python scripts with:
import mach3_4_legacy_post
mach3_4_legacy_post.export(object,"/path/to/file.ncc","")
"""
now = datetime.datetime.now()
parser = argparse.ArgumentParser(prog="mach3_4", add_help=False)
parser.add_argument("--no-header", action="store_true", help="suppress header output")
parser.add_argument("--no-comments", action="store_true", help="suppress comment output")
parser.add_argument("--line-numbers", action="store_true", help="prefix with line numbers")
parser.add_argument(
"--no-show-editor",
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(
"--preamble",
help='set commands to be issued before the first command, default="G17 G54 G40 G49 G80 G90\\n"',
)
parser.add_argument(
"--postamble",
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)"
)
parser.add_argument(
"--modal",
action="store_true",
help="Output the Same G-command Name USE NonModal Mode",
)
parser.add_argument("--axis-modal", action="store_true", help="Output the Same Axis Value Mode")
parser.add_argument(
"--no-tlo",
action="store_true",
help="suppress tool length offset (G43) following tool changes",
)
TOOLTIP_ARGS = parser.format_help()
# These globals set common customization preferences
OUTPUT_COMMENTS = True
OUTPUT_HEADER = True
OUTPUT_LINE_NUMBERS = False
SHOW_EDITOR = True
MODAL = False # if true commands are suppressed if the same as previous line.
USE_TLO = True # if true G43 will be output following tool changes
OUTPUT_DOUBLES = True # if false duplicate axis values are suppressed if the same as previous line.
COMMAND_SPACE = " "
LINENR = 100 # line number starting value
# 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"
UNIT_FORMAT = "mm"
MACHINE_NAME = "mach3_4"
CORNER_MIN = {"x": 0, "y": 0, "z": 0}
CORNER_MAX = {"x": 500, "y": 300, "z": 300}
PRECISION = 3
# 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
M2
"""
# Pre operation text will be inserted before every operation
PRE_OPERATION = """"""
# Post operation text will be inserted after every operation
POST_OPERATION = """"""
# Tool Change commands will be inserted before a tool change
TOOL_CHANGE = """"""
def processArguments(argstring):
global OUTPUT_HEADER
global OUTPUT_COMMENTS
global OUTPUT_LINE_NUMBERS
global SHOW_EDITOR
global PRECISION
global PREAMBLE
global POSTAMBLE
global UNITS
global UNIT_SPEED_FORMAT
global UNIT_FORMAT
global MODAL
global USE_TLO
global OUTPUT_DOUBLES
try:
args = parser.parse_args(shlex.split(argstring))
if args.no_header:
OUTPUT_HEADER = False
if args.no_comments:
OUTPUT_COMMENTS = False
if args.line_numbers:
OUTPUT_LINE_NUMBERS = True
if args.no_show_editor:
SHOW_EDITOR = False
print("Show editor = %d" % SHOW_EDITOR)
PRECISION = args.precision
if args.preamble is not None:
PREAMBLE = args.preamble.replace("\\n", "\n")
if args.postamble is not None:
POSTAMBLE = args.postamble.replace("\\n", "\n")
if args.inches:
UNITS = "G20"
UNIT_SPEED_FORMAT = "in/min"
UNIT_FORMAT = "in"
PRECISION = 4
if args.modal:
MODAL = True
if args.no_tlo:
USE_TLO = False
if args.axis_modal:
print("here")
OUTPUT_DOUBLES = False
except Exception:
return False
return True
def export(objectslist, filename, argstring):
if not processArguments(argstring):
return None
global UNITS
global UNIT_FORMAT
global UNIT_SPEED_FORMAT
for obj in objectslist:
if not hasattr(obj, "Path"):
print(
"the object " + obj.Name + " is not a path. Please select only path and Compounds."
)
return None
print("postprocessing...")
gcode = ""
# write header
if OUTPUT_HEADER:
gcode += linenumber() + "(Exported by FreeCAD)\n"
gcode += linenumber() + "(Post Processor: " + __name__ + ")\n"
gcode += linenumber() + "(Output Time:" + str(now) + ")\n"
# Write the preamble
if OUTPUT_COMMENTS:
gcode += linenumber() + "(begin preamble)\n"
for line in PREAMBLE.splitlines():
gcode += linenumber() + line + "\n"
gcode += linenumber() + UNITS + "\n"
for obj in objectslist:
# Skip inactive operations
if not PathUtil.activeForOp(obj):
continue
# do the pre_op
if OUTPUT_COMMENTS:
gcode += linenumber() + "(begin operation: %s)\n" % obj.Label
gcode += linenumber() + "(machine: %s, %s)\n" % (
MACHINE_NAME,
UNIT_SPEED_FORMAT,
)
for line in PRE_OPERATION.splitlines(True):
gcode += linenumber() + line
# get coolant mode
coolantMode = PathUtil.coolantModeForOp(obj)
# turn coolant on if required
if OUTPUT_COMMENTS:
if not coolantMode == "None":
gcode += linenumber() + "(Coolant On:" + coolantMode + ")\n"
if coolantMode == "Flood":
gcode += linenumber() + "M8" + "\n"
if coolantMode == "Mist":
gcode += linenumber() + "M7" + "\n"
# process the operation gcode
gcode += parse(obj)
# do the post_op
if OUTPUT_COMMENTS:
gcode += linenumber() + "(finish operation: %s)\n" % obj.Label
for line in POST_OPERATION.splitlines(True):
gcode += linenumber() + line
# turn coolant off if required
if not coolantMode == "None":
if OUTPUT_COMMENTS:
gcode += linenumber() + "(Coolant Off:" + coolantMode + ")\n"
gcode += linenumber() + "M9" + "\n"
# do the post_amble
if OUTPUT_COMMENTS:
gcode += "(begin postamble)\n"
for line in POSTAMBLE.splitlines():
gcode += linenumber() + line + "\n"
if FreeCAD.GuiUp and SHOW_EDITOR:
dia = PostUtils.GCodeEditorDialog()
dia.editor.setText(gcode)
result = dia.exec_()
if result:
final = dia.editor.toPlainText()
else:
final = gcode
else:
final = gcode
print("done postprocessing.")
if not filename == "-":
gfile = pyopen(filename, "w")
gfile.write(final)
gfile.close()
return final
def linenumber():
global LINENR
if OUTPUT_LINE_NUMBERS is True:
LINENR += 10
return "N" + str(LINENR) + " "
return ""
def parse(pathobj):
global PRECISION
global MODAL
global OUTPUT_DOUBLES
global UNIT_FORMAT
global UNIT_SPEED_FORMAT
out = ""
lastcommand = None
precision_string = "." + str(PRECISION) + "f"
currLocation = {} # keep track for no doubles
# the order of parameters
# mach3_4 doesn't want K properties on XY plane Arcs need work.
params = [
"X",
"Y",
"Z",
"A",
"B",
"C",
"I",
"J",
"F",
"S",
"T",
"Q",
"R",
"L",
"H",
"D",
"P",
]
firstmove = Path.Command("G0", {"X": -1, "Y": -1, "Z": -1, "F": 0.0})
currLocation.update(firstmove.Parameters) # set First location Parameters
if hasattr(pathobj, "Group"): # We have a compound or project.
# if OUTPUT_COMMENTS:
# out += linenumber() + "(compound: " + pathobj.Label + ")\n"
for p in pathobj.Group:
out += parse(p)
return out
else: # parsing simple path
# groups might contain non-path things like stock.
if not hasattr(pathobj, "Path"):
return out
# if OUTPUT_COMMENTS:
# out += linenumber() + "(" + pathobj.Label + ")\n"
adaptiveOp = False
opHorizRapid = 0
opVertRapid = 0
if "Adaptive" in pathobj.Name:
adaptiveOp = True
if hasattr(pathobj, "ToolController"):
if (
hasattr(pathobj.ToolController, "HorizRapid")
and pathobj.ToolController.HorizRapid > 0
):
opHorizRapid = Units.Quantity(
pathobj.ToolController.HorizRapid, FreeCAD.Units.Velocity
)
else:
FreeCAD.Console.PrintWarning(
"Tool Controller Horizontal Rapid Values are unset" + "\n"
)
if (
hasattr(pathobj.ToolController, "VertRapid")
and pathobj.ToolController.VertRapid > 0
):
opVertRapid = Units.Quantity(
pathobj.ToolController.VertRapid, FreeCAD.Units.Velocity
)
else:
FreeCAD.Console.PrintWarning(
"Tool Controller Vertical Rapid Values are unset" + "\n"
)
for c in PathUtils.getPathWithPlacement(pathobj).Commands:
outstring = []
command = c.Name
if adaptiveOp and c.Name in ["G0", "G00"]:
if opHorizRapid and opVertRapid:
command = "G1"
else:
outstring.append("(Tool Controller Rapid Values are unset)" + "\n")
outstring.append(command)
# if modal: suppress the command if it is the same as the last one
if MODAL is True:
if command == lastcommand:
outstring.pop(0)
if c.Name.startswith("(") and not OUTPUT_COMMENTS: # command is a comment
continue
# Now add the remaining parameters in order
for param in params:
if param in c.Parameters:
if param == "F" and (
currLocation[param] != c.Parameters[param] or OUTPUT_DOUBLES
):
if c.Name not in [
"G0",
"G00",
]: # mach3_4 doesn't use rapid speeds
speed = Units.Quantity(c.Parameters["F"], FreeCAD.Units.Velocity)
if speed.getValueAs(UNIT_SPEED_FORMAT) > 0.0:
outstring.append(
param
+ format(
float(speed.getValueAs(UNIT_SPEED_FORMAT)),
precision_string,
)
)
else:
continue
elif param == "T":
outstring.append(param + str(int(c.Parameters["T"])))
elif param == "H":
outstring.append(param + str(int(c.Parameters["H"])))
elif param == "D":
outstring.append(param + str(int(c.Parameters["D"])))
elif param == "S":
outstring.append(param + str(int(c.Parameters["S"])))
else:
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)
outstring.append(
param + format(float(pos.getValueAs(UNIT_FORMAT)), precision_string)
)
if adaptiveOp and c.Name in ["G0", "G00"]:
if opHorizRapid and opVertRapid:
if "Z" not in c.Parameters:
outstring.append(
"F"
+ format(
float(opHorizRapid.getValueAs(UNIT_SPEED_FORMAT)),
precision_string,
)
)
else:
outstring.append(
"F"
+ format(
float(opVertRapid.getValueAs(UNIT_SPEED_FORMAT)),
precision_string,
)
)
# store the latest command
lastcommand = command
currLocation.update(c.Parameters)
# Check for Tool Change:
if command == "M6":
# stop the spindle
out += linenumber() + "M5\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)
if command == "message":
if OUTPUT_COMMENTS is False:
out = []
else:
outstring.pop(0) # remove the command
# prepend a line number and append a newline
if len(outstring) >= 1:
if OUTPUT_LINE_NUMBERS:
outstring.insert(0, (linenumber()))
# append the line to the final output
for w in outstring:
out += w + COMMAND_SPACE
out = out.strip() + "\n"
return out
# print(__name__ + " gcode postprocessor loaded.")

View File

@@ -2,6 +2,8 @@
# ***************************************************************************
# * Copyright (c) 2014 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2022 - 2025 Larry Woestman <LarryWoestman2@gmail.com> *
# * Copyright (c) 2024 Ondsel <development@ondsel.com> *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
@@ -21,463 +23,128 @@
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************/
# ***************************************************************************
from typing import Any, Dict
from Path.Post.Processor import PostProcessor
import FreeCAD
from FreeCAD import Units
import Path
import argparse
import datetime
import shlex
import Path.Base.Util as PathUtil
import Path.Post.Utils as PostUtils
import PathScripts.PathUtils as PathUtils
from builtins import open as pyopen
import FreeCAD
TOOLTIP = """
This is a postprocessor file for the Path workbench. It is used to
take a pseudo-G-code fragment outputted by a Path object, and output
real G-code suitable for a mach3_4 3 axis mill. This postprocessor, once placed
in the appropriate PathScripts folder, can be used directly from inside
FreeCAD, via the GUI importer or via python scripts with:
translate = FreeCAD.Qt.translate
import mach3_4_post
mach3_4_post.export(object,"/path/to/file.ncc","")
"""
DEBUG = False
if DEBUG:
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
else:
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
now = datetime.datetime.now()
#
# Define some types that are used throughout this file.
#
Values = Dict[str, Any]
Visible = Dict[str, bool]
parser = argparse.ArgumentParser(prog="mach3_4", add_help=False)
parser.add_argument("--no-header", action="store_true", help="suppress header output")
parser.add_argument("--no-comments", action="store_true", help="suppress comment output")
parser.add_argument("--line-numbers", action="store_true", help="prefix with line numbers")
parser.add_argument(
"--no-show-editor",
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(
"--preamble",
help='set commands to be issued before the first command, default="G17 G54 G40 G49 G80 G90\\n"',
)
parser.add_argument(
"--postamble",
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)"
)
parser.add_argument(
"--modal",
action="store_true",
help="Output the Same G-command Name USE NonModal Mode",
)
parser.add_argument("--axis-modal", action="store_true", help="Output the Same Axis Value Mode")
parser.add_argument(
"--no-tlo",
action="store_true",
help="suppress tool length offset (G43) following tool changes",
)
TOOLTIP_ARGS = parser.format_help()
class Mach3_Mach4(PostProcessor):
"""The Mach3_Mach4 post processor class."""
# These globals set common customization preferences
OUTPUT_COMMENTS = True
OUTPUT_HEADER = True
OUTPUT_LINE_NUMBERS = False
SHOW_EDITOR = True
MODAL = False # if true commands are suppressed if the same as previous line.
USE_TLO = True # if true G43 will be output following tool changes
OUTPUT_DOUBLES = True # if false duplicate axis values are suppressed if the same as previous line.
COMMAND_SPACE = " "
LINENR = 100 # line number starting value
def __init__(
self,
job,
tooltip=translate("CAM", "Mach3_Mach4 post processor"),
tooltipargs=[""],
units="Metric",
) -> None:
super().__init__(
job=job,
tooltip=tooltip,
tooltipargs=tooltipargs,
units=units,
)
Path.Log.debug("Mach3_Mach4 post processor initialized.")
# 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"
UNIT_FORMAT = "mm"
MACHINE_NAME = "mach3_4"
CORNER_MIN = {"x": 0, "y": 0, "z": 0}
CORNER_MAX = {"x": 500, "y": 300, "z": 300}
PRECISION = 3
# 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
def init_values(self, values: Values) -> None:
"""Initialize values that are used throughout the postprocessor."""
#
super().init_values(values)
#
# Set any values here that need to override the default values set
# in the parent routine.
#
values["ENABLE_COOLANT"] = True
#
# Used in the argparser code as the "name" of the postprocessor program.
# This would normally show up in the usage message in the TOOLTIP_ARGS.
#
values["MACHINE_NAME"] = "mach3_4"
#
# Enable special processing for operations with "Adaptive" in the name.
#
values["OUTPUT_ADAPTIVE"] = True
#
# Output the machine name for mach3_mach4 instead of the machine units alone.
#
values["OUTPUT_MACHINE_NAME"] = True
#
# The order of parameters.
#
# mach3_mach4 doesn't want K properties on XY plane; Arcs need work.
#
values["PARAMETER_ORDER"] = [
"X",
"Y",
"Z",
"A",
"B",
"C",
"I",
"J",
"F",
"S",
"T",
"Q",
"R",
"L",
"H",
"D",
"P",
]
#
# Any commands in this value will be output as the last commands
# in the G-code file.
#
values[
"POSTAMBLE"
] = """M05
G17 G54 G90 G80 G40
M2
"""
M2"""
values["POSTPROCESSOR_FILE_NAME"] = __name__
#
# Any commands in this value will be output after the header and
# safety block at the beginning of the G-code file.
#
values["PREAMBLE"] = """G17 G54 G40 G49 G80 G90"""
#
# Output the machine name for mach3_mach4 instead of the machine units alone.
#
values["SHOW_MACHINE_UNITS"] = False
# Pre operation text will be inserted before every operation
PRE_OPERATION = """"""
def init_arguments_visible(self, arguments_visible: Visible) -> None:
"""Initialize which argument pairs are visible in TOOLTIP_ARGS."""
super().init_arguments_visible(arguments_visible)
#
# Modify the visibility of any arguments from the defaults here.
#
arguments_visible["axis-modal"] = True
# Post operation text will be inserted after every operation
POST_OPERATION = """"""
# Tool Change commands will be inserted before a tool change
TOOL_CHANGE = """"""
def processArguments(argstring):
global OUTPUT_HEADER
global OUTPUT_COMMENTS
global OUTPUT_LINE_NUMBERS
global SHOW_EDITOR
global PRECISION
global PREAMBLE
global POSTAMBLE
global UNITS
global UNIT_SPEED_FORMAT
global UNIT_FORMAT
global MODAL
global USE_TLO
global OUTPUT_DOUBLES
try:
args = parser.parse_args(shlex.split(argstring))
if args.no_header:
OUTPUT_HEADER = False
if args.no_comments:
OUTPUT_COMMENTS = False
if args.line_numbers:
OUTPUT_LINE_NUMBERS = True
if args.no_show_editor:
SHOW_EDITOR = False
print("Show editor = %d" % SHOW_EDITOR)
PRECISION = args.precision
if args.preamble is not None:
PREAMBLE = args.preamble.replace("\\n", "\n")
if args.postamble is not None:
POSTAMBLE = args.postamble.replace("\\n", "\n")
if args.inches:
UNITS = "G20"
UNIT_SPEED_FORMAT = "in/min"
UNIT_FORMAT = "in"
PRECISION = 4
if args.modal:
MODAL = True
if args.no_tlo:
USE_TLO = False
if args.axis_modal:
print("here")
OUTPUT_DOUBLES = False
except Exception:
return False
return True
def export(objectslist, filename, argstring):
if not processArguments(argstring):
return None
global UNITS
global UNIT_FORMAT
global UNIT_SPEED_FORMAT
for obj in objectslist:
if not hasattr(obj, "Path"):
print(
"the object " + obj.Name + " is not a path. Please select only path and Compounds."
)
return None
print("postprocessing...")
gcode = ""
# write header
if OUTPUT_HEADER:
gcode += linenumber() + "(Exported by FreeCAD)\n"
gcode += linenumber() + "(Post Processor: " + __name__ + ")\n"
gcode += linenumber() + "(Output Time:" + str(now) + ")\n"
# Write the preamble
if OUTPUT_COMMENTS:
gcode += linenumber() + "(begin preamble)\n"
for line in PREAMBLE.splitlines():
gcode += linenumber() + line + "\n"
gcode += linenumber() + UNITS + "\n"
for obj in objectslist:
# Skip inactive operations
if not PathUtil.activeForOp(obj):
continue
# do the pre_op
if OUTPUT_COMMENTS:
gcode += linenumber() + "(begin operation: %s)\n" % obj.Label
gcode += linenumber() + "(machine: %s, %s)\n" % (
MACHINE_NAME,
UNIT_SPEED_FORMAT,
)
for line in PRE_OPERATION.splitlines(True):
gcode += linenumber() + line
# get coolant mode
coolantMode = PathUtil.coolantModeForOp(obj)
# turn coolant on if required
if OUTPUT_COMMENTS:
if not coolantMode == "None":
gcode += linenumber() + "(Coolant On:" + coolantMode + ")\n"
if coolantMode == "Flood":
gcode += linenumber() + "M8" + "\n"
if coolantMode == "Mist":
gcode += linenumber() + "M7" + "\n"
# process the operation gcode
gcode += parse(obj)
# do the post_op
if OUTPUT_COMMENTS:
gcode += linenumber() + "(finish operation: %s)\n" % obj.Label
for line in POST_OPERATION.splitlines(True):
gcode += linenumber() + line
# turn coolant off if required
if not coolantMode == "None":
if OUTPUT_COMMENTS:
gcode += linenumber() + "(Coolant Off:" + coolantMode + ")\n"
gcode += linenumber() + "M9" + "\n"
# do the post_amble
if OUTPUT_COMMENTS:
gcode += "(begin postamble)\n"
for line in POSTAMBLE.splitlines():
gcode += linenumber() + line + "\n"
if FreeCAD.GuiUp and SHOW_EDITOR:
dia = PostUtils.GCodeEditorDialog()
dia.editor.setText(gcode)
result = dia.exec_()
if result:
final = dia.editor.toPlainText()
else:
final = gcode
else:
final = gcode
print("done postprocessing.")
if not filename == "-":
gfile = pyopen(filename, "w")
gfile.write(final)
gfile.close()
return final
def linenumber():
global LINENR
if OUTPUT_LINE_NUMBERS is True:
LINENR += 10
return "N" + str(LINENR) + " "
return ""
def parse(pathobj):
global PRECISION
global MODAL
global OUTPUT_DOUBLES
global UNIT_FORMAT
global UNIT_SPEED_FORMAT
out = ""
lastcommand = None
precision_string = "." + str(PRECISION) + "f"
currLocation = {} # keep track for no doubles
# the order of parameters
# mach3_4 doesn't want K properties on XY plane Arcs need work.
params = [
"X",
"Y",
"Z",
"A",
"B",
"C",
"I",
"J",
"F",
"S",
"T",
"Q",
"R",
"L",
"H",
"D",
"P",
]
firstmove = Path.Command("G0", {"X": -1, "Y": -1, "Z": -1, "F": 0.0})
currLocation.update(firstmove.Parameters) # set First location Parameters
if hasattr(pathobj, "Group"): # We have a compound or project.
# if OUTPUT_COMMENTS:
# out += linenumber() + "(compound: " + pathobj.Label + ")\n"
for p in pathobj.Group:
out += parse(p)
return out
else: # parsing simple path
# groups might contain non-path things like stock.
if not hasattr(pathobj, "Path"):
return out
# if OUTPUT_COMMENTS:
# out += linenumber() + "(" + pathobj.Label + ")\n"
adaptiveOp = False
opHorizRapid = 0
opVertRapid = 0
if "Adaptive" in pathobj.Name:
adaptiveOp = True
if hasattr(pathobj, "ToolController"):
if (
hasattr(pathobj.ToolController, "HorizRapid")
and pathobj.ToolController.HorizRapid > 0
):
opHorizRapid = Units.Quantity(
pathobj.ToolController.HorizRapid, FreeCAD.Units.Velocity
)
else:
FreeCAD.Console.PrintWarning(
"Tool Controller Horizontal Rapid Values are unset" + "\n"
)
if (
hasattr(pathobj.ToolController, "VertRapid")
and pathobj.ToolController.VertRapid > 0
):
opVertRapid = Units.Quantity(
pathobj.ToolController.VertRapid, FreeCAD.Units.Velocity
)
else:
FreeCAD.Console.PrintWarning(
"Tool Controller Vertical Rapid Values are unset" + "\n"
)
for c in PathUtils.getPathWithPlacement(pathobj).Commands:
outstring = []
command = c.Name
if adaptiveOp and c.Name in ["G0", "G00"]:
if opHorizRapid and opVertRapid:
command = "G1"
else:
outstring.append("(Tool Controller Rapid Values are unset)" + "\n")
outstring.append(command)
# if modal: suppress the command if it is the same as the last one
if MODAL is True:
if command == lastcommand:
outstring.pop(0)
if c.Name.startswith("(") and not OUTPUT_COMMENTS: # command is a comment
continue
# Now add the remaining parameters in order
for param in params:
if param in c.Parameters:
if param == "F" and (
currLocation[param] != c.Parameters[param] or OUTPUT_DOUBLES
):
if c.Name not in [
"G0",
"G00",
]: # mach3_4 doesn't use rapid speeds
speed = Units.Quantity(c.Parameters["F"], FreeCAD.Units.Velocity)
if speed.getValueAs(UNIT_SPEED_FORMAT) > 0.0:
outstring.append(
param
+ format(
float(speed.getValueAs(UNIT_SPEED_FORMAT)),
precision_string,
)
)
else:
continue
elif param == "T":
outstring.append(param + str(int(c.Parameters["T"])))
elif param == "H":
outstring.append(param + str(int(c.Parameters["H"])))
elif param == "D":
outstring.append(param + str(int(c.Parameters["D"])))
elif param == "S":
outstring.append(param + str(int(c.Parameters["S"])))
else:
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)
outstring.append(
param + format(float(pos.getValueAs(UNIT_FORMAT)), precision_string)
)
if adaptiveOp and c.Name in ["G0", "G00"]:
if opHorizRapid and opVertRapid:
if "Z" not in c.Parameters:
outstring.append(
"F"
+ format(
float(opHorizRapid.getValueAs(UNIT_SPEED_FORMAT)),
precision_string,
)
)
else:
outstring.append(
"F"
+ format(
float(opVertRapid.getValueAs(UNIT_SPEED_FORMAT)),
precision_string,
)
)
# store the latest command
lastcommand = command
currLocation.update(c.Parameters)
# Check for Tool Change:
if command == "M6":
# stop the spindle
out += linenumber() + "M5\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)
if command == "message":
if OUTPUT_COMMENTS is False:
out = []
else:
outstring.pop(0) # remove the command
# prepend a line number and append a newline
if len(outstring) >= 1:
if OUTPUT_LINE_NUMBERS:
outstring.insert(0, (linenumber()))
# append the line to the final output
for w in outstring:
out += w + COMMAND_SPACE
out = out.strip() + "\n"
return out
# print(__name__ + " gcode postprocessor loaded.")
@property
def tooltip(self):
tooltip: str = """
This is a postprocessor file for the CAM workbench.
It is used to take a pseudo-gcode fragment from a CAM object
and output 'real' GCode suitable for a Mach3_4 3 axis mill.
"""
return tooltip

View File

@@ -28,7 +28,7 @@
from typing import Any, Dict
from Path.Post.scripts.refactored_linuxcnc_post import Refactored_Linuxcnc
from Path.Post.Processor import PostProcessor
import Path
import FreeCAD
@@ -48,13 +48,13 @@ else:
Values = Dict[str, Any]
class Refactored_Masso_G3(Refactored_Linuxcnc):
"""The Refactored Masso G3 post processor class."""
class Masso_G3(PostProcessor):
"""The Masso G3 post processor class."""
def __init__(
self,
job,
tooltip=translate("CAM", "Refactored Masso G3 post processor"),
tooltip=translate("CAM", "Masso G3 post processor"),
tooltipargs=[""],
units="Metric",
) -> None:
@@ -64,25 +64,39 @@ class Refactored_Masso_G3(Refactored_Linuxcnc):
tooltipargs=tooltipargs,
units=units,
)
Path.Log.debug("Refactored Masso G3 post processor initialized.")
Path.Log.debug("Masso G3 post processor initialized.")
def init_values(self, values: Values) -> None:
"""Initialize values that are used throughout the postprocessor."""
#
super().init_values(values)
#
# Set any values here that need to override the default values set
# in the parent routine.
#
#
# Used in the argparser code as the "name" of the postprocessor program.
#
values["ENABLE_COOLANT"] = True
values["PARAMETER_ORDER"] = [
"X",
"Y",
"Z",
"A",
"B",
"C",
"I",
"J",
"F",
"S",
"T",
"Q",
"R",
"L",
"H",
"D",
"P",
]
values[
"POSTAMBLE"
] = """M05
G17 G54 G90 G80 G40
M2"""
values["PREAMBLE"] = """G17 G54 G40 G49 G80 G90"""
values["MACHINE_NAME"] = "Masso G3"
values["POSTPROCESSOR_FILE_NAME"] = __name__
#
# setting TOOL_BEFORE_CHANGE to True will output T# M6 before each tool change
# rather than M6 T#.
#
values["TOOL_BEFORE_CHANGE"] = True
@property

View File

@@ -1,196 +0,0 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# ***************************************************************************
# * Copyright (c) 2014 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2022 - 2025 Larry Woestman <LarryWoestman2@gmail.com> *
# * Copyright (c) 2024 Ondsel <development@ondsel.com> *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
# * 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. *
# * *
# * FreeCAD 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 Lesser General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with FreeCAD; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
from typing import Any, Dict
from Path.Post.Processor import PostProcessor
import Path
import FreeCAD
translate = FreeCAD.Qt.translate
DEBUG = False
if DEBUG:
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
else:
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
#
# Define some types that are used throughout this file.
#
Values = Dict[str, Any]
Visible = Dict[str, bool]
class Refactored_Centroid(PostProcessor):
"""The Refactored Centroid post processor class."""
def __init__(
self,
job,
tooltip=translate("CAM", "Refactored Centroid post processor"),
tooltipargs=[""],
units="Metric",
) -> None:
super().__init__(
job=job,
tooltip=tooltip,
tooltipargs=tooltipargs,
units=units,
)
Path.Log.debug("Refactored Centroid post processor initialized.")
def init_values(self, values: Values) -> None:
"""Initialize values that are used throughout the postprocessor."""
#
super().init_values(values)
#
# Set any values here that need to override the default values set
# in the parent routine.
#
# Use 4 digits for axis precision by default.
#
values["AXIS_PRECISION"] = 4
values["DEFAULT_AXIS_PRECISION"] = 4
values["DEFAULT_INCH_AXIS_PRECISION"] = 4
#
# Use ";" as the comment symbol
#
values["COMMENT_SYMBOL"] = ";"
#
# Use 1 digit for feed precision by default.
#
values["FEED_PRECISION"] = 1
values["DEFAULT_FEED_PRECISION"] = 1
values["DEFAULT_INCH_FEED_PRECISION"] = 1
#
# This value usually shows up in the post_op comment as "Finish operation:".
# Change it to "End" to produce "End operation:".
#
values["FINISH_LABEL"] = "End"
#
# If this value is True, then a list of tool numbers
# with their labels are output just before the preamble.
#
values["LIST_TOOLS_IN_PREAMBLE"] = True
#
# Used in the argparser code as the "name" of the postprocessor program.
# This would normally show up in the usage message in the TOOLTIP_ARGS.
#
values["MACHINE_NAME"] = "Centroid"
#
# This list controls the order of parameters in a line during output.
# centroid doesn't want K properties on XY plane; Arcs need work.
#
values["PARAMETER_ORDER"] = [
"X",
"Y",
"Z",
"A",
"B",
"I",
"J",
"F",
"S",
"T",
"Q",
"R",
"L",
"H",
]
#
# Any commands in this value will be output as the last commands
# in the G-code file.
#
values["POSTAMBLE"] = """M99"""
values["POSTPROCESSOR_FILE_NAME"] = __name__
#
# Any commands in this value will be output after the header and
# safety block at the beginning of the G-code file.
#
values["PREAMBLE"] = """G53 G00 G17"""
#
# Output any messages.
#
values["REMOVE_MESSAGES"] = False
#
# Any commands in this value are output after the header but before the preamble,
# then again after the TOOLRETURN but before the POSTAMBLE.
#
values["SAFETYBLOCK"] = """G90 G80 G40 G49"""
#
# Do not show the current machine units just before the PRE_OPERATION.
#
values["SHOW_MACHINE_UNITS"] = False
#
# Do not show the current operation label just before the PRE_OPERATION.
#
values["SHOW_OPERATION_LABELS"] = False
#
# Do not output an M5 command to stop the spindle for tool changes.
#
values["STOP_SPINDLE_FOR_TOOL_CHANGE"] = False
#
# spindle off, height offset canceled, spindle retracted
# (M25 is a centroid command to retract spindle)
#
values[
"TOOLRETURN"
] = """M5
M25
G49 H0"""
#
# Default to not outputting a G43 following tool changes
#
values["USE_TLO"] = False
#
# This was in the original centroid postprocessor file
# but does not appear to be used anywhere.
#
# ZAXISRETURN = """G91 G28 X0 Z0 G90"""
#
def init_arguments_visible(self, arguments_visible: Visible) -> None:
"""Initialize which argument pairs are visible in TOOLTIP_ARGS."""
super().init_arguments_visible(arguments_visible)
#
# Modify the visibility of any arguments from the defaults here.
#
arguments_visible["axis-modal"] = False
arguments_visible["precision"] = False
arguments_visible["tlo"] = False
@property
def tooltip(self):
tooltip: str = """
This is a postprocessor file for the CAM workbench.
It is used to take a pseudo-gcode fragment from a CAM object
and output 'real' GCode suitable for a centroid 3 axis mill.
"""
return tooltip

View File

@@ -1,190 +0,0 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# ***************************************************************************
# * Copyright (c) 2014 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2022 - 2025 Larry Woestman <LarryWoestman2@gmail.com> *
# * Copyright (c) 2024 Ondsel <development@ondsel.com> *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
# * 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. *
# * *
# * FreeCAD 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 Lesser General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with FreeCAD; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
import argparse
from typing import Any, Dict
from Path.Post.Processor import PostProcessor
import Path
import FreeCAD
translate = FreeCAD.Qt.translate
DEBUG = False
if DEBUG:
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
else:
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
#
# Define some types that are used throughout this file.
#
Defaults = Dict[str, bool]
Values = Dict[str, Any]
Visible = Dict[str, bool]
class Refactored_Grbl(PostProcessor):
"""The Refactored Grbl post processor class."""
def __init__(
self,
job,
tooltip=translate("CAM", "Refactored Grbl post processor"),
tooltipargs=[""],
units="Metric",
) -> None:
super().__init__(
job=job,
tooltip=tooltip,
tooltipargs=tooltipargs,
units=units,
)
Path.Log.debug("Refactored Grbl post processor initialized.")
def init_values(self, values: Values) -> None:
"""Initialize values that are used throughout the postprocessor."""
#
super().init_values(values)
#
# Set any values here that need to override the default values set
# in the parent routine.
#
values["ENABLE_COOLANT"] = True
#
# If this is set to True, then commands that are placed in
# comments that look like (MC_RUN_COMMAND: blah) will be output.
#
values["ENABLE_MACHINE_SPECIFIC_COMMANDS"] = True
#
# Used in the argparser code as the "name" of the postprocessor program.
# This would normally show up in the usage message in the TOOLTIP_ARGS.
#
values["MACHINE_NAME"] = "Grbl"
#
# Default to outputting Path labels at the beginning of each Path.
#
values["OUTPUT_PATH_LABELS"] = True
#
# Default to not outputting M6 tool changes (comment it) as grbl
# currently does not handle it.
#
values["OUTPUT_TOOL_CHANGE"] = False
#
# The order of the parameters.
# Arcs may only work on the XY plane (this needs to be verified).
#
values["PARAMETER_ORDER"] = [
"X",
"Y",
"Z",
"A",
"B",
"C",
"U",
"V",
"W",
"I",
"J",
"K",
"F",
"S",
"T",
"Q",
"R",
"L",
"P",
]
#
# Any commands in this value will be output as the last commands in the G-code file.
#
values[
"POSTAMBLE"
] = """M5
G17 G90
M2"""
values["POSTPROCESSOR_FILE_NAME"] = __name__
#
# Any commands in this value will be output after the header and
# safety block at the beginning of the G-code file.
#
values["PREAMBLE"] = """G17 G90"""
#
# Do not show the current machine units just before the PRE_OPERATION.
#
values["SHOW_MACHINE_UNITS"] = False
#
# Default to not outputting a G43 following tool changes
#
values["USE_TLO"] = False
def init_argument_defaults(self, argument_defaults: Defaults) -> None:
"""Initialize which arguments (in a pair) are shown as the default argument."""
super().init_argument_defaults(argument_defaults)
#
# Modify which argument to show as the default in flag-type arguments here.
# If the value is True, the first argument will be shown as the default.
# If the value is False, the second argument will be shown as the default.
#
# For example, if you want to show Metric mode as the default, use:
# argument_defaults["metric_inch"] = True
#
# If you want to show that "Don't pop up editor for writing output" is
# the default, use:
# argument_defaults["show-editor"] = False.
#
# Note: You also need to modify the corresponding entries in the "values" hash
# to actually make the default value(s) change to match.
#
argument_defaults["tlo"] = False
argument_defaults["tool_change"] = False
def init_arguments_visible(self, arguments_visible: Visible) -> None:
"""Initialize which argument pairs are visible in TOOLTIP_ARGS."""
super().init_arguments_visible(arguments_visible)
#
# Modify the visibility of any arguments from the defaults here.
#
arguments_visible["bcnc"] = True
arguments_visible["axis-modal"] = False
arguments_visible["return-to"] = True
arguments_visible["tlo"] = False
arguments_visible["tool_change"] = True
arguments_visible["translate_drill"] = True
arguments_visible["wait-for-spindle"] = True
@property
def tooltip(self):
tooltip: str = """
This is a postprocessor file for the CAM workbench.
It is used to take a pseudo-gcode fragment from a CAM object
and output 'real' GCode suitable for a Grbl 3 axis mill.
"""
return tooltip

View File

@@ -1,129 +0,0 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# ***************************************************************************
# * Copyright (c) 2014 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2022 - 2025 Larry Woestman <LarryWoestman2@gmail.com> *
# * Copyright (c) 2024 Ondsel <development@ondsel.com> *
# * Copyright (c) 2024 Carl Slater <CandLWorkshopLLC@gmail.com> *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
# * 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. *
# * *
# * FreeCAD 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 Lesser General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with FreeCAD; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
from typing import Any, Dict
from Path.Post.Processor import PostProcessor
import Path
import FreeCAD
translate = FreeCAD.Qt.translate
DEBUG = False
if DEBUG:
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
else:
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
#
# Define some types that are used throughout this file.
#
Values = Dict[str, Any]
class Refactored_Linuxcnc(PostProcessor):
"""The Refactored LinuxCNC post processor class."""
def __init__(
self,
job,
tooltip=translate("CAM", "Refactored LinuxCNC post processor"),
tooltipargs=[""],
units="Metric",
) -> None:
super().__init__(
job=job,
tooltip=tooltip,
tooltipargs=tooltipargs,
units=units,
)
Path.Log.debug("Refactored LinuxCNC post processor initialized.")
def init_values(self, values: Values) -> None:
"""Initialize values that are used throughout the postprocessor."""
#
super().init_values(values)
#
# Set any values here that need to override the default values set
# in the parent routine.
#
values["ENABLE_COOLANT"] = True
#
# The order of parameters.
#
# linuxcnc doesn't want K properties on XY plane; Arcs need work.
#
values["PARAMETER_ORDER"] = [
"X",
"Y",
"Z",
"A",
"B",
"C",
"I",
"J",
"F",
"S",
"T",
"Q",
"R",
"L",
"H",
"D",
"P",
]
#
# Used in the argparser code as the "name" of the postprocessor program.
#
values["MACHINE_NAME"] = "LinuxCNC"
#
# Any commands in this value will be output as the last commands
# in the G-code file.
#
values[
"POSTAMBLE"
] = """M05
G17 G54 G90 G80 G40
M2"""
values["POSTPROCESSOR_FILE_NAME"] = __name__
#
# Any commands in this value will be output after the header and
# safety block at the beginning of the G-code file.
#
values["PREAMBLE"] = """G17 G54 G40 G49 G80 G90"""
@property
def tooltip(self):
tooltip: str = """
This is a postprocessor file for the CAM workbench.
It is used to take a pseudo-gcode fragment from a CAM object
and output 'real' GCode suitable for a linuxcnc 3 axis mill.
"""
return tooltip

View File

@@ -1,150 +0,0 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
# ***************************************************************************
# * Copyright (c) 2014 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2022 - 2025 Larry Woestman <LarryWoestman2@gmail.com> *
# * Copyright (c) 2024 Ondsel <development@ondsel.com> *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
# * 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. *
# * *
# * FreeCAD 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 Lesser General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with FreeCAD; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
from typing import Any, Dict
from Path.Post.Processor import PostProcessor
import Path
import FreeCAD
translate = FreeCAD.Qt.translate
DEBUG = False
if DEBUG:
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
else:
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
#
# Define some types that are used throughout this file.
#
Values = Dict[str, Any]
Visible = Dict[str, bool]
class Refactored_Mach3_Mach4(PostProcessor):
"""The Refactored Mach3_Mach4 post processor class."""
def __init__(
self,
job,
tooltip=translate("CAM", "Refactored Mach3_Mach4 post processor"),
tooltipargs=[""],
units="Metric",
) -> None:
super().__init__(
job=job,
tooltip=tooltip,
tooltipargs=tooltipargs,
units=units,
)
Path.Log.debug("Refactored Mach3_Mach4 post processor initialized.")
def init_values(self, values: Values) -> None:
"""Initialize values that are used throughout the postprocessor."""
#
super().init_values(values)
#
# Set any values here that need to override the default values set
# in the parent routine.
#
values["ENABLE_COOLANT"] = True
#
# Used in the argparser code as the "name" of the postprocessor program.
# This would normally show up in the usage message in the TOOLTIP_ARGS.
#
values["MACHINE_NAME"] = "mach3_4"
#
# Enable special processing for operations with "Adaptive" in the name.
#
values["OUTPUT_ADAPTIVE"] = True
#
# Output the machine name for mach3_mach4 instead of the machine units alone.
#
values["OUTPUT_MACHINE_NAME"] = True
#
# The order of parameters.
#
# mach3_mach4 doesn't want K properties on XY plane; Arcs need work.
#
values["PARAMETER_ORDER"] = [
"X",
"Y",
"Z",
"A",
"B",
"C",
"I",
"J",
"F",
"S",
"T",
"Q",
"R",
"L",
"H",
"D",
"P",
]
#
# Any commands in this value will be output as the last commands
# in the G-code file.
#
values[
"POSTAMBLE"
] = """M05
G17 G54 G90 G80 G40
M2"""
values["POSTPROCESSOR_FILE_NAME"] = __name__
#
# Any commands in this value will be output after the header and
# safety block at the beginning of the G-code file.
#
values["PREAMBLE"] = """G17 G54 G40 G49 G80 G90"""
#
# Output the machine name for mach3_mach4 instead of the machine units alone.
#
values["SHOW_MACHINE_UNITS"] = False
def init_arguments_visible(self, arguments_visible: Visible) -> None:
"""Initialize which argument pairs are visible in TOOLTIP_ARGS."""
super().init_arguments_visible(arguments_visible)
#
# Modify the visibility of any arguments from the defaults here.
#
arguments_visible["axis-modal"] = True
@property
def tooltip(self):
tooltip: str = """
This is a postprocessor file for the CAM workbench.
It is used to take a pseudo-gcode fragment from a CAM object
and output 'real' GCode suitable for a Mach3_4 3 axis mill.
"""
return tooltip

View File

@@ -23,441 +23,265 @@
# * *
# ***************************************************************************
import os
import socket
import sys
from typing import Any, Dict, Optional
import argparse
import datetime
import Path.Post.Utils as PostUtils
import PathScripts.PathUtils as PathUtils
from Path.Post.Processor import PostProcessor
import Path
import FreeCAD
from FreeCAD import Units
import shlex
from builtins import open as pyopen
TOOLTIP = """
This is a postprocessor file for the Path workbench. It is used to
take a pseudo-G-code fragment outputted by a Path object, and output
real G-code suitable for a smoothieboard. This postprocessor, once placed
in the appropriate PathScripts folder, can be used directly from inside
FreeCAD, via the GUI importer or via python scripts with:
translate = FreeCAD.Qt.translate
import smoothie_post
smoothie_post.export(object,"/path/to/file.ncc","")
"""
now = datetime.datetime.now()
parser = argparse.ArgumentParser(prog="linuxcnc", add_help=False)
parser.add_argument("--header", action="store_true", help="output headers (default)")
parser.add_argument("--no-header", action="store_true", help="suppress header output")
parser.add_argument("--comments", action="store_true", help="output comment (default)")
parser.add_argument("--no-comments", action="store_true", help="suppress comment output")
parser.add_argument("--line-numbers", action="store_true", help="prefix with line numbers")
parser.add_argument(
"--no-line-numbers",
action="store_true",
help="don't prefix with line numbers (default)",
)
parser.add_argument(
"--show-editor",
action="store_true",
help="pop up editor before writing output (default)",
)
parser.add_argument(
"--no-show-editor",
action="store_true",
help="don't pop up editor before writing output",
)
parser.add_argument("--precision", default="4", help="number of digits of precision, default=4")
parser.add_argument(
"--preamble",
help='set commands to be issued before the first command, default="G17\\nG90\\n"',
)
parser.add_argument(
"--postamble",
help='set commands to be issued after the last command, default="M05\\nG17 G90\\nM2\\n"',
)
parser.add_argument("--IP_ADDR", help="IP Address for machine target machine")
parser.add_argument(
"--verbose",
action="store_true",
help='verbose output for debugging, default="False"',
)
parser.add_argument(
"--inches", action="store_true", help="Convert output for US imperial mode (G20)"
)
TOOLTIP_ARGS = parser.format_help()
# These globals set common customization preferences
OUTPUT_COMMENTS = True
OUTPUT_HEADER = True
OUTPUT_LINE_NUMBERS = False
IP_ADDR = None
VERBOSE = False
SPINDLE_SPEED = 0.0
if FreeCAD.GuiUp:
SHOW_EDITOR = True
DEBUG = False
if DEBUG:
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
else:
SHOW_EDITOR = False
MODAL = False # if true commands are suppressed if the same as previous line.
COMMAND_SPACE = " "
LINENR = 100 # line number starting value
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
# 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"
UNIT_FORMAT = "mm"
#
# Define some types that are used throughout this file.
#
Values = Dict[str, Any]
MACHINE_NAME = "SmoothieBoard"
CORNER_MIN = {"x": 0, "y": 0, "z": 0}
CORNER_MAX = {"x": 500, "y": 300, "z": 300}
# Preamble text will appear at the beginning of the GCODE output file.
PREAMBLE = """G17 G90
"""
class Smoothie(PostProcessor):
"""
The SmoothieBoard post processor class.
# Postamble text will appear following the last operation.
POSTAMBLE = """M05
This postprocessor outputs G-code suitable for SmoothieBoard controllers.
It supports direct network upload to the SmoothieBoard via TCP/IP.
"""
def __init__(
self,
job,
tooltip=translate("CAM", "Refactored SmoothieBoard post processor"),
tooltipargs=["ip-addr", "verbose"],
units="Metric",
) -> None:
super().__init__(
job=job,
tooltip=tooltip,
tooltipargs=tooltipargs,
units=units,
)
Path.Log.debug("Refactored SmoothieBoard post processor initialized.")
self.ip_addr: Optional[str] = None
self.verbose: bool = False
def init_values(self, values: Values) -> None:
"""Initialize values that are used throughout the postprocessor."""
#
super().init_values(values)
#
# Set any values here that need to override the default values set
# in the parent routine.
#
# The order of parameters.
# SmoothieBoard doesn't want K properties on XY plane (like LinuxCNC).
#
values["PARAMETER_ORDER"] = [
"X",
"Y",
"Z",
"A",
"B",
"I",
"J",
"F",
"S",
"T",
"Q",
"R",
"L",
]
#
# Used in the argparser code as the "name" of the postprocessor program.
#
values["MACHINE_NAME"] = "SmoothieBoard"
#
# Any commands in this value will be output as the last commands
# in the G-code file.
#
values[
"POSTAMBLE"
] = """M05
G17 G90
M2
"""
M2"""
values["POSTPROCESSOR_FILE_NAME"] = __name__
#
# Any commands in this value will be output after the header and
# safety block at the beginning of the G-code file.
#
values["PREAMBLE"] = """G17 G90"""
def init_arguments(self, values, argument_defaults, arguments_visible):
"""Initialize command-line arguments, including SmoothieBoard-specific options."""
parser = super().init_arguments(values, argument_defaults, arguments_visible)
# Pre operation text will be inserted before every operation
PRE_OPERATION = """"""
# Add SmoothieBoard-specific argument group
smoothie_group = parser.add_argument_group("SmoothieBoard-specific arguments")
# Post operation text will be inserted after every operation
POST_OPERATION = """"""
smoothie_group.add_argument(
"--ip-addr", help="IP address for direct upload to SmoothieBoard (e.g., 192.168.1.100)"
)
# Tool Change commands will be inserted before a tool change
TOOL_CHANGE = """"""
smoothie_group.add_argument(
"--verbose",
action="store_true",
help="Enable verbose output for network transfer debugging",
)
# Number of digits after the decimal point
PRECISION = 5
return parser
def process_arguments(self):
"""Process arguments and update values, including SmoothieBoard-specific settings."""
flag, args = super().process_arguments()
def processArguments(argstring):
global OUTPUT_HEADER
global OUTPUT_COMMENTS
global OUTPUT_LINE_NUMBERS
global SHOW_EDITOR
global IP_ADDR
global VERBOSE
global PRECISION
global PREAMBLE
global POSTAMBLE
global UNITS
global UNIT_SPEED_FORMAT
global UNIT_FORMAT
if flag and args:
# Update SmoothieBoard-specific values from parsed arguments
if hasattr(args, "ip_addr") and args.ip_addr:
self.ip_addr = args.ip_addr
Path.Log.info(f"SmoothieBoard IP address set to: {self.ip_addr}")
try:
args = parser.parse_args(shlex.split(argstring))
if hasattr(args, "verbose"):
self.verbose = args.verbose
if self.verbose:
Path.Log.info("Verbose mode enabled")
if args.no_header:
OUTPUT_HEADER = False
if args.header:
OUTPUT_HEADER = True
if args.no_comments:
OUTPUT_COMMENTS = False
if args.comments:
OUTPUT_COMMENTS = True
if args.no_line_numbers:
OUTPUT_LINE_NUMBERS = False
if args.line_numbers:
OUTPUT_LINE_NUMBERS = True
if args.no_show_editor:
SHOW_EDITOR = False
if args.show_editor:
SHOW_EDITOR = True
print("Show editor = %d" % SHOW_EDITOR)
PRECISION = args.precision
if args.preamble is not None:
PREAMBLE = args.preamble.replace("\\n", "\n")
if args.postamble is not None:
POSTAMBLE = args.postamble.replace("\\n", "\n")
if args.inches:
UNITS = "G20"
UNIT_SPEED_FORMAT = "in/min"
UNIT_FORMAT = "in"
return flag, args
IP_ADDR = args.IP_ADDR
VERBOSE = args.verbose
def export(self):
"""Override export to handle network upload to SmoothieBoard."""
# First, do the standard export processing
gcode_sections = super().export()
except Exception:
return False
if gcode_sections is None:
return None
return True
# If IP address is specified, send to SmoothieBoard instead of writing to file
if self.ip_addr:
# Combine all G-code sections
gcode = ""
for section_name, section_gcode in gcode_sections:
if section_gcode:
gcode += section_gcode
# Get the output filename from the job
filename = self._job.PostProcessorOutputFile
if not filename or filename == "-":
filename = "output.nc"
def export(objectslist, filename, argstring):
processArguments(argstring)
global UNITS
for obj in objectslist:
if not hasattr(obj, "Path"):
self._send_to_smoothie(self.ip_addr, gcode, filename)
# Return the gcode for display/editor
return gcode_sections
# Normal file-based export
return gcode_sections
def _send_to_smoothie(self, ip: str, gcode: str, fname: str) -> None:
"""
Send G-code directly to SmoothieBoard via network.
Args:
ip: IP address of the SmoothieBoard
gcode: G-code string to send
fname: Filename to use on the SmoothieBoard SD card
"""
fname = os.path.basename(fname)
FreeCAD.Console.PrintMessage(f"Sending to SmoothieBoard: {fname}\n")
gcode = gcode.rstrip()
filesize = len(gcode)
try:
# Make connection to SmoothieBoard SFTP server (port 115)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(4.0)
s.connect((ip, 115))
tn = s.makefile(mode="rw")
# Read startup prompt
ln = tn.readline()
if not ln.startswith("+"):
FreeCAD.Console.PrintError(f"Failed to connect with SFTP: {ln}\n")
return
if self.verbose:
print("RSP: " + ln.strip())
# Issue initial store command
tn.write(f"STOR OLD /sd/{fname}\n")
tn.flush()
ln = tn.readline()
if not ln.startswith("+"):
FreeCAD.Console.PrintError(f"Failed to create file: {ln}\n")
return
if self.verbose:
print("RSP: " + ln.strip())
# Send size of file
tn.write(f"SIZE {filesize}\n")
tn.flush()
ln = tn.readline()
if not ln.startswith("+"):
FreeCAD.Console.PrintError(f"Failed: {ln}\n")
return
if self.verbose:
print("RSP: " + ln.strip())
# Now send file
cnt = 0
for line in gcode.splitlines(True):
tn.write(line)
if self.verbose:
cnt += len(line)
print("SND: " + line.strip())
print(f"{cnt}/{filesize}\r", end="")
tn.flush()
ln = tn.readline()
if not ln.startswith("+"):
FreeCAD.Console.PrintError(f"Failed to save file: {ln}\n")
return
if self.verbose:
print("RSP: " + ln.strip())
# Exit
tn.write("DONE\n")
tn.flush()
tn.close()
FreeCAD.Console.PrintMessage("Upload complete\n")
except socket.timeout:
FreeCAD.Console.PrintError(f"Connection timeout while connecting to {ip}:115\n")
except ConnectionRefusedError:
FreeCAD.Console.PrintError(
"the object "
+ obj.Name
+ " is not a path. Please select only path and Compounds.\n"
f"Connection refused by {ip}:115. Is the SmoothieBoard running?\n"
)
return
except Exception as e:
FreeCAD.Console.PrintError(f"Error sending to SmoothieBoard: {str(e)}\n")
FreeCAD.Console.PrintMessage("postprocessing...\n")
gcode = ""
@property
def tooltip(self):
tooltip: str = """
This is a postprocessor file for the CAM workbench.
It is used to take a pseudo-gcode fragment from a CAM object
and output 'real' GCode suitable for a SmoothieBoard controller.
# Find the machine.
# The user my have overridden post processor defaults in the GUI. Make
# sure we're using the current values in the Machine Def.
myMachine = None
for pathobj in objectslist:
if hasattr(pathobj, "MachineName"):
myMachine = pathobj.MachineName
if hasattr(pathobj, "MachineUnits"):
if pathobj.MachineUnits == "Metric":
UNITS = "G21"
else:
UNITS = "G20"
if myMachine is None:
FreeCAD.Console.PrintWarning("No machine found in this selection\n")
# write header
if OUTPUT_HEADER:
gcode += linenumber() + "(Exported by FreeCAD)\n"
gcode += linenumber() + "(Post Processor: " + __name__ + ")\n"
gcode += linenumber() + "(Output Time:" + str(now) + ")\n"
# Write the preamble
if OUTPUT_COMMENTS:
gcode += linenumber() + "(begin preamble)\n"
for line in PREAMBLE.splitlines():
gcode += linenumber() + line + "\n"
gcode += linenumber() + UNITS + "\n"
for obj in objectslist:
# do the pre_op
if OUTPUT_COMMENTS:
gcode += linenumber() + "(begin operation: " + obj.Label + ")\n"
for line in PRE_OPERATION.splitlines(True):
gcode += linenumber() + line
gcode += parse(obj)
# do the post_op
if OUTPUT_COMMENTS:
gcode += linenumber() + "(finish operation: " + obj.Label + ")\n"
for line in POST_OPERATION.splitlines(True):
gcode += linenumber() + line
# do the post_amble
if OUTPUT_COMMENTS:
gcode += "(begin postamble)\n"
for line in POSTAMBLE.splitlines():
gcode += linenumber() + line + "\n"
if SHOW_EDITOR:
dia = PostUtils.GCodeEditorDialog()
dia.editor.setText(gcode)
result = dia.exec_()
if result:
final = dia.editor.toPlainText()
else:
final = gcode
else:
final = gcode
if IP_ADDR is not None:
sendToSmoothie(IP_ADDR, final, filename)
else:
if not filename == "-":
gfile = pyopen(filename, "w")
gfile.write(final)
gfile.close()
FreeCAD.Console.PrintMessage("done postprocessing.\n")
return final
def sendToSmoothie(ip, GCODE, fname):
import sys
import socket
import os
fname = os.path.basename(fname)
FreeCAD.Console.PrintMessage("sending to smoothie: {}\n".format(fname))
f = GCODE.rstrip()
filesize = len(f)
# make connection to sftp server
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(4.0)
s.connect((ip, 115))
tn = s.makefile(mode="rw")
# read startup prompt
ln = tn.readline()
if not ln.startswith("+"):
FreeCAD.Console.PrintMessage("Failed to connect with sftp: {}\n".format(ln))
sys.exit()
if VERBOSE:
print("RSP: " + ln.strip())
# Issue initial store command
tn.write("STOR OLD /sd/" + fname + "\n")
tn.flush()
ln = tn.readline()
if not ln.startswith("+"):
FreeCAD.Console.PrintError("Failed to create file: {}\n".format(ln))
sys.exit()
if VERBOSE:
print("RSP: " + ln.strip())
# send size of file
tn.write("SIZE " + str(filesize) + "\n")
tn.flush()
ln = tn.readline()
if not ln.startswith("+"):
FreeCAD.Console.PrintError("Failed: {}\n".format(ln))
sys.exit()
if VERBOSE:
print("RSP: " + ln.strip())
cnt = 0
# now send file
for line in f.splitlines(1):
tn.write(line)
if VERBOSE:
cnt += len(line)
print("SND: " + line.strip())
print(str(cnt) + "/" + str(filesize) + "\r", end="")
tn.flush()
ln = tn.readline()
if not ln.startswith("+"):
FreeCAD.Console.PrintError("Failed to save file: {}\n".format(ln))
sys.exit()
if VERBOSE:
print("RSP: " + ln.strip())
# exit
tn.write("DONE\n")
tn.flush()
tn.close()
FreeCAD.Console.PrintMessage("Upload complete\n")
def linenumber():
global LINENR
if OUTPUT_LINE_NUMBERS is True:
LINENR += 10
return "N" + str(LINENR) + " "
return ""
def parse(pathobj):
global SPINDLE_SPEED
out = ""
lastcommand = None
precision_string = "." + str(PRECISION) + "f"
# params = ['X','Y','Z','A','B','I','J','K','F','S'] #This list control
# the order of parameters
# linuxcnc doesn't want K properties on XY plane Arcs need work.
params = ["X", "Y", "Z", "A", "B", "I", "J", "F", "S", "T", "Q", "R", "L"]
if hasattr(pathobj, "Group"): # We have a compound or project.
# if OUTPUT_COMMENTS:
# out += linenumber() + "(compound: " + pathobj.Label + ")\n"
for p in pathobj.Group:
out += parse(p)
return out
else: # parsing simple path
# groups might contain non-path things like stock.
if not hasattr(pathobj, "Path"):
return out
# if OUTPUT_COMMENTS:
# out += linenumber() + "(" + pathobj.Label + ")\n"
for c in PathUtils.getPathWithPlacement(pathobj).Commands:
outstring = []
command = c.Name
outstring.append(command)
# if modal: only print the command if it is not the same as the
# last one
if MODAL is True:
if command == lastcommand:
outstring.pop(0)
# Now add the remaining parameters in order
for param in params:
if param in c.Parameters:
if param == "F":
if c.Name not in [
"G0",
"G00",
]: # linuxcnc doesn't use rapid speeds
speed = Units.Quantity(c.Parameters["F"], FreeCAD.Units.Velocity)
outstring.append(
param
+ format(
float(speed.getValueAs(UNIT_SPEED_FORMAT)),
precision_string,
)
)
elif param == "T":
outstring.append(param + str(c.Parameters["T"]))
elif param == "S":
outstring.append(param + str(c.Parameters["S"]))
SPINDLE_SPEED = c.Parameters["S"]
else:
pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length)
outstring.append(
param + format(float(pos.getValueAs(UNIT_FORMAT)), precision_string)
)
if command in ["G1", "G01", "G2", "G02", "G3", "G03"]:
outstring.append("S" + str(SPINDLE_SPEED))
# store the latest command
lastcommand = command
# Check for Tool Change:
if command == "M6":
# if OUTPUT_COMMENTS:
# out += linenumber() + "(begin toolchange)\n"
for line in TOOL_CHANGE.splitlines(True):
out += linenumber() + line
if command == "message":
if OUTPUT_COMMENTS is False:
out = []
else:
outstring.pop(0) # remove the command
# prepend a line number and append a newline
if len(outstring) >= 1:
if OUTPUT_LINE_NUMBERS:
outstring.insert(0, (linenumber()))
# append the line to the final output
for w in outstring:
out += w + COMMAND_SPACE
out = out.strip() + "\n"
return out
# print(__name__ + " gcode postprocessor loaded.")
This postprocessor supports direct network upload to SmoothieBoard
via the --ip-addr argument.
"""
return tooltip

View File

@@ -48,13 +48,13 @@ Values = Dict[str, Any]
Visible = Dict[str, bool]
class Refactored_Test(PostProcessor):
"""The Refactored Test post processor class."""
class Test(PostProcessor):
"""The Test post processor class."""
def __init__(
self,
job,
tooltip=translate("CAM", "Refactored Test post processor"),
tooltip=translate("CAM", "Test post processor"),
tooltipargs=[""],
units="Metric",
) -> None:
@@ -64,7 +64,7 @@ class Refactored_Test(PostProcessor):
tooltipargs=tooltipargs,
units=units,
)
Path.Log.debug("Refactored Test post processor initialized")
Path.Log.debug("Test post processor initialized")
def init_values(self, values: Values) -> None:
"""Initialize values that are used throughout the postprocessor."""

View File

@@ -97,17 +97,20 @@ from CAMTests.TestPathUtil import TestPathUtil
from CAMTests.TestPathVcarve import TestPathVcarve
from CAMTests.TestPathVoronoi import TestPathVoronoi
from CAMTests.TestCentroidPost import TestCentroidPost
from CAMTests.TestGrblPost import TestGrblPost
from CAMTests.TestGenericPost import TestGenericPost
from CAMTests.TestLinuxCNCPost import TestLinuxCNCPost
from CAMTests.TestGrblPost import TestGrblPost
from CAMTests.TestMassoG3Post import TestMassoG3Post
from CAMTests.TestCentroidPost import TestCentroidPost
from CAMTests.TestMach3Mach4Post import TestMach3Mach4Post
from CAMTests.TestRefactoredCentroidPost import TestRefactoredCentroidPost
from CAMTests.TestRefactoredGrblPost import TestRefactoredGrblPost
from CAMTests.TestRefactoredLinuxCNCPost import TestRefactoredLinuxCNCPost
from CAMTests.TestRefactoredMassoG3Post import TestRefactoredMassoG3Post
from CAMTests.TestRefactoredMach3Mach4Post import TestRefactoredMach3Mach4Post
from CAMTests.TestRefactoredTestDressupPost import TestRefactoredTestDressupPost
from CAMTests.TestRefactoredTestPost import TestRefactoredTestPost
from CAMTests.TestRefactoredTestPostGCodes import TestRefactoredTestPostGCodes
from CAMTests.TestRefactoredTestPostMCodes import TestRefactoredTestPostMCodes
from CAMTests.TestTestPost import TestTestPost
from CAMTests.TestPostGCodes import TestPostGCodes
from CAMTests.TestPostMCodes import TestPostMCodes
from CAMTests.TestDressupPost import TestDressupPost
from CAMTests.TestLinuxCNCLegacyPost import TestLinuxCNCLegacyPost
from CAMTests.TestGrblLegacyPost import TestGrblLegacyPost
from CAMTests.TestCentroidLegacyPost import TestCentroidLegacyPost
from CAMTests.TestMach3Mach4LegacyPost import TestMach3Mach4LegacyPost
from CAMTests.TestSnapmakerPost import TestSnapmakerPost