From 77d7182b9a81958194c30f316608eaa9b254947c Mon Sep 17 00:00:00 2001 From: Mike Date: Sun, 9 Aug 2015 23:20:24 -0700 Subject: [PATCH 1/9] initial work on new features --- command.py | 4 +- lib/argv.py | 28 ++-- lib/communicate.py | 23 ++-- lib/correction_surface.py | 100 ++++++++++++++ lib/gcode.py | 243 ++++++++++++++++++---------------- lib/gparse.py | 229 ++++++++++++++++++++++++++++++++ lib/grbl_status.py | 156 ++++++++++++++++++++++ lib/tool.py | 68 +++++----- modify.py | 53 ++++---- new_GRBL_configuration_readme | 47 +++++++ optimize.py | 38 ++---- orient.py | 212 ++++++++++++++--------------- probe_surface.py | 103 ++++++++++++++ probe_test.out | 3 + visualize.py | 7 +- zcorrect.py | 61 +++++++++ 16 files changed, 1036 insertions(+), 339 deletions(-) create mode 100644 lib/correction_surface.py create mode 100644 lib/gparse.py create mode 100644 lib/grbl_status.py create mode 100644 new_GRBL_configuration_readme create mode 100644 probe_surface.py create mode 100644 probe_test.out create mode 100644 zcorrect.py diff --git a/command.py b/command.py index 9c1b32f..602395a 100755 --- a/command.py +++ b/command.py @@ -15,7 +15,7 @@ with Communicate(args.device, args.speed, timeout=args.timeout, debug=args.debug, quiet=args.quiet) as serial: - + # now we send commands to the grbl, and wait waitTime for some response. while True: # Get some command @@ -26,7 +26,7 @@ if x in ['~']: serial.run('~\n?') # run it if is not a quit switch serial.run(x) - + except KeyboardInterrupt: puts(colored.red('Emergency Feed Hold. Enter "~" to continue')) serial.run('!\n?') diff --git a/lib/argv.py b/lib/argv.py index abd37a1..3acc9ae 100755 --- a/lib/argv.py +++ b/lib/argv.py @@ -8,14 +8,14 @@ from util import error -def arg(description=None, getDevice=True, - defaultSpeed=9600, defaultTimeout=0.70, +def arg(description=None, getDevice=True, + defaultSpeed=115200, defaultTimeout=0.70, getFile=False, getMultiFiles=False, otherOptions=None): '''This is a simple arugment parsing function for all of the command line tools''' if not description: description='python grbl arguments' - + parser = argparse.ArgumentParser(description=description) parser.add_argument('-q','--quiet', action='store_true', @@ -25,16 +25,16 @@ def arg(description=None, getDevice=True, action='store_true', default=False, help='[DEBUG] use a fake Serial port that prints to screen') - + # by default just get the device # HOWEVER if we want a file to be run, get it FIRST if getFile: nargs = '+' if getMultiFiles else 1 - parser.add_argument('gcode', + parser.add_argument('gcode', nargs=nargs, - type=argparse.FileType('r'), + type=argparse.FileType('r'), help='gCode file to be read and processed.') - + # if we want a device get an optional speed / and a device if getDevice: parser.add_argument('-s','--speed', @@ -47,13 +47,13 @@ def arg(description=None, getDevice=True, default=defaultTimeout, type=float, help='Serial Port Timeout: Amount of time to wait before gathering data for display [%.2f]'%(defaultTimeout)) - + parser.add_argument('device', nargs='?', # action='store_true', default=False, - help='GRBL serial dev. Generally this should be automatically found for you. You should specify this if it fails, or your have multiple boards attached.') - + help='GRBL serial dev. Generally this should be automatically found for you. You should specify this if it fails, or your have multiple boards attached.') + # For any specalized options lets have a general import method if otherOptions: if isinstance(otherOptions,(dict)): @@ -66,12 +66,12 @@ def arg(description=None, getDevice=True, parser.add_argument(*args, **option) args = parser.parse_args() - + # lets see if we can find a default device to connect too. if args.debug: args.device='fakeSerial' if (getFile) and (not getMultiFiles): args.gcode = args.gcode[0] if getDevice and not args.device: - # Where they generally are: + # Where they generally are: devs = ['/dev/tty.usb*','/dev/ttyACM*','/dev/tty.PL*','/dev/ttyUSB*'] founddevs = [] for d in devs: @@ -83,8 +83,8 @@ def arg(description=None, getDevice=True, else: parser.print_help() error('Found %d device(s) -- You need to connect a device, update %s, or specify wich device you want to use.'%(len(founddevs),sys.argv[0])) - - + + args.name = sys.argv[0] args.argv = sys.argv return args diff --git a/lib/communicate.py b/lib/communicate.py index 8ad8e8b..6408911 100755 --- a/lib/communicate.py +++ b/lib/communicate.py @@ -13,7 +13,7 @@ def __init__(self, device, speed, debug=False, quiet=False, timeout=None): # select the right serial device if debug: s = FakeSerial() else: s = serial.Serial(device, speed, timeout=timeout) - + if not quiet: print '''Initializing grbl at device: %s Please wait 1 second for device...'''%(device) s.write("\r\n\r\n") @@ -26,16 +26,17 @@ def __init__(self, device, speed, debug=False, quiet=False, timeout=None): # self.run('$H') # self.run('G20 (Inches)') # self.run('G90 (Absolute)') - - + + def run(self, cmd, singleLine=False): '''Extends either serial device with a nice run command that prints out the command and also gets what the device responds with.''' puts(colored.blue(' Sending: [%s]'%cmd ), newline=(not singleLine)) + out = '' # i think it is better to initialize out before sending the command self.write(cmd+'\n') - out = '' time.sleep(self.timeout) # while s.inWaiting() > 0: out += s.read(10) + #Why is it not self.s.inWaiting() ???? while self.inWaiting() > 0: out += self.readline() if out != '': if singleLine: @@ -44,27 +45,27 @@ def run(self, cmd, singleLine=False): else: puts(colored.green(''.join([' | '+o+'\n' for o in out.splitlines()]))) return out - + def sendreset(self): '''Sends crtl-x which is the reset key :: \030 24 CAN \x18 ^X (Cancel) ''' self.s.write(24) - + def reset(self): '''sends crtl-x to grbl to reset it -- clean up in the future''' self.s.write(24) def __enter__(self): return self - + def __exit__(self, type, value, traceback): #self.s.setDTR(False) #time.sleep(0.022) #self.s.setDTR(True) #self.s.close() return isinstance(value, TypeError) - + def __getattr__(self, name): '''if no command here, see if it is in serial.''' try: @@ -76,14 +77,14 @@ def __getattr__(self, name): class FakeSerial(): - '''This is a fake serial device that mimics a true serial devie and + '''This is a fake serial device that mimics a true serial devie and does fake read/write.''' def __init__(self): '''init the fake serial and print out ok''' self.waiting=1 # If we are waiting self.ichar=0 # index of the character that we are on. self.msg='ok' # the message that we print when we get any command - + def __getattr__(self, name): print 'DEBUG SERIAL: %s'%(name) return self.p @@ -115,4 +116,4 @@ def inWaiting(self): out = self.waiting if self.waiting == 0: self.waiting = 1 - return out \ No newline at end of file + return out diff --git a/lib/correction_surface.py b/lib/correction_surface.py new file mode 100644 index 0000000..6b3984d --- /dev/null +++ b/lib/correction_surface.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python + +#this code should use a 2 dimensional array of z_positions +#the element 0,0 is 0 and all other elements are relative z positions +#there should also be a probe_step_x value and a probe_step_y value +#these define the distsnces between the points measured in the z_positions array + + +#imports +from numpy import arange +from lib import argv +from lib.gparse import gparse +from lib.grbl_status import GRBL_status +from lib.communicate import Communicate +from clint.textui import puts, colored +import time, readline +import numpy as np +import re + + +class CorrectionSurface(): + def __init__(self,): + self.x_step = "nan" + self.y_step = "nan" + self.array = [] + self.Load_Correction_Surface() + + def Load_Correction_Surface(self, surface_file_name = 'probe_test.out' ): + + #initialize and load the correction surface + Surface_Data = [] + Surface_Data = np.loadtxt(surface_file_name, delimiter=',', comments = '#') + #correction_surface.array = Surface_Data + self.array = Surface_Data + + #display the dataset to be used + puts(colored.yellow('Surface Z Data Matrix')) + print Surface_Data + + probe_data_file = open(surface_file_name) + + #probe_data = probe_data_file.read() + + # retrieve the X and Y step sizes that scale the correction surface + for line in probe_data_file: + line = line.strip() + + if re.search('#', line,flags = re.IGNORECASE): + puts(colored.green('extracting scale data from file header')) + puts(colored.green( line)) + if re.search(' X_STEP:', line,flags = re.IGNORECASE): + #X_STEP:,0.5000 + X_STEP_INFO = re.findall('X_STEP:,\d*\.\d*,', line, flags = re.IGNORECASE)[0] + if X_STEP_INFO: + X_STEP = float(X_STEP_INFO.split(',')[1]) + puts(colored.yellow( 'x step size: {:.4f}'.format(X_STEP))) + #correction_surface.x_step = X_STEP + self.x_step = X_STEP + else: + puts(colored.red( 'x step size: not found!')) + if re.search(' Y_STEP:', line,flags = re.IGNORECASE): + #X_STEP:,0.5000 + Y_STEP_INFO = re.findall('Y_STEP:,\d*\.\d*,', line, flags = re.IGNORECASE)[0] + if Y_STEP_INFO: + Y_STEP = float(Y_STEP_INFO.split(',')[1]) + puts(colored.yellow( 'Y step size: {:.4f}'.format(Y_STEP))) + #correction_surface.Y_step = Y_STEP + self.y_step = Y_STEP + else: + puts(colored.red( 'Y step size: not found!')) + return self + + def estimate_surface_z_at_pozition(self,x,y): + #find the bounding triangle on the correction surface closest to the x,y coordinet + #begin with using the nearest node to the point + Ns_x = round(x/self.x_step) + Ns_y = round(y/self.y_step) + #calculate distance from the nearest node + epsilon_x= x - Ns_x*self.x_step + epsilon_y= y - Ns_y*self.x_step + #find the next two nearest nodes + if (x > Ns_x*self.x_step): + Nf_x = Ns_x + 1 + delta_x = self.x_step + else: + Nf_x = Ns_x + -1 + delta_x = -1.0*self.x_step + if (y > Ns_y*self.x_step): + Nf_y = Ns_y + 1 + delta_y = self.y_step + else: + Nf_y = Ns_y + -1 + delta_y = -1.0*self.y_step + #calculate the slopes (x and y) of the plane defined by the triangle of bounding nodes + slope_x = (self.array[Nf_x][Ns_y] - self.array[Ns_x][Ns_y])/delta_x + slope_y = (self.array[Ns_x][Nf_y] - self.array[Ns_x][Ns_y])/delta_y + #use the slope information and the origin intercept at the clsest node + # to estimate the z position of the surface + z_at_position = self.array[Ns_x][Ns_y] + epsilon_x*slope_x + epsilon_y*slope_y + return z_at_position diff --git a/lib/gcode.py b/lib/gcode.py index d212c28..f39fccf 100755 --- a/lib/gcode.py +++ b/lib/gcode.py @@ -7,7 +7,7 @@ from pprint import pprint from clint.textui import colored, puts, indent, progress -CMDS='GXYZMPIJ' +CMDS='GXYZFEMPIJK' TEMPLATE='''(UPDATED by ${tag}) (Starting) @@ -33,119 +33,128 @@ class GCode(list): - def __init__(self, gcode, limit=None): - '''start with a gcode ascii file''' - self.limit = limit - if isinstance(gcode, str): - with open(os.path.expanduser(gcode),'r') as f: - lines = f.readlines() - filename = gcode - else: - filename = gcode.name - lines = gcode.readlines() - gcode.close() - self.filename = filename - self.lines = lines - # self.ready = False - - # def append(self,item): - # '''add the next nice to the object''' - # if self.ready : self.ready = False - # super(GCode, self).append(item) - - def parse(self): - '''By default .parse() grabs only the G## commands for creating toolpaths in some space - if you need everything use .parseall()''' - everything = self._parse() - for item in everything: - toappend = False - for cmd in CMDS: - if cmd in item: - toappend=True - if toappend: - self.append(item) - - def parseAll(self): - '''Gets everything so that we can print it back out''' - everything = self._parse() - for item in everything: - self.append(item) - - def _parse(self): - ''' [INTERNAL] convert the readlines into a parsed set of commands and values''' - puts(colored.blue('Parsing gCode')) - - comment = r'\(.*?\)' - whitespace = r'\s' - command = r''.join([r'(?P<%s>%s(?P<%snum>-?\d+(?P<%sdecimal>\.?)\d*))?'%(c,c,c,c) for c in CMDS]) - output = [] - for i,line in enumerate(progress.bar(self.lines)): - if self.limit is not None and i > self.limit: break - # for i,line in enumerate(self.lines): - l = line.strip() - # find comments, save them, and then remove them - m = re.findall(comment,l) - l = re.sub(whitespace+'|'+comment,'',l).strip().upper() - # l = re.sub(whitespace,'',l).upper() - - # Grab the commands - c = re.match(command,l) - - # output commands to a nice dict - out = {} - out['index'] = i - # out['line'] = line - if m: out['comment'] = m - for cmd in CMDS: - if c.group(cmd): - # either a float if '.' or a int - fcn = float if c.group(cmd+'decimal') else int - out[cmd] = fcn(c.group(cmd+'num')) - out['index'] = i - if len(out) > 0: - output.append(out) - return output - # - # if len(out) > 0: - # self.append(out) - - def update(self,tool): - '''Updates the gcode with a toolpath only does x,y''' - UPDATE = 'xy' - for x in tool: - # print len(x) - if len(x) == 5: - # print x - for u in UPDATE: - if u.upper() in self[x[4]]: - self[x[4]][u.upper()] = x[UPDATE.index(u)] - # print self[x[4]] - - # print self[x[4]], - # print u.upper(), - # print self[x[4]][u.upper()] - # print self[x[4]][u.toupper()]#, x[UPDATE.index(u)] - # print self[x[4]][u],x[UPDATE.index(u)] - - - def copy(self): - return deepcopy(self) - - def getGcode(self, tag=__name__, start=None): - lines = [] - for i,line in enumerate(self): - l = '' - for cmd in CMDS: - if cmd in line: - l += (' %s%.3f' if cmd in 'XYZ' else '%s%02i ')%(cmd,line[cmd]) - # add in the comments - if 'comment' in line: l += ' '.join(line['comment']) - lines.append(l) - params = dict(gcode='\n'.join(lines), - init=INIT, - finish=FINISH, - tag=tag) - params['startpos'] = ' G00 X%.3f Y%.3f'%(start[0],start[1]) if start else '' - return Template(TEMPLATE).substitute(params) - - \ No newline at end of file + def __init__(self, gcode, limit=None): + '''start with a gcode ascii file''' + self.limit = limit + if isinstance(gcode, str): + with open(os.path.expanduser(gcode),'r') as f: + lines = f.readlines() + filename = gcode + else: + filename = gcode.name + lines = gcode.readlines() + gcode.close() + self.filename = filename + self.lines = lines + # self.ready = False + + # def append(self,item): + # '''add the next nice to the object''' + # if self.ready : self.ready = False + # super(GCode, self).append(item) + + def parse(self): + '''WARNING: By default .parse() grabs only the G## commands for creating toolpaths in some space + if you need everything use .parseall()''' + everything = self._parse() + for item in everything: + toappend = False + for cmd in CMDS: + if cmd in item: + toappend=True + if toappend: + self.append(item) + + def parseAll(self): + '''Gets everything so that we can print it back out''' + everything = self._parse() + for item in everything: + self.append(item) + + def _parse(self): + ''' [INTERNAL] convert the readlines into a parsed set of commands and values''' + puts(colored.blue('Parsing gCode')) + + comment = r'\(.*?\)' + whitespace = r'\s' + + #command = r''.join([r'(?P<%s>%s(?P<%snum>-?\d+(?P<%sdecimal>\.?)\d*))?'%(c,c,c,c) for c in CMDS]) + ''' + The result of the above commented line is: + that command is a new regular expression which prints in the followin way in ipython: + note the doubles of \ probably have to do with the output + '(?PG(?P-?\\d+(?P\\.?)\\d*))?(?PX(?P-?\\d+(?P\\.?)\\d*))?(?PY(?P-?\\d+(?P\\.?)\\d*))?(?PZ(?P-?\\d+(?P\\.?)\\d*))?(?PM(?P-?\\d+(?P\\.?)\\d*))?(?P

