#!/usr/bin/python

# This code is from http://www.cnczone.com/forums/pcb_milling/82628-cheap_simple_height-probing.html

# sudo apt-get install python-tk

##  Introduction
#     This code when run will ask you: 
#     (1) what units your file uses (inch or mm)
#     (2) how many steps you want your probe gride to have on the X axis
#     (3) how many steps you want your probe gride to have on the Y axis
#     (4) which PCB G code file you want to etch

#     It then gets your mill to probe a grid on a blank PCB based on your spacing
#     It stores the probe Z values to memory, then uses those Z values to adjust Z heights for etch moves along the now well probed PCB.
#     You can reuse the generated file again and again as it will always start out by re-probing the surface before etching. 
#     It is based on a neat idea by Poul-Henning Kamp. See: http://phk.freebsd.dk/CncPcb/index.html

##  Where the output is saved
#     The output is saved to a new file where 
#     file_out_name = file_in_name + file_name_suffix, 
#     and the default file out suffix (which you could change by modifying the code) is "_Zadj_[grid size].ngc"
#     Finally, and this is very handy if you are running the Python code in EMC2, it outputs the file to the screen, and you are ready to roll.
#     You can always comment out the print line if you don't need it.

##  What it doesn't do
#     It doesn't reject spurious probe values - you need to make sure the PCB blank you use is clean and free of debris
#     It doesn't optimise X Y paths (you could use opti_qt.exe to do this first - see http://pcbgcode.org/read.php?6,5,5)) 
#     It doesn't optimise X Y moves between drills (Opti_qt can do this for 'simple drill moves', Gopt is apparently better for multiple drill sizes)
#     It doesn't adjust Z heights during arc etching (G02 and G03 moves) - this should not be a problem if arcs have a diameter of less than 10mm.
#     Feel free to address any of these issues!

##  How it works
#     This Python code parses your selected G code file and looks at every etch move: G01 Xaa Ybb Zcc Fdd, where Z is greater than -0.5 
#     It ignores milling and drilling moves ie where Z is deeper than -0.5mm.  
#     It finds the max and min values of X and Y from amongst the file's etch moves.
  
#     It then generates a custom G code routine that will probe a grid that encompasses those max and min X and Y values. 
#     The probe points are spaced by the selected grid spacing, and the Z values at each probe point are stored in memory. 

#     It generates a G code subroutine that will draw on the stored probe data and does the etching on the PCB at the adjusted heights. 
#     For long etch moves it puts in a way point at a distance of half the X grid spacing and calculates a new etch depth for each way point 
#     as it goes along.

#     All the etch moves in the original file(ie G01 Xaa Ybb Zcc Fdd (where Z is greater than -0.5) are then replaced by a subroutine call
#        in the format O200 sub [x_start] [y_start] [aa] [bb]  (where O200 is the etch subroutine referred to above)

# You can change the defaults a little further down

##  For ease of use you may want to change the default start up directory here:

# UNITLESS DEFAULTS: These values are not unit sensitive (and you can change these here too)
# Remember in EMC you can probe up to 4000 points, in Mach3 up to 1000
initial_directory = '/home/'
X_grid_lines      =  10
Y_grid_lines      =   5
units = "mm"
grid_def = "step size"
file_in_name = initial_directory
  
def Unit_set():
    global units,units_G_code,G_dest, X_dest,Y_dest,Z_dest,etch_definition,etch_speed,probe_speed,z_safety,z_probe
    global etch_depth,etch_max,etch_min,z_trivial,z_probe_detach,grid_clearance,step_size,step_max,step_min
    global X_grid_lines,Y_grid_lines,grid_max,grid_min,grid_def

       # INCH DEFAULTS: if units are inches, set the defaults in inches (you can change these here)
    if units == "inch": 
        units_G_code      =    20
        G_dest            =   '00'
        X_dest            = -3.000
        Y_dest            =  2.000
        Z_dest            =  2.000
        etch_definition   = -0.020
        etch_speed        =  4.000
        probe_speed       =  0.400
        z_safety          =  0.150  
        z_probe           = -0.150
        etch_depth        =  0.004
        etch_max          =  0.020 
        z_trivial         =  0.001
        z_probe_detach    =  2.000
        grid_clearance    =  0.001
        step_size         =  0.400

        # MM DEFAULTS: if units are mm, set the defaults in mm (you can change these here too)
    elif units == "mm": 
        units_G_code      =    21
        G_dest            =   '00'
        X_dest            = -80.00
        Y_dest            =  40.00
        Z_dest            =  40.00
        etch_definition   =  -0.50
        etch_speed        = 120.00
        probe_speed       =  25.00
        z_safety          =   1.00  
        z_probe           =  -1.00
        etch_depth        =   0.10
        etch_max          =   0.50
        z_trivial         =   0.02
        z_probe_detach    =  40.00
        grid_clearance    =   0.01
        step_size         =  10.00

