18.4- Implementation
1. Fabrication
The main method of manufacturing for our team was Texas Invention Works’ laser cutter, so the majority of our components were made from plywood and acrylic. Shown below are several of our 4 bar slider crank component designs presented as .DXF files.
Geneva Drive:
Crank:
Coupler:
Larger Gear:
Figure 4. McMaster-Carr Engineering Diagram of GearPinion:
2. Design Changes
Moving from our initial prototype to our final design, we changed the material of our linkages from wood to acrylic for improved structural integrity and aesthetics. The gears were also modified to be made of acrylic as we noticed that the wood gears were wearing/chipping significantly during testing.
The figure below showcases our new slider system created from acrylic. Note that this assembly would be oriented vertically during operation. As we iterated through the design, we added supports beneath the slider mounts and foam, as we had issues where the slider would impact the mount and break it off.
We also created extra support for the system shown above to vertically mount onto our baseplate due to the weight of our components.
3. Electronics and Software
The electronics assembly above shows the final layout for all electronics mounted to the baseplate. The Arduino pictured in the bottom left is the electronic controller for all of the electronics and motor drivers. The L298N pictured in the top left is used to drive the 12 V 100 RPM DC motor mounted to the back of the Geneva drive, serving as the input to the crank system. The breadboard is utilized to add 12V rails from the DC power supply to connect the stepper motors and the L298N. Finally, there are two stepper motor driver boards assigning commands to the stepper motors. As seen in Figure 11, there are two limit switches connected to either end of the central probe, which serve as confirmation that the probe is contacting something. This signal is used to run each state of the code and to plot the output graphs.
The Stepper motors, as seen in Figure 10 are connected to two parallel belts to drive a carriage holding the block to be probed. The two motors are signaled to run at the same time, allowing for parallel motion to drive the conveyor forward a fixed amount. The step size of the conveyor belt was tuned to work with the specific probe width and block placement in our design.
Early testing was done by attempting to run the stepper motors with Stepper.h; the run commands are blocking and would not allow a parallel run of the stepper motors. Later iterations showed that the AccelStepper.h was a non-blocking function and was used from then on.
#include <AccelStepper.h>
// Define motor interface type (4 = 4-wire full step)
#define MotorInterfaceType 4
AccelStepper stepper1(MotorInterfaceType, 9, 7, 8, 6);
AccelStepper stepper2(MotorInterfaceType, 10, 12, 11, 13);
void setup() {
// set the maximum speed, acceleration factor,
stepper1.setMaxSpeed(1000.0);
stepper1.setAcceleration(50.0);
stepper1.setSpeed(200);
stepper1.moveTo(7500);
stepper2.setMaxSpeed(1000.0);
stepper2.setAcceleration(50.0);
stepper2.setSpeed(200);
stepper2.moveTo(-7500);
}
void loop() {
// If motor 1 reaches its target, set a new target further in the same direction
if (stepper1.distanceToGo() == 0) {
stepper1.moveTo(stepper1.currentPosition() + 7500);
}
// If motor 2 reaches its target, set a new target further in the same direction
if (stepper2.distanceToGo() == 0) {
stepper2.moveTo(stepper2.currentPosition() - 7500);
}
stepper1.run();
stepper2.run();
}
Additional testing trying to link the forward and reverse control of the 12V motor utilizing the limit switches, to prove the functionality of the system. This code was used to better understand the limit switches and the motor driving.
// Motor A connections
const int enA = 3;
const int in1 = 4;
const int in2 = 2;
#define LIMIT_SWITCH_TOP 14
#define LIMIT_SWITCH_BOTTOM 15
void setup() {
Serial.begin(9600);
//12V Control
pinMode(enA, OUTPUT);
pinMode(in1, OUTPUT);
pinMode(in2, OUTPUT);
//Limit Swtiches
pinMode(LIMIT_SWITCH_TOP, INPUT_PULLUP);
pinMode(LIMIT_SWITCH_BOTTOM, INPUT_PULLUP);
// Start with motor off
setMotor(0, true);
}
void loop() {
if (digitalRead(LIMIT_SWITCH_TOP) == LOW)
{
// Drive forward at 100 RPM
setMotor(255, true);
Serial.println("Activated Top ");
}
else if (digitalRead(LIMIT_SWITCH_BOTTOM) == LOW)
{
setMotor(255, false);
Serial.println("Activated Top ");
}
else
{
setMotor(0,true);
Serial.println("Not activated.");
}
}
//setMotor
//pwm speed - 0 to 255
//reverse - true for forward, false for backward
void setMotor(int speed, bool forward) {
// Control speed via PWM
analogWrite(enA, speed);
// Control direction
if (forward) {
digitalWrite(in1, HIGH);
digitalWrite(in2, LOW);
} else {
digitalWrite(in1, LOW);
digitalWrite(in2, HIGH);
}
}
The final code came in two parts: an Arduino program to control the components and the Python code to observe the incoming data from the Arduino and plot the results. The Python code was generated utilizing Claude, and the step heights were calibrated via testing of the system.
Arduino Final Code:
// Motor A connections
const int enA = 3;
const int in1 = 4;
const int in2 = 2;
#define LIMIT_SWITCH_TOP 14
#define LIMIT_SWITCH_BOTTOM 15
#include <AccelStepper.h>
#define MotorInterfaceType 4
AccelStepper stepper1(MotorInterfaceType, 9, 7, 8, 6);
AccelStepper stepper2(MotorInterfaceType, 10, 12, 11, 13);
const int stepsPerRevolution = 1250; //decreased by half
const int TOTAL_CYCLES = 6;
enum State {
STATE_WAITING,
STATE_MOVING_DOWN,
STATE_MOVING_UP,
STATE_STEPPING,
STATE_DONE
};
State currentState = STATE_WAITING;
int cycleCount = 0;
unsigned long timerStart = 0;
long timeArray[TOTAL_CYCLES] = {0};
int timeIndex = 0;
bool steppersInitialised = false;
void setMotor(int speed, bool forward) {
analogWrite(enA, speed);
if (forward) {
digitalWrite(in1, HIGH);
digitalWrite(in2, LOW);
} else {
digitalWrite(in1, LOW);
digitalWrite(in2, HIGH);
}
}
void startNewCycle() {
timerStart = millis();
setMotor(180, true);
currentState = STATE_MOVING_DOWN;
Serial.print("# Cycle ");
Serial.print(cycleCount + 1);
Serial.println(" starting");
}
// Print CSV block
void printCSV() {
Serial.println("DATA_START");
Serial.println("cycle,time_ms");
for (int i = 0; i < timeIndex; i++) {
Serial.print(i + 1);
Serial.print(",");
Serial.println(timeArray[i]);
}
Serial.println("DATA_END");
}
void setup() {
Serial.begin(9600);
pinMode(enA, OUTPUT);
pinMode(in1, OUTPUT);
pinMode(in2, OUTPUT);
stepper1.setMaxSpeed(1000.0);
stepper1.setAcceleration(50.0);
stepper2.setMaxSpeed(1000.0);
stepper2.setAcceleration(50.0);
pinMode(LIMIT_SWITCH_TOP, INPUT_PULLUP);
pinMode(LIMIT_SWITCH_BOTTOM, INPUT_PULLUP);
Serial.println("# Ready — send 's' to start");
}
void loop() {
switch (currentState) {
case STATE_WAITING:
if (Serial.available() > 0) {
char c = Serial.read();
if (c == 's' || c == 'S') {
Serial.println("# 's' received — starting cycles");
startNewCycle();
}
}
break;
case STATE_MOVING_DOWN:
if (digitalRead(LIMIT_SWITCH_BOTTOM) == LOW) {
setMotor(0, true);
if (timeIndex < TOTAL_CYCLES) {
timeArray[timeIndex] = (long)(millis() - timerStart);
Serial.print("# Time recorded (ms): ");
Serial.println(timeArray[timeIndex]);
timeIndex++;
}
setMotor(180, false);
currentState = STATE_MOVING_UP;
}
break;
case STATE_MOVING_UP:
if (digitalRead(LIMIT_SWITCH_TOP) == LOW) {
setMotor(0, true);
stepper1.setCurrentPosition(0);
stepper1.moveTo(-stepsPerRevolution);
stepper2.setCurrentPosition(0);
stepper2.moveTo(stepsPerRevolution);
steppersInitialised = true;
currentState = STATE_STEPPING;
}
break;
case STATE_STEPPING:
if (steppersInitialised) {
stepper1.run();
stepper2.run();
if (stepper1.distanceToGo() == 0 && stepper2.distanceToGo() == 0) {
steppersInitialised = false;
currentState = STATE_DONE;
}
}
break;
case STATE_DONE:
cycleCount++;
if (cycleCount < TOTAL_CYCLES) {
startNewCycle();
Serial.println("");
} else {
Serial.println("# All cycles complete");
printCSV();
while (true) {}
}
break;
}
}
Python Final Code:
"""
read_and_plot.py
Reads cycle-time CSV emitted by the Arduino over Serial,
then plots the data with matplotlib.
"""
import sys
import io
import time
import serial
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
def find_arduino_port() -> str:
return "COM10"
def read_csv_from_arduino(port: str, baud: int = 9600, timeout: int = 300) -> str:
"""
Open `port` and collect everything between DATA_START and DATA_END.
Lines starting with '#' are printed as log messages and skipped.
Returns the raw CSV text (header + rows).
"""
print(f"Opening {port} at {baud} baud …")
ser = serial.Serial(port, baud, timeout=1)
# Give the Arduino time to reset after DTR toggle
time.sleep(2)
ser.reset_input_buffer()
# Wait for the Arduino's "Ready" message, then send 's' to start
print("Waiting for Arduino ready signal …")
deadline_ready = time.time() + 10
while time.time() < deadline_ready:
raw = ser.readline()
if not raw:
continue
line = raw.decode("utf-8", errors="replace").strip()
if line.startswith("#"):
print(f"[Arduino] {line[1:].strip()}")
if "ready" in line.lower():
break
print("Waiting to start the Arduino program …")
input("Press Enter to start the Arduino program …")
ser.write(b's')
csv_lines: list[str] = []
capturing = False
deadline = time.time() + timeout
print("Waiting for Arduino to finish all cycles …")
while time.time() < deadline:
raw = ser.readline()
if not raw:
continue
line = raw.decode("utf-8", errors="replace").strip()
if line.startswith("#"):
print(f"[Arduino] {line[1:].strip()}")
continue
if line == "DATA_START":
capturing = True
print("Receiving data …")
continue
if line == "DATA_END":
break
if capturing and line:
csv_lines.append(line)
ser.close()
if not csv_lines:
raise RuntimeError(
"No data received. Check that the Arduino is running and "
"that TOTAL_CYCLES matches what you expect."
)
return "\n".join(csv_lines)
# ── Plotting ───────────────────────────────────────────────
def classify_time(t: float) -> int:
"""Map a cycle time (ms) to a discrete step level."""
if 4600 <= t <= 6000:
return 0
elif 4000 <= t < 4600:
return 1
elif t < 4000:
return 2
else:
return -1 # outside defined ranges
def plot(df: pd.DataFrame) -> None:
levels = df["time_ms"].apply(classify_time).values
n = len(levels)
# Each cycle occupies 1 unit of distance; x runs 0 → n
x_edges = np.arange(n + 1)
y_steps = np.append(levels, levels[-1]) # repeat last to close final step
fig, ax = plt.subplots(figsize=(8, 5))
ax.step(x_edges, y_steps, where="post", color="#4a90d9", linewidth=2)
ax.scatter(x_edges[:-1], levels, color="#4a90d9", s=80, zorder=3)
ax.set_xlabel("Cumulative Distance (cycles)")
ax.set_ylabel("Step Level")
ax.set_title("Probe Step Level per Cycle")
ax.set_yticks([0, 1, 2])
ax.set_yticklabels(["0 ", "1", "2"])
ax.set_xlim(-1, n)
ax.xaxis.set_major_locator(ticker.MultipleLocator(1))
# 0 at top so "higher penetration" reads downward
ax.grid(alpha=0.3)
plt.tight_layout()
plt.savefig("cycle_times.png", dpi=150, bbox_inches="tight")
print("Plot saved to cycle_times.png")
plt.show()
# ── Main ───────────────────────────────────────────────────
def main():
port = sys.argv[1] if len(sys.argv) > 1 else find_arduino_port()
csv_text = read_csv_from_arduino(port)
df = pd.read_csv(io.StringIO(csv_text))
print(f"\nData received:\n{df.to_string(index=False)}\n")
# Also save raw CSV
df.to_csv("cycle_times.csv", index=False)
print("Data saved to cycle_times.csv")
plot(df)
if __name__ == "__main__":
main()
4. Full Assembly
To assemble the entire system together, we press-fit the bearings into our acrylic board. These bearings held shafts that mounted each of our linkages, our gear reduction, as well as our DC motor, which drives the Geneva mechanism. To attach our slider to the acrylic board, we used epoxy. This entire acrylic board was attached to our plywood baseboard, where the conveyor belt sat, using bolts. The stepper motors for the conveyor were then mounted onto the plywood baseboard, completing our assembly process, as shown in the figure below.