12— Mechanical Design, Machine Design

Assignment

00—Final Project Video

Link to final Project Video

01—About the Project

As a group we chose the CNC wire bender project because we felt that it created a lot of interesting possibilities as a tool. Some of the ideas we had were to create circuit sculptures using thin brass rod or hanging sculptures/ mobiles. The design we chose also had good documentation and included a tutorial with a full assembly .step file. Since the design was well documented we felt like it we felt it made sense to build it “as is”. After creating the first version from the plans we planned to take a phased approach in line with the spiral development ethos— producing a working project at the end of each phase. Below is the original design we started with, a link to the documentation can be found here . This original step file was used as a reference when creating the final assembly Fusion file.

Link to original project writeup

Link to original CNC wire bender CAD files

02—Initial Project Planning and Allocation of Tasks

In Brief: From the beginning we broke the project into several distinct spirals. After the first spiral the Engineered CAD and the electronics as separate teams that could work mostly independent from each other. To organize our roles Katya made a multi page spreadsheet that organized BOM, progress, and project roles. That can be found here. Below is an overview of what we had originally planned to execute for the project. Minor changes were made to each spiral at the beginning of each phase.

Spiral 1—Manual test with unpowered machine

Spiral 2—Prototype electronics + essential mechanical

Spiral 3— aesthetic + mechanical + custom milled PCB

03—Full Project Development Log

Spiral 1: Manual Test With Unpowered Machine

Recap

This first step actually proved to be pretty straightforward. As a group we all read through the original documentation then ED and Mark 3D printed all of the original 3D printed components. Andrew then reworked several of the components to be printable instead of wood fabricated. Mark put together a list of fasteners and Gonzalo and Andrew then purchased the fasteners and assembled the machine. Not a lot of road blocks with this first phase especially because we were not too concerned with getting the electronics working.

Detailed summary of design changes

Spiral 2: Prototype Electronics + Mechanical Overhaul

Recap

This spiral was a full design sprint on all fronts. In a way it was multiple parallel sprints. This second version of the design did not get fully built from the engineered CAD on the left. New components were added into the design while simultaneously the electronics and software were improved.

Detailed summary of design changes

Spiral 3: Aesthetic + Mechanical + Custom Milled PCB

Recap

This was the final sprint. Camila created a new design identity for the machine and as a group we worked to implement the redesign as well as incorporate a few key additions like the planetary reducer. The Planetary gear reduction was Eds solution to fixing an issue where that we were having where the Z axis would not rotate. Thankfully we were able to get everything finished on time, but unfortunately we could not get the machine working again. We were plagued with many difficult to solve issues with the stepper controller system that we still have not been able to fully resolve. An

Detailed summary of design changes

04— Final Thoughts + Improvements

All in all, the development of the Super Chena 5000 CNC was a worthwhile process. Unfortunately… heartbreakingly we could not get the full machine working at the very end for issues with the stepper control system. However I think we were all proud of how much we were able to learn and accomplish in just a two week period. There are many many changes that could be made with the design. I think it could even be worth starting with a blank sheet design. However, if we were to continue with the same architecture below are a few common sense changes.

05— Making the Machine: Control Hardware + Software

In Brief: Below is a detailed breakdown of all relevant details regarding the electronics hardware and software and how to configure them to build your very own SUPER CHENA 5000 CNC.

Stepper Motor, Driver and Operation

The motors we are using are

Setting up the Stepper Motor using a CNC Shield

References

[1] G-code cheat sheet : https://www.probotix.com/gcode_cheatsheet.pdf

[2] Marlin Gcode docs : https://marlinfw.org/docs/gcode/G000-G001.html

[3] GRBL error codes : https://resources.sienci.com/view/gs-grbl-alarm-error-codes/

[4] https://hackmd.io/@MikelLG/Bk0Ok4t61x by Mikel Llobera Guelbenzu to give the PI serial communication - GRBL description to help us define motor movements.

[5] https://cool-emerald.blogspot.com/2021/04/drawbot-xy-plotter-with-arduino-grbl.html

[6] https://www.diyengineers.com/2023/01/05/grbl-with-arduino-cnc-shield-complete-guide/

We decided to use the GRBL library from Marlin Firmware for controlling the stepper motors on the CNC shield. https://github.com/gnea/grbl/blob/master/README.md

