Back to Home

Machine Design & Building

Group Assignment Focus

This week's assignment was to design a machine that includes mechanisms, actuation, automation, and application, as well as to build the mechanical parts and operate it manually.

Our Project: Water Marbling CNC

We decided to build a CNC machine specialized in Water Marbling. This technique involves dropping paint onto the surface of thickened water to create patterns, which are then transferred to paper or fabric. Our machine automates the drop placement and the "combing" process to create consistent, complex geometric patterns.

Initial sketches

The Team

Member Core Contribution Portfolio
Selene Roman Structural design, mechanical movement assembly, and interface development. Individual Contribution
Kamil Gallardo Electronics, programming, and motion testing. Individual Contribution
Ana Tellez Testing and calibration of the silk painting process. Individual Contribution
Carlos Perez Structural assembly, and Color switch logic design. Individual Contribution

Fabrication & Building Process

2D Axis - Mechanical Movement Development

The mechanical motion of our machine is based on the Urumbu 3D concept, adapted to our specific requirements. The design features a 2-axis motion system and a custom toolhead to house the color switching mechanism. We fully integrated the dual-axis movement logic from the Urumbu 2022 and Urumbu ULB architectures. The dual-axis system consists of a support tube parallel to the work surface. This tube houses an X-axis rail that slides internally, while the entire tube assembly moves across the workspace via two Y-axis rails. The combinated movement of these three rails allows the XY movement.

The stepper motors are mounted to the corners of the tubes, using a string-drive system to move the carriage in the desired direction.

The motion principle of our machine follows the CoreXY movement system. CoreXY is a kinematic motion architecture where two fixed stepper motors collaborate through a cross-belt system to displace a toolhead across the Cartesian plane. Its operating principle is based on the addition and subtraction of motion vectors: synchronous rotation of both motors in the same direction produces displacement along the X-axis, while counter-rotation generates movement along the Y-axis. By keeping the motors static on the chassis, the inertial mass of the mobile system is drastically reduced, allowing for higher accelerations and operating speeds with minimal structural vibration.

Initial sketches

Materials and Manufacturing

T-slotted aluminum framing rails were used as the primary components for the machine's chassis. The remaining mechanical parts were 3D printed, including original designs as well as files retrieved from the UrumbotXY-Fall2022 repository. Select files from the repository were modified based on testing results to better suit our project's needs.

Quantity Material
8 T-slotted aluminum framing rails
4 Perfil Tubular Rectangular
28 M4 Screws
21 M5 Screws
12 M3 Screws
2 M8 Screws
9 Type V Delrin Wheels
8 Dual V-Type Delrin Wheels
4 Bearings
4 Meters of Kevlar string

Modules

The initial step involved assembling all the modules responsible for the XY movement system.

Y-Plates

The Y-plates serve as the modules that attach to the central rail, allowing it to slide across the entire workspace to generate Y-axis movement. Each unit consists of three V-type Delrin wheels on the bottom, which glide along the tube to ensure structural stability. On the top, two dual V-wheels function as pulleys to drive the motion via the string system. This component is duplicated, as one is required for each side of the central rail.

Motor and pulley

The motor module is secured to two corners of the structure. It features two pulleys, one mounted directly on the motor shaft and another aligned with it, which serve to wind and store the cable as it accumulates during operation.

Vista lateral del ensamblaje del motor
Vista superior del ensamblaje del motor

Corners pulley

The pulley system modules are positioned at the corners opposite to the motors. Each module houses two dual V-wheels that act as pulleys to guide the string system. These pulleys are set at different heights to prevent the strings from overlapping. Two identical replicas of this module are required for the assembly.

Vista lateral del ensamblaje del motor
Vista superior del ensamblaje del motor

Carriage module

Finally, the toolhead carriage module is composed of three V-type wheels and a dual-plate assembly. The upper section features a dedicated component where the start and end points of the strings are secured and tied, completing the motion loop.

Assembly

The assembly process began with the base frame, which consists of four profiles joined by four 3D-printed corner brackets.

Initial sketches

To assemble the top frame, only three sides are initially connected using the 3D-printed joints. This sequence is intentional, as it allows for an easier insertion of the central rail into the structure.

Initial sketches

The motor modules are added and secured to the profile using screws. Ensure they are perfectly aligned with the rail to maintain proper motion geometry.

Initial sketches

To assemble the central rail, the first Y-plate is mounted first, followed by the carriage plate, and finally, it is secured by attaching the second Y-plate.

