Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Path: marlin_post.py #4987

Closed
wants to merge 23 commits into from
Closed

Conversation

ByronMontgomerie
Copy link

@ByronMontgomerie ByronMontgomerie commented Aug 21, 2021

Thank you for creating a pull request to contribute to FreeCAD! To ease integration, we ask you to conform to the following items. Pull requests which don't satisfy all the items below might be rejected. If you are in doubt with any of the items below, don't hesitate to ask for help in the FreeCAD forum!

  • Your pull request is confined strictly to a single module. That is, all the files changed by your pull request are either in App, Base, Gui or one of the Mod subfolders. If you need to make changes in several locations, make several pull requests and wait for the first one to be merged before submitting the next ones
  • In case your pull request does more than just fixing small bugs, make sure you discussed your ideas with other developers on the FreeCAD forum
  • Your branch is rebased on latest master git pull --rebase upstream master
  • All FreeCAD unit tests are confirmed to pass by running ./bin/FreeCAD --run-test 0
  • All commit messages are well-written ex: Fixes typo in Draft Move command text
  • Your pull request is well written and has a good description, and its title starts with the module name, ex: Draft: Fixed typos
  • Commit messages include issue #<id> or fixes #<id> where <id> is the FreeCAD bug tracker issue number in case a particular commit solves or is related to an existing issue on the tracker. Ex: Draft: fix typos - fixes #0004805

And please remember to update the Wiki with the features added or changed once this PR is merged.
Note: If you don't have wiki access, then please mention your contribution on the 0.20 Changelog Forum Thread.


Some debugging and command substitution added to the Grbl post processor for the marlin flavour of gcode.
Feed rate mods for realistic numbers for cnc router. 
Inverted the filter to allowed commands as 0.91 seems to like adding random things in that don't immediately apply to marlin.
fixed G81 macro and loop issue with that.
Added tool change macro, minimal implementation.
minor tweak to remove 'end' requirement in favour of try except
Added parameter to switch to center origin.
Added config option for spindle control, puts the m3/m5 back in for those who have marlin flashed with the compile option.
Default define for spindle control added.
Added some arguments to fine tune the coordinates by centering on only one axis, adding offsets, etc.
Manually merged G0 Z feedrate fix by Darwin MCGrath
drill emul not scaled
@luzpaz luzpaz added the WB CAM Related to the CAM/Path Workbench label Aug 21, 2021
@luzpaz
Copy link
Contributor

luzpaz commented Aug 21, 2021

Feedback: the commit titles are unhelpful (their summaries are though).

@luzpaz luzpaz changed the title marlin_post.py Path: marlin_post.py Aug 21, 2021
Add M400 for G0 Z moves to complete
#***************************************************************************
#*   (c) sliptonic (shopinthewoods@gmail.com) 2014                        *
#*                                                                         *
#*   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                                                                   *
#*                                                                         *
#***************************************************************************/


TOOLTIP='''
Generate g-code from a Path that is compatible with the marlin controller.

import marlin_post
marlin_post.export(object,"/path/to/file.ncc")
'''

import FreeCAD
import PathScripts.PostUtils as PostUtils
import argparse
import datetime
import shlex
import traceback


now = datetime.datetime.now()

