From 232969348ae25301f7828a44721267ae3c6bb05e Mon Sep 17 00:00:00 2001 From: andre Date: Wed, 24 Mar 2021 17:12:55 +0100 Subject: [PATCH] Added UC-CNC postprocessor script. --- src/Mod/Path/PathScripts/post/uccnc_post.py | 671 ++++++++++++++++++++ 1 file changed, 671 insertions(+) create mode 100644 src/Mod/Path/PathScripts/post/uccnc_post.py diff --git a/src/Mod/Path/PathScripts/post/uccnc_post.py b/src/Mod/Path/PathScripts/post/uccnc_post.py new file mode 100644 index 0000000000..52df77ac17 --- /dev/null +++ b/src/Mod/Path/PathScripts/post/uccnc_post.py @@ -0,0 +1,671 @@ +# -*- coding: utf-8 -*- + +#*************************************************************************** +#* * +#* based upon linuxcnc_post.py (c) sliptonic (shopinthewoods@gmail.com) * +#* * +#* changed, but not enough to claim copyrights 2019-2021 * +#* maintainer: A.H.M. Steenveld * +#* * +#* 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 * +#* * +#*************************************************************************** + +# See: https://wiki.freecadweb.org/Path_Post +# https://wiki.freecadweb.org/Path_Postprocessor_Customization +# for details on post processors like this one. + +from __future__ import print_function +import FreeCAD +from FreeCAD import Units +import Path +import argparse +import datetime +#import shlex +from PathScripts import PostUtils +from PathScripts import PathUtils + +POST_PROCESSOR_NAME = 'UCCNC' +VERSION="0.0.3" + +TOOLTIP=''' Post processor for UC-CNC. + +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. 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 UCCNC_post +UCCNC_post.export(object,"/path/to/file.ncc","") + +This postprocessor was tested on UC-CNC v1.2111, an UC100 and a Stepcraft 420. +It was tested on FreeCAD v0.17, v0.18 and v0.19 + +Other (Stepcraft) machines using UC-CNC and UC* controlers should be easy to adapt. +''' + +# PREAMBLE_ possible values: +# Multi line text with gcode. Preamble gcode +# The preamble text will appear at the beginning of the GCODE output file. +PREAMBLE_DEFAULT = '''G17 (Default: XY-plane) +G54 (Default: First coordinate system) +G40 (Default: Cutter radius compensation none) +G49 (Default: Tool Length Offsets: cancel tool length) +G90 (Default: Absolute distance mode selection) +G80 (Cancel canned cycle) +''' + +PREAMBLE_DEFAULT_NO_COMMENT = '''G17 +G54 +G40 +G49 +G90 +G80 +''' + + +# POSTAMBLE possible values: +# Multi line text with gcode. Postable gcode +# The postamble text will appear following the last operation. +POSTAMBLE_DEFAULT = '''M05 (stop spindel) +G17 (Default: XY-plane) +G54 (Default: First coordinate system) +G40 (Default: Cutter radius compensation none) +G90 (Default: Absolute distance mode selection) +G80 (Cancel canned cycle) +M30 (Stop program and rewind code) +''' + +POSTAMBLE_DEFAULT_NO_COMMENT = '''M05 +G17 +G54 +G40 +G90 +G80 +M30 +''' + +# PRE_OPERATION: Pre operation text will be inserted before every operation +PRE_OPERATION = '''''' + +# POST_OPERATION: Post operation text will be inserted after every operation +POST_OPERATION = '''''' + +# TOOL_CHANGE: Tool Change commands will be inserted before a tool change +TOOL_CHANGE = '''''' + +###################################################### +### Other configuration settings ### +###################################################### + +# MACHINE_NAME possible options +# "" string with the target machine name. +MACHINE_NAME = "Stepcraft-420" + +# SHOW_EDITOR possible values: +# bool Show gcode before saving. +# True before the file is written it is shown to the user for inspection +# False the file is written directly +# set with --no-show-editor +if FreeCAD.GuiUp: + SHOW_EDITOR = True +else: + SHOW_EDITOR = False + +# PROG_NAME possible values: +# text Name of the G-Code program +# set with --name +PROG_NAME = "prog1" + +# OUTPUT_HEADER possible values: +# bool Use of a document header +# True Use a predefined header. +# False Do not use a predefined header. +# set with --no-header +OUTPUT_HEADER = True + +# OUTPUT_COMMENTS possible values: +# bool (Dont) use comments in output +# True Use comments in output. +# False Suppress comments in output. +# set with --no-comment +OUTPUT_COMMENTS = True + +# OUTPUT_LINE_NUMBERS posible values: +# bool (Dont) use line numbers in output +# True Add a line number to each output line. +# False Do not add a line number. +# set with --line-numbers +# note: line numbers Nxxxx are not supported by UC-CNC and are silently ignored. +OUTPUT_LINE_NUMBERS = False + +# LINE_NUMBER_START possible values: +# int unsigned integer. Line number starting value +# 0..N +LINE_NUMBER_START = 0 + +# LINE_NUMBER_STEP possible values: +# int unsigned integer. Line number increment value +# 1..N +LINE_NUMBER_STEP = 1 + +# PREAMBLE possible values: +# Multi line text with gcode. Preamble gcode +# The preamble text will appear at the beginning of the GCODE output file. +# set with --preamble +PREAMBLE = PREAMBLE_DEFAULT + +# POSTAMBLE possible values: +# Multi line text with gcode. Postable gcode +# The postamble text will appear following the last operation. +# set with --postable +POSTAMBLE = POSTAMBLE_DEFAULT + +# MODAL possible values: +# bool Repeat/suppress repeated command arguments. +# True commands are suppressed if the same as previous line. +# False commands are repeated ith the same as previout line. +# set with --modal +MODAL = False + +# REPEAT_ARGUMENTS possible values: +# bool Duplicate/suppressed axis values from the previous line. +# True All arguments are repreated in each command. +# False Equal values for arguments from the previous command are not repeated. +# set with --repeat +REPEAT_ARGUMENTS = False + +# USE_TLO posible values: +# bool Set tool length offset. +# True G43 will be output following tool changes +# False No G43 used. +# set with --tool-length-offset +USE_TLO = False + +# PRECISION possible values: +# int Number of digits in axis positions +# 0...N +# set with --precision N +PRECISION=3 + +# UNITS possible values: +# GCODE Code to switch to specific units +# G20 US imperial [inch] +# G21 Metric [mm] +# set with --inches +# note: G20/G21 are not supported by UC-CNC, units are configured in a program profile. +# In code G20/G21 commands are silently ignored by UC-CNC +# UNITS is included in the post processor to mirror the profile settings. +UNITS_US_IMP = 'G20' +UNITS_METRIC = 'G21' +UNITS = UNITS_METRIC + +# UNIT_FORMAT possible values: (see UNITS) +# text Text with specific units +# "inch" US imperial [inch] +# "mm" Metric [mm] +# note: G20/G21 are not supported by UC-CNC, units are configured in a program profile. +# In code G20/G21 commands are silently ignored by UC-CNC +# UNITS is included in the post processor to mirror the profile settings. +UNIT_FORMAT_US_IMP = 'in' +UNIT_FORMAT_METRIC = 'mm' +UNIT_FORMAT = UNIT_FORMAT_METRIC + +# UNIT_SPEED_FORMAT possible values: (see UNITS) +# text Text with specific units over time units +# "inch/min" US imperial [inch] +# "mm/min" Metric [mm] +# note: G20/G21 are not supported by UC-CNC, units are configured in a program profile. +# In code G20/G21 commands are silently ignored by UC-CNC +# UNITS is included in the post processor to mirror the profile settings. +UNIT_SPEED_FORMAT_US_IMP = 'in/min' +UNIT_SPEED_FORMAT_METRIC = 'mm/min' +UNIT_SPEED_FORMAT = UNIT_SPEED_FORMAT_METRIC + +###################################################### +### No more configuration settings after this line ### +###################################################### + +# see: https://docs.python.org/3/library/argparse.html +parser = argparse.ArgumentParser(prog=POST_PROCESSOR_NAME, add_help=False) +parser.add_argument('--name', help='GCode program name') +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='suppress 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\nG90\nG54"') +parser.add_argument('--postamble', help='set commands to be issued after the last command, default="M05\nM30"') +parser.add_argument('--inches', action='store_true', help='lengths in [in], G20') +parser.add_argument('--metric', action='store_true', help='lengths in [mm], G21') +parser.add_argument('--modal', action='store_true', help='repeat/suppress repeated command arguments') +parser.add_argument('--tool-length-offset', action='store_true', help='suppress tool length offset G43 following tool changes') +parser.add_argument('--repeat', action='store_true', help='repeat axis arguments') + +# to distinguish python built-in open function from the one declared below +if open.__module__ in ['__builtin__', 'io']: + pythonopen = open + +# to distinguish python built-in open function from the one declared below +if open.__module__ == '__builtin__': + pythonopen = open + +# debug option, trace to screen while processing to see where things break up. +trace_gcode = False + +now = datetime.datetime.now() + +TOOLTIP_ARGS = parser.format_help() +LINENR=0 +COMMAND_SPACE = " " +UNIT_DEFAULT_CHANGED = False + +# counting warnings and problems. +# Each warning/problem will apear as a WARNING:/PROBLEM: comment in the GCode output. +warnings_count = 0 +problems_count = 0 + + +def processArguments(argstring): + global MACHINE_NAME # The target machine name + global SHOW_EDITOR # Show gcode before saving. + global PROG_NAME # Name of the G-Code program + global OUTPUT_HEADER # Use of a document header + global OUTPUT_COMMENTS # (Dont) use comments in output + global OUTPUT_LINE_NUMBERS # (Dont) use line numbers in output + global LINE_NUMBER_START # Line number starting value + global LINE_NUMBER_STEP # Line number increment value + global PREAMBLE # Preamble gcode + global POSTAMBLE # Postable gcode + global PRE_OPERATION # Pre operation text will be inserted before every operation + global POST_OPERATION # Post operation text will be inserted after every operation + global MODAL # Repeat/suppress repeated command arguments. + global USE_TLO # Set tool length offset + global PRECISION # Number of digits in feed and axis values + global UNITS # Code to switch to specific units + global UNITS_US_IMP # + global UNITS_METRIC # + global UNIT_FORMAT # Text with specific units + global UNIT_FORMAT_US_IMP # + global UNIT_FORMAT_METRIC # + global UNIT_SPEED_FORMAT # Text with specific units over time units + global UNIT_SPEED_FORMAT_US_IMP # + global UNIT_SPEED_FORMAT_METRIC # + global UNIT_DEFAULT_CHANGED # tracing changes in UNIT settings. + global REPEAT_ARGUMENTS # Repeat or surpress axis values if the same as previous line. + + try: + UNIT_DEFAULT_CHANGED = False + args = parser.parse_args(argstring.split()) + + if args.name is not None: + PROG_NAME = args.name + + 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 + + PRECISION = args.precision + + if args.preamble is not None: + PREAMBLE = args.preamble + elif OUTPUT_COMMENTS: + PREAMBLE = PREAMBLE_DEFAULT + else: + PREAMBLE = PREAMBLE_DEFAULT_NO_COMMENT + + if args.postamble is not None: + POSTAMBLE = args.postamble + elif OUTPUT_COMMENTS: + POSTAMBLE = POSTAMBLE_DEFAULT + else: + POSTAMBLE = POSTAMBLE_DEFAULT_NO_COMMENT + + if args.inches and (UNITS != UNITS_US_IMP): + print("Units: US Imperial [inch], check your UC-CNC profile.") + UNITS = UNITS_US_IMP + UNIT_FORMAT = UNIT_FORMAT_US_IMP + UNIT_SPEED_FORMAT = UNIT_SPEED_FORMAT_US_IMP + UNIT_DEFAULT_CHANGED = True + + if args.metric and (UNITS != UNITS_METRIC): + print("Units: Metric [mm], check your UC-CNC profile.") + UNITS = UNITS_METRIC + UNIT_FORMAT = UNIT_FORMAT_METRIC + UNIT_SPEED_FORMAT = UNIT_SPEED_FORMAT_METRIC + UNIT_DEFAULT_CHANGED = True + + if args.modal: + MODAL = True + + if args.tool_length_offset: + USE_TLO = True + + if args.repeat: + REPEAT_ARGUMENTS = True + + except Exception: + return False + + return True + +def append0(line): + result = line + if (trace_gcode): + print("export: >>" + result) + return result + +def append(line): + result = linenumber() + line + if (trace_gcode): + print("export: >>" + result) + return result + +def export(objectslist, filename, argstring): + + if not processArguments(argstring): + print("export: process arguments failed, '{}'".format(argstring)) + return None + + global POST_PROCESSOR_NAME + global UNITS + global UNIT_FORMAT + global UNIT_SPEED_FORMAT + global warnings_count + global problems_count + + warnings_count = 0 + problems_count = 0 + + 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("export: postprocessing...") + gcode = append0("%" + PROG_NAME + "\n") + if not argstring: + gcode += append("(" + POST_PROCESSOR_NAME + " with default settings)\n") + else: + gcode += append("({} {})\n".format(POST_PROCESSOR_NAME, argstring)) + + # write header + if OUTPUT_HEADER: + HEADER = '''(Exported by FreeCAD for {}) + (Post Processor: {}, version {}) + (CAM file: {}) + (Output Time: {}) + ''' + for line in HEADER.format(MACHINE_NAME, __name__, VERSION, FreeCAD.ActiveDocument.FileName, str(now)).splitlines(True): + gcode += append(line) + + # Write the preamble + # G20/G21 not supported by UC-CNC, *always* report the configured units and deviations from the default settings for safety. + gcode += append("(Units: '" + UNIT_FORMAT + "' and '" + UNIT_SPEED_FORMAT + "')\n") + if (UNIT_DEFAULT_CHANGED): + gcode += append("(WARNING: Units default changed, check your UC-CNC profile)\n") + warnings_count += 1 + + if OUTPUT_COMMENTS: + gcode += append("(preamble: begin)\n") + #for obj in objectslist: + # if isinstance(obj.Proxy, PathScripts.PathToolController.ToolController): + # gcode += append("(T{}={})\n".format(obj.ToolNumber, item.Name)) + # error: global name 'PathScripts' is not defined + for line in PREAMBLE.splitlines(False): + gcode += append(line + "\n") + #if OUTPUT_COMMENTS: + # gcode += append(UNITS + " (units in '" + UNIT_FORMAT + "' and '" + UNIT_SPEED_FORMAT + "')\n") + #else: + # gcode += append(UNITS + "\n") + if OUTPUT_COMMENTS: + gcode += append("(preamble: done)\n") + + # write the code body + for obj in objectslist: + + # fetch machine details + job = PathUtils.findParentJob(obj) + + myMachine = MACHINE_NAME + if hasattr(job, "MachineName"): + myMachine = job.MachineName + + if hasattr(job, "MachineUnits"): + #if job.MachineUnits == "Metric": + # UNITS = UNITS_METRIC + # UNIT_FORMAT = UNIT_FORMAT_METRIC + # UNIT_SPEED_FORMAT = UNIT_SPEED_FORMAT_METRIC + #else: + # UNITS = UNITS_US_IMP + # UNIT_FORMAT = UNIT_FORMAT_US_IMP + # UNIT_SPEED_FORMAT = UNIT_SPEED_FORMAT_US_IMP + if (job.MachineUnits == "Metric") and (UNITS != UNITS_METRIC): + print("export: MachineUnits: requested Metic [mm], using US Imperial [inch], G20/G21 not supported by UC-CNC, skipped.") + gcode += append("(" + UNITS_US_IMP + " PROBLEM: code not supported by UC-CNC, default: units in [" + UNIT_FORMAT + "])\n") + problems_count += 1 + if (job.MachineUnits != "Metric") and (UNITS == UNITS_METRIC): + print("export: MachineUnits: requested US Imperial [inch], using Metic [mm], G20/G21 not supported by UC-CNC, skipped.") + gcode += append("(" + UNITS_METRIC + " PROBLEM: code not supported by UC-CNC, default: units in[" + UNIT_FORMAT + "])\n") + problems_count += 1 + + # pre_op + if OUTPUT_COMMENTS: + gcode += append("(operation initialise: %s)\n" % obj.Label) + for line in PRE_OPERATION.splitlines(True): + gcode += append(line) + + # turn coolant on if required + if hasattr(obj, "CoolantMode"): + coolantMode = obj.CoolantMode + if coolantMode == 'Mist': + if OUTPUT_COMMENTS: + gcode += append("M7 (coolant: mist on)\n") + else: + gcode += append("M7\n") + if coolantMode == 'Flood': + if OUTPUT_COMMENTS: + gcode += append("M8 (coolant: flood on)\n") + else: + gcode += append("M8\n") + + # process the operation gcode + if OUTPUT_COMMENTS: + gcode += append("(operation start: %s)\n" % obj.Label) + gcode += parse(obj) + if OUTPUT_COMMENTS: + gcode += append("(operation done: %s)\n" % obj.Label) + + # post_op + for line in POST_OPERATION.splitlines(True): + gcode += append(line) + + # turn coolant off if required + if hasattr(obj, "CoolantMode"): + coolantMode = obj.CoolantMode + if not coolantMode == 'None': + if OUTPUT_COMMENTS: + gcode += append("M9 (coolant: off)\n") + else: + gcode += append("M9\n") + if OUTPUT_COMMENTS: + gcode += append("(operation finalised: %s)\n" % obj.Label) + + # do the post_amble + if OUTPUT_COMMENTS: + gcode += append("(postamble: begin)\n") + for line in POSTAMBLE.splitlines(True): + gcode += append(line) + if OUTPUT_COMMENTS: + gcode += append("(postamble: done)\n") + + # Show the results + 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 (0 < problems_count) or (0 < warnings_count): + print("export: postprocessing: done, warnings: {}, problems: {}, see GCode output for details.".format(warnings_count, problems_count)) + else: + print("export: postprocessing: done (none of the problems detected).") + + if not filename == '-': + print("export: writing to '{}'".format(filename)) + gfile = pythonopen(filename, "w") + gfile.write(final) + gfile.close() + + return final + +def linenumber(): + global LINE_NUMBER_START + global LINE_NUMBER_STEP + global LINENR + + if LINENR <= 0: + LINENR = LINE_NUMBER_START + if OUTPUT_LINE_NUMBERS is True: + line = LINENR + LINENR += LINE_NUMBER_STEP + return "N{:03d} ".format(line) + return "" + +def parse(pathobj): + global PRECISION + global MODAL + global REPEAT_ARGUMENTS + global UNIT_FORMAT + global UNIT_SPEED_FORMAT + + out = "" + lastcommand = None + precision_string = '.' + str(PRECISION) +'f' + currLocation = {} # keep track for no doubles + + # The params list control the order of parameters + params = ['X', 'Y', 'Z', 'A', 'B', 'C', 'I', 'J', 'K', 'R', 'F', 'S', 'T', 'H', 'L', 'Q'] + 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" + + for c in pathobj.Path.Commands: + commandlist = [] # list of elements in the command, code and params. + command = c.Name.strip() # command M or G code or comment string + 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) + + if c.Name[0] == '(' 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 REPEAT_ARGUMENTS): + if c.Name not in ["G0", "G00"]: # No F in G0 + speed = Units.Quantity(c.Parameters['F'], FreeCAD.Units.Velocity) + if speed.getValueAs(UNIT_SPEED_FORMAT) > 0.0: + commandlist.append(param + format(float(speed.getValueAs(UNIT_SPEED_FORMAT)), precision_string)) + else: + continue + elif param == 'T': + commandlist.append(param + str(int(c.Parameters['T']))) + elif param == 'H': + commandlist.append(param + str(int(c.Parameters['H']))) + elif param == 'D': + commandlist.append(param + str(int(c.Parameters['D']))) + elif param == 'S': + commandlist.append(param + str(int(c.Parameters['S']))) + else: + if (not REPEAT_ARGUMENTS) and (param in currLocation) and (currLocation[param] == c.Parameters[param]): + continue + else: + pos = Units.Quantity(c.Parameters[param], FreeCAD.Units.Length) + commandlist.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': + 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'])) + commandlist.append(tool_height) + + 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.strip() + COMMAND_SPACE + if (trace_gcode): + print("parse : >>{}".format(out)) + out = out.strip() + "\n" + + return out + +print(__name__ + " gcode postprocessor for UC-CNC loaded.") +