Initial sketches

Next, the central rail slides onto the two parallel rails, and the frame is closed by attaching the fourth profile. This step includes the pulley system modules, which must be inserted and linearly aligned with the profile.

Initial sketches

Estructure with threads

The string routing begins at one side of the carriage plate, leading into the Y-plate pulley. From there, it enters the motor module from the inside, starting from the bottom of the first spool and crossing in a figure-eight pattern between the two pulleys. It then exits from the outside toward the upper pulley on the opposite side, crosses over, and enters the lower corner pulley. Finally, it passes through a Y-plate pulley and terminates at the opposite corner of the carriage plate.

Initial sketches

Testing

A dry run was performed without the strings to verify that the wheels rotated smoothly and to ensure there was minimal rolling friction.

Once the string was installed, the structure's mobility was tested again to verify smooth movement and proper tensioning across the entire system.

Ink rotary System

The machine's toolhead consists of a rotary system that houses three pipettes of different colors. It is powered by a DC motor that provides the rotational movement for color selection, and a servomotor that acts as the plunger, pressing the pipette to release the ink drop

Initial sketches

BOARDS

CNC-SHIELD

SCHEMATIC DESIGN

Initial sketches

Motor X

To control the X-axis, a NEMA 17 motor and the DVR8825 controller were used. In the schematic, the DVR8825 pins are connected to external pins via global labels, in accordance with the DVR8825 pinout.

Initial sketches

It is also important to note that the X and Y motors share four pins: ENABLE, M0, M1, and M2, since they are intended to have the same step size and the same enable signal.

Motor Y

To control the Y-axis, a NEMA 17 motor and the DVR8825 controller were used. In the schematic, the DVR8825 pins are connected to external pins via global labels, in accordance with the DVR8825 pinout. Here it was only used the specific pins for this second driver.

Power Supply

To regulate and reduce the initial voltage it was used the AMS1117. It requires two capacitors for device stability, one in the input (12V) and one in output (3.3v). Both of 10µF. Then more pins were added, two for 12V and two for GND. It also was added two more pins for 3.3V and GND also were added to the output of the regulator.

Limit Switch

Two pairs of pins were added to connect two limit switches, designed to stop the CNCs movement if it exceeds the mechanical limit. When the limit switch is triggered, it interrupts the power supply.

Capacitors

The capacitors are 100 μF and are connected in parallel. They are configured as decoupling capacitors to filter out voltage spikes and protect the drivers.

PCB DESIGN

After using KiCads calculation tool, two trace widths were selected: 2 mm and 0.8 mm. The 2 mm width was used for the 12 V power supply to ensure the traces were robust. The 0.8 mm width was used for all other traces.

  • The outline width is 2 mm and its layer is Edge.Cuts.
  • The track's width is 0.8 mm - 2 mm and its layer is F.Cu.
  • The Holes layer is User.1.
Initial sketches

Then using the change side tool, the board was flipped. That action was important because the drivers will be on the underside of the plate.

Initial sketches

RESULT

Initial sketches

PI PICO 2

Initial sketches

For the Pi Pico 2, only this component was used, along with external pins connected to the boards pins via global labels. Since all ground pins are connected internally, ground pins were omitted from the board layout, and four separate ground pins were added instead. Four separate 3.3V pins were also added.

PCB

After using KiCads calculation tool only one width was selected: 0.8 mm. That width was used for all the traces. Using the change side tool the Raspberry Pi Pico 2 was flipped to put it on the underside of the plate.

  • The outline width is 2 mm and its layer is Edge.Cuts.
  • The track's width is 0.8 mm - 2 mm and its layer is F.Cu.
  • The Holes layer is User.1.
Initial sketches

RESULT

Initial sketches

EXTRA PINS

SCHEMATIC DESIGN

Initial sketches

Power

Pins were placed for the power supply, and two decoupling capacitors were installed to prevent voltage spikes.

Motor

To control the N20 motor and the servo motor, global labels were assigned to their pins to connect them to external pins and the H-bridge.

H-BRIDGE

The H-bridge was installed to enable programmable control of the N-20 motor. Subsequently, pins were added to connect the motor, along with a capacitor to prevent voltage spikes from reaching the N-20 motor.

PCB DESIGN

After using ALTIUM calculation tool a 0.8 mm width was selected. Two layers were used. The red tracks are for the PCB, and the blue ones are for two cables we used connect in order to not use 0 resistors.

  • The outline width is 2 mm, and its layer is Mechanical Layer.
  • The red tracks width is 0.8 mm, and its layer is Top Layer.
  • The blue tracks width is 0.8 mm, and its layer is Bottom Layer.
