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/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/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..ec73e90 --- /dev/null +++ b/lib/correction_surface.py @@ -0,0 +1,129 @@ +#!/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,surface_file_name = ''): + self.x_step = "nan" + self.y_step = "nan" + self.array = [] + 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' ): + + #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')) + puts(colored.yellow(np.array_str(Surface_Data))) + #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) + # 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 + #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 + + #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 + #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/drawing.py b/lib/drawing.py new file mode 100755 index 0000000..b86d0c7 --- /dev/null +++ b/lib/drawing.py @@ -0,0 +1,103 @@ +#!/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 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 + diff --git a/lib/gcode.py b/lib/gcode.py index d212c28..3dccdb4 100755 --- a/lib/gcode.py +++ b/lib/gcode.py @@ -47,34 +47,34 @@ def __init__(self, gcode, limit=None): 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: + for cmd in CMDS: + if cmd in item: toappend=True - if toappend: + 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]) @@ -90,7 +90,7 @@ def _parse(self): # Grab the commands c = re.match(command,l) - + # output commands to a nice dict out = {} out['index'] = i @@ -105,10 +105,10 @@ def _parse(self): 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' @@ -120,17 +120,17 @@ def update(self,tool): 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]], + # 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): @@ -147,5 +147,3 @@ def getGcode(self, tag=__name__, start=None): 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 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/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 db84d76..beda8a8 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,23 +48,29 @@ 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): - 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 @@ -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] @@ -67,25 +90,28 @@ 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): move(self, m, t) # FIXME +'''DICTIONARY OF FUNCTIONS''' +'''SEE DEFINITIONS ABOVE''' GCMD = {0: move, 1: move, 2: circle, 3: circle, 4: noop, - 17: noop, #xyplane + 17: noop, #xyplane 20: inch, 21: mm, 54: noop, # Word Coords 90: absolute, 91: relative, - 94:noop, #FeedRate/minute + 94: noop, #FeedRate/minute } @@ -97,31 +123,37 @@ 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) - - 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 @@ -131,7 +163,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,17 +180,22 @@ 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 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): @@ -172,8 +209,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 +220,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 +253,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 +270,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 +279,15 @@ 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): '''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 +296,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 +329,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 +338,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 = [] @@ -315,9 +354,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)) @@ -329,13 +368,17 @@ 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])''' + '''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): + for i,x in enumerate(loc): item[i] += x - \ No newline at end of file + 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 06bce43..e3b24f2 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,34 @@ 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 \[ \]? + # 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: + 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,34 +85,35 @@ 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. 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) out.append([loc,gcode]) - + if args.copy: locs = parse(args.copy, getUnits=True) puts(colored.blue('Copying!')) @@ -114,22 +121,23 @@ 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) 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 +175,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/optimize.py b/optimize.py index da0bb3f..9fd22b3 100755 --- a/optimize.py +++ b/optimize.py @@ -18,33 +18,33 @@ # 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 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.setMillHeight(Z_MILL,(Z_DRILL if isDrill else 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: @@ -149,18 +149,6 @@ 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)) - - - - - - - - - - - - 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..fb7477c --- /dev/null +++ b/probe_surface.py @@ -0,0 +1,138 @@ +#!/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 = 2.5 +Y_MAX = 2.0 +X_STEP = 0.5 +Y_STEP = 0.5 +HIGH_Z = 0.020 +LOW_Z = -0.030 +PROBE_FEED_RATE = 0.2 +DESCENT_SPEED = 2.0/60.0 +DESCENT_TIME = HIGH_Z/DESCENT_SPEED + +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) + 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/2.0, X_STEP): + num_x=num_x+1 + num_y=-1 + 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))) + + 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?') + + 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 new file mode 100644 index 0000000..8984ec9 --- /dev/null +++ b/probe_test.out @@ -0,0 +1,7 @@ +# 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/readme_grbl_v0.9.txt b/readme_grbl_v0.9.txt new file mode 100644 index 0000000..3f8e47b --- /dev/null +++ b/readme_grbl_v0.9.txt @@ -0,0 +1,83 @@ +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=4 (dir port invert mask:00000100) + | $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=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=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) + | $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/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..62b5993 --- /dev/null +++ b/zcorrect.py @@ -0,0 +1,142 @@ +#!/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 + +# 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 +from numpy import arange +# use argv in order to have options when using z correct as main function +from lib import argv +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 +import argparse + +FILEENDING = '_mod' # file ending for optimized file. + + +EXTRAARGS = dict(ext=dict(args=['-z','--zsurface'], + type=str, + default='probe_test.out', + nargs = '?', + dest='z_surf', + help='''Specify the z zurface data file''') ) + + +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) + + ''' the follwing doe not work. is gcode.update(tool) broken?''' + # load the changes back into the gcode object + # append the modified g code to the empty list called out + # 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()]) + + '''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:" + 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=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) + #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 + 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))