◄ 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 ESP32 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, I wanted a modern, accessible solution. I built a Client-Side Web Application using HTML, CSS, and Vanilla JavaScript.

The interface allows users to upload a PNG/JPG, adjust the contrast threshold via a slider, and preview the trajectory path. The core challenge was using the HTML5 <canvas> API to read the raw pixel data arrays and identify the dark paths that the magnet should follow.

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

The most complex part of my code was dealing with physical limitations. Since we are using a permanent neodymium magnet, we don't have a Z-Axis to "lift the pen". Everything the machine does leaves a mark in the sand.

To solve this, I wrote a custom algorithm:

  • Edge Detection: It scans the canvas to find the boundaries between dark and light pixels.
  • Nearest Neighbor: Instead of jumping randomly, it starts at coordinate (0,0) and mathematically searches for the closest next point, forcing a single, continuous toolpath.
  • Mapping to physical world: A formula scales the 500x500 web pixels into the 180x180 milimeters of our physical CNC bed: ((pixelX / 500) * 180).
Camotics Simulation
Simulating the continuous G-Code. Notice how it enters from the origin (0,0).

04. WIRELESS NETWORKING (OTA)

To eliminate the need for a USB connection to the machine, I programmed the XIAO 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 (port 80). The ESP32 receives the payload, acknowledges it, and forwards the commands to the stepper motor drivers.

Wi-Fi Payload Success
Successful payload transmission over Wi-Fi.

05. HERO SHOT

Testing the full integration: From image upload, to local 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 (C++)
#include <WiFi.h>
#include <WebServer.h>

const char* ssid = "YOUR_WIFI";
const char* password = "YOUR_PASSWORD";

WebServer server(80);

void setup() {
  Serial.begin(115200);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) { delay(500); }

  // Receive the G-Code payload via HTTP POST
  server.on("/upload", HTTP_POST, []() {
    if (server.hasArg("plain")) {
      String gcodeRecibido = server.arg("plain");
      Serial.println("=== NEW MISSION RECEIVED ===");
      Serial.println(gcodeRecibido); 
      server.send(200, "text/plain", "Mission Received by ESP32");
    } else {
      server.send(400, "text/plain", "Error: Empty payload");
    }
  });

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

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