#!/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. ) # = #1 # = #2 # = #3 # = #4 # = sqrt[ [# - #]**2 + [# - #]**2 ] # = fix[# / [#<_x_step_size>/2]] # = [[# - #] / [# + 1]] # = [[# - #] / [# + 1]] O201 while [# ge 0] #<_x_way> = [# - # * #] #<_y_way> = [# - # * #] #<_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) # = #[1000 + #<_grid_x_0> + #<_grid_y_0> * #<_x_grid_lines>] # = #[1000 + #<_grid_x_0> + #<_grid_y_1> * #<_x_grid_lines>] # = #[1000 + #<_grid_x_1> + #<_grid_y_0> * #<_x_grid_lines>] # = #[1000 + #<_grid_x_1> + #<_grid_y_1> * #<_x_grid_lines>] # = # # = [# - #] # = [# - #] # = [# - # - # + #] # = [# + #*#<_cell_x_w> + #*#<_cell_y_w> + #*#<_cell_x_w>*#<_cell_y_w>] # = [#<_etch_depth> + #] (ignore trivial z axis moves) """ intro.append(line) line = "O202 if [abs[# - #<_last_z_etch> ] lt %.4f]" % (z_trivial) intro.append(line) line = """ # = #<_last_z_etch> O202 else #<_last_z_etch> = # O202 endif (now do the move) G01 X#<_x_way> Y#<_y_way> Z[#] F[#<_etch_speed>] (and then go to the next way point) # = [# - 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()