diff --git a/.gitignore b/.gitignore index 5058ce1..9990368 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ *~ MANIFEST build* -dist* \ No newline at end of file +dist* +dist/ diff --git a/align.py b/align.py index b8a4e32..7ad8374 100755 --- a/align.py +++ b/align.py @@ -9,6 +9,8 @@ QUIT = ['q','Q'] UPDATE =['u','U'] +TWEAKL =['+','-'] +MOTOR =['m','M'] UP = ['\x1b[B'] DOWN = ['\x1b[A'] RIGHT = ['\x1b[D'] @@ -24,10 +26,13 @@ G91 (Incremental) G0 X0.000 Y0.000 Z0.000 ''' + HELP = '''\ Board Alignment Keys: q/Q : Quit u/U : Update moveLength -- Amount to nudge ++/- : Increase/Decrease moveLength by a factor of 2 (round to nearest mil) +m/M : Toggle spindle Arrow Keys : Move in X [Forward/Back] Y [Left/Right] a/A / z/Z : Move in Z [Raise/Lower] @@ -35,7 +40,7 @@ moveLength = 0.020 # Inches [0.020] : amount to move use update(), to change location = dict(X=0.0, Y=0.0, Z=0.0) # store the current location inches - +spindleState = 0; # Some helper functions, scroll down for tasty bits @@ -58,6 +63,14 @@ def move(direction=''): puts(colored.blue('(%s)'%', '.join(['%.3f'%location[k] for k in location]))) # isAt = ', '.join(['%s=%.3f'%(a,location[a]) for a in location]) # puts(colored.blue(' Currently at: %s'%isAt)) + +def toggleSpindle(state): + '''Send on/off command for spindle''' + if state: + serial.run('M05') + else: + serial.run('M03') + return not state def update(): '''Update the moveLength for each command''' @@ -78,7 +91,7 @@ def update(): value = re.sub(r'\s','',userValue.strip()) # Remove Whitespace # match to units and values - c = re.match(r'(?P\d+\.?\d+)(?P'+'|'.join(units)+')', value, re.IGNORECASE) + c = re.match(r'(?P(?:\d*\.)?\d+)(?P'+'|'.join(units)+')', value, re.IGNORECASE) # if the user was bad just go back if not c or not c.group('unit') in units: @@ -90,6 +103,17 @@ def update(): puts(colored.blue(' > moveLength is now: %.3f inch\n'%newLength)) return newLength +def tweakLength(origLength, key): + newLength = origLength + if key == '+': + newLength *= 2 + else: + newLength /= 2 + # round to the nearest mil because we're outputting that resolution with relative addressing + # if we try to use fractional mils, we'll end up with the position we tell the user not matching where the machine actually is + newLength = round(newLength*1000)/1000 + puts(colored.blue(' > moveLength is now: %.3f inch\n'%newLength)) + return newLength @@ -112,11 +136,13 @@ def update(): print '' with Terminal() as terminal: while True: - if terminal.isData(): + if terminal.waitForData(): c = terminal.getch() terminal.wait() if c in QUIT: sys.exit() # Quit the program elif c in UPDATE: moveLength = update() + elif c in TWEAKL: moveLength = tweakLength(moveLength, c) + elif c in MOTOR: spindleState = toggleSpindle(spindleState) elif c in UP: move('X-') elif c in DOWN: move('X+') elif c in RIGHT: move('Y-') diff --git a/command.py b/command.py index 6620161..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?') \ No newline at end of file + 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/home.py b/home.py index 4498440..06cfac9 100644 --- a/home.py +++ b/home.py @@ -5,22 +5,26 @@ __DOC__ = 'Setup homing for the machine' SETUP = '''(Setting up homing) -$16=0 (homing on -- requires reset) +$17=1 (homing on -- requires reset) $H $N0 = (setup Staring block) G28.1 (Soft abs home position) G30.1 (Sott adjust position) -G10 L2 P1 X0.5 Y0.5 Z0.5 (setup G55) - +G10 L2 P1 X0.5 Y0.5 Z-0.5 (setup G54) +G10 L2 P2 X2.5 Y0.5 Z-0.5 (setup G55) G28 Z0.3 (rapid to 0.3 and then go home) ''' RESET = '''(Turning off Homing) -$16=0 (Homing off -- requires reset) +$16=0 (Hard Limits off ) +$17=0 (Homing off -- requires reset) +$N1= (clear out) G28 -G10 L2 P1 X0 Y0 Z0 (Reset G55) +G10 L2 P1 X0 Y0 Z0 (Reset G54) +G10 L2 P2 X0 Y0 Z0 (Reset G54) + ''' diff --git a/lib/argv.py b/lib/argv.py index 1237ea5..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,27 +47,31 @@ 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: - for item in otherOptions: - args = otherOptions[item].pop('args') - parser.add_argument(*args, **otherOptions[item]) - + if isinstance(otherOptions,(dict)): + for item in otherOptions: + args = otherOptions[item].pop('args') + parser.add_argument(*args, **otherOptions[item]) + else: + for option in otherOptions: + args = option.pop('args') + 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: @@ -79,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 847a672..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,24 +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() + #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: @@ -73,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 @@ -112,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 old mode 100644 new mode 100755 index d212c28..3dccdb4 --- 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/terminal.py b/lib/terminal.py index 5bd39ed..fe3844f 100644 --- a/lib/terminal.py +++ b/lib/terminal.py @@ -17,6 +17,10 @@ def isData(self): '''Is there data ready to process''' return select.select([sys.stdin], [], [], 0) == ([sys.stdin], [], []) + def waitForData(self): + '''Is there data ready to process''' + return select.select([sys.stdin], [], []) == ([sys.stdin], [], []) + def echo(self): '''echo characters to screen''' termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.oldterm) diff --git a/lib/tool.py b/lib/tool.py old mode 100644 new mode 100755 index 78c6188..beda8a8 --- a/lib/tool.py +++ b/lib/tool.py @@ -1,15 +1,32 @@ #!/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 from copy import deepcopy + from clint.textui import colored, puts, indent, progress 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) @@ -31,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 @@ -58,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] @@ -66,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 } @@ -96,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 @@ -130,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): @@ -147,19 +180,48 @@ 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): + '''offset the toolpath by some offset=x,y,z + this needs to be cleaned up''' + for item in self: + for i,ax in enumerate(item): + if i == 0: + item[ax] -= offset[0] + elif i == 1: + item[ax] -= offset[1] + elif i == 2: + item[ax] -= offset[2] + + + def rotate(self, angle): + '''rotate by some angle''' + rad = math.radians(angle) + for item in self: + # 90 deg + # item[0],item[1] = -item[1], item[0] + 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] @@ -191,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): @@ -208,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): @@ -217,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''' @@ -232,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): @@ -265,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:')) @@ -274,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 = [] @@ -290,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)) @@ -304,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 old mode 100644 new mode 100755 index f1611df..e3b24f2 --- a/modify.py +++ b/modify.py @@ -7,104 +7,113 @@ from lib.gcode import GCode from lib.tool import Tool from lib.util import deltaTime, error, convertUnits -from lib.clint.textui import puts,colored - +from clint.textui import puts,colored +# HELLO WORLD +#THESE FILES ARE NOT THE SAME! FILEENDING = '_mod' # file ending for optimized file. # We need some specalized arguments for this file, so lets create them here. -otherOptions = dict(move=dict(args=['-m', '--move'], - default=None, - type=str, - nargs=1, - help='''Move the origin to a new point. - Applied before rotation. - Specify Units at the end of the x,y pos. - Example: "-m 0.1,0.2in".'''), - rotate=dict(args=['-r', '--rotate'], - default=None, - type=float, - help='''Rotate the gcode about the origin. - Applied after a Move. - In float degrees.'''), - copy=dict(args=['-c', '--copy'], - default=None, - type=str, - nargs='+', - 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".'''), - replicate=dict(args=['-x','--replicate'], - default=None, - type=str, - nargs=1, - help='''Replicate the design by N_x X N_y items. - Applied before rotation. - Example: "-x 2,2" '''), - ) - - - - - -def parse(move, getUnits=False): - '''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 +OPTIONS = [ + dict(args=['-m', '--move'], + default=None, + type=str, + nargs=1, + help='''Move the origin to a new point. + Applied before rotation. + 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. + In float degrees.'''), + dict(args=['-c', '--copy'], + default=None, + type=str, + nargs='+', + 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'], + default=None, + type=str, + nargs=1, + help='''Replicate the design by N_x X N_y items. + Applied before rotation. + 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 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.''' - units = r'(?Pin|mil|mm)' if getUnits else r'' + 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)) + # + 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)) + + # 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,[g.group('units')]*2) - # if getUnits: item = [convertUnits(x,y) for x,y in zip(item,[g.group('units')]*2)] + if getUnits: item = map(convertUnits,item,[unit]*2) + if getUnits: item = [convertUnits(x,y) for x,y in zip(item,[unit]*2)] out.append(item) - return out - - - - - - - + # + return (out[0] if len(out) == 1 else out) 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)[0] # only one move at a time. + 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!')) @@ -112,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 @@ -135,25 +145,34 @@ def mod(gfile): +if __name__ == '__main__': + print parse('0.2,0.3in', getUnits=True) + print parse('0.2,0.3in', getUnits=True) + print parse('.2,0.3in', getUnits=True) + print parse('2,.03mm', getUnits=True) + print parse('2,3mm', getUnits=True) + print parse('2222,322mm', getUnits=True) + print parse('2222.023,322.2', getUnits=True) ## I should wrap this in a __main__ section -# Initialize the args -start = datetime.now() -args = argv.arg(description='Python GCode modifications', - getFile=True, # get gcode to process - getMultiFiles=True, # accept any number of files - otherOptions=otherOptions, # Install some nice things - 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) - 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 +if __name__ == '__main__' and False: + # Initialize the args + start = datetime.now() + args = argv.arg(description='Python GCode modifications', + getFile=True, # get gcode to process + getMultiFiles=True, # accept any number of files + otherOptions=OPTIONS, # Install some nice things + 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) + if c: # either a drill.tap or etch.tap file + mod(gfile) + + print '%s finished in %s'%(args.name,deltaTime(start)) diff --git a/optimize.py b/optimize.py index 938a491..9fd22b3 100755 --- a/optimize.py +++ b/optimize.py @@ -18,35 +18,39 @@ # The Optimize function -def opt(gfile): +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:')) here = [0.0]*3 # start at the origin newMills = [] # accumulate mills here + k = 0 while len(tool.mills) > 0: # No Optimization # mill = tool.mills.pop(0) @@ -54,14 +58,20 @@ def opt(gfile): # 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() @@ -73,7 +83,7 @@ def opt(gfile): 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 @@ -86,37 +96,59 @@ def opt(gfile): const=False, action='store_const', dest='setMillHeight', - help='''Do not modify the mill height for 3d mills''') ) - - - - -# Initialize the args -start = datetime.now() -args = argv.arg(description='Python GCode optimizations', - otherOptions=EXTRAARGS, # Install some nice things - getFile=True, # get gcode to process - getMultiFiles=True, # accept any number of files - getDevice=False) - -# 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'(.+)((?P\.drill\.tap)|(?P\.etch\.tap))', gfile.name) - if c: # either a drill.tap or etch.tap file - opt(gfile) - -print '%s finished in %s'%(args.name,deltaTime(start)) - - - - - - - - - - - - + help='''Do not modify the mill height for 3d mills'''), + ext1 = dict(args=['--zmove'], + default=0.0, type=float, + help='Move Z-height in mills [%s mills]'%Z_MOVE), + ext1a = dict(args=['--zmill'], + default=0.0, type=float, + help='Mill Z-height in mills [%s mills]'%Z_MILL), + 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, + help='Set x offset length in inches'), + ext3=dict(args=['--offsety'], + default=0.0, type=float, + help='Set y offset length in inches'), + ext4=dict(args=['--offsetz'], + default=0.0, type=float, + help='Set z offset length in inches'), + ext5=dict(args=['--rotate'], + default=0.0, type=float, + help='Rotate about the origin by some angle. Rotates after offset'), + ) + + + +if __name__ == '__main__': + # Initialize the args + start = datetime.now() + args = argv.arg(description='Python GCode optimizations', + otherOptions=EXTRAARGS, # Install some nice things + 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: + Z_DRILL = args.zdrill + if args.zmill != 0: + Z_MILL = args.zmill + # print vars(args) + # import sys + # sys.exit() + + # 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'(.+)((?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, isDrill=(c.group('drill') > 0)) + + print '%s finished in %s'%(args.name,deltaTime(start)) diff --git a/orient.py b/orient.py index 97085c6..3e26d36 100755 --- a/orient.py +++ b/orient.py @@ -1,27 +1,36 @@ #!/usr/bin/env python # orient.py -- determines the orientation of the edge location -import numpy as np -import pylab -import cv -import cv2 -from lib.communicate import Communicate - - - - - +# system +import sys +from collections import deque +# installed +import cv +import cv2 +import pylab +import numpy as np +# Package +from lib.communicate import Communicate +__DOC__=''' +arrows: move +a/d: up/down +c: find circle ++/-: step size +1-4: select hole +s: set location +''' +NOTE = ''' +This is just a collection of links that I thought were interesting at the time. -''' http://uvhar.googlecode.com/hg/test/laser_tracker.py @@ -56,269 +65,280 @@ http://stackoverflow.com/questions/5368449/python-and-opencv-how-do-i-detect-all-filledcircles-round-objects-in-an-image +http://stackoverflow.com/questions/11522755/opencv-via-python-on-linux-set-frame-width-height ''' - - -def findcircle(): - - capture = cv.CaptureFromCAM(0) - cv.WaitKey(200) - - # frame = cv.QueryFrame(capture) - # gray = cv.CreateImage(cv.GetSize(frame), 8, 1) - # edges = cv.CreateImage(cv.GetSize(frame), 8, 1) - - - font = cv.InitFont(cv.CV_FONT_HERSHEY_DUPLEX, 1, 1, 0, 2, 8) - ncirc = 0 - while True: - frame = cv.QueryFrame(capture) - im = np.array(cv.GetMat(frame)) - gray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY) - blur = cv2.medianBlur(gray, 5) - blur = cv2.medianBlur(blur, 5) - blur = cv2.medianBlur(blur, 5) - # blur = cv2.blur(blur, 5) - # im = blur - - circles = cv2.HoughCircles(blur, cv.CV_HOUGH_GRADIENT, 1,20, - param1=100,param2=30,minRadius=9,maxRadius=20) - - try: - # circles = np.uint16(np.around(circles)) - n = np.shape(circles) - if len(n) == 0: - continue - circles = np.reshape(circles,(n[1],n[2])) - d = 999.0 - for x,y,r in circles: - cv2.circle(im,(x,y),r,(0,0,255)) - cv2.circle(im,(x,y),2,(0,0,255),3) - nd = (x-320)**2.0 + (y-240)**2.0 - if nd < d: - tmp = x,y,r - d = nd - - x,y,r = tmp - cv2.circle(im,(x,y),r,(255,255,255)) - if ncirc == 0: - circle = tmp - else: - circle = map(np.sum, zip(circle,tmp)) - ncirc += 1 - x,y,r = [int(c/float(ncirc)) for c in circle] - cv2.circle(im,(x,y),r,(255,0,255)) - cv2.circle(im,(x,y),2,(255,0,255),3) - - if ncirc == 20: ncirc=0 - - - except Exception as e: - raise - print 'x', - - frame = cv.fromarray(im) - cv.PutText(frame, "orient.py", (10,460), font, cv.RGB(17, 110, 255)) - cv.Line(frame, (320,0), (320,480) , 255) - cv.Line(frame, (0,240), (640,240) , 255) - cv.Circle(frame, (320,240), 100, 255) - - cv.ShowImage("Window",frame) - c = (cv.WaitKey(16) & 255) - - if c in [27, 113]: #Break if user enters 'Esc', 'q'. - break - elif c != 255: - print c - - # - # - # # cv.CvtColor(frame, gray, cv.CV_BGR2GRAY) - # gray = cv2.cvtColor(np.array(frame), cv2.COLOR_BGR2GRAY) - # blur = cv2.GaussianBlur(gray, (3,3), 0) - # - # # GaussianBlur( src_gray, src_gray, Size(9, 9), 2, 2 ); - # - # # storage = cv.CreateMat(frame.width, 1, cv.CV_32FC3) - # # cv.HoughCircles(edges, storage, cv.CV_HOUGH_GRADIENT, 25, 100, 200, 10) - # circles = cv2.HoughCircles(gray, cv.CV_HOUGH_GRADIENT, 3, 100, None, 200, 100, 5, 16) - # - # n = np.shape(circles) - # circles = np.reshape(circles,(n[1],n[2])) - # # print circles - # for circle in circles: - # cv2.circle(frame,(circle[0],circle[1]),circle[2],(0,0,255)) - # - # # for i in xrange(storage.width - 1): - # # radius = storage[i, 2] - # # center = (storage[i, 0], storage[i, 1]) - # # cv.Circle(frame, center, radius, (0, 0, 255), 3, 8, 0) - # - # - # cv.PutText(frame, "orient.py", (10,460), font, cv.RGB(17, 110, 255)) - # cv.Line(frame, (320,0), (320,480) , 255) - # cv.Line(frame, (0,240), (640,240) , 255) - # cv.Circle(frame, (320,240), 100, 255) - # - # cv.ShowImage("Window",frame) - # c = (cv.WaitKey(16) & 255) - # - # if c in [27, 113]: #Break if user enters 'Esc', 'q'. - # break - # elif c != 255: - # print c - -def findrowline(frame): - hmin = 5 - hmax = 6 # hmax = 180 - # saturation - smin = 50 - smax = 100 - # value - vmin = 250 - vmax = 256 - - - tmp = cv.CreateImage(cv.GetSize(frame), 8, 1) - # tmp = cv.cvCloneImage(frame) - cv2.cvCvtColor(frame, tmp, cv.CV_BGR2HSV) # convert to HSV - # split the video frame into color channels - cv.cvSplit(hsv_image, h_img, s_img, v_img, None) - - # Threshold ranges of HSV components. - cv.cvInRangeS(h_img, hmin, hmax, h_img) - cv.cvInRangeS(s_img, smin, smax, s_img) - cv.cvInRangeS(v_img, vmin, vmax, v_img) - cv.cvAnd(h_img, v_img, laser_img) - - - return laser_img - - -def findrow2(frame): - # return frame - im = np.array(cv.GetMat(frame)) - gray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY) - ret, thresh = cv2.threshold(gray,127,255,cv2.THRESH_BINARY) - # thresh = cv2.adaptiveThreshold(gray, maxValue=255, - # adaptiveMethod=cv2.ADAPTIVE_THRESH_GAUSSIAN_C, - # thresholdType=cv2.THRESH_BINARY, - # blockSize=3, C=127) - - contours,hier = cv2.findContours(thresh,cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE) - - # thresh = cv2.dilate(thresh, 3) - kernel = np.ones((2,2),'uint8') - thresh = cv2.dilate(thresh, kernel) - thresh = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel) - - delta = 20 - for i in np.arange(0,thresh.shape[0],delta): - ii = np.argmax(np.mean(thresh[i:i+delta,20:thresh.shape[1]-20], axis=0)) - thresh[i:i+delta,:] = 0 - thresh[i:i+delta,ii] = 255 - # return cv.fromarray(thresh) - - return cv.fromarray(thresh) - - try: - cnt = contours[0] - - # then apply fitline() function - [vx,vy,x,y] = cv2.fitLine(cnt,cv2.cv.CV_DIST_L2,0,0.01,0.01) - - # Now find two extreme points on the line to draw line - lefty = int((-x*vy/vx) + y) - righty = int(((gray.shape[1]-x)*vy/vx)+y) - - cv2.line(im,(gray.shape[1]-1,righty),(0,lefty),255,2) - - return cv.fromarray(im) - except: - return frame - - - - - class Camera(object): def __init__(self, cameranumber=0): - self.status = '' + '''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 frame - - self.size = cv.GetSize(self.frame) - self.center = tuple(x/2 for x in self.size) - - self.font = cv.InitFont(cv2.FONT_HERSHEY_DUPLEX, 0.5, 0.8, - shear=0, thickness=1, lineType=8) - self.font2 = cv.InitFont(cv2.FONT_HERSHEY_DUPLEX, 0.5, 0.8, - shear=0, thickness=3, lineType=8) - - self.color = cv.RGB(100, 130, 255) - - def write(self, msg, loc): - cv.PutText(self.frame, msg, loc, self.font2, 0) - cv.PutText(self.frame, msg, loc, self.font, self.color) - + 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) + self.points = deque(maxlen=100) + + def getfont(self, **kwargs): + '''get a font with some nice defaults''' + fontsize = kwargs.pop('fontsize', 0.5) + outline = kwargs.pop('outline', False) + params = dict(font=CV_FONT_HERSHEY_PLAIN, + hscale=fontsize*0.9, vscale=fontsize, + 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 + 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.''' if frame: self.frame = frame else: self.frame = cv.QueryFrame(self.cam) - + def addoverlay(self): - # cv.PutText(self.frame, "orient.py", (10,self.size[1]-10), self.font, self.color) + 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) - - value = 0 - count = 100 - def onChange(x,*args): - print x - cv.CreateTrackbar('test','Window', value, count, onChange) - - def display(self, text): - # cv.PutText(self.frame, text, (20,20), self.font, self.color) - self.write(text, (20,20)) - + + def addtrackbar(self): + '''Add a trackbar?!''' + # value = 0 + # count = 100 + # 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 - 0:'forward', # arrows - 1:'backward', - 2:'left', - 3:'right', - 97:'up', # a - 122:'down', # d - 43:'embiggen', # + - 95:'lessen', # - + 27:'quit', # q + 113:'quit', # esc + 0:'forward', # arrows + 1:'backward', # + 2:'left', # + 3:'right', # + 97:'up', # a + 122:'down', # d + 43:'embiggen', # + + 95:'lessen', # - + # location setting + 115:'set', # s + 49: 'lowerleft', # 1 + 50: 'upperleft', # 2 + 51: 'lowerright', # 3 + 52: 'upperright', # 4 + # circle finding + 99: 'circle', # c } if c in CHARMAP: self.status = CHARMAP[c] elif c != 255: - print repr(c) + 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. + self.nsigma -- how many sigma above background to fit + self.zero -- The vertical zero position of the laser line.''' + 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 + # 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): + imavg = np.mean(im, axis=1) + ex,ey,cut = findextreme(imavg, self.nsigma) + try: + p,x,g = fitgaussian(ex,ey,cut) + out.append([i,p['mean'].value]) + except KeyboardInterrupt as e: + print 'User canceled operation' + return -1.0 + except Exception as e: + if not quiet: + print 'Failed to fit: {} {}'.format(i,e) + # raise + try: + x,y = zip(*out) + except: + x,y = [0],[0] + if getall: + 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) + # for x in self.points: + # try: + # 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, + 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: + raise ValueError('No Circles!') + circles = np.reshape(circles,(n[1],n[2])) + 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: + cv2.circle(frame,(tmp[0],tmp[1]),tmp[2],(0,255,0),2) + 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 + 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, + 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) + except: + pass + return frame + + def centralitem(self, items): + '''Get the most central item from a list of (x,y,...) items''' + mindist = 1e4 + good = None + for item in items: + dist = (item[0]-self.center[0])**2.0 + (item[1]-self.center[1])**2.0 + if dist <= mindist: + good = item + mindist = dist + return good + + + + + class Controller(object): - def __init__(self, serial): + '''Controller for the serial -> grbl device. Generally this assumes + that the machine is in incremental mode.''' 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 + returns a nice status message of what happened.''' DELTA = 0.001 CMD = dict( forward='X', @@ -330,6 +350,7 @@ def run(self, cmd): embiggen=DELTA, lessen=-DELTA, ) + if cmd in CMD: d = CMD[cmd] if isinstance(d, str): @@ -337,46 +358,404 @@ 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 return 'movelen: {:0.3f}inch'.format(self.movelen) + elif 'G' in CMD: + self.serial.run(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).''' + POS = ['set', 'lowerleft','lowerright','upperleft','upperright'] + if cmd in pos: + return 'Set: {}'.format(cmd) else: return cmd - + def position(self): - '''TODO convert this to some nice text''' + ''' get the current state of the machine and then return a processed + bit of text for simple consuming by other programs. + TODO: debug what the machine actually produces. + ''' 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]''' + self.x = 0 + self.y = 0 + try: + self.width = int(sys.argv[2]) + self.height = int(sys.argv[3]) + self.npts = int(sys.argv[4]) + except Exception as e: + print e + raise ValueError('Could not parse the arguments'+ + 'pass in {} scan [width] [height] [pts] :: [{}]' + .format(sys.argv[0], sys.argv[2:])) + + def scan(self): + '''scan over the width and heigh with npts locations.''' + for x in np.linspace(0,self.width, self.npts): + for y in np.linspace(0, self.height, self.npts): + self.run('G0 X{:0.3f} Y{:0.2f}'.format(x,y)) + yield x,y + -def main(): - + + +def findcircles(): + '''Find the current circle closest to the center of the screen. + this will show all circles on the screen.''' 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.run(camera.status) + # camera.status = controller.position(camera.status) if camera.status == 'quit': break else: camera.display(camera.status) - + camera.status = 'circle' + camera.circle() + # + # camera.addoverlay() camera.show() - - + + + + + + +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(): + camera = Camera() + camera.setupmeasure() + while True: + camera.update() + camera.interact() + x,y = camera.measure(getall=True, quiet=True) + camera.plot(x,y) + if camera.status == 'quit': + break + camera.show() + + + + +from pysurvey.plot import line, setup, legend, minmax, embiggen +from lmfit import minimize, Parameters, report_errors, conf_interval, report_ci + +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 + middle pixel location and the image that is [heightxdelta] in + size.''' + for i,index in enumerate(np.arange(0,img.shape[1],delta)): + middle = int(np.mean([index,index+delta])) + yield middle, img[:,index:index+delta] + +def findextreme(x, nabove=1.0): + '''Returns the index, array values, and cut value of the array + that is above the median and nabove*sigma of the array. This attempts + to find any line that is above the background.''' + cut = np.median(x) + nabove*np.std(x) + ii = np.where(x >= cut)[0] + return ii, x[ii], cut + +def getimrange(x, imrange): + xmin,xmax = minmax(x) + if imrange[0] > xmin: + imrange[0] = xmin + if imrange[1] < xmax: + imrange[1] = xmax + return imrange + + +def gauss2(p, x, y=None): + '''A simple gaussian fit function. p is a Parameters() object + that has an amplitude, mean, and sigma value. Without setting + y this returns the gaussian. with y it returns the deviation from + the fit gaussian -- used for fitting.''' + if y is None: + y = np.zeros(len(x)) + return (p['amplitude'].value* + np.exp(-(x-p['mean'].value)**2/(2.*p['sigma'].value**2)) + # + p['offset'].value + - y + ) + +def getarray(width=1000, delta=0.1): + '''Get an array to plot the gaussian. + There should be a better way of doing this.''' + 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 + 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 + # 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 + 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 + not capture the pointy-ness of the line. Now I am using a guass fit. + ''' + directory = '/Users/ajmendez/Dropbox/Shared/Design/laser/test/' + # Image from the web. + filename = directory+'1mW-635nm-Red-Laser-Module-Focused-Line-M635AL12416120_1.jpg' + color='red' + nsigma=1.0 + # filename = directory + 'test2.jpg' # has ripples + # filename = directory + 'test.jpg' + 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] + index = ['blue','green','red'].index(color) + setup(figsize=(8,8), subplt=(2,2,2)) + 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) + + ex,ey,cut = findextreme(imavg, nsigma) + imrange = getimrange(ex,imrange) + 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]) + print i, mid + except Exception as e: + pylab.plot(ex, ey) + pylab.show() + raise + print e + # 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]]) + pylab.imshow(img[:,:,[2,1,0]], origin='lower', interpolation='nearest', + aspect='equal') + # 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), + 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)]) + pylab.tight_layout() + pylab.show() + + + + +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) + # img = cv2.GaussianBlur(img, (0,0), 2.1) + # img = cv2.morphologyEx(img, cv2.MORPH_OPEN, (3,3)) + # img = cv2.morphologyEx(img, cv2.MORPH_OPEN, (5,5)) + # 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, + dp=1, # accumulator res + minDist=40, #min dist to next circle + param1=100, # canny param + param2=20, # accumulator threshold + minRadius=10, + maxRadius=20) + try: + n = np.shape(circles) + if len(n) == 0: + raise ValueError('No Circles!') + 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, + 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 + and overwrite the image with hitting c again. + ''' + cap = cv.CaptureFromCAM(0) + # cv.SetCaptureProperty(cap,cv.CV_CAP_PROP_FRAME_WIDTH, 1280) + # 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]: + break + elif c == ord('c'): + print 'Saved Frame!' + cv.SaveImage('test.jpg', img) if __name__ == "__main__": - # findcircle() - # main(findrow2) - main() \ No newline at end of file + if 'capture' in sys.argv: + capture() + elif 'test' in sys.argv: + test() + elif 'circle' in sys.argv: + test_circle() + elif 'scan' in sys.argv: + scan() + elif 'roll' in sys.argv: + roll() + else: + main() 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.md b/readme.md index f6babcc..05ff65e 100644 --- a/readme.md +++ b/readme.md @@ -9,10 +9,13 @@ Commands: Here is a small list of some of the commands that one might find useful in this package: - * command.py -- Send basic commands to grbl - * align.py -- Use arrowkeys/a/z to move mill bit - * stream.py -- Stream gcode to grbl. - * optimize.py -- Optimization routine. + * command.py -- Send basic commands to grbl + * align.py -- Use arrowkeys/a/z to move mill bit + * stream.py -- Stream gcode to grbl. + * optimize.py -- Optimization routine. + * orient.py -- OpenCV Camera Orientation and Height + * home.py -- Enable homing + * visualize.py -- Visualize the 2D PCB boards * flatten.py -- Generate raster gcode script. * findheight.py -- Generate a height gcode script @@ -25,7 +28,7 @@ in this package: grbl settings: -------------- -These are the settings that work on the machine: +These are the settings that work on the old machine: $0 = 188.976 (steps/mm x) $1 = 188.976 (steps/mm y) @@ -38,6 +41,37 @@ These are the settings that work on the machine: $8 = 4.000 (acceleration in mm/sec^2) $9 = 0.050 (cornering junction deviation in mm) +New Machine 10/13: + + $0 = 755.906 (x, step/mm) + $1 = 755.906 (y, step/mm) + $2 = 755.906 (z, step/mm) + $3 = 50 (step pulse, usec) + $4 = 260.000 (default feed, mm/min) + $5 = 520.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.200 (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) + + + +Materials Settings: +------------------- +Plexyglass / Acrylic + $4 = 520 + $5 = 520 + Mill Depth = 0.010 - 0.020 + Requires oil + Drills can be problematic -- best to run one at a time + + Extra: @@ -54,5 +88,6 @@ History: * [2012/01] -- Pushed without documentation to bitbucket. * [2012/08] -- Updated documentation and cleaned up. * [2013/08] -- Switched to github public. + * [2014/03] -- working on opencv 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/script/flatten.py b/script/flatten.py old mode 100644 new mode 100755 index 70db1e6..52f9d17 --- a/script/flatten.py +++ b/script/flatten.py @@ -4,7 +4,7 @@ # What we are going with. xmax = 3.75 # inch : max x dimension of milled out area -ymax = 5.30 # inch : max y dimension of milled out area +ymax = 5.35 # inch : max y dimension of milled out area bitsize = (1/8.)/2. # inch : Radius of mill bit. milldepth = -0.130 # inch : milling depth diff --git a/stream.py b/stream.py index de9de69..16418b5 100755 --- a/stream.py +++ b/stream.py @@ -9,6 +9,10 @@ from lib.communicate import Communicate from lib.util import deltaTime +# from pysurvey import util +# util.setup_stop() + + RX_BUFFER_SIZE = 128 # Initialize the args @@ -33,6 +37,10 @@ # Strip comments/spaces/new line, capitalize, and add line ending l = re.sub('\s|\(.*?\)','',line.strip()).upper()+'\n' + # if 'M' in l: + # continue + + # if this was a comment or blank line just go to the next one if len(l.strip()) == 0: continue @@ -89,4 +97,4 @@ x = raw_input('GRBL> ').strip() serial.run(x) if '~' in x: break - \ No newline at end of file + 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))