def Unit_sel():
    global units, G_dest, X_dest,Y_dest,Z_dest,etch_definition,etch_speed,probe_speed,z_safety,z_probe
    global etch_depth,etch_max,etch_min,z_trivial,z_probe_detach,grid_clearance,step_size,step_max,step_min
    global X_grid_lines,Y_grid_lines,grid_max,grid_min,grid_def, file_in_name
    
    units = get_units.get()
    Unit_set()
    
    # Refresh the defaults in the display
    RB_step.config(text = "grid step size (" + units + ")")
    L_etch_depth.config(text = "Etch depth (" + units + "):")
    Ent_etch_depth.delete(0, END)
    Ent_etch_depth.insert(0, etch_depth)
    Ent_step.config(state=NORMAL)
    Ent_step.delete(0, END)
    Ent_step.insert(0, step_size)
    if get_grid_def.get() == "grid lines":
        Ent_step.config(state=DISABLED)
  
def Def_sel():
    grid_def = get_grid_def.get()
    if grid_def == "step size":
        Ent_X.config(state=DISABLED)
        Ent_Y.config(state=DISABLED)
        Ent_step.config(state=NORMAL)
    
    elif grid_def == "grid lines":
        Ent_X.config(state=NORMAL)
        Ent_Y.config(state=NORMAL)
        Ent_step.config(state=DISABLED)
        Ent_X.delete(0, END)
        Ent_X.insert(0, X_grid_lines)
        Ent_Y.delete(0, END)
        Ent_Y.insert(0, Y_grid_lines)        

def Browse():
    global file_in_name
    import tkFileDialog
    file_in_name = tkFileDialog.askopenfilename(parent=top,initialdir=initial_directory,
                                        filetypes= [('nc files', '*.ngc'),('nc files', '*.nc')],title='Choose file to import:')
    L_file_in_name.config(text = file_in_name)

def OK() : top.destroy()

# Entry validation functions
def IntCheck(new_string) :
    if new_string == "": new_string = "0"
    try:
        v = int(new_string)
        if v > 99 or v < 0: return False
        return True
            # True means accept the new string
    except ValueError:
        return False
            # False means don't accept it

def EtchCheck(new_string) :
    global etch_max
    if new_string == "": new_string = "0"
    try:
        v = float(new_string)
        if v > etch_max or v < 0: return False
        if len(new_string) > 6 : return False
        return True        
    except ValueError:
        return False
            # False means don't accept it

def StepCheck(new_string) :
    if new_string == "": new_string = "0"
    try:
        v = float(new_string)
        if v < 0: return False
        if len(new_string) > 6 : return False
        return True
            # True means accept the new string
    except ValueError:
        return False
            # False means don't accept it

def get_num(line,char_ptr,num_chars):
    char_ptr=char_ptr+1
    numstr = ''
    good   = '-.0123456789'  
    while char_ptr < num_chars:
        digit = line[char_ptr]
        if good.find(digit) != -1:
           numstr = numstr + digit
           char_ptr = char_ptr + 1
        else: break    
    return numstr

def test_X(X_min, X_max):
    if X_dest < X_min : X_min = X_dest
    elif X_dest > X_max : X_max = X_dest
    return X_min, X_max

def test_Y(Y_min, Y_max):
    if Y_dest < Y_min : Y_min = Y_dest
    elif Y_dest > Y_max : Y_max = Y_dest
    return Y_min, Y_max
            
from Tkinter import *
## Don't change these ...
file_in  = []
file_out = []
intro    = []
numstr   = ''
char     = ''