P(?P-?\\d+(?P\\.?)\\d*))?(?PI(?P-?\\d+(?P\\.?)\\d*))?(?PJ(?P-?\\d+(?P\\.?)\\d*))?' + ''' + #one major concwern is that this ordering is not flexible + #perhaps G code standards dictate the order of X Y Z I J K F E ... etc + # maybe it is not necessary to add any flexibility in ordering here + # the most likely malfunction will be due to F coming before x y z or after x y z + output = [] + for i,line in enumerate(progress.bar(self.lines)): + if self.limit is not None and i > self.limit: break + # for i,line in enumerate(self.lines): + l = line.strip() + # find comments, save them, and then remove them + m = re.findall(comment,l) + l = re.sub(whitespace+'|'+comment,'',l).strip().upper() + # l = re.sub(whitespace,'',l).upper() + + # Grab the commands + #c = re.match(command,l) + + # output commands to a nice dict + out = {} + out['index'] = i + # out['line'] = line + if m: out['comment'] = m + for cmd in CMDS: + #incorporating the definintion of command inside this loop should make it more flexible + command = r''.join(r'(?P<%s>%s(?P<%snum>-?\d*+(?P<%sdecimal>\.?)\d*))?'%(c,c,c,c)) + c = re.match(command,l) + if c.group(cmd): + # either a float if '.' or a int + fcn = float if c.group(cmd+'decimal') else int + out[cmd] = fcn(c.group(cmd+'num')) + out['index'] = i + if len(out) > 0: + output.append(out) + return output + # + # if len(out) > 0: + # self.append(out) + + def update(self,tool): + '''Updates the gcode with a toolpath only does x,y''' + UPDATE = 'xy' + for x in tool: + # print len(x) + if len(x) == 5: + # print x + for u in UPDATE: + if u.upper() in self[x[4]]: + self[x[4]][u.upper()] = x[UPDATE.index(u)] + # print self[x[4]] + + # print self[x[4]], + # print u.upper(), + # print self[x[4]][u.upper()] + # print self[x[4]][u.toupper()]#, x[UPDATE.index(u)] + # print self[x[4]][u],x[UPDATE.index(u)] + + + def copy(self): + return deepcopy(self) + + def getGcode(self, tag=__name__, start=None): + lines = [] + for i,line in enumerate(self): + l = '' + for cmd in CMDS: + if cmd in line: + l += (' %s%.3f' if cmd in 'XYZFEIJK' else '%s%02i ')%(cmd,line[cmd]) + # add in the comments + if 'comment' in line: l += ' '.join(line['comment']) + lines.append(l) + params = dict(gcode='\n'.join(lines),init=INIT,finish=FINISH,tag=tag) + params['startpos'] = ' G00 X%.3f Y%.3f'%(start[0],start[1]) if start else '' + return Template(TEMPLATE).substitute(params) diff --git a/lib/gparse.py b/lib/gparse.py new file mode 100644 index 0000000..69f2832 --- /dev/null +++ b/lib/gparse.py @@ -0,0 +1,229 @@ + +class g_command: + def __init__(self): + self.command_type="" + self.command_value="" + self.x="nan" + self.y="nan" + self.z="nan" + self.i="nan" + self.j="nan" + self.k="nan" + self.f="nan" + self.units="" + self.coordinates="" + self.description="" + self.interpretation="" + #self.comment="" + +import re + +def gparse(line): + #non greedily strip comments with great haste + line = re.sub(r'\([^)]*\)', '', line) + #ignor parenthesise + command_found=False + parsed_g_command = g_command() + g_found=False + + #Correct for the non witespace gcode formats you may encouter + previous_letter = "" + new_line = "" + for letter in line: + if (letter in 'GgXxYyZzIiJjKkFfMm') and (previous_letter != ' '): + new_line = new_line + ' ' + new_line = new_line + letter + #print letter + old_letter = letter + line = new_line.strip() + print line + if "g" in line or "G" in line: + + parsed_g_command.command_type="G" + lineofwords=line.split() + + for word in lineofwords: + if "g" in word or "G" in word: + g_value = re.sub("g|G","",word) + #print "G value = " + (g_value) + parsed_g_command.command_value=float(g_value) + #print line + if g_found == True: + print "to manny G's in this line" + else: + g_found=True + command_found=True + + if g_found==True and (int(g_value)==0 or int(g_value)==1): + if int(g_value)==0: + parsed_g_command.description="Rapid Move" + elif int(g_value)==1: + parsed_g_command.description="Linear Move" + elif int(g_value)==2: + parsed_g_command.description="Arc Move" + else: + print "how did i get here" + + #G2 X1.0000 Y1.1600 Z0.4500 I0.0000 J-0.1600 + for word in lineofwords: + if "x" in word or "X" in word: + x_value = re.sub("x|X","",word) + #print "X = " + (x_value) + parsed_g_command.x=float(x_value) + elif "y" in word or "Y" in word: + y_value = re.sub("y|Y","",word) + #print "Y = " + (y_value) + parsed_g_command.y=float(y_value) + elif "z" in word or "Z" in word: + z_value = re.sub("z|Z","",word) + #print "Z = " + (z_value) + parsed_g_command.z=float(z_value) + elif "i" in word or "I" in word: + i_value = re.sub("i|I","",word) + #print "Z = " + (z_value) + parsed_g_command.i=float(i_value) + elif "j" in word or "J" in word: + j_value = re.sub("j|J","",word) + #print "Z = " + (z_value) + parsed_g_command.j=float(j_value) + elif "k" in word or "K" in word: + k_value = re.sub("k|K","",word) + #print "Z = " + (z_value) + parsed_g_command.k=float(k_value) + elif "f" in word or "F" in word: + f_value = re.sub("f|F","",word) + #print "Z = " + (z_value) + parsed_g_command.f=float(f_value) + elif g_found==True and (int(g_value)==20): + parsed_g_command.units = "in" + parsed_g_command.description="Units set to: inches" + elif g_found==True and (int(g_value)==21): + parsed_g_command.units = "mm" + parsed_g_command.description="Units set to: mm" + elif g_found==True and (int(g_value)==90): + parsed_g_command.coordinates = "absolute" + parsed_g_command.description="Coords set to: absolute" + elif g_found==True and (int(g_value)==91): + parsed_g_command.coordinates = "relative" + parsed_g_command.description="Coords set to: relative" + + + + + if command_found==True: + return parsed_g_command + else: + return False + print "no command found on this line" + + + + +def gcode_interpret(code): +#variables +#internal + interpeted_gcommands=[] + AbsX="nan" + AbsY="nan" + AbsZ="nan" +#external + #DefaultMoveSpeed= +#constant + ConversionToIN=1 + + LastCommand = g_command() + + for command in code: + interpreted_gcommand = command + if command.command_type == "g": + if command.command_value==20: + ConversionToIN=1 + elif command.command_value==21: + ConversionToIN=1/25.4 + interpreted_gcommand.value = 20 + elif command.command_value==90: + coordinates=absolute + elif command.command_value==91: + coordinates=relative + interpreted_gcommand.value = 90 + elif command.command_value==0 or command.command_value==1: + if coordinates==absolute: + AbsX=ConversionToIN*command.x + AbsY=ConversionToIN*command.y + AbsZ=ConversionToIN*command.z + elif coordinates==relative: + AbsX=AbsX+ConversionToIN*command.x + AbsY=AbsY+ConversionToIN*command.y + AbsZ=AbsZ+ConversionToIN*command.z + interpreted_gcommand.x=AbsX + interpreted_gcommand.y=AbsY + interpreted_gcommand.z=AbsZ + + #interpret move type + if LastCommand.x == interpreted_gcommand.x and LastCommand.y == interpreted_gcommand.y : + if LastCommand.z == interpreted_gcommand.z : + interpreted_gcommand.interpretation = "Stationary" + elif LastCommand.z > interpreted_gcommand.z: + interpreted_gcommand.interpretation = "Plunge" + elif LastCommand.z < interpreted_gcommand.z: + interpreted_gcommand.interpretation = "Lift" + #if or elif for this next line not sure + elif LastCommand.z>0 and interpreted_gcommand.z>0: + interpreted_gcommand.interpretation = "Move" + elif LastCommand.z>0 and interpreted_gcommand.z>0: + interpreted_gcommand.interpretation = "Move" + elif LastCommand.z<=0 and interpreted_gcommand.z<=0: + interpreted_gcommand.interpretation = "Mill" + elif LastCommand.z > interpreted_gcommand.z and LastCommand.z>0: + interpreted_gcommand.interpretation = "Decending Mill" + elif LastCommand.z < interpreted_gcommand.z and interpreted_gcommand.z>0: + interpreted_gcommand.interpretation = "Ascending Mill" + else: + print "I don't know what to call this move!" + + interpeted_gcommands.append(interpreted_gcommand) + LastCommand = interpreted_gcommand + return interpeted_gcommands + +#this blck commented out for learning python purposes +''' +from sys import argv +print "number of input args is:" + str(len(argv)-1) +numberofinputs=len(argv)-1 + +#default filename +filename="findhieght.nc" + +#default verbosity +verbose = False + +#import arguments + +#warning +if numberofinputs==0: + print "please input filename as arg" +#load filename +elif numberofinputs==1: + script, filename= argv + +txt = open(filename) +mydata= txt.readlines() + +gcommands=[] + +for line in mydata: + if gparse(line): + gcommands.append(gparse(line)) + +inted_gcommands = gcode_interpret(gcommands) + +if verbose: + + for eachcommand in inted_gcommands: + + print eachcommand.x + print eachcommand.y + print eachcommand.z + print eachcommand.description + print eachcommand.interpretation +''' diff --git a/lib/grbl_status.py b/lib/grbl_status.py new file mode 100644 index 0000000..5eee40b --- /dev/null +++ b/lib/grbl_status.py @@ -0,0 +1,156 @@ +#library for parsing grbl's reply to the status request '?' +import re + +class GRBL_status(): + def __init__(self): + self.run="" + self.idle="" + self.hold="" + self.alarm="" + self.x="nan" + self.y="nan" + self.z="nan" + self.x_work="nan" + self.y_work="nan" + self.z_work="nan" + self.buf="nan" + self.rx="nan" + self.lim="nan" + self.string="" + #self.comment="" + + + + def parse_grbl_status(self,line): + line= re.findall(r'\<[^>]*\>', line)[0] + self.string=line + + if re.search('Idle', line,flags = re.IGNORECASE): + self.run=False + self.idle=True + self.hold=False + self.alarm=False + + if re.search('Run', line,flags = re.IGNORECASE): + self.run=True + self.idle=False + self.hold=False + self.alarm=False + + if re.search('Hold', line,flags = re.IGNORECASE): + self.run= False + self.idle=False + self.hold=True + self.alarm=False + + if re.search('Alarm', line,flags = re.IGNORECASE): + self.run= False + self.idle=False + self.hold=False + self.alarm=True + + + + #just for testing + #line = '' + #mpos = re.findall('MPos:\d\.\d*,', line, re.IGNORECASE) + + #mpos = re.findall('MPos:\d*\.\d*,\d*\.\d*,\d*\.\d*,', line, flags = re.IGNORECASE)[0] + mpos = re.findall('MPos:-?\d*\.\d*,-?\d*\.\d*,-?\d*\.\d*,', line, flags = re.IGNORECASE)[0] + + if mpos: + mpos= re.sub('MPos:','',mpos, flags = re.IGNORECASE) + mpos= re.split(',',mpos) + #mpos= mpos.split(',',mpos) + self.x=float(mpos[0]) + self.y=float(mpos[1]) + self.z=float(mpos[2]) + + #wpos = re.findall('WPos:\d*\.\d*,\d*\.\d*,\d*\.\d*,', line, flags = re.IGNORECASE)[0] + wpos = re.findall('WPos:-?\d*\.\d*,-?\d*\.\d*,-?\d*\.\d*,', line, flags = re.IGNORECASE)[0] + if wpos: + wpos= re.sub('WPos:','',wpos, flags = re.IGNORECASE) + wpos= re.split(',',wpos) + #mpos= mpos.split(',',mpos) + self.x_work=float(wpos[0]) + self.y_work=float(wpos[1]) + self.z_work=float(wpos[2]) + + buf = re.findall('Buf:\d*', line, flags = re.IGNORECASE)[0] + if buf: + buf= re.sub('Buf:','',buf, flags = re.IGNORECASE) + self.buf=int(buf) + + rx = re.findall('rx:\d*', line, flags = re.IGNORECASE)[0] + if rx: + rx= re.sub('RX:','',rx, flags = re.IGNORECASE) + self.rx=int(rx) + + lim = re.findall('Lim:\d*', line, flags = re.IGNORECASE)[0] + if lim: + lim= re.sub('Lim:','',lim, flags = re.IGNORECASE) + self.lim=lim + return self + + + + def x(self): + return self.x + + def y(self): + return self.Y + + def z(self): + return self.z + + def rx(self): + return self.rx + + def lim(self): + return self.lim + + def buf(self): + return self.buf + + def idle(self): + return self.idle + + def run(self): + return self.run + + def hold(self): + return self.hold + + def alarm(self): + return self.alarm + + + def get_x(self): + return self.x + + def get_y(self): + return self.Y + + def get_z(self): + return self.z + + def get_rx(self): + return self.rx + + def get_lim(self): + return self.lim + + def get_buf(self): + return self.buf + + def is_idle(self): + return self.idle + + def is_running(self): + return self.run + + def is_haulted(self): + return self.hold + + def is_alarmed(self): + return self.alarm diff --git a/lib/tool.py b/lib/tool.py index db84d76..e63e5c9 100755 --- a/lib/tool.py +++ b/lib/tool.py @@ -46,9 +46,9 @@ def relative(self,m=None,t=None): def move(self,m,cmd, z=None): '''Moves to location. if NaN, use previous. handles rel/abs''' for i,key in enumerate(m): - if not math.isnan(m[i]): + if not math.isnan(m[i]): m[i] = convert(self,m[i]) - else: + else: m[i] = self[-1][i] m[3] = cmd if z: m[2] = z @@ -79,7 +79,7 @@ def circle(self,m,t): 2: circle, 3: circle, 4: noop, - 17: noop, #xyplane + 17: noop, #xyplane 20: inch, 21: mm, 54: noop, # Word Coords @@ -97,9 +97,9 @@ def __init__(self, gcode=None): self.units = 'inch' self.mills = [] home(self) - + if gcode: self.build(gcode) - + def __repr__(self): '''Slightly more information when you print out this object.''' return '%s() : %i locations, units: %s'%(self.__class__.__name__, len(self),self.units) @@ -131,7 +131,7 @@ def build(self, gcode): # '''Parse gCode listing to follow a bit location # addindex [false] : adds the index to the last spot so that we can update and the push back''' # puts(colored.blue('Building Toolpath:')) - + # # for each line of the gcode, accumulate the location of a toolbit # for i,line in enumerate(progress.bar(gcode)): # # for i,line in enumerate(gcode): @@ -148,9 +148,9 @@ def build(self, gcode): # if addIndex and (t in [0,1]): self[-1].append(line['index']) # except KeyError: # error('Missing command in GCMD: %d(%s)'%(t, line)) - + def boundBox(self): - '''Returns the bounding box [[xmin,xmax],[ymin,ymax],[zmin,zmax]] + '''Returns the bounding box [[xmin,xmax],[ymin,ymax],[zmin,zmax]] for the toolpath''' box = [[0.,0.],[0.,0.],[0.,0.]] for item in self: @@ -172,8 +172,8 @@ def offset(self, offset): item[ax] -= offset[1] elif i == 2: item[ax] -= offset[2] - - + + def rotate(self, angle): '''rotate by some angle''' rad = math.radians(angle) @@ -183,8 +183,8 @@ def rotate(self, angle): a = math.cos(rad)*item[0] - math.sin(rad)*item[1] b = math.sin(rad)*item[0] + math.cos(rad)*item[1] item[0], item[1] = a,b - - + + # def _badclean(self): # '''A temporary fix to check the bike program''' # loc=[0.0,0.0,0.0] @@ -216,11 +216,11 @@ def millLength(self): for mill in self.mills: length += mill.length() return length - + # def _old_groupMills(self): # '''Groups the toolpath into individual mills''' # puts(colored.blue('Grouping paths:')) - + # mill = Mill(); # for x,y,z,t in progress.bar(self): # # for i,[x,y,z,t] in enumerate(self): @@ -233,7 +233,7 @@ def millLength(self): def groupMills(self): '''Groups the toolpath into individual mills''' puts(colored.blue('Grouping paths:')) - + mill = Mill(); for item in progress.bar(self): if item.cmd in (1,2,3): @@ -242,13 +242,13 @@ def groupMills(self): if len(mill) > 0: self.mills.append(mill) mill = Mill() # ready for more mills - - + + def uniqMills(self): '''Uniqify the points in each of the millings''' for mill in self.mills: mill.uniqify() - + def setMillHeight(self, millHeight=None, drillDepth=None): '''Sets the Mill height, for spot drilling millHeight and drillHeight in MILs''' @@ -257,29 +257,29 @@ def setMillHeight(self, millHeight=None, drillDepth=None): mill.setZ(drillDepth/1000.) else: mill.setZ(millHeight/1000.) - + def getNextMill(self, X): '''Gets the next next mill to point X, using just the mill start point.''' distances = [distance(mill[0],X) for mill in self.mills] index = distances.index(min(distances)) # print '%i : % 4.0f mil'%(index, min(distances)*1000.0) return self.mills.pop(index) - + def getClosestMill(self, X): - ''' Improved getNextMill, optimizes the location to start in the mill - Now checks to see if the starting and ending point are close so can be - reordered. This ends up being a traveling salesman problem, so + ''' Improved getNextMill, optimizes the location to start in the mill + Now checks to see if the starting and ending point are close so can be + reordered. This ends up being a traveling salesman problem, so keeping with this solution is easiest. ''' # get the path with a point that is closest to X distances = [distance(mill.closestLocation(X),X) for mill in self.mills] index = distances.index(min(distances)) mill = self.mills.pop(index) - + # reorder the path so that the start is close to x mill.reorderLocations(X) return mill - - + + def reTool(self, moveHeight=None): @@ -290,7 +290,7 @@ def reTool(self, moveHeight=None): home(self) self.abs=True self.units='inch' - + heightInches = moveHeight/1000. puts(colored.blue('Retooling path from mills:')) @@ -299,13 +299,13 @@ def reTool(self, moveHeight=None): millMove(self, mill[0], heightInches) for i,x in enumerate(mill): - # mill connecting each location + # mill connecting each location move(self, x, 1) # it says move, but cmd == 1 so it is a mill # ok done, so move to the to origin millMove(self, origin(), heightInches) - - + + def buildGcode(self): '''This returns a string with the GCODE in it.''' lines = [] @@ -329,13 +329,11 @@ def buildGcode(self): ncommand=len(self), footer='') return Template(TEMPLATE).substitute(params) - - + + #### some commands to move / copy and otherwise change the gcode. def move(self,loc): '''Moves the toolpath from (0,0) to (loc[0],loc[1])''' for item in self: - for i,x in enumerate(loc): + for i,x in enumerate(loc): item[i] += x - - \ No newline at end of file diff --git a/modify.py b/modify.py index 06bce43..6765174 100755 --- a/modify.py +++ b/modify.py @@ -8,7 +8,8 @@ from lib.tool import Tool from lib.util import deltaTime, error, convertUnits from clint.textui import puts,colored - +# HELLO WORLD +#THESE FILES ARE NOT THE SAME! FILEENDING = '_mod' # file ending for optimized file. @@ -18,22 +19,22 @@ default=None, type=str, nargs=1, - help='''Move the origin to a new point. + help='''Move the origin to a new point. Applied before rotation. - Specify Units at the end of the x,y pos. + Specify Units at the end of the x,y pos. Example: "-m 0.1,0.2in".'''), dict(args=['-r', '--rotate'], default=None, type=float, - help='''Rotate the gcode about the origin. - Applied after a Move. + help='''Rotate the gcode about the origin. + Applied after a Move. In float degrees.'''), dict(args=['-c', '--copy'], default=None, type=str, nargs='+', - help='''Copy the part from the origin to points. - Applied before rotation. + help='''Copy the part from the origin to points. + Applied before rotation. Specify Units after each set. Example: "-c 0.2,0.2in 20,20mil".'''), dict(args=['-x','--replicate'], @@ -45,29 +46,31 @@ Example: "-x 2,2" ''') ] - +#this is a very different parse from the gcode class parse +#it is for parsing user modifications def parse(move, getUnits=False, defaultUnit='in'): - '''For Move, Copy, and Replicate, This function evaluates the user input, grabs - any x,y values and if getUnits is passed gets the units. Parses any x,y, and + '''For Move, Copy, and Replicate, This function evaluates the user input, grabs + any x,y values and if getUnits is passed gets the units. Parses any x,y, and converts the units to inches, and then outputs an array of the locations to move, copy or whatever. You can use this with an int input (replicate), but make sure to cast it to an int.''' if isinstance(move, str): move = [move] + #does [no unit specified] need escape chars \[ \]? units = r'(?Pin|mil|mm|[NoUnitSpecified]?)' if getUnits else r'' out = [] for m in move: m = re.sub(r'\s','',m).strip() g = re.match(r'(?P-?\d*\.?\d*)\,(?P-?\d*\.?\d*)'+units, m, re.I) - if not g: + if not g: error('Argument Parse Failed on [%s] failed! Check the arguments'%(m)) - + # default to inches or a specific unit if (g.group('units') is None) or (len(g.group('units')) == 0): unit = defaultUnit else: unit = g.group('units') - + # Ok prepare them for output item = map(float,map(g.group,['x','y'])) if getUnits: item = map(convertUnits,item,[unit]*2) @@ -79,24 +82,24 @@ def parse(move, getUnits=False, defaultUnit='in'): def mod(gfile): - '''For each of the files to process either rotate, move, copy, or + '''For each of the files to process either rotate, move, copy, or replicate the code. General idea: read in ascii Process into a toolpath list. modify. Write out toolpath.''' - + start = datetime.now() puts(colored.blue('Modifying file: %s\n Started: %s'%(gfile.name,datetime.now()))) - + # Parse the gcode. gcode = GCode(gfile) gcode.parseAll() - + # Create a toolpath from the gcode # add in the index so that we can match it to the gcode - - + + out = [] if args.move: loc = parse(args.move, getUnits=True) # only one move at a time. @@ -106,7 +109,7 @@ def mod(gfile): tool.move(loc) # ok well this should work gcode.update(tool) out.append([loc,gcode]) - + if args.copy: locs = parse(args.copy, getUnits=True) puts(colored.blue('Copying!')) @@ -118,18 +121,18 @@ def mod(gfile): tool.move(loc) gc.update(tool) out.append([loc,gc]) - + # if args.replicate: # nxy = map(int,parse(args.replicate)[0]) # ensure int, and only one # puts(colored.blue('Replicating!\n nx=%i, ny=%i)'%(nxy[0],nxy[1]))) - + output = ''.join([o.getGcode(tag=args.name,start=l) for l,o in out]) - + outfile = FILEENDING.join(os.path.splitext(gfile.name)) puts(colored.green('Writing: %s'%outfile)) with open(outfile,'w') as f: f.write(output) - + # how long did this take? puts(colored.green('Time to completion: %s'%(deltaTime(start)))) print @@ -167,4 +170,4 @@ def mod(gfile): if c: # either a drill.tap or etch.tap file mod(gfile) - print '%s finished in %s'%(args.name,deltaTime(start)) \ No newline at end of file + print '%s finished in %s'%(args.name,deltaTime(start)) diff --git a/new_GRBL_configuration_readme b/new_GRBL_configuration_readme new file mode 100644 index 0000000..c33a558 --- /dev/null +++ b/new_GRBL_configuration_readme @@ -0,0 +1,47 @@ +#NEW CONFIGURATION!!! + | $0=100 (step pulse, usec) + | $1=25 (step idle delay, msec) + | $2=0 (step port invert mask:00000000) + | $3=96 (dir port invert mask:01100000) + | $4=0 (step enable invert, bool) + | $5=0 (limit pins invert, bool) + | $6=0 (probe pin invert, bool) + | $10=255 (status report mask:11111111) + | $11=0.010 (junction deviation, mm) + | $12=0.002 (arc tolerance, mm) + | $13=1 (report inches, bool) + | $20=0 (soft limits, bool) + | $21=0 (hard limits, bool) + | $22=0 (homing cycle, bool) + | $23=0 (homing dir invert mask:00000000) + | $24=130.000 (homing feed, mm/min) + | $25=260.000 (homing seek, mm/min) + | $26=250 (homing debounce, msec) + | $27=1.000 (homing pull-off, mm) + | $100=188.976 (x, step/mm) + | $101=188.976 (y, step/mm) + | $102=188.976 (z, step/mm) + | $110=500.000 (x max rate, mm/min) + | $111=500.000 (y max rate, mm/min) + | $112=500.000 (z max rate, mm/min) + | $120=4.000 (x accel, mm/sec^2) + | $121=4.000 (y accel, mm/sec^2) + | $122=4.000 (z accel, mm/sec^2) + | $130=200.000 (x max travel, mm) + | $131=200.000 (y max travel, mm) + | $132=200.000 (z max travel, mm) + + + + +THE OLD CONFIG + | $0 = 188.976 (steps/mm x) + | $1 = 188.976 (steps/mm y) + | $2 = 188.976 (steps/mm z) + | $3 = 100 (microseconds step pulse) + | $4 = 130.000 (mm/min default feed rate) + | $5 = 260.000 (mm/min default seek rate) + | $6 = 0.200 (mm/arc segment) + | $7 = 96 (step port invert mask. binary = 1100000) + | $8 = 4.000 (acceleration in mm/sec^2) + | $9 = 0.050 (cornering junction deviation in mm) diff --git a/optimize.py b/optimize.py index da0bb3f..5b9a611 100755 --- a/optimize.py +++ b/optimize.py @@ -25,26 +25,26 @@ def opt(gfile, offset=(0.0,0.0,0.0), rotate=False): figures out milling. Reorders milling to get optimal Writes out to new file.''' - + start = datetime.now() puts(colored.blue('Optimizing file: %s\n Started: %s'%(gfile.name,datetime.now()))) - + # Parse the gcode from the ascii to a list of command numbers and location gcode = GCode(gfile) gcode.parse() - + # Take the list and make a toolpath out of it. A toolpath is a list of locations # where the bit needs to be moved / milled : [ [x,y,z,t], ...] tool = Tool(gcode) tool.offset(offset) tool.rotate(rotate) - + tool.groupMills() puts(colored.blue('Toolpath length: %.2f inches, (mill only: %.2f)'%(tool.length(),tool.millLength()))) if args.setMillHeight: tool.setMillHeight(Z_MILL,Z_SPOT) tool.uniqMills() - + # This starts the optimization process: # start at here, and go to the next path which is closest is the overall plan puts(colored.blue('Starting Optimization:')) @@ -58,20 +58,20 @@ def opt(gfile, offset=(0.0,0.0,0.0), rotate=False): # Basic optimization, find the next closest one and use it. # mill = tool.getNextMill(here) - # Advanced Optimization: Assumes that each mill path closed, so finds + # Advanced Optimization: Assumes that each mill path closed, so finds # the mill path which is close to the point and reorders it to be so mill = tool.getClosestMill(here) - + # you were here, now you are there # move mills and update location - newMills.append(mill) + newMills.append(mill) here = newMills[-1][-1] - + k += 1 if (k%10) == 0: sys.stdout.write('.') sys.stdout.flush() - + tool.mills.extend(newMills) tool.reTool(Z_MOVE) tool.uniq() @@ -83,7 +83,7 @@ def opt(gfile, offset=(0.0,0.0,0.0), rotate=False): puts(colored.green('Writing: %s'%outfile)) with open(outfile,'w') as f: f.write(output) - + # how long did this take? puts(colored.green('Time to completion: %s'%(deltaTime(start)))) print @@ -106,7 +106,7 @@ def opt(gfile, offset=(0.0,0.0,0.0), rotate=False): ext1b = dict(args=['--zdrill'], default=0.0, type=float, help='Drill Z-height in mills [%s mills]'%Z_DRILL), - + ext2=dict(args=['--offsetx'], default=0, type=float, @@ -132,7 +132,7 @@ def opt(gfile, offset=(0.0,0.0,0.0), rotate=False): getFile=True, # get gcode to process getMultiFiles=True, # accept any number of files getDevice=False) - + if args.zmove != 0: Z_MOVE = args.zmove if args.zdrill != 0: @@ -152,15 +152,3 @@ def opt(gfile, offset=(0.0,0.0,0.0), rotate=False): opt(gfile, offset=(args.offsetx, args.offsety, args.offsetz), rotate=args.rotate) print '%s finished in %s'%(args.name,deltaTime(start)) - - - - - - - - - - - - diff --git a/orient.py b/orient.py index faa60c1..3e26d36 100755 --- a/orient.py +++ b/orient.py @@ -73,13 +73,13 @@ class Camera(object): def __init__(self, cameranumber=0): - '''wrapper for a cv capture object. Defaults to the + '''wrapper for a cv capture object. Defaults to the most recent camera (0).''' self.status = '' # current task at hand - + self.cam = cv.CaptureFromCAM(cameranumber) self.update() # setup self.frame - + self.shape = cv.GetSize(self.frame) self.center = tuple(x/2 for x in self.shape) self.currentcircles = deque(maxlen=40) @@ -91,34 +91,34 @@ def getfont(self, **kwargs): outline = kwargs.pop('outline', False) params = dict(font=CV_FONT_HERSHEY_PLAIN, hscale=fontsize*0.9, vscale=fontsize, - shear=0, thickness=1, + shear=0, thickness=1, lineType=cv2.CV_AA) params.update(kwargs) if outline: params['thickess'] += 2 return cv.InitFont(**params) - + def getcolor(self, red=0, green=0, blue=0): '''wrapper around cv.RGB''' return cv.RGB(red,green,blue) - + def getdefaultcolor(self): '''A nice steel blue''' return self.getcolor(100,130,255) - + def write(self, msg, loc, lineheight=20, color=None, outline=True): - '''Write a string(msg) to the screen. This handles new lines like + '''Write a string(msg) to the screen. This handles new lines like butter, and defaults to outlineing the text''' for i,line in enumerate(msg.splitlines()): l = (loc[0], loc[1]+i*lineheight) if outline: cv.PutText(self.frame, line, l, self.getfont(outline), 0) cv.PutText(self.frame, line, l, self.getfont(outline), self.color) - + def displaystatus(self, text): '''A wrapper that handles displaying of the current status''' self.write(text, (20,20)) - + def update(self, frame=None): '''Update the current frame in the buffer. If you pass in a frame object it will use it.''' @@ -126,14 +126,14 @@ def update(self, frame=None): self.frame = frame else: self.frame = cv.QueryFrame(self.cam) - + def addoverlay(self): self.write(__DOC__, (10,20)) self.write('orient.py', (10,self.size[1]-10) ) cv.Line(self.frame, (0,self.center[1]), (self.size[0],self.center[1]), self.color) cv.Line(self.frame, (self.center[0],0), (self.center[0],self.size[1]), self.color) cv.Circle(self.frame, self.center, 100, self.color) - + def addtrackbar(self): '''Add a trackbar?!''' # value = 0 @@ -141,16 +141,16 @@ def addtrackbar(self): # def onChange(x,*args): # print x # cv.CreateTrackbar('test','Window', value, count, onChange) - - + + def show(self): '''Display the current frame''' cv.ShowImage("Window", self.frame) - + def interact(self): '''Handle all of the fancy key presses''' c = (cv.WaitKey(25) & 0xFF) - + CHARMAP = { 27:'quit', # q 113:'quit', # esc @@ -175,9 +175,9 @@ def interact(self): self.status = CHARMAP[c] elif c != 255: print 'Key not recognized: {} [{}]'.format(repr(c), ord(c)) - + # Line measuring functions - + def setupmeasure(self, color='red'): '''Setup the line measureing state. self.index -- which color should we focus on. @@ -186,18 +186,18 @@ def setupmeasure(self, color='red'): self.index = ['blue','green','red'].index(color) self.nsigma = 1.0 self.zero = 0 - + def setzero(self, **kwargs): '''Set the zero location of the line location.''' self.zero = self.measure(**kwargs) - + def measure(self, delta=50, invert=False, getall=True, quiet=False): '''return the location of the point in pixels''' - # DEBUG!! invert image so that a dark green line looks like a + # DEBUG!! invert image so that a dark green line looks like a # bright red line! if invert: cv.Not(self.frame, self.frame) - + img = np.array(cv.GetMat(self.frame))[:,:,self.index] out = [] # store the found locations of the line location for i,im in vslice(img, delta): @@ -221,31 +221,31 @@ def measure(self, delta=50, invert=False, getall=True, quiet=False): return x,y else: return np.mean(y) - + def plot(self, xx, yy, pos=None, size=None): - + # show the mean x = np.mean(xx) y = np.mean(yy) cv.Circle(self.frame, (int(x),int(y)), 5, self.getdefaultcolor()) - + # rolling plot of the mean # self.points.append(int(yy)) # for x,y in enumerate(self.points): # cv.Circle(self.frame, (x,y), 2, self.getcolor(red=1)) - + # show all points for x,y in zip(xx,yy): cv.Circle(self.frame, (int(x),int(y)), 2, self.getdefaultcolor()) - + # show all the rolling points self.points.append([xx,yy]) for i,(xx,yy) in enumerate(self.points): for x,y in zip(xx,yy): cv.Circle(self.frame, (int(i+x-len(self.points)/2.0),int(y)), 1, self.getcolor(red=0.5)) - - - + + + # if pos is None: pos = 0,0 # if size is None: size = 50,200 # self.points.append(z) @@ -254,24 +254,24 @@ def plot(self, xx, yy, pos=None, size=None): # cv.Circle(self.frame, (0, int(x)), 10, self.getdefaultcolor()) # except: # print x - - - + + + # Circle finding procedures - + def circle(self): '''Determine the location of a circle in the frame.''' frame = np.array(cv.GetMat(self.frame)) img = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # img = cv2.medianBlur(img, 5) - circles = cv2.HoughCircles(img, cv.CV_HOUGH_GRADIENT, + circles = cv2.HoughCircles(img, cv.CV_HOUGH_GRADIENT, dp=1, # accumulator res minDist=40, #min dist to next circle param1=150, # canny param param2=15, # accumulator threshold minRadius=7, maxRadius=25) - + try: n = np.shape(circles) if len(n) == 0: @@ -280,7 +280,7 @@ def circle(self): for x,y,r in circles: cv2.circle(frame,(x,y),r,(255,255,255)) cv2.circle(frame,(x,y),2,(255,255,255),2) - + # add the most central one is the good one tmp = self.centralitem(circles) if tmp is not None: @@ -288,20 +288,20 @@ def circle(self): self.currentcircles.append(tmp) except Exception as e: print e - + frame = self.plotcurrentcircle(frame) self.frame = cv.fromarray(frame) - + def plotcurrentcircle(self, frame): '''Plot the most central circle -- this can fail due to - not having any points so wrap it and ignore its failings as + not having any points so wrap it and ignore its failings as a program. It is ok program I still enjoy your work.''' try: # plot the average one x,y,r = map(np.mean, zip(*self.currentcircles)) cv2.putText(frame, '{:0.1f}, {:0.1f}, {:0.2f}'.format(x,y,r), (int(x+20),int(y)), - cv2.FONT_HERSHEY_SIMPLEX, + cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,0,255), 1) cv2.circle(frame,(x,y),r,(0,0,255),2) cv2.circle(frame,(x,y),2,(0,0,255),2) @@ -332,12 +332,12 @@ def __init__(self, serial): self.serial = serial self.serial.run('G20G91 (inch, incremental)') self.movelen = 0.1 #inch - + def run(self, cmd): '''Run a gcode-command, or a specific keyword. (e.g. forward will move - the machine forward in the x direction by self.movelen.) This also - handles increasing and decreasing the self.movelength command. - If this does not consume the command it is returned. Or it + the machine forward in the x direction by self.movelen.) This also + handles increasing and decreasing the self.movelength command. + If this does not consume the command it is returned. Or it returns a nice status message of what happened.''' DELTA = 0.001 CMD = dict( @@ -350,7 +350,7 @@ def run(self, cmd): embiggen=DELTA, lessen=-DELTA, ) - + if cmd in CMD: d = CMD[cmd] if isinstance(d, str): @@ -358,7 +358,7 @@ def run(self, cmd): return self.position() elif cmd in ['embiggen', 'lessen']: self.movelen += d - if self.movelen > 1: + if self.movelen > 1: self.movelen = 1.0 elif self.movelen <= 0: self.movelen = DELTA @@ -368,7 +368,7 @@ def run(self, cmd): return 'Ran: {}'.format(cmd) else: return cmd - + def setposition(self, cmd): '''This consumes the commands that are related to figuring out the location of a set of locations (corners of a board).''' @@ -377,7 +377,7 @@ def setposition(self, cmd): return 'Set: {}'.format(cmd) else: return cmd - + def position(self): ''' get the current state of the machine and then return a processed bit of text for simple consuming by other programs. @@ -385,10 +385,10 @@ def position(self): ''' status = self.serial.run('?') return 'position: {}'.format(status) - - + + # x+y scan related procedures - + def setupscan(self): '''Get the variables from the command line. e.g. p orient.py scan [width] [height] [number of pts]''' @@ -422,11 +422,11 @@ def findcircles(): with Communicate('', None, debug=True) as serial: camera = Camera() controller = Controller(serial) - + while True: camera.update() camera.interact() - + # camera.status = controller.run(camera.status) # camera.status = controller.position(camera.status) if camera.status == 'quit': @@ -435,8 +435,8 @@ def findcircles(): camera.display(camera.status) camera.status = 'circle' camera.circle() - # - # + # + # camera.addoverlay() camera.show() @@ -448,37 +448,37 @@ def findcircles(): def scan(): pylab.ion() pylab.figure(1) - - + + with Communicate('', None, debug=True) as serial: serial.timeout = 0.0001 camera = Camera() camera.setupmeasure() - + controller = Controller(serial) controller.setupscan() - + out = [] for x,y in controller.scan(): camera.update() camera.interact() - + z = camera.measure() out.append([x,y,z]) - + if camera.status == 'quit': break camera.show() - + if len(out) > 0: pylab.cla() tmp = zip(*out) sc = pylab.scatter(tmp[0],tmp[1],s=tmp[2], c=tmp[2], vmin=0, vmax=400) print '{: 8.3f} {: 8.3f} {: 8.3f}'.format(x,y,z) - + pylab.ioff() pylab.show() - + def roll(): @@ -492,7 +492,7 @@ def roll(): if camera.status == 'quit': break camera.show() - + @@ -501,7 +501,7 @@ def roll(): def vslice(img, delta=20): '''Generates delta slices of an image that can be used - to find points as a function of the x axis. returns the + to find points as a function of the x axis. returns the middle pixel location and the image that is [heightxdelta] in size.''' for i,index in enumerate(np.arange(0,img.shape[1],delta)): @@ -544,34 +544,34 @@ def getarray(width=1000, delta=0.1): return np.arange(0, width, delta) def fitgaussian(x,y,offset=0): - '''Fit a gaussian to the data points x,y. - offset == the assumed floor for the gaussian (subtracted from + '''Fit a gaussian to the data points x,y. + offset == the assumed floor for the gaussian (subtracted from the y array). Originally I fit for both the amplitude and offset however this sometimes caused issues due to the degeneracy. ''' - + # set the parameters and some min values p = Parameters() # generally the background is 20-30, so require at least 10 above that p.add('amplitude', value=np.max(y)-offset, min=10) p.add('mean', value=np.mean(x), min=0) p.add('sigma', value=np.std(x), min=0) - - + + # minimise the fit. out = minimize(gauss2, p, args=(x, y-offset) ) - # print the fit values and uncert. I may want to check the + # print the fit values and uncert. I may want to check the # out.success value to ensure that everything worked. # report_errors(p) - + r = embiggen(minmax(x),0.2) xx = np.arange(r[0], r[1], 0.1) return p, xx, gauss2(p,xx)+offset def test(color='green', delta=20): - '''This is a simple testing function that loads an image and + '''This is a simple testing function that loads an image and attemps to find a line in it. Originally I attempted to use fit - a quadratic to the extreme bit of the data. This was ok, but did + a quadratic to the extreme bit of the data. This was ok, but did not capture the pointy-ness of the line. Now I am using a guass fit. ''' directory = '/Users/ajmendez/Dropbox/Shared/Design/laser/test/' @@ -584,11 +584,11 @@ def test(color='green', delta=20): filename = directory + 'debug_green.jpg' color='green' nsigma=1.5 - + filename = directory+'/test_circ.jpg' color='green' nsigma=0.0 - + out = [] img = cv2.imread(filename) imrange = [img.shape[1],0] @@ -597,7 +597,7 @@ def test(color='green', delta=20): cmap = pylab.cm.winter cmap2 = pylab.cm.Blues cmap3 = pylab.cm.Reds - + for i,im in vslice(img, delta): imavg = np.mean(im[:,:,index], axis=1) @@ -606,13 +606,13 @@ def test(color='green', delta=20): try: p,x,g = fitgaussian(ex,ey,cut) mid = p['mean'].value - + # plot the fit ic = 200*i/img.shape[1]+55 # line(x=mid, alpha=0.5, color=cmap(ic)) pylab.plot(ex,ey, alpha=0.7, color=cmap2(ic)) pylab.plot(x,g, alpha=0.7, color=cmap3(ic)) - + # Draw it to the image and then save the value # cv2.circle(img, (i,int(mid)), 2, 255) out.append([i,mid]) @@ -625,13 +625,13 @@ def test(color='green', delta=20): # cv2.circle(img, (i,0), 10, (0,0,255)) # Ensure that there is some space around the image setup(xr=imrange, embiggenx=0.2, embiggeny=0.2) - + x,y = map(np.array, zip(*out)) p = np.polyfit(x,y,1) fit = p[0]*x + p[1] diff = y - fit ns = np.std(diff) - + # next subplts -- add some extra analysis setup(subplt=(2,2,1), title='Points offset by 100px', xr=[0,img.shape[1]], yr=[0,img.shape[0]]) @@ -640,21 +640,21 @@ def test(color='green', delta=20): # pylab.plot(x,y+100, '.', color='white', markeredgewidth=1) pylab.scatter(x, y+100, marker='.', vmin=0, vmax=255, linewidth=0.4, c=200*x/img.shape[1]+55, edgecolor=(1,1,1,0.5), cmap=cmap2) - + # deviation from a line setup(subplt=(4,2,5), ylabel='line and fit', xticks=False) pylab.plot(x, y, color='blue', linewidth=2, alpha=0.7) pylab.plot(x, fit, color='red', linewidth=2, alpha=0.7) - + setup(subplt=(4,2,7), ylabel='Deviation from \nline [pixel]') pylab.plot(x, diff) - - setup(subplt=(2,2,4), + + setup(subplt=(2,2,4), title='Sigma:{:0.2f}px'.format(ns), xlabel='Deviation distribution [pixel]') pylab.hist(diff, np.arange(-3*ns,3*ns,ns/2.0)) line(x=[np.mean(diff), - np.mean(diff)-np.std(diff), + np.mean(diff)-np.std(diff), np.mean(diff)+np.std(diff)]) pylab.tight_layout() pylab.show() @@ -666,16 +666,16 @@ def test_circle(): directory = '/Users/ajmendez/Dropbox/Shared/Design/laser/test/' filename = directory+'/test_circ.jpg' frame = cv2.imread(filename) - + img = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # img = cv2.GaussianBlur(img, (0,0), 2.1) - - + + tmp = cv2.GaussianBlur(img, (0,0), 5.1) img = cv2.addWeighted(img,3.0,tmp, -2.0, -0.1) - + # cv2.threshold(img, 120, 0, cv2.THRESH_TOZERO, img)l - + # img = cv2.GaussianBlur(img, (0,0), 2.1) # cv2.adaptiveThreshold(img, 256, cv2.ADAPTIVE_THRESH_MEAN_C, # cv2.THRESH_BINARY_INV, 5, 0, img) @@ -685,16 +685,16 @@ def test_circle(): # img = cv2.morphologyEx(img, cv2.MORPH_CLOSE, (3,3)) # img = cv2.morphologyEx(img, cv2.MORPH_CLOSE, (5,5)) # img = cv2.GaussianBlur(img, (0,0), 5.1) - + # cv2.imshow('window',img) # cv2.waitKey() - + # img = cv2.medianBlur(img, 5) # img = cv2.medianBlur(img, 3) # img = cv2.GaussianBlur(img, (0,0), 0.1) frame = img - - circles = cv2.HoughCircles(img, cv.CV_HOUGH_GRADIENT, + + circles = cv2.HoughCircles(img, cv.CV_HOUGH_GRADIENT, dp=1, # accumulator res minDist=40, #min dist to next circle param1=100, # canny param @@ -708,27 +708,27 @@ def test_circle(): circles = np.reshape(circles,(n[1],n[2])) for x,y,r in circles: cv2.putText(frame, '{:0.2f}'.format(r), - (int(x+2),int(y+2)), - cv2.FONT_HERSHEY_SIMPLEX, + (int(x+2),int(y+2)), + cv2.FONT_HERSHEY_SIMPLEX, 1, (0,0,255), 2) - + cv2.circle(frame,(x,y),r,(0,0,255)) cv2.circle(frame,(x,y),2,(0,0,255),3) # cv2.circle(img,(x,y),r,(255,255,255)) # self.frame = cv.fromarray(img) except Exception as e: print 'Failed: {}'.format(e) - + cv2.imshow('window',frame) cv2.waitKey() - + def capture(): - ''' This is a simple capture script. Type c to capture a frame - to the current directory named test.jpg. Quit with q or esc. - This forces a high resolution image (1280 x 720). You can recapture + ''' This is a simple capture script. Type c to capture a frame + to the current directory named test.jpg. Quit with q or esc. + This forces a high resolution image (1280 x 720). You can recapture and overwrite the image with hitting c again. ''' cap = cv.CaptureFromCAM(0) @@ -736,7 +736,7 @@ def capture(): # cv.SetCaptureProperty(cap,cv.CV_CAP_PROP_FRAME_HEIGHT, 720) while True: img = cv.QueryFrame(cap) - + cv.ShowImage('window', img) c = (cv2.waitKey(16) & 0xFF) if c in [ord('q'),27]: diff --git a/probe_surface.py b/probe_surface.py new file mode 100644 index 0000000..92b676f --- /dev/null +++ b/probe_surface.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python +#probing uneven surfaces with robots +#by Mike Erickstad using libraries developed by A J Mendez PhD + +from numpy import arange +from lib import argv +from lib.grbl_status import GRBL_status +from lib.communicate import Communicate +from clint.textui import puts, colored +import time, readline +import numpy as np + + +args = argv.arg(description='Simple python grbl surface probe pattern') + +DEBUG_VERBOSE = False + +X_MAX = 1.0 +Y_MAX = 1.0 +X_STEP = 0.5 +Y_STEP = 0.5 +HIGH_Z = 0.5 +LOW_Z = -0.5 +PROBE_FEED_RATE = 0.2 +DESCENT_SPEED = 1.0/60.0 +DESCENT_TIME = HIGH_Z/DESCENT_SPEED + +Surface_Data = np.empty([len(arange(0, X_MAX, X_STEP)),len(arange(0, Y_MAX, Y_STEP))]) + +Z=HIGH_Z +converged = False + +# get a serial device and wake up the grbl, by sending it some enters +with Communicate(args.device, args.speed, timeout=args.timeout, debug=args.debug, quiet=args.quiet) as serial: + + command = "G90" + try: + serial.run(command) + except KeyboardInterrupt: + puts(colored.red('Emergency Feed Hold. Enter "~" to continue')) + serial.run('!\n?') + + command = "G20" + try: + serial.run(command) + except KeyboardInterrupt: + puts(colored.red('Emergency Feed Hold. Enter "~" to continue')) + serial.run('!\n?') + + num_x = -1 + for X in arange(0, X_MAX, X_STEP): + num_x=num_x+1 + num_y=-1 + for Y in arange(0, Y_MAX, Y_STEP): + num_y=num_y+1 + puts(colored.yellow("going to x:{:.4f} and y:{:.4f}".format(X,Y))) + + command = "G0 X{:.4f} Y{:.4f} Z{:.4f}".format(X,Y,HIGH_Z) + if DEBUG_VERBOSE: + print command + try: + serial.run(command) + except KeyboardInterrupt: + puts(colored.red('Emergency Feed Hold. Enter "~" to continue')) + serial.run('!\n?') + + command = "G38.2 Z{:.4f} F{:.4f}".format(LOW_Z,PROBE_FEED_RATE) + try: + serial.run(command) + except KeyboardInterrupt: + puts(colored.red('Emergency Feed Hold. Enter "~" to continue')) + serial.run('!\n?') + + converged = False + while not converged: + time.sleep(2) + status_report_string = serial.run('?',singleLine=True) + current_status = GRBL_status().parse_grbl_status(status_report_string) + print '' + puts(colored.yellow(''.join('Z=' + '{:.4f}'.format((float(current_status.get_z())))))) + #print 'z position :' + #print float(current_status.get_z()) + if current_status.is_idle(): + converged = True + Z=current_status.get_z() + Surface_Data[num_x,num_y] = Z + if current_status.is_alarmed(): + print 'PyGRBL: did not detect surface in specified z range, alarm tripped' + serial.run('$X') + serial.run("G0 X{:.4f} Y{:.4f} Z{:.4f}".format(X,Y,HIGH_Z)) + break + + command = "G0 X{:.4f} Y{:.4f} Z{:.4f}".format(X,Y,HIGH_Z) + if DEBUG_VERBOSE: + print command + try: + serial.run(command) + except KeyboardInterrupt: + puts(colored.red('Emergency Feed Hold. Enter "~" to continue')) + serial.run('!\n?') + +print Surface_Data +np.savetxt('probe_test.out', Surface_Data, delimiter=',',header='X_STEP:,{:.4f}, Y_STEP:,{:.4f},'.format(X_STEP,Y_STEP)) diff --git a/probe_test.out b/probe_test.out new file mode 100644 index 0000000..9b2b677 --- /dev/null +++ b/probe_test.out @@ -0,0 +1,3 @@ +# X_STEP:,0.5000, Y_STEP:,0.5000, +4.653999999999999804e-01,4.833000000000000074e-01 +4.758000000000000007e-01,4.703999999999999848e-01 diff --git a/visualize.py b/visualize.py index e0c4b12..3cd554b 100755 --- a/visualize.py +++ b/visualize.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # visualize.py : A set of nice modifications for gcode -# [2012.08.21] - Mendez +# [2012.08.21] - Mendez import os, re, sys from math import ceil from datetime import datetime @@ -30,12 +30,12 @@ def main(gfile, args=None): # Read in the gcode gcode = GCode(gfile, limit=None) gcode.parse() - + # parse the code into an array of tool moves tool = Tool(gcode) tool.uniq() box = tool.boundBox() - + # proces and save image ext = args.ext if args is not None else '.pdf' outfile = os.path.splitext(gfile.name)[0] + FILEENDING + ext @@ -71,4 +71,3 @@ def main(gfile, args=None): main(gfile, args=args) print '%s finished in %s'%(args.name,deltaTime(start)) - diff --git a/zcorrect.py b/zcorrect.py new file mode 100644 index 0000000..13caca2 --- /dev/null +++ b/zcorrect.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python + +#this code should use a 2 dimensional array of z_positions +#the element 0,0 is 0 and all other elements are relative z positions +#there should also be a probe_step_x value and a probe_step_y value +#these define the distsnces between the points measured in the z_positions array + + +#imports +from numpy import arange +from lib import argv +from lib.gparse import gparse +from lib.grbl_status import GRBL_status +from lib.communicate import Communicate +from clint.textui import puts, colored +import time, readline +import numpy as np +import re + + + +def Load_Correction_Surface(surface_file_name = 'probe_test.out' ): + + #initialize and load the correction surface + Surface_Data = [] + Surface_Data = np.loadtxt(surface_file_name, delimiter=',', comments = '#') + correction_surface.array = Surface_Data + #display the dataset to be used + puts(colored.yellow('Surface Z Data Matrix')) + print Surface_Data + + probe_data_file = open(surface_file_name) + + #probe_data = probe_data_file.read() + + # retrieve the X and Y step sizes that scale the correction surface + for line in probe_data_file: + line = line.strip() + + if re.search('#', line,flags = re.IGNORECASE): + puts(colored.green('extracting scale data from file header')) + puts(colored.green( line)) + if re.search(' X_STEP:', line,flags = re.IGNORECASE): + #X_STEP:,0.5000 + X_STEP_INFO = re.findall('X_STEP:,\d*\.\d*,', line, flags = re.IGNORECASE)[0] + if X_STEP_INFO: + X_STEP = float(X_STEP_INFO.split(',')[1]) + puts(colored.yellow( 'x step size: {:.4f}'.format(X_STEP))) + correction_surface.x_step = X_STEP + else: + puts(colored.red( 'x step size: not found!')) + if re.search(' Y_STEP:', line,flags = re.IGNORECASE): + #X_STEP:,0.5000 + Y_STEP_INFO = re.findall('Y_STEP:,\d*\.\d*,', line, flags = re.IGNORECASE)[0] + if Y_STEP_INFO: + Y_STEP = float(Y_STEP_INFO.split(',')[1]) + puts(colored.yellow( 'Y step size: {:.4f}'.format(Y_STEP))) + correction_surface.Y_step = Y_STEP + else: + puts(colored.red( 'Y step size: not found!')) + return correction_surface From e976519593246632cc168c96f47caaecb96931e4 Mon Sep 17 00:00:00 2001 From: Mike Date: Sun, 16 Aug 2015 21:08:25 -0700 Subject: [PATCH 2/9] trying to get new functions for z suface correction of pcb etching online --- lib/correction_surface.py | 4 +- lib/gcode.py | 17 +++-- lib/image.py | 45 +++++++------ lib/tool.py | 50 ++++++++++++-- lib/util.py | 18 +++-- modify.py | 5 ++ probe_test.out | 2 +- zcorrect.py | 134 ++++++++++++++++++++++++++------------ 8 files changed, 193 insertions(+), 82 deletions(-) diff --git a/lib/correction_surface.py b/lib/correction_surface.py index 6b3984d..c3a5f84 100644 --- a/lib/correction_surface.py +++ b/lib/correction_surface.py @@ -19,11 +19,13 @@ class CorrectionSurface(): - def __init__(self,): + def __init__(self,surface_file_name = ''): self.x_step = "nan" self.y_step = "nan" self.array = [] self.Load_Correction_Surface() + if surface_file_name != '': + self.Load_Correction_Surface(surface_file_name) def Load_Correction_Surface(self, surface_file_name = 'probe_test.out' ): diff --git a/lib/gcode.py b/lib/gcode.py index f39fccf..177debc 100755 --- a/lib/gcode.py +++ b/lib/gcode.py @@ -20,18 +20,18 @@ (Finished and Moving back to origin) ${finish} (DONE)''' + INIT='''\ G20 G90 G00 X0.000 Y0.000 Z0.000 G00 Z0.100''' + FINISH = '''\ G00 Z0.100 G00 X0.000 Y0.000 G00 Z0.000''' - - class GCode(list): def __init__(self, gcode, limit=None): '''start with a gcode ascii file''' @@ -109,7 +109,7 @@ def _parse(self): if m: out['comment'] = m for cmd in CMDS: #incorporating the definintion of command inside this loop should make it more flexible - command = r''.join(r'(?P<%s>%s(?P<%snum>-?\d*+(?P<%sdecimal>\.?)\d*))?'%(c,c,c,c)) + command = r''.join(r'(?P<%s>%s(?P<%snum>-?\d+(?P<%sdecimal>\.?)\d*))?'%(cmd,cmd,cmd,cmd)) c = re.match(command,l) if c.group(cmd): # either a float if '.' or a int @@ -124,17 +124,24 @@ def _parse(self): # self.append(out) def update(self,tool): - '''Updates the gcode with a toolpath only does x,y''' + '''Updates the gcode from a toolpath. ONLY does x,y''' UPDATE = 'xy' for x in tool: # print len(x) + # this conditional suggests that this code is intended only to work with linear moves + # if this is the case we may need to generalize it if len(x) == 5: # print x for u in UPDATE: if u.upper() in self[x[4]]: + '''The indexdicts of which a tool object is a list''' + '''have x,y,z,cmd = my_index_dict[0:4]''' + '''note that cmd = my_index_dict[3] is the G command number''' + '''note that index = my_index_dict[4] is the index of the G comand in the whole g code sequence''' + # set the g code command dicts x and y entries of the same index as the tool to the same x and y values as in the tool self[x[4]][u.upper()] = x[UPDATE.index(u)] - # print self[x[4]] + # print self[x[4]] # print self[x[4]], # print u.upper(), # print self[x[4]][u.upper()] diff --git a/lib/image.py b/lib/image.py index 094aaee..8c79692 100755 --- a/lib/image.py +++ b/lib/image.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # image.py : a nice image library -# [2012.08.21] - Mendez +# [2012.08.21] - Mendez import os, re, sys from datetime import datetime from random import uniform @@ -22,25 +22,26 @@ def update_path(path, tool, cmd): '''update function to simplify below''' tmp = [tool.x,tool.y] - # for arcs we need to know some special bits + # for arcs we need to know some special bits (in Mendez speak circa 2013 bits cn refer to any noun) + # (in this case he is telling you that arcs used I and J variables to plot thier path) if cmd in (2,3): tmp.extend([tool['I'],tool['J'],cmd]) # make sure that we only add new points / or if path is empty - if len(path) < 1 or tmp != path[-1] : + if len(path) < 1 or tmp != path[-1] : path.append(tmp) return cmd, path class Image(object): - def __init__(self, filename=None, + def __init__(self, filename=None, gridscale=1.0, gridsize=[[0.0,1.0],[0.0,1.0]], pagesize=(7.5,4), # width,height [in] embiggen=0.10, # percent to add to size pagemargin=0.5): - ''' Image Canvas with milling functions. + ''' Image Canvas with milling functions. gridscale : [Float] multiplication factor for grid size gridsize : [Inches] The physical space to span. [[xmin,xmax],[ymin,ymax]] pagesize : [inches] Can be (width,height) or 'letter','letter*', 'A3',A4' @@ -65,7 +66,7 @@ def __init__(self, filename=None, parter = graph.axis.parter.linear(ticks, labeldists=[1]) painter = graph.axis.painter.regular(gridattrs=[attr.changelist(gridcolors)], - # outerticklength=attr.changelist(ticklen), + # outerticklength=attr.changelist(ticklen), innerticklength=attr.changelist(ticklen) ) x = graph.axis.linear(min=grid[0][0], max=grid[0][1], painter=painter, parter=parter, @@ -73,7 +74,7 @@ def __init__(self, filename=None, y = graph.axis.linear(min=grid[1][0], max=grid[1][1], painter=painter, parter=parter, title='Y [inch]') - + print("Ratio: %f"%(delta[0][2]/delta[1][2])) self.g = graph.graphxy(x=x, y=y,width=pagesize[0], ratio=delta[0][2]/delta[1][2]) @@ -94,8 +95,8 @@ def __init__(self, filename=None, # bounding box self.g.plot(graph.data.points(zip([gridsize[0][j] for j in [0,1,1,0,0]], - [gridsize[1][j] for j in [0,0,1,1,0]]), x=1, y=2), - [graph.style.line([color.cmyk.YellowOrange, + [gridsize[1][j] for j in [0,0,1,1,0]]), x=1, y=2), + [graph.style.line([color.cmyk.YellowOrange, style.linewidth.THICK, style.linejoin.miter])]) @@ -127,7 +128,7 @@ def __exit__(self, type, value, traceback): def save(self,filename=None, pdf=False): if filename is None and self.filename is None: error("nowhere to save") fname = filename if self.filename is None else self.filename - puts(colored.green('Writing : %s'%fname)) + puts(colored.green('Writing : %s'%fname)) # self.d.writetofile(fname) if '.pdf' in fname: self.c.writePDFfile(fname) @@ -143,7 +144,7 @@ def showall(self,tool): self.mill(xarr,yarr, color=color.rgb.green) def process(self,tool): - '''Mill out a toolpath. groups together mills, moves, and + '''Mill out a toolpath. groups together mills, moves, and drill, and then plots them together''' puts(colored.blue('Processing toolpath for drawing:')) @@ -203,7 +204,7 @@ def process(self,tool): # Ok plot everything, lines will not be connected between nulls # if len(mil) > 0 : self.mill(zip(*mil)[0], zip(*mil)[1]) # # if len(arc) > 0 : self.arc(zip(*arc)[0], zip(*arc)[1]) - # if len(arc) > 0 : + # if len(arc) > 0 : # x,y = self._interpArc(*zip(*arc)) # self.arc(x,y) @@ -230,27 +231,27 @@ def _convertwidth(self,width): return width # the invidual plot commands - def mill(self,xarr,yarr, - color=color.rgb.blue, + def mill(self,xarr,yarr, + color=color.rgb.blue, width=0.010): # in inches '''Mill an x,y array defaults to red and 10mil paths.''' w = self._convertwidth(width) self.g.plot(graph.data.points(zip(xarr, yarr), x=1, y=2), [graph.style.line([w, color])]) - def move(self,xarr,yarr, + def move(self,xarr,yarr, color=color.gray(0.45), width=style.linewidth.thin): '''Moves the bit around (x,y) defaults to light blue and 1 point ('onepoint'), can pass a inch float as well. ''' w = self._convertwidth(width) - + self.g.plot(graph.data.points(zip(xarr, yarr), x=1, y=2), [graph.style.line([w, color])]) def drill(self,x,y, - r=0.032, - color=None, + r=0.032, + color=None, outlinewidth=style.linewidth.thin, outlinecolor=color.rgb.blue): ''' A nice drill hole cross and defaults to 32mil holes''' @@ -259,7 +260,7 @@ def drill(self,x,y, [graph.style.symbol(graph.style.symbol.circle, size=r*unit.w_inch, symbolattrs=[deco.stroked([w, outlinecolor])])]) - def arc(self, xarr,yarr, + def arc(self, xarr,yarr, color=color.cmyk.Cyan, width=0.010): '''Make a nice arc''' @@ -273,7 +274,7 @@ def _interpArc(self, xarr, yarr, iarr, jarr, cmd): for i in range(1,len(cmd)-1): - if cmd[i] is None or cmd[i-1] is None: + if cmd[i] is None or cmd[i-1] is None: continue print i, xarr[i-1],yarr[i-1] print ' ', xarr[i], yarr[i], iarr[i], jarr[i], cmd[i] @@ -295,7 +296,7 @@ def _interpArc(self, xarr, yarr, iarr, jarr, cmd): delta = DELTA_CURVE_IN steps = length/delta - + for j in arange(0,steps): k = j if cmd[i]==2 else steps-j x.append( center[0] + radius*cos(angle[0] + ang*float(k)/steps) ) @@ -305,5 +306,3 @@ def _interpArc(self, xarr, yarr, iarr, jarr, cmd): x.append(None) y.append(None) return x,y - - diff --git a/lib/tool.py b/lib/tool.py index e63e5c9..ef7219e 100755 --- a/lib/tool.py +++ b/lib/tool.py @@ -1,6 +1,19 @@ #!/usr/bin/env python # tool.py : Parses a gcode file # [2012.07.31] Mendez + +# Parse a series of g code moves represented by a Gcode object into a much less general however much more easily manipulated toolpath format called Tool +# of specific interest is the class IndexDict() which can be found in util +# the index dict is used as an intermediate datastructure between the GCODE instance input and the Tool instance output which is actially composed of a list of IndexDicts + +''' +#For EXAMPLE here self is a "TOOL" and has a simple orgainztion of relevant data +#Item is an IndxDict() instance from the Tool() instance called self +for i,item in enumerate(self): + x,y,z,cmd = item[0:4] +''' +# [2015.08.15] Erickstad + import re,sys,math from pprint import pprint from string import Template @@ -11,6 +24,9 @@ from util import error, distance, IndexDict from mill import Mill +#needed for zcorrect to use correction surface features +from correction_surface import CorrectionSurface + AXIS='XYZIJ' TEMPLATE='''(Built with python and a dash of Mendez) @@ -32,17 +48,23 @@ def origin(): # Moves and the sort def noop(self,m=None,t=None): pass + def home(self,m=None,t=None): self.append(origin()) # self.append(origin()+[0]) #origin + def inch(self,m=None,t=None): self.units = 'inch' + def mm(self,m=None,t=None): self.unis = 'mm' + def absolute(self,m=None,t=None): self.abs = True + def relative(self,m=None,t=None): self.abs = False + def move(self,m,cmd, z=None): '''Moves to location. if NaN, use previous. handles rel/abs''' for i,key in enumerate(m): @@ -59,6 +81,7 @@ def move(self,m,cmd, z=None): # if not self.abs: loc += self[-1][:] # rel/abs # loc.append(t) # self.append(loc) + def convert(self,m): if self.units == 'mm': m = [x*25.4 for x in m] @@ -74,6 +97,8 @@ def circle(self,m,t): move(self, m, t) # FIXME +'''DICTIONARY OF FUNCTIONS''' +'''SEE DEFINITIONS ABOVE''' GCMD = {0: move, 1: move, 2: circle, @@ -85,7 +110,7 @@ def circle(self,m,t): 54: noop, # Word Coords 90: absolute, 91: relative, - 94:noop, #FeedRate/minute + 94: noop, #FeedRate/minute } @@ -97,7 +122,6 @@ def __init__(self, gcode=None): self.units = 'inch' self.mills = [] home(self) - if gcode: self.build(gcode) def __repr__(self): @@ -105,23 +129,30 @@ def __repr__(self): return '%s() : %i locations, units: %s'%(self.__class__.__name__, len(self),self.units) - - def build(self, gcode): '''New gCode that uses the indexedDict''' puts(colored.blue('Building Toolpath:')) for i,line in enumerate(progress.bar(gcode)): # for i,line in enumerate(gcode): if 'G' in line: # only handle the gcodes + # Get the G code number assign it to cmd + # for human readablitiy cmd should be changes to g_command_number + # or somehting like that + # however notice that it is hardcoded as a dict key as well + '''copy over the relevant data x y z i j index and g_command_number''' + '''To an indexdict named move with the name attribute set to the string "move" ''' cmd = line['G'] move = IndexDict(name='move') for j,x in enumerate(AXIS): if x in line: move[x] = line[x] move['cmd'] = cmd move['index'] = line['index'] + try: fcn = GCMD[cmd] move.name = 'cmd[% 2i]'%cmd + # Try using the indexdict instance as info for the next coordinates to be attached to the toolpath + # by way of the function fcn selcted from the dict of functions GCMD above fcn(self, move, cmd) except KeyError: # raise @@ -153,12 +184,17 @@ def boundBox(self): '''Returns the bounding box [[xmin,xmax],[ymin,ymax],[zmin,zmax]] for the toolpath''' box = [[0.,0.],[0.,0.],[0.,0.]] + # for each element of the toolpath for item in self: + # for each coordinate "ax" for i,ax in enumerate(item): # print i,ax if item[ax] < box[i][0]: box[i][0] = item[ax] if item[ax] > box[i][1]: box[i][1] = item[ax] # if j == 2 : sys.exit() + # if afterwards the box has no dimensions throw an error + if box == [[0.,0.],[0.,0.],[0.,0.]]: + print 'Bounding box has no size; toolpath may not have been parsed correctly' return box def offset(self, offset): @@ -334,6 +370,12 @@ def buildGcode(self): #### some commands to move / copy and otherwise change the gcode. def move(self,loc): '''Moves the toolpath from (0,0) to (loc[0],loc[1])''' + '''Moves the toolpath from (x_n,y_n) to (x_n+loc[0],y_n+loc[1]) for all n?''' for item in self: for i,x in enumerate(loc): item[i] += x + + def zcorrect(self, correction_surface): + #correct the z position of the points on the tool path + for item in self: + item[2] += correction_surface.estimate_surface_z_at_pozition(item[0],item[1]) diff --git a/lib/util.py b/lib/util.py index d95b6df..efaaecd 100644 --- a/lib/util.py +++ b/lib/util.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # util.py : some nice things # [2012.07.30] - Mendez +# INCLUDING the defintion of INDEXDICT +# [2015.08.15] - Erickstad import sys from datetime import datetime from math import sqrt @@ -33,7 +35,7 @@ def deltaTime(start): if delta > 1: noun +='s' out.append('%d %s'%(delta,noun)) seconds -= factor*(delta) - + return ', '.join(out) @@ -45,7 +47,7 @@ def distance(A,B): return sqrt(sum([pow(alpha-beta,2) for alpha,beta in zip(a,b)])) -def uniqify(seq, idfun=None): +def uniqify(seq, idfun=None): '''order preserving uniq function''' if idfun is None: def idfun(x): return x @@ -94,11 +96,14 @@ class IndexDict(dict): ref = {0:'x',1:'y',2:'z'} full = {3:'cmd',4:'index',5:'i',6:'j'} full.update(ref) + def __init__(self, *args, **kwargs): # self._setname(name) # self.name = name dict.__init__(self, *args, **kwargs) + # get the name enrty from the dicty into a dot name member if 'name' in self: self._setname(self['name']) + #here is the finction for doing the setting of self.name def _setname(self,name=None): ''' Set the name to be something''' if name is not None: @@ -148,7 +153,7 @@ def __repr__(self): return '%s:(x=% .3f, y=% .3f, i=% .3f, j=% .3f)'%(self.name, self[0],self[1],self[5],self[6]) else: return '%s:(% .3f,% .3f,% .3f)'%(self.name, self[0],self[1],self[2]) - + def toGcode(self): ''' attempts to convert ''' if self.cmd == 2: @@ -162,6 +167,7 @@ def toGcode(self): def __iter__(self): self._current = 0 return self + def next(self): if self._current > len(self.ref.keys())-1: raise StopIteration @@ -172,11 +178,13 @@ def next(self): # sometimes we want everything def allkeys(self): return dict.keys(self) + def allvalues(self): return dict.values(self) def keys(self): return sorted([self.ref[k] for k in self.ref]) + def values(self): return [self.get(k) for k in self.keys()] @@ -223,7 +231,3 @@ def setorigin(s): e = IndexDict(d) print d print e - - - - diff --git a/modify.py b/modify.py index 6765174..e3b24f2 100755 --- a/modify.py +++ b/modify.py @@ -57,10 +57,13 @@ def parse(move, getUnits=False, defaultUnit='in'): if isinstance(move, str): move = [move] #does [no unit specified] need escape chars \[ \]? + # no because r'' units = r'(?Pin|mil|mm|[NoUnitSpecified]?)' if getUnits else r'' out = [] for m in move: + # remove all of the white space m = re.sub(r'\s','',m).strip() + # g = re.match(r'(?P-?\d*\.?\d*)\,(?P-?\d*\.?\d*)'+units, m, re.I) if not g: error('Argument Parse Failed on [%s] failed! Check the arguments'%(m)) @@ -105,6 +108,7 @@ def mod(gfile): loc = parse(args.move, getUnits=True) # only one move at a time. puts(colored.blue('Moving!\n (0,0) -> (%.3f,%.3f)'%(loc[0],loc[1]))) tool = Tool() + # is the addIndex atribut even used any longer? tool.build(gcode, addIndex=True) tool.move(loc) # ok well this should work gcode.update(tool) @@ -117,6 +121,7 @@ def mod(gfile): puts(colored.blue(' (0,0) -> (%.3f,%.3f)'%(loc[0],loc[1]))) gc = gcode.copy() tool = Tool() + # is the addIndex atribut even used any longer? tool.build(gc, addIndex=True) tool.move(loc) gc.update(tool) diff --git a/probe_test.out b/probe_test.out index 9b2b677..4680feb 100644 --- a/probe_test.out +++ b/probe_test.out @@ -1,3 +1,3 @@ -# X_STEP:,0.5000, Y_STEP:,0.5000, +# X_STEP:,4.5000, Y_STEP:,4.5000, 4.653999999999999804e-01,4.833000000000000074e-01 4.758000000000000007e-01,4.703999999999999848e-01 diff --git a/zcorrect.py b/zcorrect.py index 13caca2..b4df90c 100644 --- a/zcorrect.py +++ b/zcorrect.py @@ -7,55 +7,107 @@ #imports +import os, re, sys from numpy import arange +# use argv in order to have options when using z correct as main function from lib import argv -from lib.gparse import gparse +from lib.util import deltaTime +from datetime import datetime + +#from lib.gparse import gparse +from lib.gcode import GCode +from lib.tool import Tool from lib.grbl_status import GRBL_status from lib.communicate import Communicate +from lib.correction_surface import CorrectionSurface from clint.textui import puts, colored import time, readline import numpy as np import re +FILEENDING = '_mod' # file ending for optimized file. + + +EXTRAARGS = dict(ext=dict(args=['-z','--zsurface'], + default='probe_test.out', + const='probe_test.out', + action='store_const', + dest='z_surf', + help='''Specify for outputing an eps file rather than a pdf.''') ) + + +def zcorrect_file(gfile,surface_file_name = 'probe_test.out'): + + # Load the correction surface + correction_surface = CorrectionSurface(surface_file_name) + + # keep track of time + start = datetime.now() + + name = gfile if isinstance(gfile,str) else gfile.name + puts(colored.blue('Z correcting the file: %s\n Started: %s'%(name,datetime.now()))) + + # Load the gcode. + gcode = GCode(gfile) + #parse the Gcode + gcode.parseAll() + + # start an empty list + #out = [] + + # need to get rid of use of 'loc' + # loc = parse(args.move, getUnits=True) # only one move at a time. + # puts(colored.blue('Moving!\n (0,0) -> (%.3f,%.3f)'%(loc[0],loc[1]))) + + # create a tool object (toolpath object) + tool = Tool() + # load the gcode into the tool object + tool.build(gcode) + # adjust the z position at each point by the given amount + tool.zcorrect(correction_surface) + # load the changes back into the gcode object + gcode.update(tool) + # append the modified g code to the empty list called out + #out.append([gcode]) + out = gcode + # convert gcode to text format + #output = ''.join([o.getGcode(tag=args.name) for o in out]) + output = ''.join([out.getGcode()]) + # get an output file name + outfile = FILEENDING.join(os.path.splitext(gfile)) + print "outfile is:" + print outfile + # tell the user + puts(colored.green('Writing: %s'%outfile)) + # write to file + f = open(outfile,'w') + f.write(output) + ''' + with open(outfile,'w') as f: + f.write(output) + ''' + # how long did this take? + puts(colored.green('Time to completion: %s'%(deltaTime(start)))) + print + + +if __name__ == '__main__': + start = datetime.now() + + args = argv.arg(description='PyGRBL gcode imaging tool', + getFile=True, # get gcode to process + getMultiFiles=False, # accept any number of files + otherOptions=EXTRAARGS, # "Install some nice things" very descriptive!!! + getDevice=False) # We dont need a device + + # optimize each file in the list + for gfile in args.gcode: + # only process things not processed before. + # c = re.match(r'(?P\.drill\.tap)|(?P\.etch\.tap)', gfile.name) + c = re.match(r'(.+)(\.tap)', gfile) + # c = True # HAX and accept everything + if c: # either a drill.tap or etch.tap + zcorrect_file(gfile) #args=args) -def Load_Correction_Surface(surface_file_name = 'probe_test.out' ): - - #initialize and load the correction surface - Surface_Data = [] - Surface_Data = np.loadtxt(surface_file_name, delimiter=',', comments = '#') - correction_surface.array = Surface_Data - #display the dataset to be used - puts(colored.yellow('Surface Z Data Matrix')) - print Surface_Data - - probe_data_file = open(surface_file_name) - - #probe_data = probe_data_file.read() - - # retrieve the X and Y step sizes that scale the correction surface - for line in probe_data_file: - line = line.strip() - - if re.search('#', line,flags = re.IGNORECASE): - puts(colored.green('extracting scale data from file header')) - puts(colored.green( line)) - if re.search(' X_STEP:', line,flags = re.IGNORECASE): - #X_STEP:,0.5000 - X_STEP_INFO = re.findall('X_STEP:,\d*\.\d*,', line, flags = re.IGNORECASE)[0] - if X_STEP_INFO: - X_STEP = float(X_STEP_INFO.split(',')[1]) - puts(colored.yellow( 'x step size: {:.4f}'.format(X_STEP))) - correction_surface.x_step = X_STEP - else: - puts(colored.red( 'x step size: not found!')) - if re.search(' Y_STEP:', line,flags = re.IGNORECASE): - #X_STEP:,0.5000 - Y_STEP_INFO = re.findall('Y_STEP:,\d*\.\d*,', line, flags = re.IGNORECASE)[0] - if Y_STEP_INFO: - Y_STEP = float(Y_STEP_INFO.split(',')[1]) - puts(colored.yellow( 'Y step size: {:.4f}'.format(Y_STEP))) - correction_surface.Y_step = Y_STEP - else: - puts(colored.red( 'Y step size: not found!')) - return correction_surface + print '%s finished in %s'%(args.name,deltaTime(start)) From 664c4d7b9c1c679a83429da0a39b73402738ee4b Mon Sep 17 00:00:00 2001 From: ben Date: Sun, 23 Aug 2015 22:45:35 -0700 Subject: [PATCH 3/9] optimize: lift off at feed rate to help protect the drill bit when drilling. This is a non-optimal solution, since most times while milling we could lift-off at move speed, but I don't want to take time right now to sort out the code enough for that, so I'm just having it use feed speed every time, and relying on it not to take too long, since it's not very far and not that often. This includes a couple of updates to make this not break visualize.py --- lib/tool.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/tool.py b/lib/tool.py index db84d76..ce1ef2a 100755 --- a/lib/tool.py +++ b/lib/tool.py @@ -67,7 +67,8 @@ def convert(self,m): def millMove(self, next, height): '''Move the toolbit to the next mill location''' last = self[-1] - move(self, IndexDict(last), 0, z=height) # lift off of board + move(self, IndexDict(last), 1, z=height) # lift off of board; move at feed rate in case we're drilling. Ideally we'd only move at feed rate for the liftoff if we were drilling and not if we're milling, but the current structure of the code makes it rather tricky to find that out in a robust way, so for now just defaulting to always liftoff ad feed rate, which should be safe and have negligible effects on total job time + move(self, IndexDict(last), 0, z=height) # hack for old drawing code: lift off to same position at move speed so drawing code will put in move operations move(self, IndexDict(next), 0, z=height) # Move to next pos def circle(self,m,t): @@ -315,9 +316,9 @@ def buildGcode(self): x,y,z,cmd = item[0:4] # OK this is a crappy hack to ensure that we put a nice little name before each mill. - # so basically we need a move before this one, this one be a move and the next one be a mill + # so basically we need z>0, this one be a move and the next one be a mill # and then we write a nice message - if ([l[3] for l in self[i-1:i+2]] == [0,0,1]): + if ([l[3] for l in self[i:i+2]] == [0,1] and z > 0): lines.append('\n(Mill: %04i)'%(iMill)) iMill += 1 lines.append('G%02i X%.3f Y%.3f Z%.3f'%(cmd,x,y,z)) From b05dda16182ef763c4e72b10b381b2d299e1e8dc Mon Sep 17 00:00:00 2001 From: ben Date: Sun, 12 Apr 2015 15:48:30 -0700 Subject: [PATCH 4/9] optimize: remember to add the last mill operation Previous version of code left out the last mill step. Oops! --- lib/tool.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/tool.py b/lib/tool.py index ce1ef2a..7462248 100755 --- a/lib/tool.py +++ b/lib/tool.py @@ -243,6 +243,8 @@ def groupMills(self): if len(mill) > 0: self.mills.append(mill) mill = Mill() # ready for more mills + # and get the last one! + if len(mill) > 0: self.mills.append(mill) def uniqMills(self): From 2c9e490680615c04b8ce09fb7f8ee0a6884416a5 Mon Sep 17 00:00:00 2001 From: ben Date: Thu, 30 Apr 2015 18:06:15 -0700 Subject: [PATCH 5/9] add draw.py Try to draw what the robot will etch out, assume a 45 degree v-bit --- draw.py | 61 +++++++++++++++++++++++++++++ lib/drawing.py | 104 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100755 draw.py create mode 100755 lib/drawing.py diff --git a/draw.py b/draw.py new file mode 100755 index 0000000..5aee18a --- /dev/null +++ b/draw.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +# draw.py : simulate mill/drill/etch to an EPS file +# [2015-04-17] - bkurtz +import os, re, sys +from math import ceil +from datetime import datetime +from lib.gcode import GCode +from lib.tool import Tool +from lib.drawing import Drawing +from lib import argv +from lib.util import deltaTime +from clint.textui import puts, colored + + +def main(etch_file, args=None): + start = datetime.now() + name = etch_file if isinstance(etch_file,str) else etch_file.name + puts(colored.blue('Visualizing the file: %s\n Started: %s'%(name,datetime.now()))) + + # Read in the gcode + gcode = GCode(etch_file, limit=None) + gcode.parse() + + # parse the code into an array of tool moves + tool = Tool(gcode) + box = tool.boundBox() + + # proces and save image + outfile = os.path.splitext(etch_file.name)[0] + '.eps' + print box + print box[0:2] + image = Drawing(outfile)#, bbox=box) + image.process(tool) + image.save() + + # how long did this take? + puts(colored.green('Time to completion: %s'%(deltaTime(start)))) + print + + +if __name__ == '__main__': + ## I should wrap this in a __main__ section + # Initialize the args + start = datetime.now() + args = argv.arg(description='PyGRBL gcode imaging tool', + getFile=True, # get gcode to process + getMultiFiles=True, # accept any number of files + getDevice=False) # We dont need a device + + + # optimize each file in the list + for gfile in args.gcode: + # only process things not processed before. + # c = re.match(r'(?P\.drill\.tap)|(?P\.etch\.tap)', gfile.name) + c = re.match(r'(.+)(\.tap)', gfile.name) + # c = True # HAX and accept everything + if c: # either a drill.tap or etch.tap file + main(gfile, args=args) + + print '%s finished in %s'%(args.name,deltaTime(start)) + diff --git a/lib/drawing.py b/lib/drawing.py new file mode 100755 index 0000000..35a724e --- /dev/null +++ b/lib/drawing.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python +# image.py : a nice image library +# [2012.08.21] - Mendez +import os, re, sys +from datetime import datetime +from random import uniform +from clint.textui import puts, colored, progress +from lib.util import deltaTime, error, distance + +# from math import pi +from numpy import arange, floor, ceil, arctan2, pi, sqrt, cos, sin +from pyx import * + +unit.set(defaultunit="inch") + +DELTA_CURVE_IN = 0.002 + + +class Drawing(object): + def __init__(self, filename=None, + pagemargin=0.1): + ''' Image Canvas with milling functions. + pagemargin : [inches] Top, right, bottom, left margins.''' + if filename: + self.filename = os.path.expanduser(os.path.expandvars(filename)) + else: + self.filename = None + + self.eps_header = "%!PS-Adobe-2.0 EPSF-2.0\n" + self.ps = "gsave\n" + self.ps += "{0} {0} translate\n".format(round(72*pagemargin)) + self.ps += "1 setlinecap 1 setlinejoin\n" + self.ps += "0 0 moveto\n" # need a first point to get us started + + # bounding box + self.margin = pagemargin; + self.box = "1 0 0 setrgbcolor\n" + self.box += "{0} {0} moveto\n".format(round(72*pagemargin)) + self.bounds = {'xmax': 0, 'ymax' : 0} + + + def __enter__(self): + '''with constructor :: generate a nice plot with a plot and axis''' + return self + def __exit__(self, type, value, traceback): + '''with constructor finished -- close anything open.''' + self.save() + return isinstance(value, TypeError) + + def save(self,filename=None): + if filename is None and self.filename is None: error("nowhere to save") + fname = filename if self.filename is None else self.filename + puts(colored.green('Writing : %s'%fname)) + # add the final bounding box to the postscript header + self.apply_bounding_box() + # combine everything together + final_eps = self.eps_header + "\n" + self.box + "\n" + self.ps + "\ngrestore\n" + with open(fname, "w") as eps_file: + eps_file.write(final_eps) + + def apply_bounding_box(self): + '''generate the postscript header and drawing commands for the bounding box''' + xmax = (self.bounds['xmax']+self.margin*2)*72; + ymax = (self.bounds['ymax']+self.margin*2)*72; + self.eps_header += "%%BoundingBox: 0 0 {} {}\n".format(round(xmax), round(ymax)) + self.box += "{} {} lineto\n".format(round(72*self.margin), round((self.bounds['ymax']+self.margin)*72)) + self.box += "{} {} lineto\n".format(round((self.bounds['xmax']+self.margin)*72), round((self.bounds['ymax']+self.margin)*72)) + self.box += "{} {} lineto\n".format(round((self.bounds['xmax']+self.margin)*72), round(72*self.margin)) + self.box += "closepath stroke\n" + + def process(self,tool): + '''Mill out a toolpath. Currently etch-only - assume v-bit, i.e. depth=width and draw lines of the appropriate width to simulate etching''' + puts(colored.blue('Processing toolpath for drawing:')) + + # we assume that we are starting with a g0x0y0z0 + last_cmd = 0 + last_z = 0 + + for i,t in enumerate(progress.bar(tool)): + cmd = t.cmd + z = t.z + if t.x > self.bounds['xmax']: self.bounds['xmax'] = t.x + if t.y > self.bounds['ymax']: self.bounds['ymax'] = t.y + + # if we are doing something different or we are done: + if cmd != last_cmd or last_z != z or i == len(tool)-1: + if i == len(tool)-1: last_z = z + if last_cmd == 0 or last_z >= 0: + # draw thin grey lines for movement + self.ps += "0.1 setlinewidth 0.5 0.5 0.5 setrgbcolor stroke\n" + elif last_cmd == 1 : + self.ps += "{} setlinewidth 0 1 0 setrgbcolor stroke\n".format(last_z*-72) + elif last_cmd in (2,3) : + puts(colored.red('um... don\'t know how to draw arcs yet!')) + + # move instead of drawing a line to the new point + self.ps += "{0} {1} moveto\n{0} {1} lineto\n".format(t.x*72, t.y*72) + # then update the last_cmd and last_z params + last_cmd = cmd + else: + self.ps += "{} {} lineto\n".format(t.x*72, t.y*72) + + last_z = z + From 7d5fc52be434dfae3e9e26f929494ce0628e509d Mon Sep 17 00:00:00 2001 From: ben Date: Thu, 30 Apr 2015 18:51:20 -0700 Subject: [PATCH 6/9] drawing: remove stupid condition I thought this fixed another issue I was seeing, but it seems not to. Ooops --- lib/drawing.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/drawing.py b/lib/drawing.py index 35a724e..b86d0c7 100755 --- a/lib/drawing.py +++ b/lib/drawing.py @@ -84,7 +84,6 @@ def process(self,tool): # if we are doing something different or we are done: if cmd != last_cmd or last_z != z or i == len(tool)-1: - if i == len(tool)-1: last_z = z if last_cmd == 0 or last_z >= 0: # draw thin grey lines for movement self.ps += "0.1 setlinewidth 0.5 0.5 0.5 setrgbcolor stroke\n" From da9345def866081e1b3deb3f65c7bc6bc7839091 Mon Sep 17 00:00:00 2001 From: ben Date: Sun, 23 Aug 2015 22:12:08 -0700 Subject: [PATCH 7/9] why is there so much whitespace? well, not anymore --- optimize.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/optimize.py b/optimize.py index da0bb3f..36348d9 100755 --- a/optimize.py +++ b/optimize.py @@ -154,13 +154,3 @@ def opt(gfile, offset=(0.0,0.0,0.0), rotate=False): print '%s finished in %s'%(args.name,deltaTime(start)) - - - - - - - - - - From 600df1cade29a9af4afcf51d1623e7b82cf739a2 Mon Sep 17 00:00:00 2001 From: ben Date: Sun, 23 Aug 2015 23:08:09 -0700 Subject: [PATCH 8/9] make optimize.py not clobber drill files previously it would set all the drill depths to spot-drill depths... --- optimize.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/optimize.py b/optimize.py index 36348d9..42465b3 100755 --- a/optimize.py +++ b/optimize.py @@ -18,7 +18,7 @@ # The Optimize function -def opt(gfile, offset=(0.0,0.0,0.0), rotate=False): +def opt(gfile, offset=(0.0,0.0,0.0), rotate=False, isDrill=False): '''Optimization core function: Reads in gCode ascii file. Processes gcode into toolpath list @@ -42,7 +42,7 @@ def opt(gfile, offset=(0.0,0.0,0.0), rotate=False): tool.groupMills() puts(colored.blue('Toolpath length: %.2f inches, (mill only: %.2f)'%(tool.length(),tool.millLength()))) if args.setMillHeight: - tool.setMillHeight(Z_MILL,Z_SPOT) + tool.setMillHeight(Z_MILL,(Z_DRILL if isDrill else Z_SPOT)) tool.uniqMills() # This starts the optimization process: @@ -149,7 +149,7 @@ def opt(gfile, offset=(0.0,0.0,0.0), rotate=False): # c = re.match(r'(?P\.drill\.tap)|(?P\.etch\.tap)', gfile.name) c = re.match(r'(.+)((?P\.drill\.tap)|(?P\.etch\.tap))', gfile.name) if c: # either a drill.tap or etch.tap file - opt(gfile, offset=(args.offsetx, args.offsety, args.offsetz), rotate=args.rotate) + opt(gfile, offset=(args.offsetx, args.offsety, args.offsetz), rotate=args.rotate, isDrill=(c.group('drill') > 0)) print '%s finished in %s'%(args.name,deltaTime(start)) From 1f48d58e874a1c64b71a08a7a37bb558d308e2f1 Mon Sep 17 00:00:00 2001 From: Mike Date: Sat, 29 Aug 2015 23:46:31 -0700 Subject: [PATCH 9/9] Zcorrect related bug fixes discovered during field testing of PCB milling work-flow incorporating suface probing and z_correction --- lib/correction_surface.py | 35 ++++- lib/gcode.py | 252 ++++++++++++++++------------------ lib/tool.py | 2 +- new_GRBL_configuration_readme | 58 ++++++-- probe_surface.py | 53 +++++-- probe_test.out | 10 +- zcorrect.py | 51 +++++-- 7 files changed, 287 insertions(+), 174 deletions(-) diff --git a/lib/correction_surface.py b/lib/correction_surface.py index c3a5f84..ec73e90 100644 --- a/lib/correction_surface.py +++ b/lib/correction_surface.py @@ -23,9 +23,12 @@ def __init__(self,surface_file_name = ''): self.x_step = "nan" self.y_step = "nan" self.array = [] - self.Load_Correction_Surface() if surface_file_name != '': + #print 'this is the surface correcion name that init is loading' + #print surface_file_name self.Load_Correction_Surface(surface_file_name) + else: + self.Load_Correction_Surface() def Load_Correction_Surface(self, surface_file_name = 'probe_test.out' ): @@ -37,7 +40,8 @@ def Load_Correction_Surface(self, surface_file_name = 'probe_test.out' ): #display the dataset to be used puts(colored.yellow('Surface Z Data Matrix')) - print Surface_Data + puts(colored.yellow(np.array_str(Surface_Data))) + #print Surface_Data probe_data_file = open(surface_file_name) @@ -48,8 +52,8 @@ def Load_Correction_Surface(self, surface_file_name = 'probe_test.out' ): line = line.strip() if re.search('#', line,flags = re.IGNORECASE): - puts(colored.green('extracting scale data from file header')) - puts(colored.green( line)) + #puts(colored.green('extracting scale data from file header')) + #puts(colored.green( line)) if re.search(' X_STEP:', line,flags = re.IGNORECASE): #X_STEP:,0.5000 X_STEP_INFO = re.findall('X_STEP:,\d*\.\d*,', line, flags = re.IGNORECASE)[0] @@ -77,6 +81,9 @@ def estimate_surface_z_at_pozition(self,x,y): #begin with using the nearest node to the point Ns_x = round(x/self.x_step) Ns_y = round(y/self.y_step) + # calcualte the boundaries + Nmax_x = self.array.shape[0] + Nmax_y = self.array.shape[1] #calculate distance from the nearest node epsilon_x= x - Ns_x*self.x_step epsilon_y= y - Ns_y*self.x_step @@ -93,6 +100,26 @@ def estimate_surface_z_at_pozition(self,x,y): else: Nf_y = Ns_y + -1 delta_y = -1.0*self.y_step + + #force the data into the boundaries + if Ns_x<0: + Ns_x = 0 + if Ns_y<0: + Ns_y = 0 + if Nf_x<0: + Nf_x = 0 + if Nf_y<0: + Nf_y = 0 + + if Ns_x>Nmax_x: + Ns_x = Nmax_x + if Ns_y>Nmax_y: + Ns_y = Nmax_y + if Nf_x>Nmax_x: + Nf_x = Nmax_x + if Nf_y>Nmax_y: + Nf_y = Nmax_y + #calculate the slopes (x and y) of the plane defined by the triangle of bounding nodes slope_x = (self.array[Nf_x][Ns_y] - self.array[Ns_x][Ns_y])/delta_x slope_y = (self.array[Ns_x][Nf_y] - self.array[Ns_x][Ns_y])/delta_y diff --git a/lib/gcode.py b/lib/gcode.py index 177debc..3dccdb4 100755 --- a/lib/gcode.py +++ b/lib/gcode.py @@ -7,7 +7,7 @@ from pprint import pprint from clint.textui import colored, puts, indent, progress -CMDS='GXYZFEMPIJK' +CMDS='GXYZMPIJ' TEMPLATE='''(UPDATED by ${tag}) (Starting) @@ -20,148 +20,130 @@ (Finished and Moving back to origin) ${finish} (DONE)''' - INIT='''\ G20 G90 G00 X0.000 Y0.000 Z0.000 G00 Z0.100''' - FINISH = '''\ G00 Z0.100 G00 X0.000 Y0.000 G00 Z0.000''' + + class GCode(list): - def __init__(self, gcode, limit=None): - '''start with a gcode ascii file''' - self.limit = limit - if isinstance(gcode, str): - with open(os.path.expanduser(gcode),'r') as f: - lines = f.readlines() - filename = gcode - else: - filename = gcode.name - lines = gcode.readlines() - gcode.close() - self.filename = filename - self.lines = lines - # self.ready = False - - # def append(self,item): - # '''add the next nice to the object''' - # if self.ready : self.ready = False - # super(GCode, self).append(item) - - def parse(self): - '''WARNING: By default .parse() grabs only the G## commands for creating toolpaths in some space - if you need everything use .parseall()''' - everything = self._parse() - for item in everything: - toappend = False - for cmd in CMDS: - if cmd in item: - toappend=True - if toappend: - self.append(item) - - def parseAll(self): - '''Gets everything so that we can print it back out''' - everything = self._parse() - for item in everything: - self.append(item) - - def _parse(self): - ''' [INTERNAL] convert the readlines into a parsed set of commands and values''' - puts(colored.blue('Parsing gCode')) - - comment = r'\(.*?\)' - whitespace = r'\s' - - #command = r''.join([r'(?P<%s>%s(?P<%snum>-?\d+(?P<%sdecimal>\.?)\d*))?'%(c,c,c,c) for c in CMDS]) - ''' - The result of the above commented line is: - that command is a new regular expression which prints in the followin way in ipython: - note the doubles of \ probably have to do with the output - '(?PG(?P-?\\d+(?P\\.?)\\d*))?(?PX(?P-?\\d+(?P\\.?)\\d*))?(?PY(?P-?\\d+(?P\\.?)\\d*))?(?PZ(?P-?\\d+(?P\\.?)\\d*))?(?PM(?P-?\\d+(?P\\.?)\\d*))?(?P

