◄ PAGE 11 PAGE 13 ►
WEEK #12

MACHINE BUILDING

Individual Log: Software Architecture & Wireless Communication.

00. MISSION BRIEFING

This week, our team built a CNC Sand Plotter (Sisyphus style) where a neodymium magnet moves a steel ball through sand to draw patterns.

My specific role: I was responsible for the software architecture and communications. I designed a custom web-based CAM tool ("Sand-Path Mission Control") that runs entirely on the browser to convert images into continuous G-Code, and developed the OTA (Over-The-Air) communication protocol to send this data wirelessly to the ESP32-C6 controller.

CO-OP MISSION: TEAM PROTOCOLS

To see the full mechanical design, electronic wiring, and assembly of our machine, please visit our Group Project Page.

🛠️ OPEN CNC TEAM LOGS

01. SAND-PATH MISSION CONTROL (APP)

Instead of relying on external software, I built my own **Client-Side Web Application**. This tool allows the user to upload a PNG image, adjust the contrast threshold, preview the generated path, and send the G-Code wirelessly to the ESP32 CNC controller.

🚀 LAUNCH SAND-PATH APP

*Note: The CNC machine and the laptop must be connected to the same Wi-Fi network for the wireless transmission to work.

02. THE APP INTERFACE (UI/UX)

Instead of relying on clunky desktop software or installing dependencies, I wanted a modern, accessible, and frictionless solution. I built a Client-Side Web Application using HTML, CSS, and Vanilla JavaScript.

The interface allows users to upload a PNG/JPG file, adjust the contrast threshold via a slider (to define what is considered a "drawing path"), and preview the trajectory. The core technical achievement here is using the HTML5 <canvas> API to read the raw pixel data arrays directly in the browser. By extracting the RGBA values of each pixel, the script identifies the dark paths that the physical magnet should follow, translating digital pixels into a virtual coordinate map without needing any backend server processing.

App Interface Screenshot
The Sand-Path Mission Control interface running locally in the browser.

03. THE LOGIC: IMAGE TO G-CODE

THE PERMANENT MAGNET PARADOX

Generating G-Code for a traditional CNC or 3D printer is straightforward: you lift the Z-axis, move to the next coordinate, and lower it. However, dealing with a permanent neodymium magnet beneath a table presents a unique physical limitation: we don't have a Z-Axis. Everything the machine does leaves a mark in the sand. If the magnet jumps from one corner to another, it will draw a straight line through the artwork.

To solve this paradox, I had to write a custom mathematical algorithm in JavaScript:

  • Edge Detection & Mapping: It scans the canvas to find the boundaries between dark and light pixels, generating an array of valid target coordinates.
  • Nearest Neighbor Algorithm: Instead of jumping randomly through the array, the code starts at the origin coordinate (0,0). It then mathematically searches the entire array for the closest next point using the Pythagorean distance formula (dx²+dy²). Once found, it moves there and repeats, forcing a single, continuous, non-lifting toolpath.
  • Physical World Scaling: Finally, a formula scales the 500x500 web pixels into the 180x180 millimeters of our physical CNC bed: ((pixelX / 500) * 180), outputting standard G0/G1 commands.
Camotics Simulation
Simulating the continuous G-Code. Notice how it enters from the origin (0,0) and traces everything without breaks.

04. WIRELESS NETWORKING (OTA)

To completely eliminate the need for a USB connection (since the machine sits on a standalone table), I programmed the ESP32-C6 to act as an asynchronous HTTP Server on our local 2.4GHz Wi-Fi network.

When the user clicks the "Send to ESP32" button on the webpage, a JavaScript fetch() request packages the entire G-Code string and sends an HTTP POST request to the microcontroller's IP address.

Overcoming the CORS Block: One of the biggest challenges in this step was dealing with browser security. Modern browsers block HTTP requests sent from local files (or local web servers) to external IP addresses due to CORS (Cross-Origin Resource Sharing) policies. To bypass this, I had to program the ESP32 to explicitly handle HTTP OPTIONS requests and reply with the headers Access-Control-Allow-Origin: *. This tells the browser that the ESP32 is a safe device, allowing the G-Code payload to pass through flawlessly.

Wi-Fi Payload Success
Successful payload transmission over Wi-Fi handled by the ESP32 WebServer.

05. HERO SHOT

Testing the full integration: From image upload, to local continuous G-Code processing, to wireless transmission, and finally, physical movement in the sand!

Software and Hardware working together perfectly.

06. THE CODE VAULT

// 1. NEAREST NEIGHBOR ALGORITHM (JAVASCRIPT)
// Finding the closest next pixel to create a continuous line
while (edgePoints.length > 0) {
    let nearestIdx = 0;
    let minDist = Infinity;

    for (let i = 0; i < edgePoints.length; i++) {
        let dx = current.x - edgePoints[i].x;
        let dy = current.y - edgePoints[i].y;
        let d = dx*dx + dy*dy;
        if (d < minDist) {
            minDist = d;
            nearestIdx = i;
        }
    }
    current = edgePoints.splice(nearestIdx, 1)[0];
    path.push(current);
}
// 2. ESP32 OTA RECEIVER & CORS HANDLING (C++)
#include <WiFi.h>
#include <WebServer.h>

const char* ssid ="WIFI NAME";
const char* password ="PASSWORD";

WebServer server(80);

void setup() {
  Serial.begin(115200);
  delay(1000);
  
  WiFi.begin(ssid, password);
  Serial.print("Conectando al Wi-Fi");
  //THE LAPTOP AND THE ESP32 MUST BE ON THE SAME WIFI NETWORK FOR THIS TO WORK
  while (WiFi.status() != WL_CONNECTED) { 
    delay(500); 
    Serial.print("."); 
  }

  Serial.println("\n¡Conexion exitosa!");
  Serial.print("=== TU IP DE HOY ES: ");
  Serial.print(WiFi.localIP());
  Serial.println(" ===");

  // PREFLIGHT CORS HANDLER
  server.on("/upload", HTTP_OPTIONS, []() {
    server.sendHeader("Access-Control-Allow-Origin", "*");
    server.sendHeader("Access-Control-Max-Age", "10000");
    server.sendHeader("Access-Control-Allow-Methods", "POST,GET,OPTIONS");
    server.sendHeader("Access-Control-Allow-Headers", "*");
    server.send(204);
  });

  // GCODE PAYLOAD HANDLER
  server.on("/upload", HTTP_POST, []() {
    server.sendHeader("Access-Control-Allow-Origin", "*");
    
    if (server.hasArg("plain")) {
      String gcodeRecibido = server.arg("plain");
      Serial.println("\n=== NUEVA MISION RECIBIDA ===");
      Serial.println(gcodeRecibido); 
      Serial.println("=============================");
      
      server.send(200, "text/plain", "¡Mision Recibida por la ESP32 de Javi!");
    } else {
      server.send(400, "text/plain", "Error: El payload llego vacio");
    }
  });

  server.enableCORS(true);
  server.begin();
}

void loop() {
  server.handleClient();
}