Initial sketches

RESULT

Initial sketches

EXTRA PINS

SCHEMATIC DESIGN

Initial sketches

The extra pins board was created because more GND pins were needed during the process of connecting the modules. The schematic consists of just three columns of four pins connected to each other.

PCB DESIGN

After using KiCads calculation tool only one width was selected: 0.8 mm. That width was used for all the traces.

  • The outline width is 2 mm and its layer is Edge.Cuts.
  • The track's width is 0.8 mm - 2 mm and its layer is F.Cu.
  • The Holes layer is User.1.
Initial sketches

RESULT

Initial sketches

EXPORTING AS SVG

To export SVG first we must go to the top left corner and press file, after that select Fabrication Outputs. Then choose Gerbers and change the plot format to SVG. After that select the layers used and select the folder where the files are going to be saved. Finally click on plot.

CUTTING

The tools for cutting are these ones: The first one is for drilling and making the outline. The second one is for drilling the holes and the third one is for the tracks.

To place the copper plate first we have to use double-sided tape in the back of the plate, then place it over the sacrifice bed and finally putting them inside the SRM-20 monofab.

Then we must set the origin in VPANEL. Change the tool and add our files

RESULTS

SOLDERING RESULTS

PROGRAMMING

CODE

cnc_controller.ino
Arduino
#include <AccelStepper.h>
#include <Servo.h>

// -------- PINS (PICO 2) --------
#define enableSteppers 0
#define stepA 3
#define dirA  7
#define stepB 1
#define dirB  2

const int PWMA = 8;
const int AIN1 = 11;
const int AIN2 = 12;
const int pinServo = 15;

// -------- OBJECTS --------
AccelStepper motorA(1, stepA, dirA);
AccelStepper motorB(1, stepB, dirB);
Servo myservo;

// -------- CONFIG --------
const float velocidadNema = 200.0;
const float aceleracionNema = 400.0;

// N20
const float MS_POR_VUELTA = 30000.0;
const float MS_POR_GOTERO = MS_POR_VUELTA / 3.0;

// SERVO
const int SERVO_REPOSO = 180;
const int SERVO_GOTA   = 60;

// GLOBAL POSITION
long posX = 0;
long posY = 0;

void setup() {
  Serial.begin(115200);

  pinMode(enableSteppers, OUTPUT);
  digitalWrite(enableSteppers, LOW);

  motorA.setMaxSpeed(velocidadNema);
  motorA.setAcceleration(aceleracionNema);

  motorB.setMaxSpeed(velocidadNema);
  motorB.setAcceleration(aceleracionNema);

  pinMode(PWMA, OUTPUT);
  pinMode(AIN1, OUTPUT);
  pinMode(AIN2, OUTPUT);

  pararN20();

  myservo.attach(pinServo, 500, 2400);
  myservo.write(SERVO_REPOSO);

  Serial.println("CNC AUTO READY");
}

void loop() {
  if (Serial.available()) {
    String comando = Serial.readStringUntil('\n');
    comando.trim();

    // -------- JOG --------
    if (comando.startsWith("JOG")) {
      int c1 = comando.indexOf(',');
      int c2 = comando.indexOf(',', c1 + 1);

      int dx = comando.substring(c1 + 1, c2).toInt();
      int dy = comando.substring(c2 + 1).toInt();

      posX += dx;
      posY += dy;

      moverCoreXY(dx, dy);

      Serial.println("JOG OK");
    }

    // -------- GOTO --------
    else if (comando.startsWith("GOTO")) {
      int c1 = comando.indexOf(',');
      int c2 = comando.indexOf(',', c1 + 1);

      long targetX = comando.substring(c1 + 1, c2).toInt();
      long targetY = comando.substring(c2 + 1).toInt();

      long dx = targetX - posX;
      long dy = targetY - posY;

      posX = targetX;
      posY = targetY;

      moverCoreXY(dx, dy);

      Serial.println("GOTO OK");
    }

    // -------- DROP --------
    else if (comando == "DROP") {
      myservo.write(SERVO_GOTA);
      delay(400);

      digitalWrite(AIN1, HIGH);
      digitalWrite(AIN2, LOW);
      analogWrite(PWMA, 160);

      delay(2000);

      pararN20();
      delay(100);
      myservo.write(SERVO_REPOSO);

      Serial.println("DROP OK");
    }

    // -------- COLOR --------
    else if (comando == "COLOR") {
      digitalWrite(AIN1, HIGH);
      digitalWrite(AIN2, LOW);
      analogWrite(PWMA, 180);

      delay((int)MS_POR_GOTERO);

      pararN20();
      Serial.println("COLOR OK");
    }

    // -------- SET ZERO --------
    else if (comando == "SET_ZERO") {
      posX = 0;
      posY = 0;

      motorA.setCurrentPosition(0);
      motorB.setCurrentPosition(0);

      Serial.println("ZERO OK");
    }

    // -------- HOME --------
    else if (comando == "HOME") {
      motorA.moveTo(0);
      motorB.moveTo(0);

      posX = 0;
      posY = 0;

      Serial.println("HOME OK");
    }
  }

  motorA.run();
  motorB.run();
}

