A computerized numerical controller or CNC machine is the backbone of a modern day machine shop. A CNC is a computer controlled machine that can programmed to produce a specific part. These are useful in manufacturing when a large copies of a part are needed or when a part contains geometries that are impossible to make with a manual machine.
These machines can range in price from $10k to over $1 million. One of the disparities between a “low end” CNC and a “top of the line” CNC is the accuracy at which it can make different parts. In order to insure the greatest accuracy and precision in part manufacturing, modern CNC’s rely on dynamic feedback from a variety of sensors.
The goal of this project is to develop a low-cost supplemental add on device to a CNC system to provide similar feedback control like a more expensive machine. This would allows smaller shops to produce higher quality parts, while avoiding the significant expense of upgrading machinery. The final result of this project would be to create a sub $100 easy to use device that can easily be integrated into the machine shop environment.
In order to accomplish our goal of improving performance of “low end” CNC machines, we first had to quantify what we considered good performance. For the scope of this project, we determined that we consider high levels of chatter to be bad performance. Chatter occurs when the machine hits a self-excited level of vibration while machining a part. This can be detrimental to the surface finish of a part, tool life, and the operator's eardrums. There are many ways to reduce chatter, such as using a stiffer setup and better tools.
However, these are parameters that our device cannot control so we sought out other ways to reduce chatter. Other methods of reducing chatter are modifying the feeds and speeds on the machine. The feedrate is defined by how fast the machine will move over the part. The spindle speed is how fast the tool is spinning. Both of these parameters can be set in software and presented an opening that we sought to exploit to reduce chatter.
We used an Okuma Cadet V OSP5020m for testing. This machine was available to use for Blue Apron Machinists in the Emerson Machine Shop in Rhodes Hall. Since one of the group members had extensive experience working with this machine, we were able to get access to experiment on the machine without any hiccups.
We also used a standard USB microphone in order to record noise from the mill for chatter detection. The USB microphone was capable of recording frequencies as high as 44.1KHz, the maximum frequency audible by human ears. This was far greater than the application requirement, since chatter frequencies almost always occur below 2KHz.
Lastly, we used two serial cables, two Serial-to-USB adapters and one null modem. This was required because the pi intercepted the serial communication line between the computer and the mill by connecting to the computer and the mill over serial lines and then forwarding messages between the two systems. The Serial-to-USB adapters were used connect the pi to each of the two serial cables - one connecting to the computer and the other to the mill. The null modem was required between the computer and the pi because they both act as serial hosts; the null modem was capable of crossing the Tx and Rx lines and all other host-device line pairs so the computer and the pi would each act as devices to the other. The configuration of the communication system can be seen below:
Setting up the USB microphone first consisted of installing the proper libraries to interface with the USB microphone. First, the ALSA sound API was installed to allow python programs to more easily interface with sound cards. Then, pyaudio and portaudio (a dependency for pyaudio) were installed; pyaudio was then used to to read from the microphone using python. Additionally, python’s wave module was used to write this audio data to .wav files.
To test the USB microphone’s functionality, simple python scripts were written to use pyaudio to record audio and then write the data to .wav files. These scripts were successfully tested by recording arbitrary sounds.
Then, the audio recording was easily transformed into a spectrogram using the specgram method in pylab. A sample spectrogram can be found below:
Our first milestone was determining how to communicate with the CNC via RS-232. The software aspect of this simply consisted of using the pySerial library to write python scripts that could easily read and write from serial ports. For initial testing, we used a laptop to read the initial file request send from the CNC mill over serial. A Diablo High Speed USB to DB0 Serial Adapter (not in the parts list since this was only for testing) was connected to the laptop’s USB port, and then to the CNC mill via a serial cable. However, we were unable to read meaningful information over the serial port because the USB-serial adapter did not have the hardware required to implement any flow serial flow control, which was required for proper communication with the CNC.
After the initial failed attempts, new USB-serial adapters with the FTDI chips required for serial flow control were acquired and used for testing. Even with the new FTDI chips, testing was required to determine the proper serial parameters used by the CNC mill. Further testing yielded in the following parameters:
Once the parameters were acquired, it was possible to connect the pi as shown in FIGURE777 and proceed with testing. Then, using the We were able to send a program request from the CNC, capture that transmission on the pi, and then forward that transmission to the CNC computer. The CNC computer then proceeded to dripfeed the GCode program via serial through our pi, which passed it to the CNC. The CNC was actually able to receive the program and run properly. (Our hands were over the emergency stop on the machine the entire time this was happening.)
The CNC machine posed a major problem while we were testing. An important part of our process was mapping noise levels received to a specific instruction. However, due to the way the CNC operates, there is no real way to know exactly which instruction is being executed at any point in time. This happens because the CNC machine buffers instructions so when the Pi would send an instruction, it doesn’t mean that the CNC is currently executing that instruction. This buffering helps the machine operate by making the transitions between instructions much smoother, but hurts the scope of our project.
In order to get around this problem, we calculated the time it would take for each instruction to execute and attempted to map the time at which we experienced high noise to what instruction the Pi thought it was executing at that time. These timing calculations were embedded into the GCode parser.
An important part of our project was generating a visual from the provided gcode. This allows the machinist to visualize areas that are prone to high noise. This also allows the visualization of how the noise profile changes as the part is machined.
In order to create a plot of the part being machined, the GCode parser was leveraged as well as the MatPlotLib library for python. We were able to take the parameters maintained by the parser and use them to create 3D plots of the parts being machined. This was not as simple as it sounds because these GCode programs are so long that we would be plotting 40,000+ lines per program. This was an intense memory and CPU overhead for the Pi. This was something that required a lot of testing and tuning, because there are many different ways this library works and each of them provide different performance. We were able to eventually fine tune our approach and achieve 100x speedup in our plotting compared to when we started. Microphone and FFT Recompiling
The parser was tested with various GCode programs provided by the Cornell Baja SAE and Resistance Racing: Cornell Electric Motorcycle project teams. These programs were considered good candidates because they were quite long (~50,000 lines) and were all parts that we had access to the CADs of to check our visualization against.
The CNC machine posed a major problem while we were testing. An important part of our process was mapping noise levels received to a specific instruction. However, due to the way the CNC operates, there is no real way to know exactly which instruction is being executed at any point in time. This happens because the CNC machine buffers instructions so when the Pi would send an instruction, it doesn’t mean that the CNC is currently executing that instruction. This buffering helps the machine operate by making the transitions between instructions much smoother, but hurts the scope of our project.
In order to get around this problem, we calculated the time it would take for each instruction to execute and attempted to map the time at which we experienced high noise to what instruction the Pi thought it was executing at that time.
Our final system that we designed was able to accomplish all of the milestones that we sought out to achieve. We built a fully functional GCode Parser, inserted the Raspberry Pi into the communication loop, were able to plot GCode programs, and use a microphone and fft for noise detection. All of our subsystems for the project worked properly.
However, there was a major design flaw that were not able to overcome: The inability to determine which instruction the CNC machine is executing. This flaw prevented us from confidently recompiling the program to modulate the feeds and speeds to reduce chatter.
This project was an experience. We were able to accomplish all of our subsystems, but were held up with unexpected difficulty with the CNC machine itself. We were able to communicate with the CNC without using a licensed Direct Numerical Control software package. In essence, we removed the CNC computer from the equation entirely which is pretty cool. So in conclusion, we are very happy with what we were able to accomplish with this project and how it turned out.
In the future it would be useful to take this system and figure out a good way to recompiling the GCode on the fly. After talking with some of the Emerson Machine Shop Staff, there are some very hacky avenues we can attempt using the CNC and how it operates. We also believe that with more testing, we can finetune our timing estimation and add in sync points to the program to help it keep track of what code is executing. Another avenue would be to have the Pi interact with the manual overrides on the machine and modulate the feeds and speeds that way. We discouraged this approach because it would not allow the user to recompile the program with better feeds and speeds.
Another avenue we wish to explore would be reatime plotting as instructions are being executed. This would be a very cool feature for an operator to have so they can know how far along a part is in case they cannot view the part.
The Pi is also a very good platform for creating a DNC computer. It is small, cheap, and does everything you would want that computer to be doing, plus has the ability to be easily be expanded upon. Our current project removes the existing computer from the Emerson machine shop and with a little touching up on the UI, can be just as configurable.
We would also like to develop a waterproof/shockproof case for the Pi to make sure it stays safe when it is in the machine shop environment.
Jay worked on the Gcode-Parsing and dealing with the CNC. Also, started the fft code. Worked on the Website and the report.
Ricardo worked on the microphone and the serial communication. Also, worked on integrating everything as well as the Website and report.
Part |
Source |
Quanity | Cost |
---|---|---|---|
Raspberry Pi |
http://www.mcmelectronics.com/product/83-16530 | 1 | $35 |
Cable | http://www.amazon.com/InstallerParts-Male-Female-Serial-Cable/dp/B008NCC85C | 2 | $5 |
Null Modem |
http://www.amazon.com/StarTech-RS232-Serial-Modem-Adapter/dp/B000DZH4V0 | 1 | $3 |
Serial-USB Adapter | http://www.amazon.com/Chipset-Speed-Serial-RS-232-Converter/dp/B005S72HHO | 2 | $14 |
Microphone | http://www.amazon.com/Adjustable-Microphone-Compatible-Chatting-Recording/dp/B00UZY2YQE | 1 | $8 |
Total Cost |
$84 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | #!/usr/local/bin/python3 # http://stackoverflow.com/questions/10733903/pyaudio-input-overflowed import pyaudio import wave import time CHUNK = 2**12 FORMAT=pyaudio.paInt16 CHANNELS = 1 RATE = 44100 RECORD_SECONDS = 5 TIMESTAMP = str(int(time.time())) WAVE_OUTPUT_FILENAME = "audio/async"+TIMESTAMP+".wav" wf = wave.open(WAVE_OUTPUT_FILENAME, 'wb') wf.setnchannels(CHANNELS) wf.setsampwidth(2) wf.setframerate(RATE) end_signal = 0 def callback(in_data, frame_count, time_info, status): global end_signal print("Callback") wf.writeframes(in_data) if(end_signal == 1): wf.close() exit() else: return(in_data, pyaudio.paContinue) p=pyaudio.PyAudio() inStream = p.open(format = FORMAT, channels=CHANNELS, rate=RATE, input=True, output=True, frames_per_buffer=CHUNK, stream_callback=callback) while True: try: continue except KeyboardInterrupt: end_signal = 1 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | import pyaudio import wave import time CHUNK = 2000 FORMAT = pyaudio.paInt16 CHANNELS = 1 RATE = 2000 RECORD_SECONDS = 5 TIMESTAMP = str(int(time.time())) WAVE_OUTPUT_FILENAME = "audio/output"+TIMESTAMP+".wav" p = pyaudio.PyAudio() stream = p.open(format=FORMAT, channels=CHANNELS, rate=RATE, input=True, frames_per_buffer=CHUNK) print("* recording") frames = [] for i in range(0, int(RATE / CHUNK * RECORD_SECONDS)): data = stream.read(CHUNK) frames.append(data) print("* done recording") stream.stop_stream() stream.close() p.terminate() wf = wave.open(WAVE_OUTPUT_FILENAME, 'wb') wf.setnchannels(CHANNELS) wf.setsampwidth(p.get_sample_size(FORMAT)) wf.setframerate(RATE) wf.writeframes(b''.join(frames)) wf.close() |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | #http://glowingpython.blogspot.ro/2011/08/how-to-plot-frequency-spectrum-with.html from pylab import plot, show, title, xlabel, ylabel, subplot, savefig from scipy import fft, arange, ifft from numpy import sin, linspace, pi from scipy.io.wavfile import read,write def plotSpectru(y,Fs): n = len(y) # lungime semnal k = arange(n) T = n/Fs frq = k/T # two sides frequency range frq = frq[range(n/2)] # one side frequency range Y = fft(y)/n # fft computing and normalization Y = Y[range(n/2)] plot(frq,abs(Y),'r') # plotting the spectrum xlabel('Freq (Hz)') ylabel('|Y(freq)|') Fs,data=read('audio/test2.wav') y=data[:,1] lungime=len(y) timp=len(y)/Fs t=linspace(0,timp,len(y)) subplot(2,1,1) plot(t,y) xlabel('Time') ylabel('Amplitude') subplot(2,1,2) plotSpectru(y,Fs) show() |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 | #!/usr/bin/python # This is the main gcode parser for the project. # Rev. 1.0 will take in static Gcode files and parse them # Rev. 2.0 will take in the Gcode dynamically via Serial # import sys, fileinput, math, re # -*- coding: utf-8 -*- import numpy as np import matplotlib import matplotlib.pyplot as plt import matplotlib.patches as mpatches import mpl_toolkits.mplot3d.axes3d as p3 import matplotlib.animation as animation from matplotlib import cm, colors, patches prevX = float(0.0) prevY = float(0.0) prevZ = float(0.0) globalX = float(0.0) globalY = float(0.0) globalZ = float(0.0) globalI = float(0.0) globalJ = float(0.0) feedRate = 0 spindleSpeed = 0 toolNum = 19 toolSize = 0 rapid = 100 toolChangeTime = 5/60 def parseM(line, index): index += 1 return index def parseX(line, index): global globalX if(not line[index][0] == 'X'): exit(1) prevX = globalX globalX = float(line[index][1:]) #print("Parsing X: %f\n") % globalX index += 1 return index def parseY(line, index): global globalY if(not line[index][0] == 'Y'): exit(1) prevY = globalY globalY = float(line[index][1:]) #print("Parsing Y: %f\n") % globalY index += 1 return index def parseZ(line, index): global globalZ if(not line[index][0] == 'Z'): exit(1) prevZ = globalZ globalZ = float(line[index][1:]) #print("Parsing Z: %f\n") % globalZ index += 1 return index def parseI(line, index): global globalI if(not line[index][0] == 'I'): exit(1) globalI = float(line[index][1:]) #print("Parsing Z: %f\n") % globalZ index += 1 return index def parseJ(line, index): global globalJ if(not line[index][0] == 'J'): exit(1) globalJ = float(line[index][1:]) #print("Parsing Z: %f\n") % globalZ index += 1 return index def parseG0(line, index): while(index < len(line) and not (line[index] in validCmds)): index = validDirections[line[index][0]](line, index) return index def parseG1(line, index): while(index < len(line) and not (line[index][0] in validCmds)): index = validDirections[line[index][0]](line, index) return index #Circular Move CCW def parseG2(line, index): while(index < len(line) and not (line[index][0] in validCmds)): index = validDirections[line[index][0]](line, index) return index #Circular Move CW: IJ=centerpoint, XY = endpoints def parseG3(line, index): while(index < len(line) and not (line[index][0] in validCmds)): index = validDirections[line[index][0]](line, index) return index def parseG(line, index): global movementType #print("Found a valid G cmd: %s. Index: %d\n") % (line, index) if(not line[index][0] == 'G'): exit(1) index += 1 if(line[index-1] in validGCmds): movementType = line[index-1] index = validGCmds[line[index-1]](line, index) return index def parseT(line, index): global toolNum if(not line[index][0] == 'T'): exit(1) if(len(line[index]) == 1): toolNum = int(line[index+1]) index += 2 else: toolNum = int(line[index][1:]) index += 1 return index #assume its in the format for now: S##### def parseSpindle(line, index): global spindleSpeed #print("Found a valid spindle: Here is the line: %s\n") % line #If this happens, throw an error if(not line[index][0] == 'S'): exit(1) if(len(line[index]) == 1): spindleSpeed = int(line[index+1]) index += 2 else: spindleSpeed = int(line[index][1:]) index += 1 #print("Here is the new spindleSpeed: %s\n") % spindleSpeed return index def parseFeed(line, index): global feedRate #print("Found a valid feedRate: Here is the line: %s\n") % line if(not line[index][0] == 'F'): exit(1) if(len(line[index]) == 1): feedRate = float(line[index+1]) index += 2 else: feedRate = float(line[index][1:]) index += 1 return index def parseComment(line, index): #print("Found a comment: %s\n") % line[index:] i = index while ( i < len(line)): i += 1 if ')' in line[i-1]: #print("Found end of the comment: %s\n") % line[i-1] return i validCmds = {'G':parseG, 'M':parseM, 'T':parseT, 'S':parseSpindle, 'F':parseFeed, '(': parseComment} validDirections = {'X': parseX, 'Y':parseY, 'Z': parseZ, 'I': parseI, 'J':parseJ} validGCmds = { 'G00':parseG0, 'G0': parseG0, 'G01':parseG1, 'G1':parseG1, 'G02':parseG2, 'G2':parseG2, 'G03':parseG3, 'G3':parseG3} ''' validMCmds = { 'M03':parseM3, 'M3': parseM3, 'M05':parseM5, 'M5':parseM5, 'M06':parseM6, 'M6':parseM6, 'M08':parseM8, 'M8':parseM8} ''' #Movement Type: #Sticks with whatever the last movement type was #So if the last command to specify movement was g1 #Then an X____Y____ will really mean: G1 X______Y_____ movementType = '' def parseLine(line): i = 0 while( i < len(line)): #print("This is the index Value: %d\n") % i a = str(line[i][0]).split()[0] #print("a: %s\n") % a if( a in validCmds): #print("Is a valid cmd: %s\n") % a i = validCmds[a](line,i) elif( a[0] in validDirections and re.match(r"^([-]?\d+\.?\d+)$",line[i][1:]) is not None): i = validGCmds[movementType](line,i) else: i += 1 plot_scale = 1 def calc_line(x1, y1, x2, y2): if(x2 - x1 == 0): slope = float("inf") else: slope = (y2-y1)/(x2-x1) intercept = y2 - slope*x2 return (slope, intercept) #returns the distance between point 1 and point 2 def calcDistance(x1, y1, z1, x2, y2, z2): global plot_scale return math.sqrt((x2-x1)**2 + (y2-y1)**2 + (z2-z1)**2) def calcTime(distance, feedRate): if(feedRate == 0): return 0 return distance/feedRate def point_under(point, line): slope, intercept = line if(slope != float("inf")): return point[0]*slope + intercept > point[1] else: return point[0] < intercept def point_over(point, line): slope, intercept = line if(slope != float("inf")): return point[0]*slope + intercept < point[1] else: return point[0] > intercept def law_of_cosines(A_X, A_Y, B_X, B_Y, C_X, C_Y): #CNC doesn't do 3D arcs a = calcDistance(B_X, B_Y, 0, C_X, C_Y, 0) b = calcDistance(A_X, A_Y, 0, C_X, C_Y, 0) c = calcDistance(A_X, A_Y, 0, B_X, B_Y, 0) angle = math.acos((b*b+ b*b - a*a)/(2*b*b)) if(point_over([B_X, B_Y], calc_line(C_X, C_Y, A_X, A_Y))): angle = 2*math.pi-angle return (angle,angle*b) def drawArc(): global globalX, globalY, globalZ, globalI, globalJ, prevX, prevY center = [prevX + globalI, prevY+globalJ] radius = calcDistance(globalX, globalY, globalZ, center[0], center[1], prevZ) (angle, length) = law_of_cosines(center[0], center[1], globalX, globalY, prevX, prevY) #Slope is just really globalJ/globalY return (center, radius, angle, length) def parseFile(fileName): with open(fileName) as file: global plot_scale global globalX, globalY, globalZ, globalI, globalJ, feedRate, spindleSpeed, toolNum, toolSize, rapid global prevX, prevY, prevZ total_time = 0 lineNum = 0 text = '' count = 0 add = '' prevTool = 19 first_time = 1 arcs = [] lines = [] fig = plt.figure() ax = p3.Axes3D(fig) for line in file: lineNum += 1 gcode = line.upper().split() parseLine(gcode) time = 0 local_feed = feedRate distance = 0 if(movementType == 'G02' or movementType == 'G2'): center, radius, angle, length = drawArc() distance = length arcs.append(((globalX,globalY,globalZ),(prevX,prevY,prevZ),radius)) elif(movementType == 'G03' or movementType == 'G3'): center, radius, angle, length = drawArc() distance = length arcs.append(((prevX,prevY,prevZ),(globalX, globalY, globalZ), radius)) elif(movementType == 'G00' or movementType == 'G0'): distance = calcDistance(globalX, globalY, globalZ, prevX, prevY, prevZ) lines.append(((prevX, prevY, prevZ), (globalX, globalY, globalZ))) local_feed = rapid line1 = [(prevX, prevY), (globalX, globalY)] (line1_xs, line1_ys) = zip(*line1) if(first_time == 0): ax.plot((prevX, globalX), (prevY, globalY), (prevZ, globalZ)) else: first_time -= 1 elif(movementType == 'G01' or movementType == 'G1'): distance = calcDistance(globalX, globalY, globalZ, prevX, prevY, prevZ) lines.append(((prevX, prevY, prevZ), (globalX, globalY, globalZ))) line1 = [(prevX, prevY), (globalX, globalY)] (line1_xs, line1_ys) = zip(*line1) if(first_time == 0): ax.plot((prevX, globalX), (prevY, globalY), (prevZ, globalZ)) else: first_time -= 1 if(prevTool != toolNum): total_time += toolChangeTime prevTool = toolNum time = calcTime(distance, local_feed) total_time += time prevX = globalX prevY = globalY prevZ = globalZ ax.set_xlabel('X') ax.set_ylabel('Y') ax.set_zlabel('Z') ax.set_title('3D Test') plt.show() parseFile(str(sys.argv[1])) |
Jay Fetter
jdf258@cornell.edu
Ricardo Stephen
rls454@cornell.edu