We followed the following guide to setup the motors https://www.diyengineers.com/2023/01/05/grbl-with-arduino-cnc-shield-complete-guide/ [6]

According to [5] cloned CNC v4 shields are known to have hardware issues.

We downloaded the grbl-master file from github, then had to rezip the grbl library within on its own to then import it in Arduino IDE :

We then had to modify the config.h for our design following the steps described in [5]. The difference between the original and modified files is highlighted in the screenshots below.

First step was to define the different motors in our design to an X, Y, Z or A from the CNC shield:

According to [5] in cpu_map.h verify that line 147 is commented and line 148 is as follows (changes PWN frequency 1kHz for Laser to approximate 50 Hz for servo):

147  // #define SPINDLE_TCCRB_INIT_MASK      (1<<CS22)               // 1/64 prescaler -> 0.98kHz (J-tech laser)
148  #define SPINDLE_TCCRB_INIT_MASK ((1<<CS22)|(1<<CS21)|(1<<CS20)) //1/1024 prescaler -> 61 Hz (for Servo)

Also verify that the undesirable PWN power changing is disabled (commented):

587 // #define DISABLE_LASER_DURING_HOLD

When compiling the GRBL there is a trick to delete all of the temporary files in the arduino folder to ensure that the Arduino IDE is takin the correct config.h and cpu_map.h files and not the old ones. https://github.com/gnea/grbl/wiki/Compiling-Grbl give trouble shooting for this step.

Stepper Motor Driver

We chose to use the DRV8825 motor drivers with the CNC shield. More details can be found on the following page:

For our design we chose to use a microstep resolution offering 1/16 steps = 3200 microsteps.

Resolution to give a 1/16 step according to table below
Mode 0Mode 1Mode 2Microstep resolultion
000Full step
100Half step
0101/4 step
1101/8 step
0011/16 step
1011/32 step
0111/32 step
1111/32 step

Calibration

The first driver took the most time as we encountered the following problems :

  1. not enough voltage to turn the motor connected to VMOT pin
  1. wires coming from the motor are not in correct order for the A1 A2 B1 B2 pins
  1. reference voltage and limiting current too low

To resolve 1 we simply needed to increase the voltage above 8V.

To resolve 2 we needed to identify the pairs by connecting two of the 4 wires together to see which gave more resistance. Two wires sharing the same loop will create more resistance. One pair is then connected to A1 and A2, and the other B1 and B2.

[5] explains clearly how to calibrate the reference voltage and adjust the potentiometer on the driver until the multimeter receives 0.7-0.9 V between the cathode on the potentiometer itself and the anode connected to ground. Be careful to not touch any of the other components on the driver with the cathode, as it risks to burn the driver.

The current limit for the DRV8825 is given by :

Ilim=Vr5.RsI_{lim}=\frac {V_r}{5.R_s}

Since Rs is 0.1 ohm, the IlimI_{lim} = 2×Vr2 \times V_r 

 Ilim=1.41.8I_{lim} = 1.4-1.8  amps

Homing

To initiate the motor, we need to home the positioning: first installing the python serial library;

python3 -m pip install pyserial 

and understanding the grbl settings:

Table of grbl settings from [4]

$H sends the motor to home it’s position using grbl. It will set the point where the end stop is triggered as 0 for the motor. It will home twice, first seeking the home at a faster speed and then refining the home at a slower speed.

If it starts to move in the wrong direction we need to invert the direction using Gcode : $23=1

G92 X0Y0Z0 allows the user to set the origin at the current position.

Bending motor calibration

Here we are going to calibrate the bending motor, X, to rotate the bending platform 1º with the command G0 X1, which would typically move 1.8º.

We initially have 1.8º per step per 200steps. We also have two gears with 18 and 30 teeth, the former being the one coaxial and attached with the motor. Therefore we have a 5/3 gear ratio.

360 / (200*5/3)= 360º/333.3 = 1.08º per step of the end gear.

$100, $101 and $102 set the steps/mm for the X, Y and Z motors respectively. We need to modify this so it is steps/º. Not only that we need to consider that we are using a 1/16 microstepping.

 for the X bending motor, $100 needs to be set to

200×16×53360=5333.33360=14.815\frac {200 \times 16 \times \frac {5} {3}} {360} = \frac {5333.33} {360} = 14.815

These are the default values for the Arduino board and CNC shield, retrieved by entering ‘$$’ in the serial monitor.

