18.4- Implementation

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:

image-20260428-211341.png
image-20260428-211418.png
Figure 1. DXF File of Geneva Drive Components
  • Crank:

image-20260428-211713.png
Figure 2. DXF File of Link 2, Crank
  • Coupler:

image-20260428-211957.png
Figure 3. DXF File of Link 3, Coupler
  • Larger Gear:

    image-20260428-212203.png
    Figure 4. McMaster-Carr Engineering Diagram of Gear
  • Pinion:

image-20260428-212233.png
Figure 5. McMaster-Carr Engineering Diagram of Pinion

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.

image-20260424-202641.png
Figure 6. Laser Cut Gear Components in Wood and Acryllic

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.

image-20260428-213601.png
Figure 7. Design Iterations of Slider Mounts

We also created extra support for the system shown above to vertically mount onto our baseplate due to the weight of our components.

image-20260428-214212.png
Figure 8. Vertical Support Mount for Slider-Crank Baseplate

3. Electronics and Software

Electronics.png
Figure 9. Final Electronics Layout

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.

 

Stepper Motor.png
Figure 10: Stepper Motors

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(); }

 

Conveyor Testing.mp4
Figure 11. Video of Conveyor Belt Testing

 

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.

Untitled design.png
Figure 12: Final Assembly