Commit 113d8b1d authored by Chetan Sharma's avatar Chetan Sharma
Browse files

Polishing up for push

parent fa2497ae
...@@ -2,4 +2,28 @@ ...@@ -2,4 +2,28 @@
A CNC controller that learns material characteristics and optimizes its own feeds and speeds. A CNC controller that learns material characteristics and optimizes its own feeds and speeds.
# Project Status # Project Status
This project is currently unpublished and is still undergoing planning. ## Current Goals
\ No newline at end of file The current goal of this project is to make a system that faces a material using an endmill while simultaneously performing regression on sensor data to complete its model. This model is used to optimize subsequent passes (feedrate and WOC) by means of an objective function that weighs MMR against the chance of failure (deflection, breakage, spindle overload).
## Modeling
Models for forces experienced during the cutting process and models for tool / machine failure are in [software/models.py](software/models.py).
## Hardware
The hardware setup is finished. The machine is a Taig Micro Mill (kindly donated by Ted Hall).
![](assets/machine.jpg)
The spindle motor is an MDX servomotor from Applied Motion products.
![](assets/spindle.jpg)
A 1D tool-force dynamometer was constructed using a Schneeberger frictionless slide and a disc-type preloaded load cell.
![](assets/tfd_1.jpg)
The machine electronics are enclosed for safety.
![](assets/cabinet.jpg)
## Software
The machine controller is in the [software](software/) folder.
\ No newline at end of file
name: ampd
channels:
- conda-forge
- defaults
dependencies:
- _libgcc_mutex=0.1=conda_forge
- _openmp_mutex=4.5=1_llvm
- attrs=19.3.0=py_0
- backcall=0.1.0=py_0
- bleach=3.1.4=pyh9f0ad1d_0
- bzip2=1.0.8=h516909a_2
- ca-certificates=2020.1.1=0
- cairo=1.16.0=hcf35c78_1003
- certifi=2020.4.5.1=py38_0
- cffi=1.14.0=py38hd463f26_0
- chardet=3.0.4=py38h32f6830_1006
- cryptography=2.8=py38h766eaa4_2
- dbus=1.13.6=he372182_0
- decorator=4.4.2=py_0
- defusedxml=0.6.0=py_0
- entrypoints=0.3=py38h32f6830_1001
- expat=2.2.9=he1b5a44_2
- ffmpeg=4.1.3=h167e202_0
- fontconfig=2.13.1=h86ecdb6_1001
- freetype=2.10.1=he06d7ca_0
- gettext=0.19.8.1=hc5be6a0_1002
- giflib=5.2.1=h516909a_2
- glib=2.58.3=py38h73cb85d_1003
- gmp=6.2.0=he1b5a44_2
- gnutls=3.6.5=hd3a4fd2_1002
- graphite2=1.3.13=he1b5a44_1001
- gst-plugins-base=1.14.5=h0935bb2_2
- gstreamer=1.14.5=h36ae1b5_2
- harfbuzz=2.4.0=h9f30f68_3
- hdf5=1.10.5=nompi_h3c11f04_1104
- icu=64.2=he1b5a44_1
- idna=2.9=py_1
- importlib-metadata=1.6.0=py38h32f6830_0
- importlib_metadata=1.6.0=0
- ipykernel=5.2.0=py38h23f93f0_1
- ipython=7.13.0=py38h32f6830_2
- ipython_genutils=0.2.0=py_1
- jasper=1.900.1=h07fcdf6_1006
- jedi=0.16.0=py38h32f6830_1
- jinja2=2.11.1=py_0
- jpeg=9c=h14c3975_1001
- json5=0.9.0=py_0
- jsonschema=3.2.0=py38h32f6830_1
- jupyter_client=6.1.2=py_0
- jupyter_core=4.6.3=py38h32f6830_1
- jupyterlab=2.1.0=py_0
- jupyterlab_server=1.1.0=py_1
- lame=3.100=h14c3975_1001
- ld_impl_linux-64=2.34=h53a641e_0
- libblas=3.8.0=16_openblas
- libcblas=3.8.0=16_openblas
- libclang=9.0.1=default_hde54327_0
- libffi=3.2.1=he1b5a44_1007
- libgcc-ng=9.2.0=h24d8f2e_2
- libgfortran-ng=7.3.0=hdf63c60_5
- libiconv=1.15=h516909a_1006
- liblapack=3.8.0=16_openblas
- liblapacke=3.8.0=16_openblas
- libllvm9=9.0.1=hc9558a2_0
- libopenblas=0.3.9=h5ec1e0e_0
- libopencv=4.2.0=py38_3
- libpng=1.6.37=hed695b0_1
- libsodium=1.0.17=h516909a_0
- libstdcxx-ng=9.2.0=hdf63c60_2
- libtiff=4.1.0=hc3755c2_3
- libuuid=2.32.1=h14c3975_1000
- libwebp=1.0.2=h56121f0_5
- libxcb=1.13=h14c3975_1002
- libxkbcommon=0.10.0=he1b5a44_0
- libxml2=2.9.10=hee79883_0
- llvm-openmp=9.0.1=hc9558a2_2
- lz4-c=1.8.3=he1b5a44_1001
- markupsafe=1.1.1=py38h1e0a361_1
- mistune=0.8.4=py38h516909a_1000
- nbconvert=5.6.1=py38_0
- nbformat=5.0.4=py_0
- ncurses=6.1=hf484d3e_1002
- nettle=3.4.1=h1bed415_1002
- nodejs=10.13.0=he6710b0_0
- notebook=6.0.3=py38_0
- nspr=4.25=he1b5a44_0
- nss=3.47=he751ad9_0
- numpy=1.18.1=py38h8854b6b_1
- opencv=4.2.0=py38_3
- openh264=1.8.0=hdbcaa40_1000
- openssl=1.1.1f=h7b6447c_0
- pandoc=2.9.2=0
- pandocfilters=1.4.2=py_1
- parso=0.6.2=py_0
- pcre=8.44=he1b5a44_0
- pexpect=4.8.0=py38h32f6830_1
- pickleshare=0.7.5=py38h32f6830_1001
- pip=20.0.2=py_2
- pixman=0.38.0=h516909a_1003
- prometheus_client=0.7.1=py_0
- prompt-toolkit=3.0.5=py_0
- pthread-stubs=0.4=h14c3975_1001
- ptyprocess=0.6.0=py_1001
- py-opencv=4.2.0=py38h23f93f0_3
- pycparser=2.20=py_0
- pygments=2.6.1=py_0
- pyopenssl=19.1.0=py_1
- pyrsistent=0.16.0=py38h1e0a361_0
- pyserial=3.4=py38_0
- pysocks=1.7.1=py38h32f6830_1
- python=3.8.2=h8356626_5_cpython
- python-dateutil=2.8.1=py_0
- python_abi=3.8=1_cp38
- pyzmq=19.0.0=py38ha71036d_1
- qt=5.12.5=hd8c4c69_1
- readline=8.0=hf8c457e_0
- requests=2.23.0=pyh8c360ce_2
- send2trash=1.5.0=py_0
- setuptools=46.1.3=py38h32f6830_0
- six=1.14.0=py_1
- sqlite=3.30.1=hcee41ef_0
- terminado=0.8.3=py38h32f6830_1
- testpath=0.4.4=py_0
- tk=8.6.10=hed695b0_0
- tornado=6.0.4=py38h1e0a361_1
- traitlets=4.3.3=py38h32f6830_1
- urllib3=1.25.7=py38h32f6830_1
- wcwidth=0.1.9=pyh9f0ad1d_0
- webencodings=0.5.1=py_1
- wheel=0.34.2=py_1
- x264=1!152.20180806=h14c3975_0
- xorg-kbproto=1.0.7=h14c3975_1002
- xorg-libice=1.0.10=h516909a_0
- xorg-libsm=1.2.3=h84519dc_1000
- xorg-libx11=1.6.9=h516909a_0
- xorg-libxau=1.0.9=h14c3975_0
- xorg-libxdmcp=1.1.3=h516909a_0
- xorg-libxext=1.3.4=h516909a_0
- xorg-libxrender=0.9.10=h516909a_1002
- xorg-renderproto=0.11.1=h14c3975_1002
- xorg-xextproto=7.3.0=h14c3975_1002
- xorg-xproto=7.0.31=h14c3975_1007
- xz=5.2.4=h516909a_1002
- zeromq=4.3.2=he1b5a44_2
- zipp=3.1.0=py_0
- zlib=1.2.11=h516909a_1006
- zstd=1.4.4=h3b9ef0a_2
- pip:
- crccheck==0.6
- cycler==0.10.0
- ipympl==0.5.6
- ipywidgets==7.5.1
- kiwisolver==1.2.0
- matplotlib==3.2.1
- pyparsing==2.4.7
- widgetsnbextension==3.5.1
prefix: /home/cactode/anaconda3/envs/ampd
#include <HX711.h>
#define LOADCELL_DOUT_PIN 4
#define LOADCELL_SCK_PIN 5
HX711 loadcell;
void setup() {
Serial.begin(115200);
loadcell.begin(LOADCELL_DOUT_PIN, LOADCELL_SCK_PIN);
loadcell.tare();
}
void loop() {
if (Serial.available() > 0) {
char inByte = Serial.read();
if (inByte == 'F') {
float weight = loadcell.get_value();
Serial.println(weight);
} else if (inByte == 'T') {
loadcell.tare();
Serial.println("TARED");
}
}
}
#include <HX711.h>
#define LOADCELL_DOUT_PIN_1 4
#define LOADCELL_SCK_PIN_1 5
#define LOADCELL_DOUT_PIN_2 6
#define LOADCELL_SCK_PIN_2 7
HX711 loadcell_1;
HX711 loadcell_2;
char loadcell_ready;
void setup() {
Serial.begin(115200);
loadcell_1.begin(LOADCELL_DOUT_PIN_1, LOADCELL_SCK_PIN_1);
loadcell_2.begin(LOADCELL_DOUT_PIN_2, LOADCELL_SCK_PIN_2);
loadcell_1.tare()
loadcell_2.tare()
loadcell_ready = 0b00;
}
void loop() {
if (loadcell_1.update()) loadcell_ready |= 0b01;
if (loadcell_2.update()) loadcell_ready |= 0b10;
if (loadcell_ready == 0b11 && Serial.available() > 0) {
char inByte = Serial.read();
if (inByte == 'F') {
float weight_1 = loadcell_1.getData();
float weight_2 = loadcell_2.getData();
Serial.println(abs(weight_1) - abs(weight_2));
loadcell_ready = 0b00;
} else if (inByte == 'T') {
loadcell_1.tare();
loadcell_2.tare();
Serial.println("TARED");
}
}
}
#include <due_can.h>
#define CAN_COMM_MB_IDX 0
#define CAN_TRANSFER_ID 0x07
#define CAN_TX_PRIO 0
CAN_FRAME pack_frame(float p, float v, float t) {
}
void setup() {
// put your setup code here, to run once:
Serial.begin(115200);
Can0.begin(CAN_BPS_1000K);
Can0.watchFor();
}
void loop() {
// put your main code here, to run repeatedly:
}
from ml import LinearModel import numpy as np
from fake_cut import Fake_Cut
from objects import EndMill, Conditions
PARAMS = [858494934.9591976, -696.3150933941946, 858494934.9591976, -696.3150933941946]
# ERROR = [0.01, 0.01]
# NOISE = [0.1, 0.1]
ERROR = [0, 0]
NOISE = [0, 0]
CONDITIONS = Conditions(1e-3, 3.175e-3, 5e-3, 5000, EndMill(3, 3.175e-3, 3.175e-3, 10e-3, 10e-3))
model = LinearModel() from objects import EndMill, Conditions, MachineChar
from cut import Cut
cut = Fake_Cut(PARAMS, ERROR, NOISE) from ml import LinearModel
from optimize import Optimizer
data = cut.cut(CONDITIONS)
model.ingest_datum(data)
print("{0:.20f}".format(model.params[1])) import logging
\ No newline at end of file logging.basicConfig(level="INFO")
\ No newline at end of file
from sensors import Machine, Spindle, TFD
import numpy as np
import time
import shelve
import logging
from queue import PriorityQueue
from tqdm import tqdm
class RawDataPacket():
def __init__(self, D, W, R, N, f_r, w, measurements):
self.D = D
self.W = W
self.R = R
self.N = N
self.f_r = f_r
self.w = w
self.measurements = measurements
def decompose_packet_sorta(self):
tTs, tFys = list(), list()
Ts, Fys = list(), list()
for measurement in self.measurements:
t, reading = measurement
reading_type, = reading.keys()
reading_data, = reading.values()
if reading_type == 'spindle':
tTs.append(t)
Ts.append(reading_data['torque'])
elif reading_type == 'tfd':
tFys.append(t)
Fys.append(reading_data)
return (tTs, tFys, Ts, Fys)
def decompose_packet(self):
Ts, Fys = list(), list()
for measurement in self.measurements:
_, reading = measurement
reading_type, = reading.keys()
reading_data, = reading.values()
if reading_type == 'spindle':
Ts.append(reading_data['torque'])
elif reading_type == 'tfd':
Fys.append(reading_data)
T = np.median(Ts)
Fy = np.median(Fys)
return DataPoint(self.D, self.W, self.R, self.N, self.f_r, self.w, T, Fy)
class MillingTest():
def __init__(self, machine_port, spindle_port, tfd_port):
self._logger = logging.Logger("Milling Test")
self._logger.setLevel(logging.INFO)
self.machine = Machine(machine_port)
self.spindle = Spindle(spindle_port)
self.tfd = TFD(tfd_port)
print("Initialized all serial objects")
# initialize machine
self.machine.unlock()
self.machine.zero()
print("Machine unlocked and zeroed.")
# create test dbs
self.db = shelve.open('results')
def run_test(self, x, y, D, W, R, fmin, fmax, w, N, test_name, offset_depth = 0):
# save params
# calculate constants
CLEARANCE = R
Y_START = -CLEARANCE * 1.5
Y_END = CLEARANCE * 1.5 + y
SWEEPS = int((x - R * 2) / W)
Y_DATA_START = CLEARANCE
Y_DATA_END = y - CLEARANCE
# calculate arrays and whatnot
feeds = np.linspace(fmin, fmax, SWEEPS)
xpos = np.linspace(CLEARANCE + W, CLEARANCE + W * SWEEPS, SWEEPS)
# start spindle
self.spindle.set_w(w)
print("Spindle started")
# initial clearing cycle
self.machine.rapid({'x': CLEARANCE, 'y': Y_START, 'z': 1})
self.machine.rapid({'z': -(D + offset_depth)})
self.machine.cut({'y': Y_END}, (fmin + fmax)/2)
print("Clearing cycle sent")
self.machine.hold_until_still()
print("Starting actual cuts. " + str(SWEEPS) + " cuts in queue.")
# begin collecting data and cutting
results = []
for f_r, x in tqdm(zip(feeds, xpos)):
# position
self.machine.rapid({'z': 1})
self.machine.rapid({'x': x, 'y': Y_START})
self.machine.rapid({'z': -(D + offset_depth)})
self.machine.hold_until_still()
print("Positioned for cut at x = " + str(x) + " with feed " + str(f_r))
# measure spindle
self.spindle.find_avg_current()
# tare
self.tfd.tare()
print("Current calibrated, tfd re-tared")
# send cut command
self.machine.cut({'y': Y_END}, f_r)
# delay until we get to a significant place
print("Waiting until fully in cut")
self.machine.hold_until_condition(lambda state : state['wpos']['y'] > Y_DATA_START)
# start counting time
outQueue = PriorityQueue()
start = time.perf_counter()
self.machine.start_measurement(outQueue, start)
self.spindle.start_measurement(outQueue, start)
self.tfd.start_measurement(outQueue, start)
# fetch all data until finished
print("Measurement started, waiting until cut is left")
self.machine.hold_until_condition(lambda state : state['wpos']['y'] > Y_DATA_END)
print("Cut finished")
# stop measurements
self.machine.stop_measurement()
self.spindle.stop_measurement()
self.tfd.stop_measurement()
# dump queue
data = []
print("Dumping data from queue")
while not outQueue.empty():
data.append(outQueue.get())
print("Queue dumped")
# add result
result = RawDataPacket(D * 1e-3, W * 1e-3, R * 1e-3, N, f_r * 1.6667e-5, w, data)
results.append(result)
print("Cycle finished.")
# return home
print("Finished with all cycles")
self.machine.rapid({'z': 1})
self.machine.rapid({'x': 0, 'y': 0})
self.machine.rapid({'z': 0})
self.machine.hold_until_still()
self.db[test_name] = results
self.db.sync()
def close(self):
self.spindle.close()
self.db.close()
if __name__ == "__main__":
test = MillingTest('/dev/ttyS25', '/dev/ttyS5', '/dev/ttyS26')
test.run_test(40, 50.8, 0.5, 3.175, 6.35 / 2, 499, 500, 500, 3, 'fake', 0)
test.run_test(40, 50.8, 1, 3.175 / 4, 6.35 / 2, 100, 600, 500, 3, '4', 0.5)
test.run_test(39.9, 50.8, 1, 3.175 / 2, 6.35 / 2, 100, 600, 500, 3, '5', 1.5)
test.run_test(35.8, 50.8, 1, 3.175 / 1, 6.35 / 2, 100, 600, 500, 3, '6', 2.5)
test.close()
\ No newline at end of file
import numpy as np import numpy as np
import logging
import time import time
import shelve
import os
from queue import PriorityQueue from queue import PriorityQueue
from objects import Data, EndMill from objects import Data, EndMill
from sensors import Machine, Applied_Spindle, TFD from sensors import Machine, Spindle_Applied, TFD
import logging
log = logging.getLogger(__name__)
class MachineCrash(Exception): class MachineCrash(Exception):
...@@ -12,12 +16,11 @@ class MachineCrash(Exception): ...@@ -12,12 +16,11 @@ class MachineCrash(Exception):
class Cut: class Cut:
def __init__(self, machine_port, spindle_port, tfd_port, endmill, x_max, y_max, initial_z = 0): def __init__(self, machine_port, spindle_port, tfd_port, endmill, x_max, y_max, initial_z=0, save_as=None):
self._logger = logging.Logger("Cut")
self.machine = Machine(machine_port) self.machine = Machine(machine_port)
self.spindle = Applied_Spindle(spindle_port) self.spindle = Spindle_Applied(spindle_port)
self.tfd = TFD(tfd_port) self.tfd = TFD(tfd_port)
self._logger.info("Initialized all systems.") log.info("Initialized all systems.")
# saving other variables # saving other variables
self.endmill = endmill self.endmill = endmill
...@@ -26,6 +29,7 @@ class Cut: ...@@ -26,6 +29,7 @@ class Cut:
self.cut_x = 0 self.cut_x = 0
self.cut_z = initial_z self.cut_z = initial_z
self.D = 0 self.D = 0
self.save_as = save_as
# deriving some constants # deriving some constants
self.Y_START = - 2 * endmill.R self.Y_START = - 2 * endmill.R
...@@ -37,13 +41,20 @@ class Cut: ...@@ -37,13 +41,20 @@ class Cut:
def __del__(self): def __del__(self):
self.spindle.set_w(0) self.spindle.set_w(0)
def begin_layer(self, D, f_r_clearing, w_clearing): def begin_layer(self, D, f_r_clearing, w_clearing):
""" """
Prepares to start facing with cut depth D. Successive calls will compensate for previous cut depths. Prepares to start facing with cut depth D. Successive calls will compensate for previous cut depths.
Args:
D: Depth of cut for this layer.
f_r_clearing: feedrate used for this clearing pass.
w_clearing: spindle speed used for this clearing pass.
Returns:
A data blob from this operation.
""" """
X_START = self.endmill.R X_START = self.endmill.R
self._logger.info("Preparing to clear layer to depth " + str(D) + " at feedrate " + str(f_r_clearing) + " with speed " + str(w_clearing)) log.info("Preparing to clear layer to depth " + str(D) +
" at feedrate " + str(f_r_clearing) + " with speed " + str(w_clearing))
self.D = D self.D = D
self.cut_z -= D self.cut_z -= D
...@@ -59,45 +70,51 @@ class Cut: ...@@ -59,45 +70,51 @@ class Cut:
self.x_cut = self.endmill.r_c * 2 self.x_cut = self.endmill.r_c * 2
self._logger.info("Layer prepared for clearing") log.info("Layer prepared for clearing")
def cut(self, conditions): def cut(self, conditions):
""" """
Performs a stroke of facing. Returns a data blob. Performs a stroke of facing. Returns a data blob.
""" """
_, W, f_r, w, _ = conditions.unpack() _, W, f_r, w, _ = conditions.unpack()
X_START = x_cut - self.endmill.r_c + W X_START = self.x_cut - self.endmill.r_c + W
if X_START > self.X_END: if X_START > self.X_END:
raise MachineCrash("Cutting too far in X direction: X = " + str(X_START)) raise MachineCrash(
"Cutting too far in X direction: X = " + str(X_START))
outQueue = PriorityQueue() outQueue = PriorityQueue()
self._logger.info("Performing cut at position " + str(X_START) + " with WOC " + str(W) + " and feedrate " + str(f_r) + " at speed " + str(w)) log.info("Performing cut at position " + str(X_START) + " with WOC " +
str(W) + " and feedrate " + str(f_r) + " at speed " + str(w))
self.machine.rapid({'x': X_START, 'y': self.Y_START})