$0=10
$1=25
$2=0
$3=0
$4=0
$5=0
$6=0
$10=1
$11=0.010
$12=0.002
$13=0
$20=0
$21=0
$22=0
$23=0
$24=25.000
$25=500.000
$26=250
$27=1.000
$30=1000
$31=0
$32=0
$100=250.000
$101=250.000
$102=250.000
$110=500.000
$111=500.000
$112=500.000
$120=10.000
$121=10.000
$122=10.000
$130=200.000
$131=200.000
$132=200.000

These are the values we change for the Bender

$4=180
$10=2
$22=1
$23=1
$24=500
$100=14.815
$110=5000
$120=100

We start by homing X and then move on to calibrate the Feeder, Y in the same manner as it has a similar gear ratio.

We then move on to calibrate the feeder, Z. The feeder pulls the wire through the motor at a given feed rate in mm/min. $102 is the number of steps the Y-axis motors needs to move to travel 1 mm. By trial and error we determine $102 = 95

$1 = 180 allows the motors to remain enabled with full power even when not solicited (range between 0-255) to maintain position and hold the wire position.

Calibration of Servo Motor

The following is inspired from the following drawing bot [5].

For the servo, as mentioned previously, we hack the stepper motor GRBL code by modifying the config.h and cpumap.h, adding the following lines of code :

cpu_map.h : comment line 147 and add line 148

147    // #define SPINDLE_TCCRB_INIT_MASK      (1<<CS22)               // 1/64 prescaler -> 0.98kHz (J-tech laser)
148    #define SPINDLE_TCCRB_INIT_MASK ((1<<CS22)|(1<<CS21)|(1<<CS20))

The following commands are used to control the servo motor so that the bending pipe is either down or up:

M03 S120    // down position for skip
M03 S50     // up position for bend

Using the Z+ endstop pin to control the servo. The servo to copper bar mechanism can be a little temperamental, so these S values have to be changed from time to time using intuition, as they cannot be zeroed with G92.

Optimised Stepper motor settings:

The GRBL setup for the motors is as follows:

 $1=180 
 $2=0  
 $3=0 
 $4=0 
 $5=0 
 $6=0 
 $10=2 
 $11=0.010 
 $12=0.002 
 $13=0 
 $20=0 
 $21=0 
 $22=1 
 $23=1 
 $24=500.000 
 $25=500.000 
 $26=250 
 $27=1.000 
 $30=1000 
 $31=0 
 $32=0 
 $100=14.815 
 $101=14.815
 $102=95.000 
 $110=5000.000 
 $111=250.000 
 $112=250.000 
 $120=100.000 
 $121=20.000 
 $122=20.000 
 $130=200.000 
 $131=200.000 
 $132=200.000 

Improving the Y axis rotation

We noticed that the gears on the motor would slip under the weight of the rotating platform hosting the X motor at angles above 26º. In order to prevent this we decided to look for a suitable gear reduction design to the Y NEMA17 motor. We found the following design :

https://www.printables.com/model/281222-nema17-planetary-gearbox/files

We took the following .stl file. modified it to our design and uploaded the different reducer parts into chitabox for print. The first print with the resin printer did not turn out correctly as I did not separate all of the parts correctly.

We reprinted with the Bamboo X1 Carbon printer in PLA to save time.

The reducer has allowed us to hold the position past 90º which is fantastic.

G-ing the code

We’ve reached the point were we want to feed the machine a predefined set of instructions in .gcode format. Let’s start with a very simple shape.

; Remember to run the inizialitation sequence

; --- Side 1 ---
G1 Z50 F1000       ; Feed 50mm of wire 

G1 X87 F500        ; Close the gap (~90 degrees). We should calibrate the aprox dist.
G1 X177 F500       ; Bend 90 degrees more (87 + 90 = 177 total)

; --- Side 2 ---

G1 X0 F500           ; Bring machine home
G1 Z50 F1000       ; Feed next side
G1 X87 F500        ; 
G1 X177 F500       ;

; --- Side 3 ---
G1 X0 F500           ;
G1 Z50 F1000       ;
G1 X87 F500        ; 
G1 X177 F500       ;

; --- Side 4 ---
G1 X0 F500           ;
G1 Z50 F1000       ;
G1 X87 F500        ; 
G1 X177 F500       ;

M5 ; (Optional) stop any spindles or motors
M30 ; End of program

