Addition of CAM postprocessor for the Masso controller (#18845)

* Mods to accommodate tool order in Gcode. Some controllers want T# M6 others want M6 T#. Masso wants T# M6.

Added file refactored_masso_g3_post.py (based on refactored_linuxcnc_post.py) and modified UtilsParse.py

* Added tests and additional comments

* Working on the testing framework

* Tried to make refactored linux and masso as compatable line by line to support fil diffing

* A space in the file name caused all sorts of grief ... Thanks Larry

* added files to , swapped tool order in testrefactored_masso

* Added note regarding how files are "clones" of one another

* Added space in comment line for consistent formatting.

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

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

* Updated formatting and comments

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
CandL
2025-01-27 11:41:18 -05:00
committed by GitHub
parent 44a1e8de4d
commit 65daa70c6b
7 changed files with 559 additions and 2 deletions

View File

@@ -21,6 +21,14 @@
# * *
# ***************************************************************************
# ***************************************************************************
# * Note: TestRefactoredMassoG3Post.py is a modified clone of this file *
# * any changes to this file should be applied to the other *
# * *
# * *
# ***************************************************************************
from importlib import reload
import FreeCAD

View File

@@ -0,0 +1,294 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2022 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2022 Larry Woestman <LarryWoestman2@gmail.com> *
# * Copyright (c) 2024 Carl Slater <CandLWorkshopllc@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 *
# * *
# ***************************************************************************
# ***************************************************************************
# * Note: This file is a modified clone of TestRefactoredLinuxCNCPost.py *
# * any changes to this file should be applied to the other *
# * *
# * *
# ***************************************************************************
from importlib import reload
import FreeCAD
import Path
import CAMTests.PathTestUtils as PathTestUtils
from Path.Post.scripts import refactored_masso_g3_post as postprocessor
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
Path.Log.trackModule(Path.Log.thisModule())
class TestRefactoredMassoG3Post(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()) == 14)
# 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
"""
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' --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)
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()[6], "M5")
self.assertEqual(gcode.splitlines()[7], "T2 M6")
self.assertEqual(gcode.splitlines()[8], "G43 H2")
self.assertEqual(gcode.splitlines()[9], "M3 S3000")
# suppress TLO
args = "--no-header --no-tlo --no-show-editor"
gcode = postprocessor.export(postables, "-", args)
self.assertEqual(gcode.splitlines()[8], "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

@@ -165,6 +165,7 @@ SET(PathPythonPostScripts_SRCS
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/rml_post.py
Path/Post/scripts/rrf_post.py
@@ -348,6 +349,7 @@ SET(Tests_SRCS
CAMTests/TestRefactoredGrblPost.py
CAMTests/TestRefactoredLinuxCNCPost.py
CAMTests/TestRefactoredMach3Mach4Post.py
CAMTests/TestRefactoredMassoG3Post.py
CAMTests/TestRefactoredTestPost.py
CAMTests/TestRefactoredTestPostGCodes.py
CAMTests/TestRefactoredTestPostMCodes.py

View File

@@ -6,6 +6,7 @@
# * Copyright (c) 2018, 2019 Gauthier Briere *
# * Copyright (c) 2019, 2020 Schildkroet *
# * Copyright (c) 2022 Larry Woestman <LarryWoestman2@gmail.com> *
# * Copyright (c) 2024 Carl Slater <CandLWorkshopLLC@gmail.com> *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
@@ -656,6 +657,11 @@ def parse_a_path(values: Values, gcode: Gcode, pathobj) -> None:
parameter: str
parameter_value: str
# Check to see if values["TOOL_BEFORE_CHANGE"] is set and value is true
# doing it here to reduce the number of times it is checked
swap_tool_change_order = False
if "TOOL_BEFORE_CHANGE" in values and values["TOOL_BEFORE_CHANGE"]:
swap_tool_change_order = True
current_location.update(Path.Command("G0", {"X": -1, "Y": -1, "Z": -1, "F": 0.0}).Parameters)
adaptive_op_variables = determine_adaptive_op(values, pathobj)
@@ -720,9 +726,21 @@ def parse_a_path(values: Values, gcode: Gcode, pathobj) -> None:
command_line = []
if check_for_suppressed_commands(values, gcode, command, command_line):
command_line = []
# Add a line number to the front and a newline to the end of the command line
if command_line:
gcode += f"{linenumber(values)}{format_command_line(values, command_line)}{nl}"
if command in ("M6", "M06") and swap_tool_change_order:
swapped_command_line = [
command_line[1],
command_line[0],
] # swap the order of the commands
# Add a line number to the front and a newline to the end of the command line
gcode += (
f"{linenumber(values)}{format_command_line(values, swapped_command_line)}{nl}"
)
else:
# Add a line number to the front and a newline to the end of the command line
gcode += f"{linenumber(values)}{format_command_line(values, command_line)}{nl}"
check_for_tlo(values, gcode, command, c.Parameters)
check_for_machine_specific_commands(values, gcode, command)

View File

@@ -1,6 +1,7 @@
# ***************************************************************************
# * Copyright (c) 2014 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2022 Larry Woestman <LarryWoestman2@gmail.com> *
# * Copyright (c) 2024 Carl Slater <CandLWorkshopLLC@gmail.com> *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
@@ -22,6 +23,11 @@
# * *
# ***************************************************************************
# ***************************************************************************
# * Note: refactored_masso_g3_Post.py is a modified clone of this file *
# * any changes to this file should be applied to the other *
# * *
# ***************************************************************************
import argparse

View File

@@ -0,0 +1,227 @@
# ***************************************************************************
# * Copyright (c) 2014 sliptonic <shopinthewoods@gmail.com> *
# * Copyright (c) 2022 Larry Woestman <LarryWoestman2@gmail.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 *
# * *
# ***************************************************************************
# ***************************************************************************
# * Note: this is copy & mod of refactored_linuxcnc_post.py *
# * *
# * *
# ***************************************************************************
import argparse
from typing import Any, Dict, Union
import Path.Post.UtilsArguments as PostUtilsArguments
import Path.Post.UtilsExport as PostUtilsExport
# Define some types that are used throughout this file
Parser = argparse.ArgumentParser
Values = Dict[str, Any]
#
# The following variables need to be global variables
# to keep the PathPostProcessor.load method happy:
#
# TOOLTIP
# TOOLTIP_ARGS
# UNITS
#
# The "argument_defaults", "arguments_visible", and the "values" hashes
# need to be defined before the "init_shared_arguments" routine can be
# called to create TOOLTIP_ARGS, so they also end up having to be globals.
#
TOOLTIP: str = """This is a postprocessor file for the Path workbench. It is used to
take a pseudo-gcode fragment outputted by a Path object, and output
real GCode suitable for a Masso G3 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 refactored_masso_g3_post
refactored_masso_g3_post.export(object,"/path/to/file.ncc","")
"""
#
# Default to metric mode
#
UNITS: str = "G21"
def init_values(values: Values) -> None:
"""Initialize values that are used throughout the postprocessor."""
#
PostUtilsArguments.init_shared_values(values)
#
# Set any values here that need to override the default values set
# in the init_shared_values routine.
#
values["ENABLE_COOLANT"] = True
# the order of parameters
# Masso G3 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.
# This would normally show up in the usage message in the TOOLTIP_ARGS,
# but we are suppressing the usage message, so it doesn't show up after all.
#
values["MACHINE_NAME"] = "Masso G3"
#
# 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"""
#
# setting TOOL_BEFORE_CHANGE to True will output T# M6 before each tool change
# rather than M6 T#.
#
values["TOOL_BEFORE_CHANGE"] = type(True)
values["UNITS"] = UNITS
def init_argument_defaults(argument_defaults: Dict[str, bool]) -> None:
"""Initialize which arguments (in a pair) are shown as the default argument."""
PostUtilsArguments.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.
#
def init_arguments_visible(arguments_visible: Dict[str, bool]) -> None:
"""Initialize which argument pairs are visible in TOOLTIP_ARGS."""
PostUtilsArguments.init_arguments_visible(arguments_visible)
#
# Modify the visibility of any arguments from the defaults here.
#
def init_arguments(
values: Values,
argument_defaults: Dict[str, bool],
arguments_visible: Dict[str, bool],
) -> Parser:
"""Initialize the shared argument definitions."""
parser: Parser = PostUtilsArguments.init_shared_arguments(
values, argument_defaults, arguments_visible
)
#
# Add any argument definitions that are not shared with all other
# postprocessors here.
#
return parser
#
# Creating global variables and using functions to modify them
# is useful for being able to test things later.
#
global_values: Values = {}
init_values(global_values)
global_argument_defaults: Dict[str, bool] = {}
init_argument_defaults(global_argument_defaults)
global_arguments_visible: Dict[str, bool] = {}
init_arguments_visible(global_arguments_visible)
global_parser: Parser = init_arguments(
global_values, global_argument_defaults, global_arguments_visible
)
#
# The TOOLTIP_ARGS value is created from the help information about the arguments.
#
TOOLTIP_ARGS: str = global_parser.format_help()
#
# Create another parser just to get a list of all possible arguments
# that may be output using --output_all_arguments.
#
global_all_arguments_visible: Dict[str, bool] = {}
for k in iter(global_arguments_visible):
global_all_arguments_visible[k] = True
global_all_visible: Parser = init_arguments(
global_values, global_argument_defaults, global_all_arguments_visible
)
def export(objectslist, filename: str, argstring: str) -> str:
"""Postprocess the objects in objectslist to filename."""
args: Union[str, argparse.Namespace]
flag: bool
global UNITS # pylint: disable=global-statement
# print(parser.format_help())
(flag, args) = PostUtilsArguments.process_shared_arguments(
global_values, global_parser, argstring, global_all_visible, filename
)
if not flag:
return args # type: ignore
#
# Process any additional arguments here
#
#
# Update the global variables that might have been modified
# while processing the arguments.
#
UNITS = global_values["UNITS"]
return PostUtilsExport.export_common(global_values, objectslist, filename)

View File

@@ -75,6 +75,7 @@ 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.TestRefactoredTestPost import TestRefactoredTestPost
from CAMTests.TestRefactoredTestPostGCodes import TestRefactoredTestPostGCodes
@@ -130,6 +131,7 @@ False if TestMach3Mach4Post.__name__ else True
False if TestRefactoredCentroidPost.__name__ else True
False if TestRefactoredGrblPost.__name__ else True
False if TestRefactoredLinuxCNCPost.__name__ else True
False if TestRefactoredMassoG3Post.__name__ else True
False if TestRefactoredMach3Mach4Post.__name__ else True
False if TestRefactoredTestPost.__name__ else True
False if TestRefactoredTestPostGCodes.__name__ else True