// -------- COREXY --------
void moverCoreXY(long dx, long dy) {
  long moveA = dx + dy;
  long moveB = dx - dy;

  motorA.move(moveA);
  motorB.move(moveB);
}

// -------- STOP N20 --------
void pararN20() {
  digitalWrite(PWMA, LOW);
  digitalWrite(AIN1, LOW);
  digitalWrite(AIN2, LOW);
}
        

CODE EXPLANATION

This code functions as the control for a CNC machine that uses a CoreXY system for positioning and a circular carousel for dispensing. In this setup, the stepper motors move the main assembly, a DC motor (N20) rotates the carousel base to select different droppers, and a servo motor acts as a mechanical press to squeeze those droppers against a stationary wall.

Pin Definitions and Hardware Mapping

The code starts by assigning the Raspberry Pi Pico 2's physical pins to the machine's components. The stepper motors, which handle the X and Y movement of the assembly, are assigned to pins 1, 2, 3, and 7, while pin 0 is used as a unique enable pin to power the drivers. The N20 motor (the carousel base) is connected to an H-bridge via pins PWMA, AIN1, and AIN2, allowing for speed and direction control. Finally, pin 15 is dedicated to the servo that presses the droppers.

Configuration and Calibration Constants

This section defines the "timing" and "physics" of the machine. The variables velocidadNema and aceleracionNema establish how quickly the assembly moves between points. For the carousel, the constant MS_POR_VUELTA (30,000ms) represents the time it takes for the DC motor to complete one full rotation, which is then used to calculate MS_POR_GOTERO. This ensures that when the machine needs to switch "colors," the DC motor runs for the exact duration required to advance the circular base to the next dropper position.

System Setup and Initial State

The setup() function initializes the hardware once the machine is powered on. It opens a serial communication line at 115200 baud to receive instructions and configures the stepper motors' speed and acceleration profiles through the AccelStepper library. To ensure safety, it immediately calls pararN20() to make sure the circular base is not spinning and moves the servo to SERVO_REPOSO (180°), keeping the pressing arm retracted so it doesn't accidentally hit the droppers or the wall during startup.

Serial Command Parsing

The loop() function acts as the brain of the machine, constantly listening for incoming text strings from a connected computer. When a command like JOG, GOTO, or DROP is received, the code uses string manipulation functions (indexOf and substring) to pull out coordinate values or specific instructions. This allows the user to send simple text commands to move the carousel assembly to a specific spot or trigger a dispensing sequence.

CoreXY Movement Logic

The moverCoreXY function handles the specific mathematics required for the belt-driven system. In a CoreXY machine, the motors must work in tandem to move the toolhead; the code calculates the steps for each motor using the formulas moveA = dx + dy and moveB = dx - dy. This translates standard X and Y distances into the synchronized motor pulses needed to shift the entire carousel assembly linearly or diagonally across the workspace.

Carousel Rotation and Dropper Pressing

The DROP and COLOR commands manage the interaction between the circular base and the press. When DROP is called, the servo rotates to 60° (SERVO_GOTA) to press the dropper against the wall, while the N20 motor is briefly activated to provide the necessary force or positioning. The COLOR command specifically uses the MS_POR_GOTERO timing to rotate the circular base by one increment, essentially "indexing" the carousel to the next available dropper.

Motion Execution and Calibration

At the very end of the main loop, motorA.run() and motorB.run() are called to process the motor steps. These functions are "non-blocking," meaning they only move the motors a tiny fraction at a time, which allows the machine to keep listening for new commands even while it is moving. Additionally, the SET_ZERO and HOME commands allow the user to define or return to a starting "origin" point, ensuring that the machine's coordinate system remains accurate throughout the plotting process.