P(?P-?\\d+(?P\\.?)\\d*))?(?PI(?P-?\\d+(?P\\.?)\\d*))?(?PJ(?P-?\\d+(?P\\.?)\\d*))?' - ''' - #one major concwern is that this ordering is not flexible - #perhaps G code standards dictate the order of X Y Z I J K F E ... etc - # maybe it is not necessary to add any flexibility in ordering here - # the most likely malfunction will be due to F coming before x y z or after x y z - output = [] - for i,line in enumerate(progress.bar(self.lines)): - if self.limit is not None and i > self.limit: break - # for i,line in enumerate(self.lines): - l = line.strip() - # find comments, save them, and then remove them - m = re.findall(comment,l) - l = re.sub(whitespace+'|'+comment,'',l).strip().upper() - # l = re.sub(whitespace,'',l).upper() - - # Grab the commands - #c = re.match(command,l) - - # output commands to a nice dict - out = {} - out['index'] = i - # out['line'] = line - if m: out['comment'] = m - for cmd in CMDS: - #incorporating the definintion of command inside this loop should make it more flexible - command = r''.join(r'(?P<%s>%s(?P<%snum>-?\d+(?P<%sdecimal>\.?)\d*))?'%(cmd,cmd,cmd,cmd)) - c = re.match(command,l) - if c.group(cmd): - # either a float if '.' or a int - fcn = float if c.group(cmd+'decimal') else int - out[cmd] = fcn(c.group(cmd+'num')) - out['index'] = i - if len(out) > 0: - output.append(out) - return output - # - # if len(out) > 0: - # self.append(out) - - def update(self,tool): - '''Updates the gcode from a toolpath. ONLY does x,y''' - UPDATE = 'xy' - for x in tool: - # print len(x) - # this conditional suggests that this code is intended only to work with linear moves - # if this is the case we may need to generalize it - if len(x) == 5: - # print x - for u in UPDATE: - if u.upper() in self[x[4]]: - '''The indexdicts of which a tool object is a list''' - '''have x,y,z,cmd = my_index_dict[0:4]''' - '''note that cmd = my_index_dict[3] is the G command number''' - '''note that index = my_index_dict[4] is the index of the G comand in the whole g code sequence''' - # set the g code command dicts x and y entries of the same index as the tool to the same x and y values as in the tool - self[x[4]][u.upper()] = x[UPDATE.index(u)] - - # print self[x[4]] - # print self[x[4]], - # print u.upper(), - # print self[x[4]][u.upper()] - # print self[x[4]][u.toupper()]#, x[UPDATE.index(u)] - # print self[x[4]][u],x[UPDATE.index(u)] - - - def copy(self): - return deepcopy(self) - - def getGcode(self, tag=__name__, start=None): - lines = [] - for i,line in enumerate(self): - l = '' - for cmd in CMDS: - if cmd in line: - l += (' %s%.3f' if cmd in 'XYZFEIJK' else '%s%02i ')%(cmd,line[cmd]) - # add in the comments - if 'comment' in line: l += ' '.join(line['comment']) - lines.append(l) - params = dict(gcode='\n'.join(lines),init=INIT,finish=FINISH,tag=tag) - params['startpos'] = ' G00 X%.3f Y%.3f'%(start[0],start[1]) if start else '' - return Template(TEMPLATE).substitute(params) + def __init__(self, gcode, limit=None): + '''start with a gcode ascii file''' + self.limit = limit + if isinstance(gcode, str): + with open(os.path.expanduser(gcode),'r') as f: + lines = f.readlines() + filename = gcode + else: + filename = gcode.name + lines = gcode.readlines() + gcode.close() + self.filename = filename + self.lines = lines + # self.ready = False + + # def append(self,item): + # '''add the next nice to the object''' + # if self.ready : self.ready = False + # super(GCode, self).append(item) + + def parse(self): + '''By default .parse() grabs only the G## commands for creating toolpaths in some space + if you need everything use .parseall()''' + everything = self._parse() + for item in everything: + toappend = False + for cmd in CMDS: + if cmd in item: + toappend=True + if toappend: + self.append(item) + + def parseAll(self): + '''Gets everything so that we can print it back out''' + everything = self._parse() + for item in everything: + self.append(item) + + def _parse(self): + ''' [INTERNAL] convert the readlines into a parsed set of commands and values''' + puts(colored.blue('Parsing gCode')) + + comment = r'\(.*?\)' + whitespace = r'\s' + command = r''.join([r'(?P<%s>%s(?P<%snum>-?\d+(?P<%sdecimal>\.?)\d*))?'%(c,c,c,c) for c in CMDS]) + output = [] + for i,line in enumerate(progress.bar(self.lines)): + if self.limit is not None and i > self.limit: break + # for i,line in enumerate(self.lines): + l = line.strip() + # find comments, save them, and then remove them + m = re.findall(comment,l) + l = re.sub(whitespace+'|'+comment,'',l).strip().upper() + # l = re.sub(whitespace,'',l).upper() + + # Grab the commands + c = re.match(command,l) + + # output commands to a nice dict + out = {} + out['index'] = i + # out['line'] = line + if m: out['comment'] = m + for cmd in CMDS: + if c.group(cmd): + # either a float if '.' or a int + fcn = float if c.group(cmd+'decimal') else int + out[cmd] = fcn(c.group(cmd+'num')) + out['index'] = i + if len(out) > 0: + output.append(out) + return output + # + # if len(out) > 0: + # self.append(out) + + def update(self,tool): + '''Updates the gcode with a toolpath only does x,y''' + UPDATE = 'xy' + for x in tool: + # print len(x) + if len(x) == 5: + # print x + for u in UPDATE: + if u.upper() in self[x[4]]: + self[x[4]][u.upper()] = x[UPDATE.index(u)] + # print self[x[4]] + + # print self[x[4]], + # print u.upper(), + # print self[x[4]][u.upper()] + # print self[x[4]][u.toupper()]#, x[UPDATE.index(u)] + # print self[x[4]][u],x[UPDATE.index(u)] + + + def copy(self): + return deepcopy(self) + + def getGcode(self, tag=__name__, start=None): + lines = [] + for i,line in enumerate(self): + l = '' + for cmd in CMDS: + if cmd in line: + l += (' %s%.3f' if cmd in 'XYZ' else '%s%02i ')%(cmd,line[cmd]) + # add in the comments + if 'comment' in line: l += ' '.join(line['comment']) + lines.append(l) + params = dict(gcode='\n'.join(lines), + init=INIT, + finish=FINISH, + tag=tag) + params['startpos'] = ' G00 X%.3f Y%.3f'%(start[0],start[1]) if start else '' + return Template(TEMPLATE).substitute(params) diff --git a/lib/tool.py b/lib/tool.py index ef7219e..933cbb5 100755 --- a/lib/tool.py +++ b/lib/tool.py @@ -378,4 +378,4 @@ def move(self,loc): def zcorrect(self, correction_surface): #correct the z position of the points on the tool path for item in self: - item[2] += correction_surface.estimate_surface_z_at_pozition(item[0],item[1]) + item[2] += correction_surface.estimate_surface_z_at_pozition(item[0],item[1]) diff --git a/new_GRBL_configuration_readme b/new_GRBL_configuration_readme index c33a558..3f8e47b 100644 --- a/new_GRBL_configuration_readme +++ b/new_GRBL_configuration_readme @@ -1,8 +1,21 @@ -#NEW CONFIGURATION!!! - | $0=100 (step pulse, usec) - | $1=25 (step idle delay, msec) +PROBLEM + spindle pin out number: is pin D11 in GRBL 0.9 + the hardware that we have is using it on pin D12 which used to be the spindle previosly + we rewired the terminals on the breakout board so that the Z lim switch and spindle are swapped + They swapped the so that the spindle could have a PWM in keeping with this added capacity they have implemented the cutting speed parameter + This major change comes with the hurtle that the variable "S" for cutting speed is by default zero on boot up + Setting S to 1000 was enough to keep our relay ON + "M03 S1000" + +PROBLEM + G01 commands must have a Feed rate defined at least one time before they can be issued + (ie your first G01 command must have an F as in G01X0Y0Z0F9) + +1#NEW CONFIGURATION!!! + | $0=50 (step pulse, usec) + | $1=50 (step idle delay, msec) | $2=0 (step port invert mask:00000000) - | $3=96 (dir port invert mask:01100000) + | $3=4 (dir port invert mask:00000100) | $4=0 (step enable invert, bool) | $5=0 (limit pins invert, bool) | $6=0 (probe pin invert, bool) @@ -18,21 +31,44 @@ | $25=260.000 (homing seek, mm/min) | $26=250 (homing debounce, msec) | $27=1.000 (homing pull-off, mm) - | $100=188.976 (x, step/mm) - | $101=188.976 (y, step/mm) - | $102=188.976 (z, step/mm) + | $100=755.906 (x, step/mm) + | $101=755.906 (y, step/mm) + | $102=755.906 (z, step/mm) | $110=500.000 (x max rate, mm/min) | $111=500.000 (y max rate, mm/min) | $112=500.000 (z max rate, mm/min) - | $120=4.000 (x accel, mm/sec^2) - | $121=4.000 (y accel, mm/sec^2) - | $122=4.000 (z accel, mm/sec^2) + | $120=50.000 (x accel, mm/sec^2) + | $121=50.000 (y accel, mm/sec^2) + | $122=50.000 (z accel, mm/sec^2) | $130=200.000 (x max travel, mm) | $131=200.000 (y max travel, mm) | $132=200.000 (z max travel, mm) + | ok - +THE LESS OLD CONFIG + | $0=755.906 (x, step/mm) + | $1=755.906 (y, step/mm) + | $2=755.906 (z, step/mm) + | $3=50 (step pulse, usec) + | $4=240.000 (default feed, mm/min) + | $5=240.000 (default seek, mm/min) + | $6=128 (step port invert mask, int:10000000) + | $7=50 (step idle delay, msec) + | $8=50.000 (acceleration, mm/sec^2) + | $9=0.050 (junction deviation, mm) + | $10=0.020 (arc, mm/segment) + | $11=25 (n-arc correction, int) + | $12=3 (n-decimals, int) + | $13=1 (report inches, bool) + | $14=1 (auto start, bool) + | $15=0 (invert step enable, bool) + | $16=0 (hard limits, bool) + | $17=0 (homing cycle, bool) + | $18=127 (homing dir invert mask, int:01111111) + | $19=25.000 (homing feed, mm/min) + | $20=500.000 (homing seek, mm/min) + | $21=200 (homing debounce, msec) THE OLD CONFIG | $0 = 188.976 (steps/mm x) diff --git a/probe_surface.py b/probe_surface.py index 92b676f..fb7477c 100644 --- a/probe_surface.py +++ b/probe_surface.py @@ -15,24 +15,24 @@ DEBUG_VERBOSE = False -X_MAX = 1.0 -Y_MAX = 1.0 +X_MAX = 2.5 +Y_MAX = 2.0 X_STEP = 0.5 Y_STEP = 0.5 -HIGH_Z = 0.5 -LOW_Z = -0.5 +HIGH_Z = 0.020 +LOW_Z = -0.030 PROBE_FEED_RATE = 0.2 -DESCENT_SPEED = 1.0/60.0 +DESCENT_SPEED = 2.0/60.0 DESCENT_TIME = HIGH_Z/DESCENT_SPEED -Surface_Data = np.empty([len(arange(0, X_MAX, X_STEP)),len(arange(0, Y_MAX, Y_STEP))]) +Surface_Data = np.empty([len(arange(0, X_MAX+X_STEP/2.0, X_STEP)),len(arange(0, Y_MAX+Y_STEP/2.0, Y_STEP))]) Z=HIGH_Z converged = False # get a serial device and wake up the grbl, by sending it some enters with Communicate(args.device, args.speed, timeout=args.timeout, debug=args.debug, quiet=args.quiet) as serial: - + time.sleep(10) command = "G90" try: serial.run(command) @@ -48,10 +48,10 @@ serial.run('!\n?') num_x = -1 - for X in arange(0, X_MAX, X_STEP): + for X in arange(0, X_MAX+X_STEP/2.0, X_STEP): num_x=num_x+1 num_y=-1 - for Y in arange(0, Y_MAX, Y_STEP): + for Y in arange(0, Y_MAX+Y_STEP/2.0, Y_STEP): num_y=num_y+1 puts(colored.yellow("going to x:{:.4f} and y:{:.4f}".format(X,Y))) @@ -99,5 +99,40 @@ puts(colored.red('Emergency Feed Hold. Enter "~" to continue')) serial.run('!\n?') + command = "G0 X{:.4f} Y{:.4f} Z{:.4f}".format(0.0,0.0,HIGH_Z) + if DEBUG_VERBOSE: + print command + try: + serial.run(command) + except KeyboardInterrupt: + puts(colored.red('Emergency Feed Hold. Enter "~" to continue')) + serial.run('!\n?') + + command = "G0 X{:.4f} Y{:.4f} Z{:.4f}".format(0.0,0.0,0.0) + if DEBUG_VERBOSE: + print command + try: + serial.run(command) + except KeyboardInterrupt: + puts(colored.red('Emergency Feed Hold. Enter "~" to continue')) + serial.run('!\n?') + + print Surface_Data np.savetxt('probe_test.out', Surface_Data, delimiter=',',header='X_STEP:,{:.4f}, Y_STEP:,{:.4f},'.format(X_STEP,Y_STEP)) + +puts( +colored.green(''' +gCode finished streaming!''' + +colored.red(''' +!!! WARNING: Please make sure that the buffer clears before finishing...''') ) +try: + raw_input('') + raw_input(' Are you sure? Any key to REALLY exit.') +except KeyboardInterrupt as e: + serial.run('!\n?') + puts(colored.red('Emergency Stop! Enter "~" to continue. You can enter gCode to run here as well.')) + while True: + x = raw_input('GRBL> ').strip() + serial.run(x) + if '~' in x: break diff --git a/probe_test.out b/probe_test.out index 4680feb..8984ec9 100644 --- a/probe_test.out +++ b/probe_test.out @@ -1,3 +1,7 @@ -# X_STEP:,4.5000, Y_STEP:,4.5000, -4.653999999999999804e-01,4.833000000000000074e-01 -4.758000000000000007e-01,4.703999999999999848e-01 +# X_STEP:,0.5000, Y_STEP:,0.5000, +-1.159999999999999920e-02,-1.519999999999999997e-02,-1.660000000000000017e-02,-1.689999999999999836e-02,-1.650000000000000078e-02 +-1.540000000000000049e-02,-1.949999999999999997e-02,-2.089999999999999844e-02,-2.110000000000000070e-02,-2.060000000000000026e-02 +-1.689999999999999836e-02,-2.089999999999999844e-02,-2.239999999999999977e-02,-2.280000000000000082e-02,-2.210000000000000159e-02 +-1.619999999999999912e-02,-1.990000000000000102e-02,-2.160000000000000114e-02,-2.189999999999999933e-02,-2.120000000000000009e-02 +-1.280000000000000061e-02,-1.660000000000000017e-02,-1.830000000000000029e-02,-1.880000000000000074e-02,-1.830000000000000029e-02 +-1.299999999999999883e-02,-1.080000000000000057e-02,-1.230000000000000017e-02,-1.290000000000000001e-02,-1.280000000000000061e-02 diff --git a/zcorrect.py b/zcorrect.py index b4df90c..62b5993 100644 --- a/zcorrect.py +++ b/zcorrect.py @@ -5,6 +5,9 @@ #there should also be a probe_step_x value and a probe_step_y value #these define the distsnces between the points measured in the z_positions array +# as an example of using this code in ipython +# run -i zcorrect.py /home/mike/Desktop/greg.tap -z probe_test_2.out + #imports import os, re, sys @@ -24,16 +27,17 @@ import time, readline import numpy as np import re +import argparse FILEENDING = '_mod' # file ending for optimized file. EXTRAARGS = dict(ext=dict(args=['-z','--zsurface'], + type=str, default='probe_test.out', - const='probe_test.out', - action='store_const', + nargs = '?', dest='z_surf', - help='''Specify for outputing an eps file rather than a pdf.''') ) + help='''Specify the z zurface data file''') ) def zcorrect_file(gfile,surface_file_name = 'probe_test.out'): @@ -65,14 +69,22 @@ def zcorrect_file(gfile,surface_file_name = 'probe_test.out'): tool.build(gcode) # adjust the z position at each point by the given amount tool.zcorrect(correction_surface) + + ''' the follwing doe not work. is gcode.update(tool) broken?''' # load the changes back into the gcode object - gcode.update(tool) # append the modified g code to the empty list called out - #out.append([gcode]) - out = gcode + # out.append([gcode]) + # gcode.update(tool) + # out = gcode # convert gcode to text format - #output = ''.join([o.getGcode(tag=args.name) for o in out]) - output = ''.join([out.getGcode()]) + # output = ''.join([o.getGcode(tag=args.name) for o in out]) + # output = ''.join([out.getGcode()]) + + '''instead the following simgle lin suffices''' + '''is any info lost by doing it this way? E F M''' + # generate a gcode file from the tool object + output = tool.buildGcode() + # get an output file name outfile = FILEENDING.join(os.path.splitext(gfile)) print "outfile is:" @@ -96,18 +108,35 @@ def zcorrect_file(gfile,surface_file_name = 'probe_test.out'): args = argv.arg(description='PyGRBL gcode imaging tool', getFile=True, # get gcode to process - getMultiFiles=False, # accept any number of files + getMultiFiles=True, # accept any number of files; WHY MUST THIS BE TRUE for it to work? otherOptions=EXTRAARGS, # "Install some nice things" very descriptive!!! getDevice=False) # We dont need a device + ''' + if type(args.z_surf) == str: + print "using the z surface file" + print args.z_surf + surface_file_name = args.z_surf + else: + print "looking for a z correction surface file in the default name: probe_test.out" + surface_file_name = 'probe_test.out' + ''' + + surface_file_name = args.z_surf + #print "using the z surface file" + #print args.z_surf + # optimize each file in the list for gfile in args.gcode: # only process things not processed before. # c = re.match(r'(?P\.drill\.tap)|(?P\.etch\.tap)', gfile.name) - c = re.match(r'(.+)(\.tap)', gfile) + #puts(colored.blue('Z correcting the G code file: %s'%(gfile.name))) + c = re.match(r'(.+)(\.tap)', gfile.name) # c = True # HAX and accept everything if c: # either a drill.tap or etch.tap - zcorrect_file(gfile) #args=args) + puts(colored.blue('Using the z surface file: %s'%(args.z_surf))) + zcorrect_file(gfile.name, surface_file_name) #args=args) + print '%s finished in %s'%(args.name,deltaTime(start))