Now let’s try to do the Eiffel Tower or a BAUHAUS chandelier.

Main Script

The main script has two main components: 1) the machine control loop (including Main menu and GCODE processing) 2) the svg to gcode code

import serial
import time
import os
from svgpathtools import svg2paths
from math import atan2, degrees, sqrt

# --- Serial & G-code File Config ---
serial_port = "COM9"  # Replace with your serial port
baud_rate = 115200     # Default baud rate for Grbl
timeout = 1            # Timeout in seconds

# Define the G-code file name (must be in same folder as this script)
gcode_file_name = "square_bend.gcode" 

# --- Predefined Initialization Commands ---
initialization_commands = [
    "G21",  # Set units to millimeters
    "G90",  # Set to absolute positioning
    "$H",   # Home all axes
    "G92 X0Y0Z0",  # Set current position as origin
]

# --- Helper: Send G-code to Grbl ---
def send_gcode(grbl, gcode):
    grbl.write((gcode.strip() + "\n").encode())
    time.sleep(0.1)  # Adjust delay if necessary
    response = []
    while grbl.in_waiting > 0:
        line = grbl.readline().decode().strip()
        response.append(line)
    return response

# --- Run G-code File ---
def run_gcode_file(grbl, file_path):
    if not os.path.isfile(file_path):
        print(f"File not found: {file_path}")
        return

    with open(file_path, 'r') as f:
        lines = f.readlines()

    print(f"Sending G-code from {file_path}...")
    for line in lines:
        stripped = line.strip()
        if stripped == "" or stripped.startswith(";"):
            continue  # Skip empty lines and comments
        print(f"> {stripped}")
        response = send_gcode(grbl, stripped)
        for resp in response:
            print(resp)
    print("Finished running file.")

# --- SVG to Bending Instructions Converter ---

# Gcode generation variables
gap_angle_left = 92
gap_angle_right = -65

# Helper functions for SVG processing
def calculate_distance(p1, p2):
    return sqrt((p2[0] - p1[0])**2 + (p2[1] - p1[1])**2)

def calculate_angle(p1, p2):
    return degrees(atan2(p2[1] - p1[1], p2[0] - p1[0]))

def normalize_angle(angle):
    while angle > 180:
        angle -= 360
    while angle < -180:
        angle += 360
    return angle

from math import atan2, degrees, pi

def angle_between_segments(p1, p2, p3):
    # Vector A (from p1 to p2)
    ax, ay = p2[0] - p1[0], p2[1] - p1[1]
    # Vector B (from p2 to p3)
    bx, by = p3[0] - p2[0], p3[1] - p2[1]

    # Angle of each vector
    angle1 = atan2(ay, ax)
    angle2 = atan2(by, bx)

    # Difference in angles
    delta = degrees(angle2 - angle1)

    # Normalize between -180 and 180
    while delta <= -180:
        delta += 360
    while delta > 180:
        delta -= 360

    return delta


def extract_points_from_svg(svg_file):
    """
    Extract key vertices from all SVG paths.
    Returns a list of (x, y) points where the wire should bend.
    """
    paths, _ = svg2paths(svg_file)
    points = []

    for path in paths:
        for segment in path:
            start = (segment.start.real, segment.start.imag)
            end = (segment.end.real, segment.end.imag)
            # Add the starting point if it's the beginning of the path or different from last point
            if not points or points[-1] != start:
                points.append(start)
            # Add the end point if different
            if points[-1] != end:
                points.append(end)
    return points