parser = argparse.ArgumentParser(prog='marlin', 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"')
parser.add_argument('--postamble', help='set commands to be issued after the last command, default="M05\nG17 G90\n; M2"')
parser.add_argument('--tool-change', help='0 ... suppress all tool change commands\n1 ... insert M6 for all tool changes\n2 ... insert M6 for all tool changes except the initial tool')
parser.add_argument('--centeroriginal', action='store_true', help='only Z is adjusted.')
parser.add_argument('--centerorigin', action='store_true', help='center all xy coords around mid point, default is bottom left')
parser.add_argument('--centeroriginx', action='store_true', help='center all x coords around mid point')
parser.add_argument('--centeroriginy', action='store_true', help='center all y coords around mid point')
parser.add_argument('--swapxy', action='store_true', help='swaps the X and Y values')
parser.add_argument('--invertx', action='store_true', help='X values are swapped 180 degrees')
parser.add_argument('--inverty', action='store_true', help='Y values are swapped 180 degrees')
parser.add_argument('--offsetx',  help='X values are offset')
parser.add_argument('--offsety', help='Y values are offset')
parser.add_argument('--spindlecontrol', action='store_true', help='Allow M3 / M5 for Marlin compiled with spindle support')
TOOLTIP_ARGS=parser.format_help()

#These globals set common customization preferences
OUTPUT_COMMENTS = True
OUTPUT_HEADER = True
OUTPUT_LINE_NUMBERS = False
OUTPUT_TOOL_CHANGE = False
SHOW_EDITOR = True
MODAL = False #if true commands 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
MACHINE_NAME = "MARLIN"
CORNER_MIN = {'x':0, 'y':0, 'z':0 }
CORNER_MAX = {'x':500, 'y':300, 'z':300 }
PRECISION = 4

RAPID_MOVES = ['G0', 'G00']

G0XY_FEEDRATE = 30000
G0Z_UP_FEEDRATE = 3600
G0Z_DOWN_FEEDRATE = 3600
SAFEZ = 5
CLEARZ = 3
ATTENTIONZ = 20
DRILLZ = 5

#Preamble text will appear at the Beginning of the GCODE output file.
PREAMBLE = '''G90
G92 X0 Y0 Z0
%units%
G0 Z''' + format(SAFEZ, 'd') + ''' F''' + format(G0Z_UP_FEEDRATE /60.0, '.' + str(PRECISION) +'f') + '''
'''

#Postamble text will appear following the last operation.
POSTAMBLE = '''
G0 Z''' + format(ATTENTIONZ , 'd') + '''
M5
G0 X0 Y0 F''' + format(G0XY_FEEDRATE /60.0, '.' + str(PRECISION) +'f') + '''
G0 Z''' + format(SAFEZ, 'd') + ''' F''' + format(G0Z_UP_FEEDRATE /60.0, '.' + str(PRECISION) +'f') + '''
; M2
'''
#Marlin is primarily for 3d printing, the output from freeCAD for cnc router work is rather limited
#Add more as support exists, tool changes maybe

ALLOWED_COMMANDS = ['G0', 'G1', 'G2', 'G3', 'G5', 'G20', 'G21', 'G90', 'G91', 'G92', 'M0', 'M400']

# These commands are ignored by commenting them out
# SUPPRESS_COMMANDS = [ 'G98', 'G80' ]

#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 = ''''''
SUPPRESS_TOOL_CHANGE=0
ASSUME_FIRST_TOOL = True

CENTER_ORIGIN_X = False
CENTER_ORIGIN_Y = False
CENTER_OFF = False

SWAP_XY = False
INVERT_X = False
INVERT_Y = False
OFFSET_X = 0.0
OFFSET_Y = 0.0

SPINDLE_CONTROL = False

# to distinguish python built-in open function from the one declared below
if open.__module__ in ['__builtin__','io']:
    pythonopen = open


def processArguments(argstring):
    global OUTPUT_HEADER
    global OUTPUT_COMMENTS
    global OUTPUT_LINE_NUMBERS
    global OUTPUT_TOOL_CHANGE
    global SHOW_EDITOR
    global PRECISION
    global PREAMBLE
    global POSTAMBLE
    global SUPPRESS_TOOL_CHANGE
    global CENTER_OFF
    global CENTER_ORIGIN_X
    global CENTER_ORIGIN_Y
    global SWAP_XY
    global INVERT_X
    global INVERT_Y
    global OFFSET_X
    global OFFSET_Y
    global SPINDLE_CONTROL

    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
        
        print("Show editor = %d" % SHOW_EDITOR)
        PRECISION = args.precision
        
        if not args.preamble is None:
            PREAMBLE = args.preamble
        if not args.postamble is None:
            POSTAMBLE = args.postamble
        if not args.tool_change is None:
            OUTPUT_TOOL_CHANGE = int(args.tool_change) > 0
            SUPPRESS_TOOL_CHANGE = min(1, int(args.tool_change) - 1)
        if args.swapxy:
            SWAP_XY = True
        if args.invertx:
            INVERT_X = True
        if args.inverty:
            INVERT_Y = True
        if not args.offsetx is None:
            OFFSET_X = float(args.offsetx)
        if not args.offsety is None:
            OFFSET_Y = float(args.offsety)
        if args.centerorigin:
            CENTER_ORIGIN_X = True
            CENTER_ORIGIN_Y = True
        else:
            if args.centeroriginal:
                CENTER_OFF = True
                print("Center OFf")
            else:
                if args.centeroriginx:
                    CENTER_ORIGIN_X = not SWAP_XY
                if args.centeroriginy:
                    CENTER_ORIGIN_Y = not SWAP_XY
        if args.spindlecontrol:
            SPINDLE_CONTROL = True
            
    except Exception as e:
        traceback.print_exc(e)
        return False

    return True

def export(objectslist,filename,argstring):
    if not processArguments(argstring):
        return None

    global UNITS

    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
            
    if SPINDLE_CONTROL:
       ALLOWED_COMMANDS.extend(['M3', 'M4', 'M5'])
       
    print("postprocessing...")
    gcode = ""

    #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,"Group"): #We have a compound or project.
            for p in pathobj.Group:
                if p.Name == "Machine":
                    myMachine = p
    if myMachine is None:
        print("No machine found in this project")
    else:
        if myMachine.MachineUnits == "Metric":
           UNITS = "G21"
        else:
           UNITS = "G20"
           SAFEZ = SAFEZ / 2.54
           ATTENTIONZ = ATTENTIONZ / 2.54
           DRILLZ = DRILLZ / 2.54


    # 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(True):
        gcode += linenumber() + line.replace("%units%", UNITS)
        
    gcode += "\n;End preamble\n\n"
    
    data_stats = {"Xmin":10000, "Xmax":0,
                  "Ymin":10000, "Ymax":0,
                  "Zmin":10000, "Zmax":0,
                  "Xmin'":10000, "Xmax'":0,
                  "Ymin'":10000, "Ymax'":0,
                  "Zmin'":10000, "Zmax'":0}
    
    gcodebody = ""
    
    for obj in objectslist:
         parse(obj, data_stats, True)
    
    for obj in objectslist:

        #do the pre_op
        if OUTPUT_COMMENTS: gcodebody += linenumber() + ";Begin operation: " + obj.Label + "\n"
        for line in PRE_OPERATION.splitlines(True):
            gcodebody += linenumber() + line

        gcodebody += parse(obj, data_stats, False)

        #do the post_op
        if OUTPUT_COMMENTS: gcodebody += linenumber() + ";finish operation: " + obj.Label + "\n"
        
        for line in POST_OPERATION.splitlines(True):
            gcodebody += linenumber() + line
    
    precision_string = '.' + str(PRECISION) +'f'
     
    if OUTPUT_COMMENTS:
        wassup = 'Origin: ' + ('Original' if CENTER_OFF else 'Bottom Left' if not (CENTER_ORIGIN_X or CENTER_ORIGIN_Y) else 'Centered') + (' (Swap X and Y)' if SWAP_XY else '') + ('\n;' + ('x offset: ' + format(OFFSET_X, '0.2f')) if OFFSET_X > 0 else '') + ('\n;' + ('y offset: ' + format(OFFSET_Y, '0.2f')) if OFFSET_Y > 0 else '')
        print(wassup);
        
        gcode += ';' + wassup + '\n'
        
        for key in data_stats:
            if len(key) == 4:
                tkey = key
                if SWAP_XY:
                    if tkey[0] == 'X': tkey = 'Y' + tkey[1:4]
                    elif tkey[0] == 'Y': tkey= 'X' + tkey[1:4]
                wassup = tkey + ' is ' +  format(data_stats[key], precision_string) + ' ==> ' + format(data_stats[key+"'"], precision_string)
                print(wassup)
                gcode += ";" + wassup + '\n'
            else:
                break
               
    if OUTPUT_COMMENTS: gcode += '\n;GCode Commands detected:\n\n'
    
    if OUTPUT_COMMENTS:
        for key in data_stats:
            if len(key) < 4:
                nottoolate = key + ' detected, count is ' +  str(data_stats[key])
                print(nottoolate)
                gcode += ";" + nottoolate + "\n"
    
    gcode += '\n'
    gcode += gcodebody
    gcodebody = ''
        
    #do the post_amble

    if OUTPUT_COMMENTS: gcode += ";Begin postamble\n"
    for line in POSTAMBLE.splitlines(True):
        gcode += linenumber() + line

    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.")

    gfile = pythonopen(filename,"w")
    gfile.write(gcode)
    gfile.close()


def linenumber():
    global LINENR
    if OUTPUT_LINE_NUMBERS == True:
        LINENR += 10
        return "N" + str(LINENR) + " "
    return ""

def tallycmds(data_stats, command):
    if not command.startswith("("):
        if command in data_stats:
            data_stats[command] = data_stats[command] + 1
        else:
            data_stats[command] = 1
    return 0
    
def boxlimits(data_stats, cmd, param, value, checkbounds):
    # param is upper always
    
    if (INVERT_X and param == 'X') or (INVERT_Y and param == 'Y'):
        value = -value
        
    if checkbounds:
        if data_stats[param + "min"] > value:
            data_stats[param + "min"] = value
        if data_stats[param + "max"] < value:
            data_stats[param + "max"] = value
    else:
            
        if param == 'Z':
           value -= (data_stats[param + "max"] - 5)
        else:
            if not CENTER_OFF:
               if (CENTER_ORIGIN_X and param =='X') or (CENTER_ORIGIN_Y and param =='Y'):
                   value -=  ((data_stats[param + "max"] + data_stats[param + "min"]) / 2.0)
               else:
                   value -= data_stats[param + "min"]
        
        if param == 'X': value += OFFSET_X
        if param == 'Y': value += OFFSET_Y
        
        if data_stats[param + "min'"] > value:
            data_stats[param + "min'"] = value
        if data_stats[param + "max'"] < value:
            data_stats[param + "max'"] = value
                   
    return value

def emuldrill(c, state): #G81

    # something for defaults in emergency
    
    X = c.Parameters['X'] if 'X' in c.Parameters else state["lastx"]
    Y = c.Parameters['Y'] if 'Y' in c.Parameters else state["lasty"]
    Z = c.Parameters['Z'] if 'Z' in c.Parameters else state['lastz']
    
    cmdlist =  [["G1", {'X' : X, 'Y' : Y, 'Z' :  SAFEZ,  'F' : G0Z_UP_FEEDRATE /60}],
                ["G1", {'X' : X, 'Y' : Y, 'Z' : Z * 0.25, 'F' : G0Z_DOWN_FEEDRATE / 60}],
                ["G1", {'X' : X, 'Y' : Y, 'Z' : 0, 'F' : G0Z_UP_FEEDRATE / 60}],
                ["G1", {'X' : X, 'Y' : Y, 'Z' : Z * 0.5, 'F' : G0Z_DOWN_FEEDRATE / 60}],
                ["G1", {'X' : X, 'Y' : Y, 'Z' : 0, 'F' : G0Z_UP_FEEDRATE / 60}],
                ["G1", {'X' : X, 'Y' : Y, 'Z' : Z * 0.75, 'F' : G0Z_DOWN_FEEDRATE / 60}],
                ["G1", {'X' : X, 'Y' : Y, 'Z' : 0, 'F' : G0Z_UP_FEEDRATE / 60}],
                ["G1", {'X' : X, 'Y' : Y, 'Z' : Z, 'F' : G0Z_DOWN_FEEDRATE / 60}],
                ["G1", {'X' : X, 'Y' : Y, 'Z' : SAFEZ,  'F' : G0Z_UP_FEEDRATE /60}]
    ]
    
    return iter(cmdlist)
    
def emultoolchange(c, state): #M6 T?
    #print(state['notoolyet'])
    if ASSUME_FIRST_TOOL and state['notoolyet'] and state['output']:
        state['notoolyet'] = False
        cmdlist =  [
                    [";assumed starting tool", {'T' : c.Parameters['T']}]
        ]
    else:
        cmdlist =  [["G0", {'z' : ATTENTIONZ, 'F': G0Z_UP_FEEDRATE / 60.0}],
                    ["G0", {'x' : 0, 'y' : 0, 'F' : G0XY_FEEDRATE / 60.0}],
                    ["M5" if SPINDLE_CONTROL else ";M5", {}],
                    ["M0", {'T': c.Parameters['T']}], # Pause and wait for click, turn off spindle, swap bit, home it, turn on spindle
                    ["G92", {'z' : 0}],
                    ["G0", {'z' : ATTENTIONZ, 'F': G0Z_UP_FEEDRATE / 60.0}],
                    ["G0", {'x' : state['lastx'], 'y': state['lasty'], 'F': G0XY_FEEDRATE / 60.0}],
                    ["M3" if SPINDLE_CONTROL else ";M3", {'S' : state['lasts']}]
        ]
    
    return iter(cmdlist)

def emulFastMove(c, state): #Let's separate Z
    params = c.Parameters
    
    if 'F' in params:
        F = params['F']
    else:
        F = G0XY_FEEDRATE / 60.0
        
    if F > (G0Z_DOWN_FEEDRATE / 60.0):
        F2 = G0Z_DOWN_FEEDRATE / 60.0
    else:
        F2 = F
        
    if 'Z' in params:
        if 'X' in params or 'Y' in params:
            cmdlist =  [["G1", {'Z' : params['Z'], 'F': F2}],
                        ["M400", {}],
                        ["G1", {'X' : params['X'], 'Y': params['Y'], 'F': F}]]
        else:
            cmdlist = [["G1", {'Z' : params['Z'], 'F': F2}]]
    else:
        if 'X' in params:
            if 'Y' in params:
                cmdlist = [["G1", {'X' : params['X'], 'Y': params['Y'], 'F': F}]]
            else:
                cmdlist = [["G1", {'X' : params['X'], 'F': F}]]
        else:
            cmdlist = [["G1", {'Y' : params['Y'], 'F': F}]]
                
    return iter(cmdlist)
   
class Commands:
    tobe = {'G81': emuldrill, 'M6' : emultoolchange, 'G0' : emulFastMove}
    state = {'output': False, 'notoolyet': True, 'lastz' : 100, 'lastx' : 0, 'lasty' : 0, 'lastf' : G0Z_DOWN_FEEDRATE, 'lasts' : 0}
             
    def __init__(self, pathobj = None, output = False):
        self.paths = iter(pathobj.Path.Commands)
        Commands.state['output'] = output
        self.epath = None

    def __iter__(self):
        return self

    def __next__(self):
        res = None
        
        if self.epath != None:
            try:
                res = next(self.epath)
                res = [res[0], res[1]]
            except:
                self.epath = None
                res = None

        if res == None:
            item = next(self.paths)
            command = item.Name
            params = item.Parameters
                           
            #print (command)
            
            if command in Commands.tobe:
                func = Commands.tobe[command]
                self.epath = func(item, Commands.state)
                command = ';' + command
            elif Commands.state['output']:
                if command == 'G0':
                    if 'Z' in params:
                        params['F'] = (G0Z_UP_FEEDRATE if params['Z'] > Commands.state['lastz'] else G0Z_DOWN_FEEDRATE if params['Z'] < Commands.state['lastz'] else G0XY_FEEDRATE) / 60.0
                        #print ('lastz ' + format(Commands.state['lastz'], '0.2f') + ' currentZ ' + format(params['Z'], '0.2f') + ' G0 ' + format(params['F'] * 60.0, '0.2f') if 'F' in params else 'wtf')
                    else:
                        params['F'] = G0XY_FEEDRATE / 60.0
                        #print ('G0 F' + format(params['F'] * 60.0, '0.2f') if 'F' in params else 'wtf')
    
                if 'X' in params: Commands.state['lastx'] = params['X']
                if 'Y' in params: Commands.state['lasty'] = params['Y']
                if 'Z' in params: Commands.state['lastz'] = params['Z']
                if 'F' in params: Commands.state['lastf'] = params['F']
                if 'S' in params: Commands.state['lasts'] = params['S']
                
            res = [command, params]
            
        return res

def parse(pathobj, data_stats, checkbounds):
    out = ""
    lastcommand = None
    precision_string = '.' + str(PRECISION) +'f'
    global SUPPRESS_TOOL_CHANGE

    #params = ['X','Y','Z','A','B','I','J','K','F','S'] #This list control the order of parameters
    params = ['X', 'x', 'Y', 'y', 'Z','z','A','B','I','J','F','S','T','Q','R','L'] #linuxcnc doesn't want K properties on XY plane  Arcs need work.

    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, data_stats, checkbounds)
        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 command, Parameters in Commands(pathobj, not checkbounds):
            outstring = []
            
            if not checkbounds:
                tallycmds(data_stats, command)
                           
            outstring.append(command)
            # if modal: only print the command if it is not the same as the last one
            if MODAL == True:
                if command == lastcommand:
                    outstring.pop(0)
                
            # Now add the remaining parameters in order
            for param in params:
                if param in Parameters:
                    if param == 'F':
                        if not checkbounds:
                            outstring.append(param + format(Parameters[param] * 60, '.2f'))
                    elif param == 'T':
                        outstring.append(param + str(int(Parameters['T'])))
                    else:
                        value = Parameters[param]
                        if param in ['X', 'Y', 'Z']:
                            value = boxlimits(data_stats, command, param, value, checkbounds)
                            
                        if SWAP_XY:
                            if param.upper() == 'X': param = 'Y'
                            elif param.upper() == 'Y': param = 'X'
                            
                        outstring.append(param.upper() + format(value, precision_string))

            # store the latest command
            lastcommand = command

            # Check for Tool Change:
            if command == 'M6':
                if OUTPUT_COMMENTS:
                    out += linenumber() + ";Begin toolchange\n"
                if not OUTPUT_TOOL_CHANGE or SUPPRESS_TOOL_CHANGE > 0:
                    outstring.insert(0, ";")
                    SUPPRESS_TOOL_CHANGE = SUPPRESS_TOOL_CHANGE - 1
                else:
                    for line in TOOL_CHANGE.splitlines(True):
                        out += linenumber() + line

            if command == "message":
                if OUTPUT_COMMENTS == False:
                    out = []
                else:
                    outstring.pop(0) #remove the command

            if not command in ALLOWED_COMMANDS:
                if ';' not in outstring[0]:
                    print(outstring)
                    outstring.insert(0, ";")

            #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.")
