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

Polishing up for push

parent fa2497ae
......@@ -2,4 +2,28 @@
A CNC controller that learns material characteristics and optimizes its own feeds and speeds.
# Project Status
This project is currently unpublished and is still undergoing planning.
\ No newline at end of file
## Current Goals
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
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))
import numpy as np
model = LinearModel()
cut = Fake_Cut(PARAMS, ERROR, NOISE)
data = cut.cut(CONDITIONS)
model.ingest_datum(data)
from objects import EndMill, Conditions, MachineChar
from cut import Cut
from ml import LinearModel
from optimize import Optimizer
print("{0:.20f}".format(model.params[1]))
\ No newline at end of file
import logging
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 logging
import time
import shelve
import os
from queue import PriorityQueue
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):
......@@ -12,12 +16,11 @@ class MachineCrash(Exception):
class Cut:
def __init__(self, machine_port, spindle_port, tfd_port, endmill, x_max, y_max, initial_z = 0):
self._logger = logging.Logger("Cut")
def __init__(self, machine_port, spindle_port, tfd_port, endmill, x_max, y_max, initial_z=0, save_as=None):
self.machine = Machine(machine_port)
self.spindle = Applied_Spindle(spindle_port)
self.spindle = Spindle_Applied(spindle_port)
self.tfd = TFD(tfd_port)
self._logger.info("Initialized all systems.")
log.info("Initialized all systems.")
# saving other variables
self.endmill = endmill
......@@ -26,6 +29,7 @@ class Cut:
self.cut_x = 0
self.cut_z = initial_z
self.D = 0
self.save_as = save_as
# deriving some constants
self.Y_START = - 2 * endmill.R
......@@ -37,13 +41,20 @@ class Cut:
def __del__(self):
self.spindle.set_w(0)
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.
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
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.cut_z -= D
......@@ -59,45 +70,51 @@ class Cut:
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):
"""
Performs a stroke of facing. Returns a data blob.
"""
_, 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:
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()
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})
self.machine.rapid({'z': self.cut_z})
self.machine.hold_until_still()
self._logger.info("Calibrating spindle")
log.info("Calibrating spindle")
self.spindle.calibrate()
self._logger.info("Calibrating TFD")
log.info("Calibrating TFD")
self.tfd.calibrate()
self._logger.info("Starting cut")
log.info("Starting cut")
self.machine.cut({'y': self.Y_END}, f_r)
time.sleep(0.1)
self.machine.hold_until_condition(lambda state : state['wpos']['y'] > self.Y_DATA_START)
self.machine.hold_until_condition(
lambda state: state['wpos']['y'] > self.Y_DATA_START)
log.info("Beginning data collection")
time_start = time.perf_counter
self.spindle.start_measurements(outQueue, time_start)
self.spindle.start_measurement(outQueue, time_start)
self.tfd.start_measurement(outQueue, time_start)
self.machine.hold_until_condition(lambda state : state['wpos']['y'] > self.Y_DATA_END)
self.machine.hold_until_condition(
lambda state: state['wpos']['y'] > self.Y_DATA_END)
self.spindle.stop_measurements()
log.info("Ending data collection")
self.spindle.stop_measurement()