def compute_instructions_absolute(points, angle_threshold=1):
    instructions = []
    if len(points) < 2:
        return instructions
    instructions.append(f"M3 S50")
    i = 0
    while i < len(points) - 1:
        start = points[i]
        next_point = points[i + 1]
        segment_length = calculate_distance(start, next_point)
        last_angle = calculate_angle(start, next_point)

        j = i + 1
        while j + 1 < len(points):
            angle = calculate_angle(points[j], points[j + 1])
            if abs(normalize_angle(angle - last_angle)) > angle_threshold:
                break
            segment_length += calculate_distance(points[j], points[j + 1])
            j += 1

        instructions.append(f"G0 Y{round(segment_length)}")
        instructions.append(f"G92 Y0")
        

        if j + 1 < len(points):
            # p1 = last point of the previous straight run
            # p2 = the actual corner / bend point
            # p3 = the first point of the next run
            p1 = points[j - 1]
            p2 = points[j]
            p3 = points[j + 1]

            bend = angle_between_segments(p1, p2, p3)
            abs_bend = normalize_angle(bend)
            if(abs_bend >= 0):
                angle_gap = abs_bend + gap_angle_left
                instructions.append(f"G0 X{round(angle_gap)}")
                instructions.append(f"G0 X0")
                instructions.append(f"")
            else:
                instructions.append(f"M3 S110")
                instructions.append(f"G0 X{round(180)}")
                instructions.append(f"M3 S50")
                print(f"Angle: {abs_bend + gap_angle_right}")
                angle_gap = 180 + abs_bend + gap_angle_right
                instructions.append(f"G0 X{round(angle_gap)}")
                instructions.append(f"M3 S110")
                instructions.append(f"G0 X0")
                instructions.append(f"M3 S50")
                instructions.append(f"")
            

        i = j

    return instructions

def export_instructions(instructions, output_file="shape.gcode"):
    with open(output_file, 'w') as f:
        f.write("; Custom wire bending instructions\n")
        for instr in instructions:
            f.write(instr + "\n")

def svg_to_instructions(svg_path, output_path="shape.gcode"):
    print(f"Parsing SVG: {svg_path}")
    points = extract_points_from_svg(svg_path)
    if len(points) < 2:
        print("Not enough points extracted.")
        return
    print(f"Extracted {len(points)} points.")
    instructions = compute_instructions_absolute(points)
    export_instructions(instructions, output_path)
    print(f"Instructions written to {output_path}")

# --- Main Menu ---
try:
    # Open the serial port to the Grbl board
    with serial.Serial(serial_port, baud_rate, timeout=timeout) as grbl:
        print(f"Connected to {serial_port} at {baud_rate} baud.")
        grbl.write(b"\r\n\r\n")
        time.sleep(2)
        grbl.flushInput()

        while True:
            print("\nMenu:")
            print("1. Enter G-code manually")
            print("2. Run initialization sequence")
            print("3. Run G-code file")
            print("4. SVG to G-code")
            print("5. Exit")
            choice = input("Select an option: ")

            if choice == "1":
                while True:
                    gcode = input("Enter G-code (or type 'back'): ")
                    if gcode.lower() == "back":
                        break
                    response = send_gcode(grbl, gcode)
                    for line in response:
                        print(line)

            elif choice == "2":
                print("Running initialization sequence...")
                for cmd in initialization_commands:
                    print(f"Sending: {cmd}")
                    response = send_gcode(grbl, cmd)
                    for line in response:
                        print(line)
                print("Initialization complete.")

            elif choice == "3":
                gcode_file_name = input("Enter SVG file name: ")
                script_dir = os.path.dirname(os.path.abspath(__file__))
                gcode_file_path = os.path.join(script_dir, gcode_file_name)
                run_gcode_file(grbl, gcode_file_path)

            elif choice == "4":
                svg_name = input("Enter SVG file name: ") #Make sure it is in the same folder
                if not svg_name.lower().endswith(".svg"):
                    svg_name += ".svg"
                script_dir = os.path.dirname(os.path.abspath(__file__))
                svg_path = os.path.join(script_dir, svg_name)
                svg_to_instructions(svg_path)

            elif choice == "5":
                print("Exiting...")
                break

            else:
                print("Invalid option. Please try again.")

except serial.SerialException as e:
    print(f"Error: {e}")
except KeyboardInterrupt:
    print("\nScript interrupted by user.")

06— Making the Machine: Physical Construction

About the Mechanical CAD

The mechanical design is a heavily modified version of the original design with a number of additions. Several pieces like the straightening assembly and the planetary gear reduction were adapted from pre-existing designs (straightener and planetary gear reducer). Additionally, the extrusion mechanism is produced by Creality and can be sourced here. All fasteners are common metric machine screws and bolts. These fasteners can be sourced at your local hardware store. The aluminum pipe may be tricky to source as it was just scrap laying around the lab. Fits have been adjusted for a well calibrated 3D printer (we used a Bambu Carbon X1).

Two important notes

Project Files

Below is the included final Assembly CAD file to reproduce the project. Components types are clearly labelled in the assembly file names. The .step assembly (with labeled components) are provided for the mechanical assembly and a KiCAD file is provided for the milled PCB.