Minor, replaced G0 with G1 for preamble, set feed rate to default for XY after.
More feed rate fixes.  preamble and postamble don't need conversion back and forth from minutes.
#***************************************************************************
#*   (c) sliptonic (shopinthewoods@gmail.com) 2014                        *
#*                                                                         *
#*   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                                                                   *
#*                                                                         *
#***************************************************************************/


TOOLTIP='''
Generate g-code from a Path that is compatible with the marlin controller.

import marlin_post
marlin_post.export(object,"/path/to/file.ncc")
'''

import FreeCAD
import PathScripts.PostUtils as PostUtils
import argparse
import datetime
import shlex
import traceback


now = datetime.datetime.now()

parser = argparse.ArgumentParser(prog='marlin', 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"')
parser.add_argument('--postamble', help='set commands to be issued after the last command, default="M05\nG17 G90\n; M2"')
parser.add_argument('--tool-change', help='0 ... suppress all tool change commands\n1 ... insert M6 for all tool changes\n2 ... insert M6 for all tool changes except the initial tool')
parser.add_argument('--centeroriginal', action='store_true', help='only Z is adjusted.')
parser.add_argument('--centerorigin', action='store_true', help='center all xy coords around mid point, default is bottom left')
parser.add_argument('--centeroriginx', action='store_true', help='center all x coords around mid point')
parser.add_argument('--centeroriginy', action='store_true', help='center all y coords around mid point')
parser.add_argument('--swapxy', action='store_true', help='swaps the X and Y values')
parser.add_argument('--invertx', action='store_true', help='X values are swapped 180 degrees')
parser.add_argument('--inverty', action='store_true', help='Y values are swapped 180 degrees')
parser.add_argument('--offsetx',  help='X values are offset')
parser.add_argument('--offsety', help='Y values are offset')
parser.add_argument('--spindlecontrol', action='store_true', help='Allow M3 / M5 for Marlin compiled with spindle support')
TOOLTIP_ARGS=parser.format_help()

#These globals set common customization preferences
OUTPUT_COMMENTS = True
OUTPUT_HEADER = True
OUTPUT_LINE_NUMBERS = False
OUTPUT_TOOL_CHANGE = False
SHOW_EDITOR = True
MODAL = False #if true commands 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
MACHINE_NAME = "MARLIN"
CORNER_MIN = {'x':0, 'y':0, 'z':0 }
CORNER_MAX = {'x':500, 'y':300, 'z':300 }
PRECISION = 4

RAPID_MOVES = ['G0', 'G00']

G0XY_FEEDRATE = 1000
G0Z_UP_FEEDRATE = 250
G0Z_DOWN_FEEDRATE = 150
SAFEZ = 5
CLEARZ = 3
ATTENTIONZ = 20
DRILLZ = 5

#Preamble text will appear at the Beginning of the GCODE output file.
#Note no /60.0 as it is not processed.

PREAMBLE = '''G90
G92 X0 Y0 Z0
%units%
G1 Z''' + format(SAFEZ, 'd') + ''' F''' + format(G0Z_UP_FEEDRATE , '.' + str(PRECISION) +'f') + '''
G1 X0 Y0 F''' + format(G0XY_FEEDRATE , '.' + str(PRECISION) +'f') + '''
'''

#Postamble text will appear following the last operation.
#Note no /60.0 as it is not processed.

POSTAMBLE = '''
G1 Z''' + format(ATTENTIONZ , 'd') + '''
M5
G1 X0 Y0 F''' + format(G0XY_FEEDRATE, '.' + str(PRECISION) +'f') + '''
G1 Z''' + format(SAFEZ, 'd') + ''' F''' + format(G0Z_UP_FEEDRATE , '.' + str(PRECISION) +'f') + '''
; M2
'''
#Marlin is primarily for 3d printing, the output from freeCAD for cnc router work is rather limited
#Add more as support exists, tool changes maybe

ALLOWED_COMMANDS = ['G0', 'G1', 'G2', 'G3', 'G5', 'G20', 'G21', 'G90', 'G91', 'G92', 'M0', 'M400']

# These commands are ignored by commenting them out
# SUPPRESS_COMMANDS = [ 'G98', 'G80' ]

#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 = ''''''
SUPPRESS_TOOL_CHANGE=0
ASSUME_FIRST_TOOL = True

CENTER_ORIGIN_X = False
CENTER_ORIGIN_Y = False
CENTER_OFF = False

SWAP_XY = False
INVERT_X = False
INVERT_Y = False
OFFSET_X = 0.0
OFFSET_Y = 0.0

SPINDLE_CONTROL = False

# to distinguish python built-in open function from the one declared below
if open.__module__ in ['__builtin__','io']:
    pythonopen = open


def processArguments(argstring):
    global OUTPUT_HEADER
    global OUTPUT_COMMENTS
    global OUTPUT_LINE_NUMBERS
    global OUTPUT_TOOL_CHANGE
    global SHOW_EDITOR
    global PRECISION
    global PREAMBLE
    global POSTAMBLE
    global SUPPRESS_TOOL_CHANGE
    global CENTER_OFF
    global CENTER_ORIGIN_X
    global CENTER_ORIGIN_Y
    global SWAP_XY
    global INVERT_X
    global INVERT_Y
    global OFFSET_X
    global OFFSET_Y
    global SPINDLE_CONTROL

    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
        
        print("Show editor = %d" % SHOW_EDITOR)
        PRECISION = args.precision
        
        if not args.preamble is None:
            PREAMBLE = args.preamble
        if not args.postamble is None:
            POSTAMBLE = args.postamble
        if not args.tool_change is None:
            OUTPUT_TOOL_CHANGE = int(args.tool_change) > 0
            SUPPRESS_TOOL_CHANGE = min(1, int(args.tool_change) - 1)
        if args.swapxy:
            SWAP_XY = True
        if args.invertx:
            INVERT_X = True
        if args.inverty:
            INVERT_Y = True
        if not args.offsetx is None:
            OFFSET_X = float(args.offsetx)
        if not args.offsety is None:
            OFFSET_Y = float(args.offsety)
        if args.centerorigin:
            CENTER_ORIGIN_X = True
            CENTER_ORIGIN_Y = True
        else:
            if args.centeroriginal:
                CENTER_OFF = True
                print("Center OFf")
            else:
                if args.centeroriginx:
                    CENTER_ORIGIN_X = not SWAP_XY
                if args.centeroriginy:
                    CENTER_ORIGIN_Y = not SWAP_XY
        if args.spindlecontrol:
            SPINDLE_CONTROL = True
            
    except Exception as e:
        traceback.print_exc(e)
        return False

    return True

def export(objectslist,filename,argstring):
    if not processArguments(argstring):
        return None

    global UNITS

    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
            
    if SPINDLE_CONTROL:
       ALLOWED_COMMANDS.extend(['M3', 'M4', 'M5'])
       
    print("postprocessing...")
    gcode = ""

    #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,"Group"): #We have a compound or project.
            for p in pathobj.Group:
                if p.Name == "Machine":
                    myMachine = p
    if myMachine is None:
        print("No machine found in this project")
    else:
        if myMachine.MachineUnits == "Metric":
           UNITS = "G21"
        else:
           UNITS = "G20"
           SAFEZ = SAFEZ / 2.54
           ATTENTIONZ = ATTENTIONZ / 2.54
           DRILLZ = DRILLZ / 2.54


    # 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(True):
        gcode += linenumber() + line.replace("%units%", UNITS)
        
    gcode += "\n;End preamble\n\n"
    
    data_stats = {"Xmin":10000, "Xmax":0,
                  "Ymin":10000, "Ymax":0,
                  "Zmin":10000, "Zmax":0,
                  "Xmin'":10000, "Xmax'":0,
                  "Ymin'":10000, "Ymax'":0,
                  "Zmin'":10000, "Zmax'":0}
    
    gcodebody = ""
    
    for obj in objectslist:
         parse(obj, data_stats, True)
    
    for obj in objectslist:

        #do the pre_op
        if OUTPUT_COMMENTS: gcodebody += linenumber() + ";Begin operation: " + obj.Label + "\n"
        for line in PRE_OPERATION.splitlines(True):
            gcodebody += linenumber() + line

        gcodebody += parse(obj, data_stats, False)

        #do the post_op
        if OUTPUT_COMMENTS: gcodebody += linenumber() + ";finish operation: " + obj.Label + "\n"
        
        for line in POST_OPERATION.splitlines(True):
            gcodebody += linenumber() + line
    
    precision_string = '.' + str(PRECISION) +'f'
     
    if OUTPUT_COMMENTS:
        wassup = 'Origin: ' + ('Original' if CENTER_OFF else 'Bottom Left' if not (CENTER_ORIGIN_X or CENTER_ORIGIN_Y) else 'Centered') + (' (Swap X and Y)' if SWAP_XY else '') + ('\n;' + ('x offset: ' + format(OFFSET_X, '0.2f')) if OFFSET_X > 0 else '') + ('\n;' + ('y offset: ' + format(OFFSET_Y, '0.2f')) if OFFSET_Y > 0 else '')
        print(wassup);
        
        gcode += ';' + wassup + '\n'
        
        for key in data_stats:
            if len(key) == 4:
                tkey = key
                if SWAP_XY:
                    if tkey[0] == 'X': tkey = 'Y' + tkey[1:4]
                    elif tkey[0] == 'Y': tkey= 'X' + tkey[1:4]
                wassup = tkey + ' is ' +  format(data_stats[key], precision_string) + ' ==> ' + format(data_stats[key+"'"], precision_string)
                print(wassup)
                gcode += ";" + wassup + '\n'
            else:
                break
               
    if OUTPUT_COMMENTS: gcode += '\n;GCode Commands detected:\n\n'
    
    if OUTPUT_COMMENTS:
        for key in data_stats:
            if len(key) < 4:
                nottoolate = key + ' detected, count is ' +  str(data_stats[key])
                print(nottoolate)
                gcode += ";" + nottoolate + "\n"
    
    gcode += '\n'
    gcode += gcodebody
    gcodebody = ''
        
    #do the post_amble

    if OUTPUT_COMMENTS: gcode += ";Begin postamble\n"
    for line in POSTAMBLE.splitlines(True):
        gcode += linenumber() + line

    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.")

    gfile = pythonopen(filename,"w")
    gfile.write(gcode)
    gfile.close()


def linenumber():
    global LINENR
    if OUTPUT_LINE_NUMBERS == True:
        LINENR += 10
        return "N" + str(LINENR) + " "
    return ""

def tallycmds(data_stats, command):
    if not command.startswith("("):
        if command in data_stats:
            data_stats[command] = data_stats[command] + 1
        else:
            data_stats[command] = 1
    return 0
    
def boxlimits(data_stats, cmd, param, value, checkbounds):
    # param is upper always
    
    if (INVERT_X and param == 'X') or (INVERT_Y and param == 'Y'):
        value = -value
        
    if checkbounds:
        if data_stats[param + "min"] > value:
            data_stats[param + "min"] = value
        if data_stats[param + "max"] < value:
            data_stats[param + "max"] = value
    else:
            
        if param == 'Z':
           value -= (data_stats[param + "max"] - 5)
        else:
            if not CENTER_OFF:
               if (CENTER_ORIGIN_X and param =='X') or (CENTER_ORIGIN_Y and param =='Y'):
                   value -=  ((data_stats[param + "max"] + data_stats[param + "min"]) / 2.0)
               else:
                   value -= data_stats[param + "min"]
        
        if param == 'X': value += OFFSET_X
        if param == 'Y': value += OFFSET_Y
        
        if data_stats[param + "min'"] > value:
            data_stats[param + "min'"] = value
        if data_stats[param + "max'"] < value:
            data_stats[param + "max'"] = value
                   
    return value

def emuldrill(c, state): #G81

    # something for defaults in emergency
    
    X = c.Parameters['X'] if 'X' in c.Parameters else state["lastx"]
    Y = c.Parameters['Y'] if 'Y' in c.Parameters else state["lasty"]
    Z = c.Parameters['Z'] if 'Z' in c.Parameters else state['lastz']
    
    cmdlist =  [["G1", {'X' : X, 'Y' : Y, 'Z' :  SAFEZ,  'F' : G0Z_UP_FEEDRATE /60}],
                ["G1", {'X' : X, 'Y' : Y, 'Z' : Z * 0.25, 'F' : G0Z_DOWN_FEEDRATE / 60}],
                ["G1", {'X' : X, 'Y' : Y, 'Z' : 0, 'F' : G0Z_UP_FEEDRATE / 60}],
                ["G1", {'X' : X, 'Y' : Y, 'Z' : Z * 0.5, 'F' : G0Z_DOWN_FEEDRATE / 60}],
                ["G1", {'X' : X, 'Y' : Y, 'Z' : 0, 'F' : G0Z_UP_FEEDRATE / 60}],
                ["G1", {'X' : X, 'Y' : Y, 'Z' : Z * 0.75, 'F' : G0Z_DOWN_FEEDRATE / 60}],
                ["G1", {'X' : X, 'Y' : Y, 'Z' : 0, 'F' : G0Z_UP_FEEDRATE / 60}],
                ["G1", {'X' : X, 'Y' : Y, 'Z' : Z, 'F' : G0Z_DOWN_FEEDRATE / 60}],
                ["G1", {'X' : X, 'Y' : Y, 'Z' : SAFEZ,  'F' : G0Z_UP_FEEDRATE /60}]
    ]
    
    return iter(cmdlist)
    
def emultoolchange(c, state): #M6 T?
    #print(state['notoolyet'])
    if ASSUME_FIRST_TOOL and state['notoolyet'] and state['output']:
        state['notoolyet'] = False
        cmdlist =  [
                    [";assumed starting tool", {'T' : c.Parameters['T']}]
        ]
    else:
        cmdlist =  [["G1", {'z' : ATTENTIONZ, 'F': G0Z_UP_FEEDRATE / 60.0}],
                    ["G1", {'x' : 0, 'y' : 0, 'F' : G0XY_FEEDRATE / 60.0}],
                    ["M5" if SPINDLE_CONTROL else ";M5", {}],
                    ["M0", {'T': c.Parameters['T']}], # Pause and wait for click, turn off spindle, swap bit, home it, turn on spindle
                    ["G92", {'z' : 0}],
                    ["G1", {'z' : ATTENTIONZ, 'F': G0Z_UP_FEEDRATE / 60.0}],
                    ["G1", {'x' : state['lastx'], 'y': state['lasty'], 'F': G0XY_FEEDRATE / 60.0}],
                    ["M3" if SPINDLE_CONTROL else ";M3", {'S' : state['lasts']}]
        ]
    
    return iter(cmdlist)

def emulFastMove(c, state): #Let's separate Z
    params = c.Parameters
    
    if 'F' in params:
        F = params['F']
        if F > (G0XY_FEEDRATE / 60.0):
            F = G0XY_FEEDRATE / 60.0
            
    else:
        F = G0XY_FEEDRATE / 60.0
        
    if F > (G0Z_DOWN_FEEDRATE / 60.0):
        F2 = G0Z_DOWN_FEEDRATE / 60.0
    else:
        F2 = F
        
    if 'Z' in params:
        if 'X' in params or 'Y' in params:
            cmdlist =  [["G1", {'Z' : params['Z'], 'F': F2}],
                        ["M400", {}],
                        ["G1", {'X' : params['X'], 'Y': params['Y'], 'F': F}]]
        else:
            cmdlist = [["G1", {'Z' : params['Z'], 'F': F2}]]
    else:
        if 'X' in params:
            if 'Y' in params:
                cmdlist = [["G1", {'X' : params['X'], 'Y': params['Y'], 'F': F}]]
            else:
                cmdlist = [["G1", {'X' : params['X'], 'F': F}]]
        else:
            cmdlist = [["G1", {'Y' : params['Y'], 'F': F}]]
                
    return iter(cmdlist)
   
class Commands:
    tobe = {'G81': emuldrill, 'M6' : emultoolchange, 'G0' : emulFastMove}
    state = {'output': False, 'notoolyet': True, 'lastz' : 100, 'lastx' : 0, 'lasty' : 0, 'lastf' : G0Z_DOWN_FEEDRATE / 60.0, 'lasts' : 0}
             
    def __init__(self, pathobj = None, output = False):
        self.paths = iter(pathobj.Path.Commands)
        Commands.state['output'] = output
        self.epath = None

    def __iter__(self):
        return self

    def __next__(self):
        res = None
        
        if self.epath != None:
            try:
                res = next(self.epath)
                res = [res[0], res[1]]
            except:
                self.epath = None
                res = None

        if res == None:
            item = next(self.paths)
            command = item.Name
            params = item.Parameters
                           
            #print (command)
            
            if command in Commands.tobe:
                func = Commands.tobe[command]
                self.epath = func(item, Commands.state)
                command = ';' + command
            elif Commands.state['output']:
 #               if command == 'G0':
 #                   if 'Z' in params:
 #                       params['F'] = (G0Z_UP_FEEDRATE if params['Z'] > Commands.state['lastz'] else G0Z_DOWN_FEEDRATE if params['Z'] < Commands.state['lastz'] else G0XY_FEEDRATE) / 60.0
                        #print ('lastz ' + format(Commands.state['lastz'], '0.2f') + ' currentZ ' + format(params['Z'], '0.2f') + ' G0 ' + format(params['F'] * 60.0, '0.2f') if 'F' in params else 'wtf')
 #                   else:
 #                       params['F'] = G0XY_FEEDRATE / 60.0
                        #print ('G0 F' + format(params['F'] * 60.0, '0.2f') if 'F' in params else 'wtf')
    
                if 'X' in params: Commands.state['lastx'] = params['X']
                if 'Y' in params: Commands.state['lasty'] = params['Y']
                if 'Z' in params: Commands.state['lastz'] = params['Z']
                if 'F' in params: Commands.state['lastf'] = params['F'] / 60.0
                if 'S' in params: Commands.state['lasts'] = params['S']
                
            res = [command, params]
            
        return res

def parse(pathobj, data_stats, checkbounds):
    out = ""
    lastcommand = None
    precision_string = '.' + str(PRECISION) +'f'
    global SUPPRESS_TOOL_CHANGE

    #params = ['X','Y','Z','A','B','I','J','K','F','S'] #This list control the order of parameters
    params = ['X', 'x', 'Y', 'y', 'Z','z','A','B','I','J','F','S','T','Q','R','L'] #linuxcnc doesn't want K properties on XY plane  Arcs need work.

    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, data_stats, checkbounds)
        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 command, Parameters in Commands(pathobj, not checkbounds):
            outstring = []
            
            if not checkbounds:
                tallycmds(data_stats, command)
                           
            outstring.append(command)
            # if modal: only print the command if it is not the same as the last one
            if MODAL == True:
                if command == lastcommand:
                    outstring.pop(0)
                
            # Now add the remaining parameters in order
            for param in params:
                if param in Parameters:
                    if param == 'F':
                        if not checkbounds:
                            outstring.append(param + format(Parameters[param] * 60, '.2f'))
                    elif param == 'T':
                        outstring.append(param + str(int(Parameters['T'])))
                    else:
                        value = Parameters[param]
                        if param in ['X', 'Y', 'Z']:
                            value = boxlimits(data_stats, command, param, value, checkbounds)
                            
                        if SWAP_XY:
                            if param.upper() == 'X': param = 'Y'
                            elif param.upper() == 'Y': param = 'X'
                            
                        outstring.append(param.upper() + format(value, precision_string))

            # store the latest command
            lastcommand = command

            # Check for Tool Change:
            if command == 'M6':
                if OUTPUT_COMMENTS:
                    out += linenumber() + ";Begin toolchange\n"
                if not OUTPUT_TOOL_CHANGE or SUPPRESS_TOOL_CHANGE > 0:
                    outstring.insert(0, ";")
                    SUPPRESS_TOOL_CHANGE = SUPPRESS_TOOL_CHANGE - 1
                else:
                    for line in TOOL_CHANGE.splitlines(True):
                        out += linenumber() + line

            if command == "message":
                if OUTPUT_COMMENTS == False:
                    out = []
                else:
                    outstring.pop(0) #remove the command

            if not command in ALLOWED_COMMANDS:
                if ';' not in outstring[0]:
                    print(outstring)
                    outstring.insert(0, ";")

            #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 time a regression fix.
