From 2380343104087e2bea494d5c36db67ef4a27bbda Mon Sep 17 00:00:00 2001 From: Alexander Mendez Date: Sat, 23 Nov 2013 17:33:54 -0800 Subject: [PATCH 01/29] some minor library changes --- .gitignore | 3 ++- align.py | 1 + stream.py | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) 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..7af3d10 100755 --- a/align.py +++ b/align.py @@ -24,6 +24,7 @@ G91 (Incremental) G0 X0.000 Y0.000 Z0.000 ''' + HELP = '''\ Board Alignment Keys: q/Q : Quit diff --git a/stream.py b/stream.py index de9de69..2e894a2 100755 --- a/stream.py +++ b/stream.py @@ -32,7 +32,7 @@ for i,line in enumerate(progress.bar(lines)): # Strip comments/spaces/new line, capitalize, and add line ending l = re.sub('\s|\(.*?\)','',line.strip()).upper()+'\n' - + # if this was a comment or blank line just go to the next one if len(l.strip()) == 0: continue @@ -89,4 +89,4 @@ x = raw_input('GRBL> ').strip() serial.run(x) if '~' in x: break - \ No newline at end of file + From 06713cc9b612f2ab2057d7bd1fc5ac8f7e168b67 Mon Sep 17 00:00:00 2001 From: Alexander Mendez Date: Sun, 1 Dec 2013 13:05:10 -0800 Subject: [PATCH 02/29] added soft reset -- is broken --- command.py | 2 +- lib/communicate.py | 11 +++++++---- stream.py | 8 ++++++++ 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/command.py b/command.py index 6620161..9c1b32f 100755 --- a/command.py +++ b/command.py @@ -29,4 +29,4 @@ 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/lib/communicate.py b/lib/communicate.py index 847a672..8ad8e8b 100755 --- a/lib/communicate.py +++ b/lib/communicate.py @@ -51,15 +51,18 @@ def sendreset(self): ''' 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): diff --git a/stream.py b/stream.py index 2e894a2..df6ae3b 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 @@ -32,6 +36,10 @@ for i,line in enumerate(progress.bar(lines)): # 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: From 7fd46ed16d3698ec09822288da9bfd4c28520ac8 Mon Sep 17 00:00:00 2001 From: Alexander Mendez Date: Mon, 7 Apr 2014 01:46:02 -0700 Subject: [PATCH 03/29] updated documentation --- readme.md | 45 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/readme.md b/readme.md index f6babcc..abd779a 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: + Sending: [$$] + | $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 From 78440fad57ca5fc3d6094ab702338a84556572d3 Mon Sep 17 00:00:00 2001 From: Alexander Mendez Date: Mon, 7 Apr 2014 01:47:48 -0700 Subject: [PATCH 04/29] Update readme.md --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index abd779a..837bf06 100644 --- a/readme.md +++ b/readme.md @@ -42,7 +42,7 @@ These are the settings that work on the old machine: $9 = 0.050 (cornering junction deviation in mm) New Machine 10/13: - Sending: [$$] + | $0 = 755.906 (x, step/mm) | $1 = 755.906 (y, step/mm) | $2 = 755.906 (z, step/mm) From 7543b979da38bc58bcae8521d156e500111c5c67 Mon Sep 17 00:00:00 2001 From: Alexander Mendez Date: Mon, 7 Apr 2014 01:48:42 -0700 Subject: [PATCH 05/29] fixed formatting --- readme.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/readme.md b/readme.md index 837bf06..05ff65e 100644 --- a/readme.md +++ b/readme.md @@ -43,22 +43,22 @@ These are the settings that work on the old machine: 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) + $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) From 778e974661d5e5c2a17b3ed7f5e703caddbbdeae Mon Sep 17 00:00:00 2001 From: Alexander Mendez Date: Tue, 8 Apr 2014 00:51:41 -0700 Subject: [PATCH 06/29] working line finder --- orient.py | 180 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 179 insertions(+), 1 deletion(-) diff --git a/orient.py b/orient.py index 97085c6..b4f83fc 100755 --- a/orient.py +++ b/orient.py @@ -56,6 +56,7 @@ 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 ''' @@ -373,10 +374,187 @@ def main(): camera.addoverlay() camera.show() + + + +from pysurvey.plot import line, setup, legend, minmax +from scipy.optimize import curve_fit + +def vslice(img, delta=20): + 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): + cut = np.median(x) + nabove*np.std(x) + ii = np.where(x >= cut)[0] + return ii, x[ii], cut + +def fitquad(x,y): + xx = np.arange(0,1000,0.1) + a,b,c = np.polyfit(x,y,2) + yy = c + b*xx + a*xx**2.0 + jj = np.where(yy > 0)[0] + return xx[jj],yy[jj] + +def gauss(x, *p): + c, A, mu, sigma = p + return c+A*np.exp(-(x-mu)**2/(2.*sigma**2)) + +def fitgauss(x,y,offset=0): + xx = np.arange(0, 1000, 0.1) + p0 = [offset, np.max(y)-offset, np.mean(x), np.std(x)] + coeff, var_matrix = curve_fit(gauss, x, y, p0=p0) + gg = gauss(xx, *coeff) + return xx,gg + + +from lmfit import minimize, Parameters, report_errors, conf_interval, report_ci +def gauss2(p, x, y=None): + 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 fitgauss2(x,y,offset=0): + p = Parameters() + # p.add('offset', value=offset, min=50) + 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) + + out = minimize(gauss2, p, args=(x, y-offset) ) + + # report_errors(p) + + xx = np.arange(0,1000,0.1) + return xx, gauss2(p,xx)+offset + + + +def test(): + # filename = '/Users/ajmendez/Dropbox/Shared/Design/laser/test/1mW-635nm-Red-Laser-Module-Focused-Line-M635AL12416120_1.jpg' + filename = '/Users/ajmendez/Dropbox/Shared/Design/laser/test/debug_green.jpg' + img = cv2.imread(filename) + hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) + + out = [] + for i,im in vslice(img): + vv = np.mean(im[:,:,1], axis=1) + + ii,v,cut = findextreme(vv, 0.2) + # x,y = fitquad(ii,v) + + + try: + # xx,gg = fitgauss(ii,v,cut) + xx,gg = fitgauss2(ii,v,cut) + + # setup(xr=[260,300]) + # pylab.plot(vv) + # pylab.plot(xx,gg) + + mid = xx[np.argmax(gg)] + print i, mid + cv2.circle(img, (i,int(mid)), 2, 255) + out.append([i,mid]) + except Exception as e: + print e + # zz,gz = fitgauss2(ii,v,cut) + + # setup(xr=minmax(ii)) + # pylab.plot(ii,v) + # pylab.plot(xx,gg) + # pylab.plot(zz,gz) + # line(y=cut) + # pylab.show() + # return + cv2.circle(img, (i,0), 10, 75) + + # setup(figsize=(14,6), subplt=(1,3,1)) + # pylab.imshow(im, interpolation=None, origin='lower', aspect='auto') + # + # setup(subplt=(1,3,2)) + # pylab.plot(vv) + # pylab.plot(ii, v) + # line(y=cut) + # + # + # setup(subplt=(1,2,2), xr=minmax(ii)) + # pylab.plot(vv, '-s') + # pylab.plot(x,y) + # pylab.plot(xx,gg) + # + # + # setup(embiggenx=0.2, embiggeny=0.2) + # line(y=cut, x=x[np.argmax(y)], color='r') + # line(y=cut, x=np.average(ii,weights=v), color='k') + # line(y=cut, x=xx[np.argmax(gg)], color='b') + # + # + # pylab.show() + # return + + setup(figsize=(12,6), subplt=(1,2,1)) + x,y = map(np.array,zip(*out)) + print x,y + pylab.plot(x,y) + a,b = np.polyfit(x,y,1) + pylab.plot(x, b+a*x) + setup(subplt=(1,2,2)) + diff = y-(b+a*x) + pylab.hist(diff, np.arange(-5,5,0.4)) + line(x=[np.mean(diff),np.mean(diff)-np.std(diff), np.mean(diff)+np.std(diff)]) + print np.std(diff) + pylab.show() + + + + cv2.imshow('window', img) + cv2.waitKey(0) + + +def highres(): + + + # this does not seem to work?! + # def set_res(cap, x,y): + # cap.set(cv.CV_CAP_PROP_FRAME_WIDTH, int(x)) + # cap.set(cv.CV_CAP_PROP_FRAME_HEIGHT, int(y)) + # return str(cap.get(cv.CV_CAP_PROP_FRAME_WIDTH)),str(cap.get(cv.CV_CAP_PROP_FRAME_HEIGHT)) + # cap = cv2.VideoCapture(0) + # x = [160 ,160 ,144 ,160 ,160 ,140 ,160 ,224 ,208 ,240 ,220 ,160 ,208 ,256 ,280 ,240 ,320 ,320 ,256 ,320 ,320 ,320 ,320 ,400 ,320 ,432 ,560 ,400 ,480 ,480 ,400 ,376 ,640 ,480 ,512 ,416 ,640 ,480 ,640 ,512 ,800 ,512 ,640 ,640 ,640 ,480 ,720 ,720 ,640 ,720 ,800 ,600 ,640 ,640 ,768 ,800 ,848 ,854 ,800 ,960 ,832 ,960 ,1024,1024,960 ,1024,960 ,1136,1024,1024,1152,1152,1280,1120,1280,1152,1280,1152,1024,1366,1280,1600,1280,1440,1280] + # + # y = [120, 144, 168, 152, 160, 192, 200, 144, 176, 160, 176, 256, 208, 192, 192, 240, 192, 200, 256, 208, 224, 240, 256, 240, 320, 240, 192, 270, 234, 250, 300, 240, 200, 272, 256, 352, 240, 320, 256, 342, 240, 384, 320, 350, 360, 500, 348, 350, 400, 364, 352, 480, 480, 512, 480, 480, 480, 480, 600, 540, 624, 544, 576, 600, 640, 640, 720, 640, 768, 800, 720, 768, 720, 832, 768, 864, 800, 900, 1024, 768, 854, 768, 960, 900, 1024, ] + # + # for w,h in zip(x,y): + # print w,h,set_res(cap, w,h) + # break + + 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'): + cv.SaveImage('test.jpg', img) + + + if __name__ == "__main__": # findcircle() # main(findrow2) - main() \ No newline at end of file + # main() + test() + + # highres() \ No newline at end of file From b50b556a4fbfdf0997b40c87042159722381e0fb Mon Sep 17 00:00:00 2001 From: Alexander Mendez Date: Wed, 9 Apr 2014 01:15:53 -0700 Subject: [PATCH 07/29] added new analysis tests --- orient.py | 322 ++++++++++++++++++++++++------------------------------ 1 file changed, 145 insertions(+), 177 deletions(-) diff --git a/orient.py b/orient.py index b4f83fc..ceb360f 100755 --- a/orient.py +++ b/orient.py @@ -1,27 +1,25 @@ #!/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 +# installed +import cv +import cv2 +import pylab +import numpy as np +# Package +from lib.communicate import Communicate +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 @@ -135,43 +133,7 @@ def findcircle(): 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 @@ -377,40 +339,43 @@ def main(): -from pysurvey.plot import line, setup, legend, minmax -from scipy.optimize import curve_fit + + + +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 fitquad(x,y): - xx = np.arange(0,1000,0.1) - a,b,c = np.polyfit(x,y,2) - yy = c + b*xx + a*xx**2.0 - jj = np.where(yy > 0)[0] - return xx[jj],yy[jj] +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 gauss(x, *p): - c, A, mu, sigma = p - return c+A*np.exp(-(x-mu)**2/(2.*sigma**2)) -def fitgauss(x,y,offset=0): - xx = np.arange(0, 1000, 0.1) - p0 = [offset, np.max(y)-offset, np.mean(x), np.std(x)] - coeff, var_matrix = curve_fit(gauss, x, y, p0=p0) - gg = gauss(xx, *coeff) - return xx,gg - - -from lmfit import minimize, Parameters, report_errors, conf_interval, report_ci 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* @@ -419,121 +384,128 @@ def gauss2(p, x, y=None): - y ) -def fitgauss2(x,y,offset=0): +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() - # p.add('offset', value=offset, min=50) + # 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) - xx = np.arange(0,1000,0.1) - return xx, gauss2(p,xx)+offset - - - -def test(): - # filename = '/Users/ajmendez/Dropbox/Shared/Design/laser/test/1mW-635nm-Red-Laser-Module-Focused-Line-M635AL12416120_1.jpg' - filename = '/Users/ajmendez/Dropbox/Shared/Design/laser/test/debug_green.jpg' - img = cv2.imread(filename) - hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) + 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.0 out = [] - for i,im in vslice(img): - vv = np.mean(im[:,:,1], axis=1) - - ii,v,cut = findextreme(vv, 0.2) - # x,y = fitquad(ii,v) - - + 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: - # xx,gg = fitgauss(ii,v,cut) - xx,gg = fitgauss2(ii,v,cut) + p,x,g = fitgaussian(ex,ey,cut) + mid = p['mean'].value - # setup(xr=[260,300]) - # pylab.plot(vv) - # pylab.plot(xx,gg) + # 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)) - mid = xx[np.argmax(gg)] - print i, mid - cv2.circle(img, (i,int(mid)), 2, 255) + # 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(imgavg) + pylab.show() + raise print e - # zz,gz = fitgauss2(ii,v,cut) - - # setup(xr=minmax(ii)) - # pylab.plot(ii,v) - # pylab.plot(xx,gg) - # pylab.plot(zz,gz) - # line(y=cut) - # pylab.show() - # return - cv2.circle(img, (i,0), 10, 75) - - # setup(figsize=(14,6), subplt=(1,3,1)) - # pylab.imshow(im, interpolation=None, origin='lower', aspect='auto') - # - # setup(subplt=(1,3,2)) - # pylab.plot(vv) - # pylab.plot(ii, v) - # line(y=cut) - # - # - # setup(subplt=(1,2,2), xr=minmax(ii)) - # pylab.plot(vv, '-s') - # pylab.plot(x,y) - # pylab.plot(xx,gg) - # - # - # setup(embiggenx=0.2, embiggeny=0.2) - # line(y=cut, x=x[np.argmax(y)], color='r') - # line(y=cut, x=np.average(ii,weights=v), color='k') - # line(y=cut, x=xx[np.argmax(gg)], color='b') - # - # - # pylab.show() - # return - - setup(figsize=(12,6), subplt=(1,2,1)) - x,y = map(np.array,zip(*out)) - print x,y - pylab.plot(x,y) - a,b = np.polyfit(x,y,1) - pylab.plot(x, b+a*x) - setup(subplt=(1,2,2)) - diff = y-(b+a*x) - pylab.hist(diff, np.arange(-5,5,0.4)) - line(x=[np.mean(diff),np.mean(diff)-np.std(diff), np.mean(diff)+np.std(diff)]) - print np.std(diff) + # 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, 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() - - - - cv2.imshow('window', img) - cv2.waitKey(0) - - -def highres(): - - - # this does not seem to work?! - # def set_res(cap, x,y): - # cap.set(cv.CV_CAP_PROP_FRAME_WIDTH, int(x)) - # cap.set(cv.CV_CAP_PROP_FRAME_HEIGHT, int(y)) - # return str(cap.get(cv.CV_CAP_PROP_FRAME_WIDTH)),str(cap.get(cv.CV_CAP_PROP_FRAME_HEIGHT)) - # cap = cv2.VideoCapture(0) - # x = [160 ,160 ,144 ,160 ,160 ,140 ,160 ,224 ,208 ,240 ,220 ,160 ,208 ,256 ,280 ,240 ,320 ,320 ,256 ,320 ,320 ,320 ,320 ,400 ,320 ,432 ,560 ,400 ,480 ,480 ,400 ,376 ,640 ,480 ,512 ,416 ,640 ,480 ,640 ,512 ,800 ,512 ,640 ,640 ,640 ,480 ,720 ,720 ,640 ,720 ,800 ,600 ,640 ,640 ,768 ,800 ,848 ,854 ,800 ,960 ,832 ,960 ,1024,1024,960 ,1024,960 ,1136,1024,1024,1152,1152,1280,1120,1280,1152,1280,1152,1024,1366,1280,1600,1280,1440,1280] - # - # y = [120, 144, 168, 152, 160, 192, 200, 144, 176, 160, 176, 256, 208, 192, 192, 240, 192, 200, 256, 208, 224, 240, 256, 240, 320, 240, 192, 270, 234, 250, 300, 240, 200, 272, 256, 352, 240, 320, 256, 342, 240, 384, 320, 350, 360, 500, 348, 350, 400, 364, 352, 480, 480, 512, 480, 480, 480, 480, 600, 540, 624, 544, 576, 600, 640, 640, 720, 640, 768, 800, 720, 768, 720, 832, 768, 864, 800, 900, 1024, 768, 854, 768, 960, 900, 1024, ] - # - # for w,h in zip(x,y): - # print w,h,set_res(cap, w,h) - # break +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) @@ -545,16 +517,12 @@ def highres(): break elif c == ord('c'): cv.SaveImage('test.jpg', img) - - - - if __name__ == "__main__": - # findcircle() - # main(findrow2) - # main() - test() - - # highres() \ No newline at end of file + if 'capture' in sys.argv: + capture(sys.argv) + elif 'test' in sys.argv: + test() + else: + main() From 85cbdf9231d74cb6880052275ed9e05ef709bb3a Mon Sep 17 00:00:00 2001 From: Alexander Mendez Date: Fri, 11 Apr 2014 00:31:33 -0700 Subject: [PATCH 08/29] added circle and height measurements to orient --- orient.py | 302 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 271 insertions(+), 31 deletions(-) diff --git a/orient.py b/orient.py index ceb360f..fe1ca80 100755 --- a/orient.py +++ b/orient.py @@ -3,6 +3,7 @@ # system import sys +from collections import deque # installed import cv @@ -13,6 +14,16 @@ # 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 +''' + + @@ -216,17 +227,21 @@ def __init__(self, cameranumber=0): self.size = cv.GetSize(self.frame) self.center = tuple(x/2 for x in self.size) + self.currentcircles = deque(maxlen=40) - self.font = cv.InitFont(cv2.FONT_HERSHEY_DUPLEX, 0.5, 0.8, + self.color = cv.RGB(100, 130, 255) + self.font = cv.InitFont(cv2.FONT_HERSHEY_DUPLEX, 0.1, 0.5, shear=0, thickness=1, lineType=8) - self.font2 = cv.InitFont(cv2.FONT_HERSHEY_DUPLEX, 0.5, 0.8, + self.font2 = cv.InitFont(cv2.FONT_HERSHEY_DUPLEX, 0.1, 0.5, 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) + for i,line in enumerate(msg.splitlines()): + l = (loc[0], loc[1]+i*20) + cv.PutText(self.frame, line, l, self.font2, 0) + cv.PutText(self.frame, line, l, self.font, self.color) def update(self, frame=None): if frame: @@ -236,16 +251,17 @@ def update(self, frame=None): 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) + # 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) @@ -258,24 +274,122 @@ def interact(self): 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) + + + + def setupmeasure(self, color='red'): + self.index = ['blue','green','red'].index(color) + self.nsigma = 1.0 + self.zero = 0 + self.zero = self.measure() + + + def measure(self, delta=200): + '''return the location of the point in pixels''' + # cv.CvtColor(self.frame, self.frame, cv.CV_BGR2HLS) + cv.Not(self.frame, self.frame) + + img = np.array(cv.GetMat(self.frame))[:,:,self.index] + out = [] + 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(p['mean'].value) + except KeyboardInterrupt as e: + print 'User canceled operation' + # sys.exit() + except Exception as e: + print 'Failed to fit: {} {}'.format(i,e) + # raise + return np.mean(out) + + + def circle(self): + if self.status != 'circle': + return + # get some circles + 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 + 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.plot_currentcircle(frame) + self.frame = cv.fromarray(frame) + + def plot_currentcircle(self, frame): + 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): + 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): self.serial = serial self.serial.run('G20G91 (inch, incremental)') @@ -293,6 +407,7 @@ def run(self, cmd): embiggen=DELTA, lessen=-DELTA, ) + if cmd in CMD: d = CMD[cmd] if isinstance(d, str): @@ -305,13 +420,47 @@ def run(self, cmd): 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): + 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''' status = self.serial.run('?') return 'position: {}'.format(status) + + + + def setupscan(self): + 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 + + @@ -327,19 +476,57 @@ def main(): 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() + + from pysurvey.plot import line, setup, legend, minmax, embiggen @@ -402,6 +589,7 @@ def fitgaussian(x,y,offset=0): 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 @@ -428,7 +616,11 @@ def test(color='green', delta=20): # filename = directory + 'test.jpg' filename = directory + 'debug_green.jpg' color='green' - nsigma=1.0 + nsigma=1.5 + + filename = './test.jpg' + color='green' + nsigma=0.0 out = [] img = cv2.imread(filename) @@ -459,7 +651,7 @@ def test(color='green', delta=20): out.append([i,mid]) print i, mid except Exception as e: - pylab.plot(imgavg) + pylab.plot(ex, ey) pylab.show() raise print e @@ -476,7 +668,7 @@ def test(color='green', delta=20): # 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, origin='lower', interpolation='nearest', + 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, @@ -499,7 +691,49 @@ def test(color='green', delta=20): np.mean(diff)+np.std(diff)]) pylab.tight_layout() pylab.show() + + + + +def test_circle(): + frame = cv2.imread('./test.jpg') + img = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + # 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=20, #min dist to next circle + param1=100, # canny param + param2=20, # accumulator threshold + minRadius=5, + 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. @@ -507,22 +741,28 @@ def capture(): 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) + # 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__": if 'capture' in sys.argv: - capture(sys.argv) + capture() elif 'test' in sys.argv: test() + elif 'circle' in sys.argv: + test_circle() + elif 'scan' in sys.argv: + scan() else: main() From 06bd23967bce74024075c19dad82e4af148828c1 Mon Sep 17 00:00:00 2001 From: Alexander Mendez Date: Fri, 11 Apr 2014 19:52:24 -0700 Subject: [PATCH 09/29] working on cleaning up the code and documenting it all --- orient.py | 321 +++++++++++++++++++++--------------------------------- 1 file changed, 127 insertions(+), 194 deletions(-) diff --git a/orient.py b/orient.py index fe1ca80..a3b41ea 100755 --- a/orient.py +++ b/orient.py @@ -71,206 +71,83 @@ - - -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 - - -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.update() # setup self.frame - self.size = cv.GetSize(self.frame) - self.center = tuple(x/2 for x in self.size) + self.shape = cv.GetSize(self.frame) + self.center = tuple(x/2 for x in self.shape) self.currentcircles = deque(maxlen=40) + + 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) - self.color = cv.RGB(100, 130, 255) - self.font = cv.InitFont(cv2.FONT_HERSHEY_DUPLEX, 0.1, 0.5, - shear=0, thickness=1, lineType=8) - self.font2 = cv.InitFont(cv2.FONT_HERSHEY_DUPLEX, 0.1, 0.5, - shear=0, thickness=3, lineType=8) - - + def getdefaultcolor(self): + '''A nice steel blue''' + return self.color(100,130,255) - def write(self, msg, loc): + 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*20) - cv.PutText(self.frame, line, l, self.font2, 0) - cv.PutText(self.frame, line, l, self.font, self.color) + 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) - + + def addtrackbar(self): + '''Add a trackbar?!''' # 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 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 = { @@ -296,24 +173,31 @@ def interact(self): 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 - self.zero = self.measure() - + self.zero = 0 + + def setzero(self, **kwargs): + '''Set the zero location of the line location.''' + self.zero = self.measure(**kwargs) def measure(self, delta=200): '''return the location of the point in pixels''' - # cv.CvtColor(self.frame, self.frame, cv.CV_BGR2HLS) + # DEBUG!! invert image so that a dark green line looks like a + # bright red line! cv.Not(self.frame, self.frame) img = np.array(cv.GetMat(self.frame))[:,:,self.index] - out = [] + 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) @@ -322,17 +206,17 @@ def measure(self, delta=200): out.append(p['mean'].value) except KeyboardInterrupt as e: print 'User canceled operation' - # sys.exit() + return -1.0 except Exception as e: print 'Failed to fit: {} {}'.format(i,e) # raise return np.mean(out) + # Circle finding procedures + def circle(self): - if self.status != 'circle': - return - # get some circles + '''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) @@ -353,7 +237,7 @@ def circle(self): cv2.circle(frame,(x,y),r,(255,255,255)) cv2.circle(frame,(x,y),2,(255,255,255),2) - # add the most central one + # 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) @@ -361,10 +245,13 @@ def circle(self): except Exception as e: print e - frame = self.plot_currentcircle(frame) + frame = self.plotcurrentcircle(frame) self.frame = cv.fromarray(frame) - def plot_currentcircle(self, 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)) @@ -379,6 +266,7 @@ def plot_currentcircle(self, frame): 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: @@ -389,13 +277,24 @@ def centralitem(self, items): 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', @@ -427,6 +326,8 @@ def run(self, cmd): 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) @@ -434,13 +335,19 @@ def setposition(self, cmd): 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: @@ -459,15 +366,15 @@ def scan(self): 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) @@ -492,6 +399,8 @@ def main(): + + def scan(): pylab.ion() pylab.figure(1) @@ -617,8 +526,8 @@ def test(color='green', delta=20): filename = directory + 'debug_green.jpg' color='green' nsigma=1.5 - - filename = './test.jpg' + + filename = directory+'/test_circ.jpg' color='green' nsigma=0.0 @@ -696,19 +605,43 @@ def test(color='green', delta=20): def test_circle(): - frame = cv2.imread('./test.jpg') + 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.medianBlur(img, 3) # img = cv2.GaussianBlur(img, (0,0), 0.1) - # frame = img + frame = img circles = cv2.HoughCircles(img, cv.CV_HOUGH_GRADIENT, dp=1, # accumulator res - minDist=20, #min dist to next circle + minDist=40, #min dist to next circle param1=100, # canny param param2=20, # accumulator threshold - minRadius=5, + minRadius=10, maxRadius=20) try: n = np.shape(circles) From 8556788751def3538fc0502bee8c1f1d8290513f Mon Sep 17 00:00:00 2001 From: Alexander Mendez Date: Tue, 13 May 2014 00:59:29 -0700 Subject: [PATCH 10/29] added offset and rotate keywords to optimize --- lib/gcode.py | 0 lib/tool.py | 15 +++++++++++ optimize.py | 71 +++++++++++++++++++++++++++++++++++++--------------- 3 files changed, 66 insertions(+), 20 deletions(-) mode change 100644 => 100755 lib/gcode.py mode change 100644 => 100755 lib/tool.py diff --git a/lib/gcode.py b/lib/gcode.py old mode 100644 new mode 100755 diff --git a/lib/tool.py b/lib/tool.py old mode 100644 new mode 100755 index 78c6188..fa76ad5 --- a/lib/tool.py +++ b/lib/tool.py @@ -160,6 +160,21 @@ def boundBox(self): # if j == 2 : sys.exit() return box + def offset(self, offset): + '''offset the toolpath by some offset=x,y''' + for item in self: + for i,ax in enumerate(item): + if i == 0: + item[ax] -= offset[0] + if i == 1: + item[ax] -= offset[1] + + def rotate(self): + '''rotate by 90''' + for item in self: + item[0],item[1] = -item[1], item[0] + + # def _badclean(self): # '''A temporary fix to check the bike program''' # loc=[0.0,0.0,0.0] diff --git a/optimize.py b/optimize.py index 938a491..ccc92c2 100755 --- a/optimize.py +++ b/optimize.py @@ -18,7 +18,7 @@ # The Optimize function -def opt(gfile): +def opt(gfile, offset=(0.0,0.0), rotate=False): '''Optimization core function: Reads in gCode ascii file. Processes gcode into toolpath list @@ -36,6 +36,10 @@ def opt(gfile): # 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) + if rotate: + tool.rotate() + tool.groupMills() puts(colored.blue('Toolpath length: %.2f inches, (mill only: %.2f)'%(tool.length(),tool.millLength()))) if args.setMillHeight: @@ -47,6 +51,7 @@ def opt(gfile): 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) @@ -62,6 +67,12 @@ def opt(gfile): # move mills and update location 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() @@ -86,28 +97,48 @@ def opt(gfile): const=False, action='store_const', dest='setMillHeight', - help='''Do not modify the mill height for 3d mills''') ) - - - + help='''Do not modify the mill height for 3d mills'''), + ext2=dict(args=['--offsetx'], + default=0, + type=float, + # dest='offsetx', + help='Set x offset length in inches'), + ext3=dict(args=['--offsety'], + default=0, + type=float, + # dest='offsetx', + help='Set y offset length in inches'), + ext4=dict(args=['--rotate'], + default=False, + const=False, + action='store_const', + help='rotate by 90'), + ) -# 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)) +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) + + # 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), rotate=args.rotate) + + print '%s finished in %s'%(args.name,deltaTime(start)) From 8f577b1bfe195116a94b7e3d4c3c5b95c9ef3621 Mon Sep 17 00:00:00 2001 From: Alexander Mendez Date: Tue, 13 May 2014 01:25:13 -0700 Subject: [PATCH 11/29] some more tests for rotate --- lib/tool.py | 4 ++++ optimize.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/tool.py b/lib/tool.py index fa76ad5..3f53a49 100755 --- a/lib/tool.py +++ b/lib/tool.py @@ -172,6 +172,10 @@ def offset(self, offset): def rotate(self): '''rotate by 90''' for item in self: + # for i,ax in enumerate(item): + # from pysurvey import util + # util.setup_stop() + # raise ValueError() item[0],item[1] = -item[1], item[0] diff --git a/optimize.py b/optimize.py index ccc92c2..872fab1 100755 --- a/optimize.py +++ b/optimize.py @@ -110,7 +110,7 @@ def opt(gfile, offset=(0.0,0.0), rotate=False): help='Set y offset length in inches'), ext4=dict(args=['--rotate'], default=False, - const=False, + const=True, action='store_const', help='rotate by 90'), ) From aa2918f2571fd8ba4450e470ad450bc8648a81c8 Mon Sep 17 00:00:00 2001 From: Alexander Mendez Date: Tue, 13 May 2014 01:37:54 -0700 Subject: [PATCH 12/29] should split off rotate and offset to different function --- lib/tool.py | 16 +++++++++------- optimize.py | 19 +++++++------------ 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/lib/tool.py b/lib/tool.py index 3f53a49..2f7f164 100755 --- a/lib/tool.py +++ b/lib/tool.py @@ -6,6 +6,7 @@ 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 @@ -169,14 +170,15 @@ def offset(self, offset): if i == 1: item[ax] -= offset[1] - def rotate(self): - '''rotate by 90''' + def rotate(self, angle): + '''rotate by some angle''' + rad = math.radians(angle) for item in self: - # for i,ax in enumerate(item): - # from pysurvey import util - # util.setup_stop() - # raise ValueError() - item[0],item[1] = -item[1], item[0] + # 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): diff --git a/optimize.py b/optimize.py index 872fab1..00b304b 100755 --- a/optimize.py +++ b/optimize.py @@ -37,8 +37,7 @@ def opt(gfile, offset=(0.0,0.0), rotate=False): # where the bit needs to be moved / milled : [ [x,y,z,t], ...] tool = Tool(gcode) tool.offset(offset) - if rotate: - tool.rotate() + tool.rotate(rotate) tool.groupMills() puts(colored.blue('Toolpath length: %.2f inches, (mill only: %.2f)'%(tool.length(),tool.millLength()))) @@ -104,15 +103,11 @@ def opt(gfile, offset=(0.0,0.0), rotate=False): # dest='offsetx', help='Set x offset length in inches'), ext3=dict(args=['--offsety'], - default=0, - type=float, - # dest='offsetx', + default=0.0, type=float, help='Set y offset length in inches'), ext4=dict(args=['--rotate'], - default=False, - const=True, - action='store_const', - help='rotate by 90'), + default=0.0, type=float, + help='Rotate about the origin by some angle. Rotates after offset'), ) @@ -126,9 +121,9 @@ def opt(gfile, offset=(0.0,0.0), rotate=False): getMultiFiles=True, # accept any number of files getDevice=False) - # print vars(args) - # import sys - # sys.exit() + print vars(args) + import sys + sys.exit() # optimize each file in the list for gfile in args.gcode: From dac1dbfe312f57a090776634093b9d92009ca515 Mon Sep 17 00:00:00 2001 From: Alexander Mendez Date: Tue, 13 May 2014 01:42:10 -0700 Subject: [PATCH 13/29] more rotation checks --- lib/tool.py | 2 +- optimize.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/tool.py b/lib/tool.py index 2f7f164..dad6256 100755 --- a/lib/tool.py +++ b/lib/tool.py @@ -177,7 +177,7 @@ def rotate(self, angle): # 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] + b = math.sin(rad)*item[0] + math.cos(rad)*item[1] item[0], item[1] = a,b diff --git a/optimize.py b/optimize.py index 00b304b..6967144 100755 --- a/optimize.py +++ b/optimize.py @@ -121,9 +121,9 @@ def opt(gfile, offset=(0.0,0.0), rotate=False): getMultiFiles=True, # accept any number of files getDevice=False) - print vars(args) - import sys - sys.exit() + # print vars(args) + # import sys + # sys.exit() # optimize each file in the list for gfile in args.gcode: From d44b991242f8e9f71b63a503939ee2addf3bb0b2 Mon Sep 17 00:00:00 2001 From: Alexander Mendez Date: Thu, 15 May 2014 23:32:25 -0700 Subject: [PATCH 14/29] adding some things to orient and getting stream streaming --- home.py | 6 +++--- lib/tool.py | 8 ++++++-- optimize.py | 28 +++++++++++++++++++++++----- stream.py | 8 ++++---- 4 files changed, 36 insertions(+), 14 deletions(-) diff --git a/home.py b/home.py index 4498440..ee97499 100644 --- a/home.py +++ b/home.py @@ -5,14 +5,14 @@ __DOC__ = 'Setup homing for the machine' SETUP = '''(Setting up homing) -$16=0 (homing on -- requires reset) +$16=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) ''' diff --git a/lib/tool.py b/lib/tool.py index dad6256..db84d76 100755 --- a/lib/tool.py +++ b/lib/tool.py @@ -162,13 +162,17 @@ def boundBox(self): return box def offset(self, offset): - '''offset the toolpath by some offset=x,y''' + '''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] - if i == 1: + elif i == 1: item[ax] -= offset[1] + elif i == 2: + item[ax] -= offset[2] + def rotate(self, angle): '''rotate by some angle''' diff --git a/optimize.py b/optimize.py index 6967144..da0bb3f 100755 --- a/optimize.py +++ b/optimize.py @@ -18,7 +18,7 @@ # The Optimize function -def opt(gfile, offset=(0.0,0.0), rotate=False): +def opt(gfile, offset=(0.0,0.0,0.0), rotate=False): '''Optimization core function: Reads in gCode ascii file. Processes gcode into toolpath list @@ -97,15 +97,27 @@ def opt(gfile, offset=(0.0,0.0), rotate=False): action='store_const', dest='setMillHeight', 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, - # dest='offsetx', 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=['--rotate'], + 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'), ) @@ -120,7 +132,13 @@ def opt(gfile, offset=(0.0,0.0), rotate=False): getFile=True, # get gcode to process getMultiFiles=True, # accept any number of files getDevice=False) - + + if args.zmove != 0: + Z_MOVE = args.zmove + if args.zdrill != 0: + Z_DRILL = args.zdrill + if args.zmill != 0: + Z_MILL = args.zmill # print vars(args) # import sys # sys.exit() @@ -131,7 +149,7 @@ def opt(gfile, offset=(0.0,0.0), rotate=False): # c = re.match(r'(?P\.drill\.tap)|(?P\.etch\.tap)', gfile.name) c = re.match(r'(.+)((?P\.drill\.tap)|(?P\.etch\.tap))', gfile.name) if c: # either a drill.tap or etch.tap file - opt(gfile, offset=(args.offsetx, args.offsety), rotate=args.rotate) + opt(gfile, offset=(args.offsetx, args.offsety, args.offsetz), rotate=args.rotate) print '%s finished in %s'%(args.name,deltaTime(start)) diff --git a/stream.py b/stream.py index df6ae3b..16418b5 100755 --- a/stream.py +++ b/stream.py @@ -9,8 +9,8 @@ from lib.communicate import Communicate from lib.util import deltaTime -from pysurvey import util -util.setup_stop() +# from pysurvey import util +# util.setup_stop() RX_BUFFER_SIZE = 128 @@ -37,8 +37,8 @@ # Strip comments/spaces/new line, capitalize, and add line ending l = re.sub('\s|\(.*?\)','',line.strip()).upper()+'\n' - if 'M' in l: - continue + # if 'M' in l: + # continue # if this was a comment or blank line just go to the next one From 40ae1a82f180ebdd56d7a80cfa27eb627049a205 Mon Sep 17 00:00:00 2001 From: Alexander Mendez Date: Wed, 21 May 2014 01:56:05 -0700 Subject: [PATCH 15/29] laser scan --- lib/argv.py | 12 +++-- modify.py | 149 ++++++++++++++++++++++++++++------------------------ orient.py | 72 ++++++++++++++++++++++--- 3 files changed, 154 insertions(+), 79 deletions(-) mode change 100644 => 100755 modify.py diff --git a/lib/argv.py b/lib/argv.py index 1237ea5..abd37a1 100755 --- a/lib/argv.py +++ b/lib/argv.py @@ -56,10 +56,14 @@ def arg(description=None, getDevice=True, # 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() diff --git a/modify.py b/modify.py old mode 100644 new mode 100755 index f1611df..06bce43 --- a/modify.py +++ b/modify.py @@ -7,72 +7,74 @@ 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 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): +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" ''') +] + + +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] + units = r'(?Pin|mil|mm|[NoUnitSpecified]?)' if getUnits else r'' out = [] for m in move: m = re.sub(r'\s','',m).strip() - g = re.match(r'(?P-?\d+\.?\d*)\,(?P-?\d+\.?\d*)'+units, m, re.I) - if not g: 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) @@ -97,7 +99,7 @@ def mod(gfile): 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() tool.build(gcode, addIndex=True) @@ -135,25 +137,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)) \ No newline at end of file diff --git a/orient.py b/orient.py index a3b41ea..faa60c1 100755 --- a/orient.py +++ b/orient.py @@ -83,6 +83,7 @@ def __init__(self, cameranumber=0): 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''' @@ -103,7 +104,7 @@ def getcolor(self, red=0, green=0, blue=0): def getdefaultcolor(self): '''A nice steel blue''' - return self.color(100,130,255) + 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 @@ -190,11 +191,12 @@ def setzero(self, **kwargs): '''Set the zero location of the line location.''' self.zero = self.measure(**kwargs) - def measure(self, delta=200): + 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! - cv.Not(self.frame, self.frame) + 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 @@ -203,14 +205,56 @@ def measure(self, delta=200): ex,ey,cut = findextreme(imavg, self.nsigma) try: p,x,g = fitgaussian(ex,ey,cut) - out.append(p['mean'].value) + out.append([i,p['mean'].value]) except KeyboardInterrupt as e: print 'User canceled operation' return -1.0 except Exception as e: - print 'Failed to fit: {} {}'.format(i,e) + if not quiet: + print 'Failed to fit: {} {}'.format(i,e) # raise - return np.mean(out) + 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 @@ -437,6 +481,20 @@ def scan(): +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 @@ -697,5 +755,7 @@ def capture(): test_circle() elif 'scan' in sys.argv: scan() + elif 'roll' in sys.argv: + roll() else: main() From 39e7d8b0a887774de94032d96b5a044687ed79ea Mon Sep 17 00:00:00 2001 From: Alexander Mendez Date: Thu, 31 Jul 2014 20:41:24 -0700 Subject: [PATCH 16/29] working on home --- home.py | 10 +++++++--- script/flatten.py | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) mode change 100644 => 100755 script/flatten.py diff --git a/home.py b/home.py index ee97499..06cfac9 100644 --- a/home.py +++ b/home.py @@ -5,7 +5,7 @@ __DOC__ = 'Setup homing for the machine' SETUP = '''(Setting up homing) -$16=1 (homing on -- requires reset) +$17=1 (homing on -- requires reset) $H $N0 = (setup Staring block) G28.1 (Soft abs home position) @@ -18,9 +18,13 @@ ''' 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/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 From 87c5519d8bd0d7de3e2bf9d931ac3f0e9d44c233 Mon Sep 17 00:00:00 2001 From: ben Date: Sat, 11 Apr 2015 22:01:45 -0700 Subject: [PATCH 17/29] align: use select() properly to wait for input so we don't burn CPU cycles unnecessarily --- align.py | 2 +- lib/terminal.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/align.py b/align.py index 7af3d10..c057332 100755 --- a/align.py +++ b/align.py @@ -113,7 +113,7 @@ 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 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) From 4fed51329f3c8d06d7af8c1f221ff45bfca8ec7c Mon Sep 17 00:00:00 2001 From: ben Date: Sat, 11 Apr 2015 22:25:26 -0700 Subject: [PATCH 18/29] align: use m/M key to toggle spindle since having the spindle running is useful for determining Z alignment --- align.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/align.py b/align.py index c057332..0a13c2b 100755 --- a/align.py +++ b/align.py @@ -9,6 +9,7 @@ QUIT = ['q','Q'] UPDATE =['u','U'] +MOTOR =['m','M'] UP = ['\x1b[B'] DOWN = ['\x1b[A'] RIGHT = ['\x1b[D'] @@ -29,6 +30,7 @@ Board Alignment Keys: q/Q : Quit u/U : Update moveLength -- Amount to nudge +m/M : Toggle spindle Arrow Keys : Move in X [Forward/Back] Y [Left/Right] a/A / z/Z : Move in Z [Raise/Lower] @@ -36,7 +38,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 @@ -59,6 +61,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''' @@ -118,6 +128,7 @@ def update(): terminal.wait() if c in QUIT: sys.exit() # Quit the program elif c in UPDATE: moveLength = update() + 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-') From 48320f8a7af63667f5e302320da7de1e1e34d88d Mon Sep 17 00:00:00 2001 From: ben Date: Sat, 11 Apr 2015 22:55:10 -0700 Subject: [PATCH 19/29] align: tweak move step with +/- because I'm lazy and don't want to type 'u 5 mil' --- align.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/align.py b/align.py index 0a13c2b..5e7261e 100755 --- a/align.py +++ b/align.py @@ -9,6 +9,7 @@ QUIT = ['q','Q'] UPDATE =['u','U'] +TWEAKL =['+','-'] MOTOR =['m','M'] UP = ['\x1b[B'] DOWN = ['\x1b[A'] @@ -30,6 +31,7 @@ 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] @@ -101,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 @@ -128,6 +141,7 @@ def update(): 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+') From f19aa769765654f69be00e711abc4e11f86de12e Mon Sep 17 00:00:00 2001 From: ben Date: Sat, 11 Apr 2015 22:58:06 -0700 Subject: [PATCH 20/29] align: fix length regex Because I want to be able to type '5mil' and '.005 inch'. Previously '5 mil' would be rejected because 2 digits were required, and '.005 inch' would be rejected because it wanted a leading zero. --- align.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/align.py b/align.py index 5e7261e..7ad8374 100755 --- a/align.py +++ b/align.py @@ -91,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: From 77d7182b9a81958194c30f316608eaa9b254947c Mon Sep 17 00:00:00 2001 From: Mike Date: Sun, 9 Aug 2015 23:20:24 -0700 Subject: [PATCH 21/29] initial work on new features --- command.py | 4 +- lib/argv.py | 28 ++-- lib/communicate.py | 23 ++-- lib/correction_surface.py | 100 ++++++++++++++ lib/gcode.py | 243 ++++++++++++++++++---------------- lib/gparse.py | 229 ++++++++++++++++++++++++++++++++ lib/grbl_status.py | 156 ++++++++++++++++++++++ lib/tool.py | 68 +++++----- modify.py | 53 ++++---- new_GRBL_configuration_readme | 47 +++++++ optimize.py | 38 ++---- orient.py | 212 ++++++++++++++--------------- probe_surface.py | 103 ++++++++++++++ probe_test.out | 3 + visualize.py | 7 +- zcorrect.py | 61 +++++++++ 16 files changed, 1036 insertions(+), 339 deletions(-) create mode 100644 lib/correction_surface.py create mode 100644 lib/gparse.py create mode 100644 lib/grbl_status.py create mode 100644 new_GRBL_configuration_readme create mode 100644 probe_surface.py create mode 100644 probe_test.out create mode 100644 zcorrect.py diff --git a/command.py b/command.py index 9c1b32f..602395a 100755 --- a/command.py +++ b/command.py @@ -15,7 +15,7 @@ with Communicate(args.device, args.speed, timeout=args.timeout, debug=args.debug, quiet=args.quiet) as serial: - + # now we send commands to the grbl, and wait waitTime for some response. while True: # Get some command @@ -26,7 +26,7 @@ if x in ['~']: serial.run('~\n?') # run it if is not a quit switch serial.run(x) - + except KeyboardInterrupt: puts(colored.red('Emergency Feed Hold. Enter "~" to continue')) serial.run('!\n?') diff --git a/lib/argv.py b/lib/argv.py index abd37a1..3acc9ae 100755 --- a/lib/argv.py +++ b/lib/argv.py @@ -8,14 +8,14 @@ from util import error -def arg(description=None, getDevice=True, - defaultSpeed=9600, defaultTimeout=0.70, +def arg(description=None, getDevice=True, + defaultSpeed=115200, defaultTimeout=0.70, getFile=False, getMultiFiles=False, otherOptions=None): '''This is a simple arugment parsing function for all of the command line tools''' if not description: description='python grbl arguments' - + parser = argparse.ArgumentParser(description=description) parser.add_argument('-q','--quiet', action='store_true', @@ -25,16 +25,16 @@ def arg(description=None, getDevice=True, action='store_true', default=False, help='[DEBUG] use a fake Serial port that prints to screen') - + # by default just get the device # HOWEVER if we want a file to be run, get it FIRST if getFile: nargs = '+' if getMultiFiles else 1 - parser.add_argument('gcode', + parser.add_argument('gcode', nargs=nargs, - type=argparse.FileType('r'), + type=argparse.FileType('r'), help='gCode file to be read and processed.') - + # if we want a device get an optional speed / and a device if getDevice: parser.add_argument('-s','--speed', @@ -47,13 +47,13 @@ def arg(description=None, getDevice=True, default=defaultTimeout, type=float, help='Serial Port Timeout: Amount of time to wait before gathering data for display [%.2f]'%(defaultTimeout)) - + parser.add_argument('device', nargs='?', # action='store_true', default=False, - help='GRBL serial dev. Generally this should be automatically found for you. You should specify this if it fails, or your have multiple boards attached.') - + help='GRBL serial dev. Generally this should be automatically found for you. You should specify this if it fails, or your have multiple boards attached.') + # For any specalized options lets have a general import method if otherOptions: if isinstance(otherOptions,(dict)): @@ -66,12 +66,12 @@ def arg(description=None, getDevice=True, parser.add_argument(*args, **option) args = parser.parse_args() - + # lets see if we can find a default device to connect too. if args.debug: args.device='fakeSerial' if (getFile) and (not getMultiFiles): args.gcode = args.gcode[0] if getDevice and not args.device: - # Where they generally are: + # Where they generally are: devs = ['/dev/tty.usb*','/dev/ttyACM*','/dev/tty.PL*','/dev/ttyUSB*'] founddevs = [] for d in devs: @@ -83,8 +83,8 @@ def arg(description=None, getDevice=True, else: parser.print_help() error('Found %d device(s) -- You need to connect a device, update %s, or specify wich device you want to use.'%(len(founddevs),sys.argv[0])) - - + + args.name = sys.argv[0] args.argv = sys.argv return args diff --git a/lib/communicate.py b/lib/communicate.py index 8ad8e8b..6408911 100755 --- a/lib/communicate.py +++ b/lib/communicate.py @@ -13,7 +13,7 @@ def __init__(self, device, speed, debug=False, quiet=False, timeout=None): # select the right serial device if debug: s = FakeSerial() else: s = serial.Serial(device, speed, timeout=timeout) - + if not quiet: print '''Initializing grbl at device: %s Please wait 1 second for device...'''%(device) s.write("\r\n\r\n") @@ -26,16 +26,17 @@ def __init__(self, device, speed, debug=False, quiet=False, timeout=None): # self.run('$H') # self.run('G20 (Inches)') # self.run('G90 (Absolute)') - - + + def run(self, cmd, singleLine=False): '''Extends either serial device with a nice run command that prints out the command and also gets what the device responds with.''' puts(colored.blue(' Sending: [%s]'%cmd ), newline=(not singleLine)) + out = '' # i think it is better to initialize out before sending the command self.write(cmd+'\n') - out = '' time.sleep(self.timeout) # while s.inWaiting() > 0: out += s.read(10) + #Why is it not self.s.inWaiting() ???? while self.inWaiting() > 0: out += self.readline() if out != '': if singleLine: @@ -44,27 +45,27 @@ def run(self, cmd, singleLine=False): else: puts(colored.green(''.join([' | '+o+'\n' for o in out.splitlines()]))) return out - + def sendreset(self): '''Sends crtl-x which is the reset key :: \030 24 CAN \x18 ^X (Cancel) ''' self.s.write(24) - + def reset(self): '''sends crtl-x to grbl to reset it -- clean up in the future''' self.s.write(24) def __enter__(self): return self - + def __exit__(self, type, value, traceback): #self.s.setDTR(False) #time.sleep(0.022) #self.s.setDTR(True) #self.s.close() return isinstance(value, TypeError) - + def __getattr__(self, name): '''if no command here, see if it is in serial.''' try: @@ -76,14 +77,14 @@ def __getattr__(self, name): class FakeSerial(): - '''This is a fake serial device that mimics a true serial devie and + '''This is a fake serial device that mimics a true serial devie and does fake read/write.''' def __init__(self): '''init the fake serial and print out ok''' self.waiting=1 # If we are waiting self.ichar=0 # index of the character that we are on. self.msg='ok' # the message that we print when we get any command - + def __getattr__(self, name): print 'DEBUG SERIAL: %s'%(name) return self.p @@ -115,4 +116,4 @@ def inWaiting(self): out = self.waiting if self.waiting == 0: self.waiting = 1 - return out \ No newline at end of file + return out diff --git a/lib/correction_surface.py b/lib/correction_surface.py new file mode 100644 index 0000000..6b3984d --- /dev/null +++ b/lib/correction_surface.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python + +#this code should use a 2 dimensional array of z_positions +#the element 0,0 is 0 and all other elements are relative z positions +#there should also be a probe_step_x value and a probe_step_y value +#these define the distsnces between the points measured in the z_positions array + + +#imports +from numpy import arange +from lib import argv +from lib.gparse import gparse +from lib.grbl_status import GRBL_status +from lib.communicate import Communicate +from clint.textui import puts, colored +import time, readline +import numpy as np +import re + + +class CorrectionSurface(): + def __init__(self,): + self.x_step = "nan" + self.y_step = "nan" + self.array = [] + self.Load_Correction_Surface() + + def Load_Correction_Surface(self, surface_file_name = 'probe_test.out' ): + + #initialize and load the correction surface + Surface_Data = [] + Surface_Data = np.loadtxt(surface_file_name, delimiter=',', comments = '#') + #correction_surface.array = Surface_Data + self.array = Surface_Data + + #display the dataset to be used + puts(colored.yellow('Surface Z Data Matrix')) + print Surface_Data + + probe_data_file = open(surface_file_name) + + #probe_data = probe_data_file.read() + + # retrieve the X and Y step sizes that scale the correction surface + for line in probe_data_file: + line = line.strip() + + if re.search('#', line,flags = re.IGNORECASE): + puts(colored.green('extracting scale data from file header')) + puts(colored.green( line)) + if re.search(' X_STEP:', line,flags = re.IGNORECASE): + #X_STEP:,0.5000 + X_STEP_INFO = re.findall('X_STEP:,\d*\.\d*,', line, flags = re.IGNORECASE)[0] + if X_STEP_INFO: + X_STEP = float(X_STEP_INFO.split(',')[1]) + puts(colored.yellow( 'x step size: {:.4f}'.format(X_STEP))) + #correction_surface.x_step = X_STEP + self.x_step = X_STEP + else: + puts(colored.red( 'x step size: not found!')) + if re.search(' Y_STEP:', line,flags = re.IGNORECASE): + #X_STEP:,0.5000 + Y_STEP_INFO = re.findall('Y_STEP:,\d*\.\d*,', line, flags = re.IGNORECASE)[0] + if Y_STEP_INFO: + Y_STEP = float(Y_STEP_INFO.split(',')[1]) + puts(colored.yellow( 'Y step size: {:.4f}'.format(Y_STEP))) + #correction_surface.Y_step = Y_STEP + self.y_step = Y_STEP + else: + puts(colored.red( 'Y step size: not found!')) + return self + + def estimate_surface_z_at_pozition(self,x,y): + #find the bounding triangle on the correction surface closest to the x,y coordinet + #begin with using the nearest node to the point + Ns_x = round(x/self.x_step) + Ns_y = round(y/self.y_step) + #calculate distance from the nearest node + epsilon_x= x - Ns_x*self.x_step + epsilon_y= y - Ns_y*self.x_step + #find the next two nearest nodes + if (x > Ns_x*self.x_step): + Nf_x = Ns_x + 1 + delta_x = self.x_step + else: + Nf_x = Ns_x + -1 + delta_x = -1.0*self.x_step + if (y > Ns_y*self.x_step): + Nf_y = Ns_y + 1 + delta_y = self.y_step + else: + Nf_y = Ns_y + -1 + delta_y = -1.0*self.y_step + #calculate the slopes (x and y) of the plane defined by the triangle of bounding nodes + slope_x = (self.array[Nf_x][Ns_y] - self.array[Ns_x][Ns_y])/delta_x + slope_y = (self.array[Ns_x][Nf_y] - self.array[Ns_x][Ns_y])/delta_y + #use the slope information and the origin intercept at the clsest node + # to estimate the z position of the surface + z_at_position = self.array[Ns_x][Ns_y] + epsilon_x*slope_x + epsilon_y*slope_y + return z_at_position diff --git a/lib/gcode.py b/lib/gcode.py index d212c28..f39fccf 100755 --- a/lib/gcode.py +++ b/lib/gcode.py @@ -7,7 +7,7 @@ from pprint import pprint from clint.textui import colored, puts, indent, progress -CMDS='GXYZMPIJ' +CMDS='GXYZFEMPIJK' TEMPLATE='''(UPDATED by ${tag}) (Starting) @@ -33,119 +33,128 @@ class GCode(list): - def __init__(self, gcode, limit=None): - '''start with a gcode ascii file''' - self.limit = limit - if isinstance(gcode, str): - with open(os.path.expanduser(gcode),'r') as f: - lines = f.readlines() - filename = gcode - else: - filename = gcode.name - lines = gcode.readlines() - gcode.close() - self.filename = filename - self.lines = lines - # self.ready = False - - # def append(self,item): - # '''add the next nice to the object''' - # if self.ready : self.ready = False - # super(GCode, self).append(item) - - def parse(self): - '''By default .parse() grabs only the G## commands for creating toolpaths in some space - if you need everything use .parseall()''' - everything = self._parse() - for item in everything: - toappend = False - for cmd in CMDS: - if cmd in item: - toappend=True - if toappend: - self.append(item) - - def parseAll(self): - '''Gets everything so that we can print it back out''' - everything = self._parse() - for item in everything: - self.append(item) - - def _parse(self): - ''' [INTERNAL] convert the readlines into a parsed set of commands and values''' - puts(colored.blue('Parsing gCode')) - - comment = r'\(.*?\)' - whitespace = r'\s' - command = r''.join([r'(?P<%s>%s(?P<%snum>-?\d+(?P<%sdecimal>\.?)\d*))?'%(c,c,c,c) for c in CMDS]) - output = [] - for i,line in enumerate(progress.bar(self.lines)): - if self.limit is not None and i > self.limit: break - # for i,line in enumerate(self.lines): - l = line.strip() - # find comments, save them, and then remove them - m = re.findall(comment,l) - l = re.sub(whitespace+'|'+comment,'',l).strip().upper() - # l = re.sub(whitespace,'',l).upper() - - # Grab the commands - c = re.match(command,l) - - # output commands to a nice dict - out = {} - out['index'] = i - # out['line'] = line - if m: out['comment'] = m - for cmd in CMDS: - if c.group(cmd): - # either a float if '.' or a int - fcn = float if c.group(cmd+'decimal') else int - out[cmd] = fcn(c.group(cmd+'num')) - out['index'] = i - if len(out) > 0: - output.append(out) - return output - # - # if len(out) > 0: - # self.append(out) - - def update(self,tool): - '''Updates the gcode with a toolpath only does x,y''' - UPDATE = 'xy' - for x in tool: - # print len(x) - if len(x) == 5: - # print x - for u in UPDATE: - if u.upper() in self[x[4]]: - self[x[4]][u.upper()] = x[UPDATE.index(u)] - # print self[x[4]] - - # print self[x[4]], - # print u.upper(), - # print self[x[4]][u.upper()] - # print self[x[4]][u.toupper()]#, x[UPDATE.index(u)] - # print self[x[4]][u],x[UPDATE.index(u)] - - - def copy(self): - return deepcopy(self) - - def getGcode(self, tag=__name__, start=None): - lines = [] - for i,line in enumerate(self): - l = '' - for cmd in CMDS: - if cmd in line: - l += (' %s%.3f' if cmd in 'XYZ' else '%s%02i ')%(cmd,line[cmd]) - # add in the comments - if 'comment' in line: l += ' '.join(line['comment']) - lines.append(l) - params = dict(gcode='\n'.join(lines), - init=INIT, - finish=FINISH, - tag=tag) - params['startpos'] = ' G00 X%.3f Y%.3f'%(start[0],start[1]) if start else '' - return Template(TEMPLATE).substitute(params) - - \ No newline at end of file + def __init__(self, gcode, limit=None): + '''start with a gcode ascii file''' + self.limit = limit + if isinstance(gcode, str): + with open(os.path.expanduser(gcode),'r') as f: + lines = f.readlines() + filename = gcode + else: + filename = gcode.name + lines = gcode.readlines() + gcode.close() + self.filename = filename + self.lines = lines + # self.ready = False + + # def append(self,item): + # '''add the next nice to the object''' + # if self.ready : self.ready = False + # super(GCode, self).append(item) + + def parse(self): + '''WARNING: By default .parse() grabs only the G## commands for creating toolpaths in some space + if you need everything use .parseall()''' + everything = self._parse() + for item in everything: + toappend = False + for cmd in CMDS: + if cmd in item: + toappend=True + if toappend: + self.append(item) + + def parseAll(self): + '''Gets everything so that we can print it back out''' + everything = self._parse() + for item in everything: + self.append(item) + + def _parse(self): + ''' [INTERNAL] convert the readlines into a parsed set of commands and values''' + puts(colored.blue('Parsing gCode')) + + comment = r'\(.*?\)' + whitespace = r'\s' + + #command = r''.join([r'(?P<%s>%s(?P<%snum>-?\d+(?P<%sdecimal>\.?)\d*))?'%(c,c,c,c) for c in CMDS]) + ''' + The result of the above commented line is: + that command is a new regular expression which prints in the followin way in ipython: + note the doubles of \ probably have to do with the output + '(?PG(?P-?\\d+(?P\\.?)\\d*))?(?PX(?P-?\\d+(?P\\.?)\\d*))?(?PY(?P-?\\d+(?P\\.?)\\d*))?(?PZ(?P-?\\d+(?P\\.?)\\d*))?(?PM(?P-?\\d+(?P\\.?)\\d*))?(?P

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

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