Interface

At the very end of the main loop, motorA.run() and motorB.run() are called to process the motor steps. These functions are "non-blocking," meaning they only move the motors a tiny fraction at a time, which allows the machine to keep listening for new commands even while it is moving. Additionally, the SET_ZERO and HOME commands allow the user to define or return to a starting "origin" point, ensuring that the machine's coordinate system remains accurate throughout the plotting process.

cnc_controller_gui.py
Python
import sys
import serial
import time

from PyQt5.QtWidgets import (
    QApplication, QWidget, QPushButton, QVBoxLayout,
    QHBoxLayout, QLabel, QComboBox, QGridLayout
)
from PyQt5.QtCore import Qt


class CNCManual(QWidget):
    def __init__(self):
        super().__init__()

        self.serial = None
        self.pos_x = 0
        self.pos_y = 0

        try:
            self.serial = serial.Serial("COM12", 115200, timeout=1)
            print("✅ Conected")
        except:
            print("❌ Connection error")

        self.puntos = []
        self.grid_size = 5

        self.current_x = 0
        self.current_y = 0

        self.initUI()

    def initUI(self):
        self.setWindowTitle("WATER MARBLING CNC")
        self.resize(1100, 1200)

        main_layout = QVBoxLayout()

        self.estado = QLabel("● DESCONECTED")
        self.estado.setAlignment(Qt.AlignCenter)
        self.estado.setStyleSheet("font-size:20px; color:red;")

        if self.serial:
            self.estado.setText("● CONECTED")
            self.estado.setStyleSheet("font-size:20px; color:green;")

        main_layout.addWidget(self.estado)

        titulo = QLabel("CONTROL CNC PANEL")
        titulo.setAlignment(Qt.AlignCenter)
        titulo.setStyleSheet("font-size:36px; font-weight:bold; color:#333;")
        main_layout.addWidget(titulo)

        self.label_pos = QLabel("X: 0.0   Y: 0.0")
        self.label_pos.setAlignment(Qt.AlignCenter)
        self.label_pos.setStyleSheet("font-size:24px; color:#444;")
        main_layout.addWidget(self.label_pos)

        step_layout = QHBoxLayout()

        step_label = QLabel("DISTANCE (mm)")
        step_label.setStyleSheet("font-size:20px;")

        self.step_box = QComboBox()
        self.step_box.addItems(["40", "60", "80", "100"])
        self.step_box.setStyleSheet("font-size:18px; padding:6px;")

        step_layout.addStretch()
        step_layout.addWidget(step_label)
        step_layout.addSpacing(20)
        step_layout.addWidget(self.step_box)
        step_layout.addStretch()

        main_layout.addLayout(step_layout)
        main_layout.addSpacing(20)

        grid = QGridLayout()

        btn_up = QPushButton("Y+ ↑")
        btn_down = QPushButton("Y- ↓")
        btn_left = QPushButton("X- ←")
        btn_right = QPushButton("X+ →")

        for b in [btn_up, btn_down]:
            b.setFixedSize(140, 140)
            b.setStyleSheet("""
                font-size:28px;
                background-color:#4da6ff;
                color:white;
                border-radius:20px;
            """)

        for b in [btn_left, btn_right]:
            b.setFixedSize(140, 140)
            b.setStyleSheet("""
                font-size:28px;
                background-color:#5ed988;
                color:white;
                border-radius:20px;
            """)

        btn_up.clicked.connect(lambda: self.jog(0, 1))
        btn_down.clicked.connect(lambda: self.jog(0, -1))
        btn_left.clicked.connect(lambda: self.jog(-1, 0))
        btn_right.clicked.connect(lambda: self.jog(1, 0))

        grid.addWidget(btn_up, 0, 1)
        grid.addWidget(btn_left, 1, 0)
        grid.addWidget(btn_right, 1, 2)
        grid.addWidget(btn_down, 2, 1)

        main_layout.addLayout(grid)
        main_layout.addSpacing(20)

        fila1 = QHBoxLayout()

        btn_drop = QPushButton("DROP 🫟")
        btn_color = QPushButton("CHANGE COLOR 🎨")

        btn_drop.setFixedHeight(80)
        btn_color.setFixedHeight(80)

        btn_drop.setStyleSheet("""
            font-size:20px;
            background-color:#f27db7;
            color:white;
            border-radius:20px;
        """)

        btn_color.setStyleSheet("""
            font-size:20px;
            background-color:#c595f9;
            color:white;
            border-radius:20px;
        """)

        btn_drop.clicked.connect(self.drop)
        btn_color.clicked.connect(self.color)

        fila1.addWidget(btn_drop)
        fila1.addWidget(btn_color)

        main_layout.addLayout(fila1)

        fila2 = QHBoxLayout()

        btn_setzero = QPushButton("SET ORIGIN 📌")
        btn_home = QPushButton("MOVE TO ORIGIN 🏠")

        btn_setzero.setFixedHeight(80)
        btn_home.setFixedHeight(80)

        btn_setzero.setStyleSheet("background:#ff944d; color:white; border-radius:20px;")
        btn_home.setStyleSheet("background:#47c2ad; color:white; border-radius:20px;")

        btn_setzero.clicked.connect(self.set_zero)
        btn_home.clicked.connect(self.home)

        fila2.addWidget(btn_setzero)
        fila2.addWidget(btn_home)

        main_layout.addLayout(fila2)

        main_layout.addWidget(QLabel("X →   Y ↑   (Area 22cm x 22cm)"))

        grid_auto = QGridLayout()
        self.grid_botones = []

        for i in range(self.grid_size):
            fila = []
            for j in range(self.grid_size):
                btn = QPushButton("")
                btn.setFixedSize(50, 50)
                btn.clicked.connect(lambda _, x=j, y=i: self.toggle(x, y))
                grid_auto.addWidget(btn, self.grid_size - 1 - i, j)
                fila.append(btn)
            self.grid_botones.append(fila)

        main_layout.addLayout(grid_auto)

        btn_start = QPushButton("START ▶️")
        btn_start.setFixedHeight(90)
        btn_start.setStyleSheet("background:#222; color:white; border-radius:20px;")
        btn_start.clicked.connect(self.ejecutar)

        main_layout.addWidget(btn_start)

        self.setLayout(main_layout)
        self.show()

        self.actualizar_grid()

    def enviar(self, cmd):
        if self.serial:
            self.serial.write((cmd + "\n").encode())

    def actualizar_label(self):
        self.label_pos.setText(f"X: {self.pos_x:.1f}   Y: {self.pos_y:.1f}")

    def actualizar_grid(self):
        for i in range(self.grid_size):
            for j in range(self.grid_size):
                if (j, i) == (self.current_x, self.current_y):
                    self.grid_botones[i][j].setStyleSheet("background:red;")
                elif (j, i) in self.puntos:
                    self.grid_botones[i][j].setStyleSheet("background:#4da6ff;")
                else:
                    self.grid_botones[i][j].setStyleSheet("background:white; border:1px solid black;")

    def jog(self, dx, dy):
        step = int(self.step_box.currentText())

        self.pos_x += dx * step
        self.pos_y += dy * step

        self.current_x += dx
        self.current_y += dy

        self.current_x = max(0, min(self.grid_size - 1, self.current_x))
        self.current_y = max(0, min(self.grid_size - 1, self.current_y))

        self.actualizar_grid()
        self.actualizar_label()

        self.enviar(f"JOG,{dx*step},{dy*step}")

    def drop(self):
        self.enviar("DROP")

    def color(self):
        self.enviar("COLOR")

    def set_zero(self):
        self.pos_x = 0
        self.pos_y = 0
        self.current_x = 0
        self.current_y = 0
        self.actualizar_label()
        self.actualizar_grid()
        self.enviar("SET_ZERO")

    def home(self):
        self.pos_x = 0
        self.pos_y = 0
        self.current_x = 0
        self.current_y = 0
        self.actualizar_label()
        self.actualizar_grid()
        self.enviar("HOME")

    def toggle(self, x, y):
        if (x, y) in self.puntos:
            self.puntos.remove((x, y))
        else:
            self.puntos.append((x, y))
        self.actualizar_grid()

    def ejecutar(self):
        tam_celda = int(self.step_box.currentText())

        self.enviar("SET_ZERO")
        time.sleep(0.5)

        for (x, y) in self.puntos:
            target_x = x * tam_celda
            target_y = y * tam_celda

            self.current_x = x
            self.current_y = y
            self.actualizar_grid()

            self.enviar(f"GOTO,{target_x},{target_y}")
            time.sleep(1.2)

            self.enviar("DROP")
            time.sleep(0.8)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    ventana = CNCManual()
    sys.exit(app.exec_())
        
Initial sketches

Project Files

Mechanical CAD

Solid Works & STL Files

Download