Better to have it too slow and have it scale with feed rate changes.
@luzpaz
Copy link
Contributor

luzpaz commented Aug 28, 2021

@ByronMontgomerie perhaps make this PR a Draft PR until you're ready for it to be merged?

@sliptonic
Copy link
Member

Please rebase the PR and squash the commits down to just one. Thanks

@ByronMontgomerie
Copy link
Author

Please rebase the PR and squash the commits down to just one. Thanks

I am currently on a chromebook, is it something that can be done on the web page?

@luzpaz
Copy link
Contributor

luzpaz commented Sep 9, 2021

@ByronMontgomerie you should be able to use the command line interface to do that.
@sliptonic OP didn't separate changes in to a new branch but worked on master. Do you want me to help out here?

@luzpaz
Copy link
Contributor

luzpaz commented Sep 17, 2021

@sliptonic looks like it can be rebased, squashed, and merged pretty easily. Is there anything else that needs to be done here?

Note: once this post-processor is merged, will the wiki docs need to be updated?

@berndhahnebach
Copy link
Contributor

would you rebase the PR to make it run on CI?

@berndhahnebach
Copy link
Contributor

berndhahnebach commented Sep 24, 2021

Following a link to the branch on the CI-repository:

https://gitlab.com/freecad/FreeCAD-CI/-/commits/PR_4987