Unit_set()
top = Tk()
top.title("Etch_Z_adjust setup")

# Define the Tkinter variables
get_units       = StringVar()
get_grid_def    = StringVar()
get_X           = IntVar()
get_Y           = IntVar()
get_step        = DoubleVar()
get_etch        = DoubleVar()
get_file_in     = StringVar()

# define the label, radiobutton, button and entry widgets:
# Label widgets:
L_blank1        = Label(top, text="")
L_blank2        = Label(top, text="")
L_blank3        = Label(top, text="")                       
L_blank4        = Label(top, text="")
L_blank5        = Label(top, text="")    
L_units         = Label(top, text="Units to use:")
L_grid_def      = Label(top, text="Define grid by:")
L_X_eq          = Label(top, text="X  = ")
L_Y_eq          = Label(top, text="Y  = ")
L_etch_depth    = Label(top, text="Etch depth:")
L_file_in_quest = Label(top, text="File to import:")
L_file_in_name  = Label(top, text="")

# Radiobutton widgets:
RB_inch         = Radiobutton(top, padx=45, text="inch",                 variable=get_units,    value="inch",       command=Unit_sel)
RB_mm           = Radiobutton(top, padx=45, text="millimetre",           variable=get_units,    value="mm",         command=Unit_sel)
RB_lines        = Radiobutton(top, padx=45, text="number of grid lines", variable=get_grid_def, value="grid lines", command=Def_sel)
RB_step         = Radiobutton(top, padx=45, text="grid step size",       variable=get_grid_def, value="step size",  command=Def_sel)

# Button widgets:
B_browse        = Button(top, text ="Browse...", command = Browse)
B_cancel        = Button(top, text ="CANCEL",    command = OK)
B_OK            = Button(top, text ="OK",        command = OK)

# Entry widgets with ***validation***

# top.register(func_name) gives a number that when called by validatecommand as a command
# enables validatecommand to pass Tk % parameters to the function called eg func_name. 
# ' %P' (the space is important) is the string value in the Entry widget that would result if the edit was allowed.
# See http://www.tcl.tk/man/tcl8.4/TkCmd/entry.htm#M16 for a list of these % parameters.

val_int  = top.register(IntCheck)
val_etch = top.register(EtchCheck)
val_step = top.register(StepCheck)

Ent_X           = Entry(master=top, width = 2,  textvariable = get_X,
                        validate = "key", validatecommand = val_int + ' %P')
Ent_Y           = Entry(master=top, width = 2,  textvariable = get_Y,
                        validate = "key", validatecommand = val_int + ' %P')
Ent_step        = Entry(master=top, width = 6,  textvariable = get_step,
                        validate = "key", validatecommand = val_step + ' %P')
Ent_etch_depth  = Entry(master=top, width = 6,  textvariable = get_etch,
                        validate = "key", validatecommand = val_etch + ' %P ')
Ent_file_in     = Entry(master=top, width = 70, textvariable =  get_file_in, justify = RIGHT, state = DISABLED)  


# lay out the widgets:
L_file_in_quest.grid(row=0, column=0)
B_browse.grid       (row=0, column=1)
L_file_in_name.grid (row=1, column=0, columnspan = 7)
L_blank5.grid       (row=2, column=0, columnspan = 7)

L_units.grid        (row=3, column=0, sticky=W, padx = 25)
RB_mm.grid          (row=4, column=0, sticky=W)
RB_inch.grid        (row=5, column=0, sticky=W)
L_blank2.grid       (row=6, column=0)

L_etch_depth.grid   (row=7, column=0, sticky=W, padx = 25)
Ent_etch_depth.grid (row=7, column=1, sticky=W)
L_blank3.grid       (row=8, column=1)

L_grid_def.grid     (row=3, column=1, sticky=W, padx = 25)
RB_step.grid        (row=4, column=1, sticky=W)
Ent_step.grid       (row=4, column=2, sticky=W)

RB_lines.grid       (row=5, column=1, sticky=W)
L_X_eq.grid         (row=6, column=1, sticky=E)
Ent_X.grid          (row=6, column=2, sticky=W)
L_Y_eq.grid         (row=7, column=1, sticky=E)
Ent_Y.grid          (row=7, column=2, sticky=W)


B_cancel.grid       (row=11, column=0)
B_OK.grid           (row=11, column=1)
L_blank4.grid       (row=12, column=1)

# set the initial units to mm 
RB_mm.select()
Unit_sel()

# set the initial grid definition to step size
RB_step.select()
Def_sel()

top.mainloop()

units        = get_units.get()
grid_def     = get_grid_def.get()
step_size    = get_step.get()
etch_depth   = - get_etch.get()
X_grid_lines = get_X.get()
Y_grid_lines = get_Y.get()

# read in G code file
if file_in_name != None:   
    f = open(file_in_name, 'r')
    for line in f:
        file_in.append(line)
    f.close()

# Check for max and min values in your ngc file
is_first_X = True
is_first_Y = True
is_first_Z = True

# check each line
line_ptr=0
num_lines=len(file_in)
while line_ptr < num_lines:
    line = file_in[line_ptr]
    X_start = X_dest
    Y_start = Y_dest
    Z_start = Z_dest
    
# check each character
    char_ptr = 0
    num_chars= len(line)
    while char_ptr < num_chars:
        char = line[char_ptr]      
        if '(;'.find(char) != -1:
            break              
        elif char == 'G' :
            G_dest = get_num(line,char_ptr,num_chars)   
        elif char == 'X' :
            X_dest = float(get_num(line,char_ptr,num_chars))
        elif char == 'Y' :
            Y_dest = float(get_num(line,char_ptr,num_chars))
        elif char == 'Z' :
            Z_dest = float(get_num(line,char_ptr,num_chars))
        char_ptr = char_ptr + 1
        
    # if the line is an etch move, then replace the line with an etch call        
    if G_dest == '01' and Z_dest > etch_definition:

        line = 'O200 call [%.4f] [%.4f] [%.4f] [%.4f]\n' % (X_start, Y_start, X_dest, Y_dest)

        # and now check for max and min X and Y values
        if is_first_X == True :
            X_min = X_dest
            X_max = X_dest
            is_first_X = False
        else : (X_min, X_max) = test_X(X_min, X_max)

        if is_first_Y == True :
            Y_min = Y_dest
            Y_max = Y_dest
            is_first_Y = False
        else : (Y_min, Y_max) = test_Y(Y_min, Y_max)
         
    file_out.append(line)
    line_ptr=line_ptr+1

if is_first_X == False :

    # then there were etch moves so get to work!

    # first stretch the X and Y max and min values a _tiny_ amount so the grid is just outside all the etch points
    X_min = X_min - grid_clearance
    X_max = X_max + grid_clearance
    Y_min = Y_min - grid_clearance
    Y_max = Y_max + grid_clearance    

    # Use max and min values for the etch moves to work out the probe grid dimensions
    X_span = X_max - X_min
    X_grid_origin = X_min 
    Y_span = Y_max - Y_min
    Y_grid_origin = Y_min

    # Now work out the X and Y step sizes
    if grid_def == "step size" :  
        X_grid_lines = 2 + int(X_span/step_size)
        Y_grid_lines = 2 + int(Y_span/step_size)
        
    # Make sure grid lines are at least 2 
    if X_grid_lines < 2 : X_grid_lines = 2
    if Y_grid_lines < 2 : Y_grid_lines = 2
    
    Y_step_size = Y_span / (Y_grid_lines - 1)
    X_step_size = X_span / (X_grid_lines - 1)

    # Now we can name the output file
    file_name_suffix = "_Zadj_%dx%d.ngc" % (X_grid_lines, Y_grid_lines)
    n = file_in_name.rfind(".")
    if n != -1:
        file_out_name = file_in_name[0:n] + file_name_suffix
    else: file_out_name = file_in_name + file_name_suffix

    # OK now output the G code intro (probe subroutine + etch subroutine + customised grid code)
    from time import localtime, strftime
    line = "(imported from   " + file_in_name + " at " + strftime("%I:%M %p on %d %b %Y", localtime())+ ")\n"
    intro.append(line)
    line = "(output saved as " + file_out_name + ")\n\n"
    intro.append(line)
    line =  "(G code configuration section)\n(you can change these values in the python code or in the G code output:)\n"
    intro.append(line)
    line = ("G%2d (" + units + ")\n") % (units_G_code)
    intro.append(line)
    line = "#<_etch_speed>    =  %.4f \n" % (etch_speed)
    intro.append(line)
    line = "#<_probe_speed>   =   %.4f \n" % (probe_speed)
    intro.append(line)
    line = "#<_z_safety>      =    %.4f \n" % (z_safety)
    intro.append(line)
    line = "#<_z_probe>       =   %.4f \n" % (z_probe)
    intro.append(line)
    line = "#<_etch_depth>    =   %.4f \n\n" % (etch_depth)
    intro.append(line)
    line = "(Don't change these values here, they were calculated earlier)\n"
    intro.append(line)
    line =  '#<_x_grid_origin> =  %.4f \n' % (X_grid_origin) 
    intro.append(line )
    line =  '#<_x_grid_lines>  =    %.4f \n' % (X_grid_lines )
    intro.append(line)
    line =  '#<_y_grid_origin> =    %.4f \n' % (Y_grid_origin)
    intro.append(line)
    line =  '#<_y_grid_lines>  =    %.4f \n' % (Y_grid_lines )
    intro.append(line)
    line =  '#<_x_step_size>   =    %.4f \n' % (X_step_size)  
    intro.append(line)
    line =  '#<_y_step_size>   =    %.4f \n' % (Y_step_size)  
    intro.append(line)

    line =  """#<_last_z_etch>   =   #<_etch_depth>

    O100 sub (probe subroutine)
         G00 X [#<_x_grid_origin> + #<_x_step_size>*#<_grid_x>]   
         G38.2 Z#<_z_probe> F#<_probe_speed>   
         #[1000 + #<_grid_x> + #<_grid_y> * #<_x_grid_lines>] = #5063   
         G00 Z#<_z_safety>
    O100 endsub

    O200 sub (etch subroutine)
         ( This subroutine calculates way points on the way to x_dest, y_dest, )
         ( and calculates the Z adjustment at each way point.                  )
         ( It moves to each way point using the etch level and etch speed set  )
         ( in the configuration section above.                                 )

         #<x_start>         = #1
         #<y_start>         = #2
         #<x_dest>          = #3
         #<y_dest>          = #4
         #<distance>        = sqrt[ [#<x_dest> - #<x_start>]**2 + [#<y_dest> - #<y_start>]**2 ]
         #<waypoint_number> = fix[#<distance> / [#<_x_step_size>/2]]
         #<x_step>          = [[#<x_dest> - #<x_start>] / [#<waypoint_number> + 1]]
         #<y_step>          = [[#<y_dest> - #<y_start>] / [#<waypoint_number> + 1]]
         
         O201 while [#<waypoint_number> ge 0]
              #<_x_way>     =  [#<x_dest> - #<waypoint_number> * #<x_step>]
              #<_y_way>     =  [#<y_dest> - #<waypoint_number> * #<y_step>]   
              #<_grid_x_w>  =  [[#<_x_way> - #<_x_grid_origin>]/#<_x_step_size>]
              #<_grid_y_w>  =  [[#<_y_way> - #<_y_grid_origin>]/#<_y_step_size>]
              #<_grid_x_0>  =  fix[#<_grid_x_w>]
              #<_grid_y_0>  =  fix[#<_grid_y_w>]
              #<_grid_x_1>  =  fup[#<_grid_x_w>]
              #<_grid_y_1>  =  fup[#<_grid_y_w>]
              #<_cell_x_w>  =  [#<_grid_x_w> - #<_grid_x_0>]
              #<_cell_y_w>  =  [#<_grid_y_w> - #<_grid_y_0>]

              (Bilinear interpolation equations from http://en.wikipedia.org/wiki/Bilinear_interpolation)
              #<F00>        =  #[1000 + #<_grid_x_0> + #<_grid_y_0> * #<_x_grid_lines>]
              #<F01>        =  #[1000 + #<_grid_x_0> + #<_grid_y_1> * #<_x_grid_lines>]
              #<F10>        =  #[1000 + #<_grid_x_1> + #<_grid_y_0> * #<_x_grid_lines>]
              #<F11>        =  #[1000 + #<_grid_x_1> + #<_grid_y_1> * #<_x_grid_lines>] 
              #<b1>         =  #<F00>
              #<b2>         =  [#<F10> - #<F00>]
              #<b3>         =  [#<F01> - #<F00>]
              #<b4>         =  [#<F00> - #<F10> - #<F01> + #<F11>]          
              #<z_adj>      =  [#<b1> + #<b2>*#<_cell_x_w> + #<b3>*#<_cell_y_w> + #<b4>*#<_cell_x_w>*#<_cell_y_w>]
              #<z_etch>     =  [#<_etch_depth> + #<z_adj>]
                       
              (ignore trivial z axis moves)
              """
    intro.append(line)
    line = "O202 if [abs[#<z_etch> - #<_last_z_etch> ] lt %.4f]" % (z_trivial)
    intro.append(line)
    line = """
                   #<z_etch> = #<_last_z_etch> 
              O202 else
                   #<_last_z_etch> = #<z_etch>
              O202 endif
              
              (now do the move)
              G01 X#<_x_way>  Y#<_y_way>  Z[#<z_etch>] F[#<_etch_speed>]
              
              (and then go to the next way point)
              #<waypoint_number> = [#<waypoint_number> - 1]         
         O201 endwhile
    O200 endsub

    ( Probe section                                                 )
    ( This section probes the grid and writes the probe results     )
    ( sequentially to variables #1000, #1001, #1002 etc etc         )
    ( such that the result at x,y on the grid is stored in          )
    (               #[1000 + x + y*[x_grid_lines]]                  )

    ( You'll run out of memory if you probe more than 4,000 points! ) 

    #<_grid_x> = 0
    #<_grid_y> = 0

    G00 Z#<_z_safety>
    G00 X#<_x_grid_origin> Y#<_y_grid_origin>
    O001 while [#<_grid_y> lt #<_y_grid_lines>]
         G00 Y[#<_y_grid_origin> + #<_y_step_size> * #<_grid_y>]    
         O002 if [[#<_grid_y> / 2] - fix[#<_grid_y> / 2] eq 0]
              #<_grid_x> = 0
              O003 while [#<_grid_x> lt #<_x_grid_lines>]
                   O100 call (probe subroutine)
                   #<_grid_x> = [#<_grid_x> + 1]
              O003 endwhile       
         O002 else 
              #<_grid_x> = #<_x_grid_lines>
              O004 while [#<_grid_x> gt 0]
                   #<_grid_x> = [#<_grid_x> - 1]  
                   O100 call (probe subroutine)  
              O004 endwhile
         O002 endif
         #<_grid_y> = [#<_grid_y> + 1]
    O001 endwhile
    """
    intro.append(line)
    line = "G00 Z%.4f \n" % (z_probe_detach)
    intro.append(line)
    line = """
    ( Main ngc program section                                                          )
    ( Python has replaced all G01 etch moves from original file eg G01 Xaa Ybb Zcc Fdd  )
    ( with an adjusted etch move in the format O200 sub [x_start] [y_start] [aa] [bb]   )
    ( O200 is the etch subroutine                                                       )

    (MSG, OK folks - power up the mill...)

    M00

    """
    intro.append(line)

    # create and then save the output file
    file_out = intro + file_out

    f = open(file_out_name, 'w')
    for line in file_out:
        f.write(line)
    f.close()


    # now output the altered ngc file to the screen
    for line in file_out:
        print line,
    
else:
    from Tkinter import *
    def OK() : top.destroy()

    top = Tk()
    top.title("   Check file")
    var = IntVar()

    L1 = Label(top, text="")
    L2 = Label(top, text="Sorry, no etch moves found in that file.", font = "12")
    L3 = Label(top, text="")
    L4 = Label(top, text="Check file name, units, etch move definition etc.", font = "12")
    L5 = Label(top, text="")
    L6 = Label(top, text="")
    B1 = Button(top, text ="OK", command = OK, font = "18")

    L1.pack()
    L2.pack(anchor = W, padx=45)
    L3.pack()
    L4.pack(anchor = W, padx=45)
    L5.pack()    
    B1.pack()
    L6.pack()
    top.mainloop()
  
    