The CI-status is available on the latest commit of the branch.
If there is no status available the PR should be rebased on latest master.
Check pipeline branches to see if your PR has been run by the CI.

https://gitlab.com/freecad/FreeCAD-CI/-/pipelines?scope=branches

@sliptonic
Copy link
Member

The pull request is a request to pull from the submitters repo into the target. Therefore only the submitter can rebase and squash his commits. It's a pretty easy operation but they need to do it since we can't do it for them.

@luzpaz
Copy link
Contributor

luzpaz commented Oct 9, 2021

@ByronMontgomerie I use gitkraken as a git GUI. It's easy to rebase and squash commits on it. Let me know if you need help with that? I even made a tutorial for it on wiki.freecadweb.org/gitkraken

@berndhahnebach
Copy link
Contributor

We could do the rebase and just merge it. It is not recommended but faster than dozens of comments ...

We just would loose the connection to the PR. If that matters we could make the rebase make a new PR and connect them in the comments.

emul fast move covers case of random one axis moves.
@berndhahnebach
Copy link
Contributor

berndhahnebach commented Oct 25, 2021

Squashed it and rebased it on master ...

https://github.com/berndhahnebach/FreeCAD_bhb/commits/path_marlinpost_pr

All the module marlin_post.py has changed ...

berndhahnebach@a03378d?diff=split

even the licence text was totally formated ...

@luzpaz
Copy link
Contributor

luzpaz commented Nov 18, 2021

@berndhahnebach care to rebase again?

@luzpaz
Copy link
Contributor

luzpaz commented Dec 22, 2021

@berndhahnebach ping

@sliptonic
Copy link
Member

This is silly. I'm closing this pull request. Original submitter isn't interested in getting it merged. I've tried diffing it with the current marlin post and there are massive changes but I can't see what specifically they address.
This version appears to be based on a very old version of the post and much has already been done on it.
In other words, what problems with the current marlin post are fixed by this version? I don't know.
We aren't getting complaints about the marlin post so I'm closing for now. If OP wants to reopen it, that's fine. Please reconcile your work with the current version of marlin in master branch.

@sliptonic sliptonic closed this Jan 6, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
WB CAM Related to the CAM/Path Workbench
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants