Sending GCODE from ESP32 to Uno (GRBL-MI)

Hello... So long story short i am trying to make a GRBL CNC Drawing plotter. I have an ESP32 and it is connected to blynk. So i send a link, then the ESP32 downloads it and saves it to spiffs and then sends it line by line to the arduino. This is all controlled by blynk. My code:-

#define BLYNK_TEMPLATE_ID   "xxxxx"
#define BLYNK_TEMPLATE_NAME "xxxxx"
#define BLYNK_AUTH_TOKEN    "xxxxx"

#include <WiFi.h>
#include <BlynkSimpleEsp32.h>
#include <HTTPClient.h>
#include <SPIFFS.h>

char ssid[] = "xxxxxx";
char pass[] = "xxxxx";

HardwareSerial SerialGrbl(2);

// ── GRBL buffer config ────────────────────────────────────────────────────────
#define RX_BUFFER_SIZE 128
#define QUEUE_SIZE     150

int  sent_lengths[QUEUE_SIZE];
int  head       = 0;
int  tail       = 0;
int  char_count = 0;

// ── Job state ─────────────────────────────────────────────────────────────────
bool    isStreaming  = false;
bool    isPaused     = false;
String  gcodeURL     = "";
File    gcodeFile;
int     totalLines   = 0;
int     linesSent    = 0;

// ─────────────────────────────────────────────────────────────────────────────
// GRBL response handler — must be called frequently to free buffer space
// ─────────────────────────────────────────────────────────────────────────────
void handleGrblResponse() {
  while (SerialGrbl.available()) {
    String res = SerialGrbl.readStringUntil('\n');
    res.trim();
    if (res.length() == 0) continue;

    Serial.print("GRBL >> ");
    Serial.println(res);

    if (res.startsWith("ok") || res.indexOf("error") != -1) {
      // Free one slot from the buffer queue
      if (head != tail) {
        char_count -= sent_lengths[tail];
        tail = (tail + 1) % QUEUE_SIZE;
      }

      // FIX 4: stop immediately on any GRBL error
      if (res.indexOf("error") != -1) {
        Serial.println("GRBL error — stopping job");
        isStreaming = false;
        gcodeFile.close();
        SerialGrbl.print((char)0x18);   // soft reset
        delay(100);
        SerialGrbl.println("$X");       // unlock
        SerialGrbl.println("M5");       // pen up
        SerialGrbl.println("G0 X0 Y0"); // return home
        Blynk.virtualWrite(V5, "Error: " + res);
        Blynk.virtualWrite(V6, 0);
      }
    }
  }
}

// ─────────────────────────────────────────────────────────────────────────────
// FIX 1: Download entire file to SPIFFS before streaming
// Decouples WiFi jitter from GRBL timing completely
// ─────────────────────────────────────────────────────────────────────────────
bool downloadToSPIFFS(const String& url) {
  Serial.println("Downloading: " + url);
  Blynk.virtualWrite(V5, "Downloading...");
  Blynk.virtualWrite(V6, 0);

  HTTPClient http;
  http.begin(url);
  int code = http.GET();

  if (code != HTTP_CODE_OK) {
    Serial.printf("HTTP error: %d\n", code);
    Blynk.virtualWrite(V5, "HTTP Error: " + String(code));
    http.end();
    return false;
  }

  File f = SPIFFS.open("/job.gcode", "w");
  if (!f) {
    Serial.println("SPIFFS open failed");
    Blynk.virtualWrite(V5, "SPIFFS Error");
    http.end();
    return false;
  }

  WiFiClient* stream = http.getStreamPtr();
  uint8_t buf[512];
  int total   = http.getSize();
  int written = 0;

  while (http.connected() && (written < total || total == -1)) {
    int avail = stream->available();
    if (avail) {
      int n = stream->readBytes(buf, min(avail, (int)sizeof(buf)));
      f.write(buf, n);
      written += n;
      if (total > 0)
        Blynk.virtualWrite(V6, (written * 100) / total);
    }
    Blynk.run();
    delay(1);
  }

  f.close();
  http.end();

  if (written == 0) {
    Blynk.virtualWrite(V5, "Download empty");
    return false;
  }

  Serial.printf("Downloaded %d bytes to /job.gcode\n", written);
  return true;
}

// ─────────────────────────────────────────────────────────────────────────────
// FIX 2: Count valid G-code lines for accurate progress %
// ─────────────────────────────────────────────────────────────────────────────
int countLines(const char* path) {
  File f = SPIFFS.open(path, "r");
  if (!f) return 0;
  int count = 0;
  while (f.available()) {
    String l = f.readStringUntil('\n');
    l.trim();
    if (l.length() > 0 && !l.startsWith(";") && !l.startsWith("(") && !l.startsWith("$H"))
      count++;
  }
  f.close();
  Serial.printf("Total valid lines: %d\n", count);
  return count;
}

// ─────────────────────────────────────────────────────────────────────────────
// Wake up and unlock GRBL
// ─────────────────────────────────────────────────────────────────────────────
void wakeGrbl() {
  SerialGrbl.print("\r\n\r\n");
  delay(2000);
  while (SerialGrbl.available()) SerialGrbl.read(); // flush startup text

  SerialGrbl.println("$X");
  delay(500);
  SerialGrbl.println("$X"); // double unlock
  delay(500);
  while (SerialGrbl.available()) SerialGrbl.read();

  SerialGrbl.println("G90"); // absolute positioning
  delay(100);
  SerialGrbl.println("M5");  // pen up
  delay(100);
  while (SerialGrbl.available()) SerialGrbl.read();
}

// ─────────────────────────────────────────────────────────────────────────────
// Start a job: download → count → open file → wake GRBL → begin streaming
// ─────────────────────────────────────────────────────────────────────────────
void startJob(const String& url) {
  if (isStreaming) {
    Blynk.virtualWrite(V5, "Already running");
    return;
  }

  if (SPIFFS.exists("/job.gcode")) {
    SPIFFS.remove("/job.gcode");
    Serial.println("Cleared previous job from SPIFFS");
  }

  // FIX 1: download first
  if (!downloadToSPIFFS(url)) return;

  // FIX 2: count lines for real progress
  totalLines = countLines("/job.gcode");
  if (totalLines == 0) {
    Blynk.virtualWrite(V5, "Empty file");
    return;
  }

  // Open file for streaming
  gcodeFile = SPIFFS.open("/job.gcode", "r");
  if (!gcodeFile) {
    Blynk.virtualWrite(V5, "File open failed");
    return;
  }

  // Reset state
  linesSent  = 0;
  char_count = 0;
  head       = 0;
  tail       = 0;
  isPaused   = false;

  wakeGrbl();

  isStreaming = true;
  Blynk.virtualWrite(V5, "Streaming...");
  Blynk.virtualWrite(V6, 0);
  Serial.println("--- Streaming started ---");
}

// ─────────────────────────────────────────────────────────────────────────────
// FIX 3: Non-blocking tick — sends ONE line per loop() call
// Blynk buttons and responses are always processed without waiting
// ─────────────────────────────────────────────────────────────────────────────
void tickStream() {
  if (!isStreaming || isPaused) return;

  // Job complete
  if (!gcodeFile || !gcodeFile.available()) {
    // Wait for GRBL to finish remaining queued commands
    if (char_count > 0) return;
    finishJob();
    return;
  }

  // Read one valid line
  String line = "";
  while (gcodeFile.available() && line.length() == 0) {
    line = gcodeFile.readStringUntil('\n');
    line.trim();

    // Strip inline comments
    int semiIdx = line.indexOf(';');
    if (semiIdx >= 0) line = line.substring(0, semiIdx);
    line.trim();

    // Skip empty / comment-only / homing lines
    if (line.startsWith("(") || line.startsWith("$H"))
      line = "";
  }

  if (line.length() == 0) return;

  String cmd    = line + "\n";
  int    cmdLen = cmd.length();

  // Buffer full — come back next loop() tick
  if (char_count + cmdLen >= RX_BUFFER_SIZE - 1) return;

  // Send to GRBL
  SerialGrbl.print(cmd);
  Serial.print("TX: "); Serial.println(line);

  // Track in queue
  sent_lengths[head] = cmdLen;
  head = (head + 1) % QUEUE_SIZE;
  char_count += cmdLen;
  linesSent++;

  // FIX 2: accurate line-based progress
  int pct = (linesSent * 100) / totalLines;
  Blynk.virtualWrite(V6, pct);
}

// ─────────────────────────────────────────────────────────────────────────────
// Clean finish
// ─────────────────────────────────────────────────────────────────────────────
void finishJob() {
  isStreaming = false;
  gcodeFile.close();

  SerialGrbl.println("M5");       // pen up
  SerialGrbl.println("G0 X0 Y0"); // return to origin

  Blynk.virtualWrite(V5, "Finished");
  Blynk.virtualWrite(V6, 100);
  Serial.println("--- Job complete ---");
}

// ─────────────────────────────────────────────────────────────────────────────
// Blynk virtual pin handlers
// ─────────────────────────────────────────────────────────────────────────────

BLYNK_WRITE(V0) {  // Start / Stop button
  int state = param.asInt();
  if (state == 1) {
    if (gcodeURL.length() == 0) {
      Blynk.virtualWrite(V5, "No URL set (V4)");
      return;
    }
    startJob(gcodeURL);
  } else {
    // Stop pressed
    isStreaming = false;
    gcodeFile.close();
    SerialGrbl.print((char)0x18);   // soft reset
    delay(100);
    SerialGrbl.println("$X");       // unlock alarm
    SerialGrbl.println("M5");       // pen up
    SerialGrbl.println("G0 X0 Y0"); // return home
    char_count = 0;
    head = tail = 0;
    Blynk.virtualWrite(V5, "Stopped");
    Blynk.virtualWrite(V6, 0);
  }
}

BLYNK_WRITE(V1) {  // Pause button
  if (param.asInt() == 1 && isStreaming) {
    isPaused = true;
    SerialGrbl.print("!");
    Blynk.virtualWrite(V5, "Paused");
  }
}

BLYNK_WRITE(V2) {  // Resume button
  if (param.asInt() == 1 && isStreaming) {
    isPaused = false;
    SerialGrbl.print("~");
    Blynk.virtualWrite(V5, "Running");
  }
}

BLYNK_WRITE(V4) {  // URL input
  gcodeURL = param.asString();
  Serial.println("URL set: " + gcodeURL);
  Blynk.virtualWrite(V5, "URL ready");
}

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

  if (!SPIFFS.begin(true)) {
    Serial.println("SPIFFS mount failed!");
  } else {
    Serial.printf("SPIFFS: %d bytes free\n",
                  SPIFFS.totalBytes() - SPIFFS.usedBytes());
  }

  SerialGrbl.begin(115200, SERIAL_8N1, 16, 17);

  Blynk.begin(BLYNK_AUTH_TOKEN, ssid, pass);
  Blynk.virtualWrite(V5, "Ready");
  Blynk.virtualWrite(V6, 0);

  Serial.println("Boot complete");
}

// ─────────────────────────────────────────────────────────────────────────────
// FIX 3: loop() — fully non-blocking, one line sent per tick
// ─────────────────────────────────────────────────────────────────────────────
void loop() {
  Blynk.run();
  handleGrblResponse(); // always drain GRBL responses
  tickStream();         // send next line if ready
}

I did use Claude to debug the code and appy some fixes. So, when streaming the gcode, the ESP32 sends the lines to the buffer, waiting for an ok. I downloaded a 30000+ line gcode file on to the ESP's spiffs. It aparrently "finished" it in less then 10 seconds.
I only have the arduino, ESP32 and a servo attached to the arduino for testing. The arduino is running grbl-mi.
Could anyone help me fix this problem?

What is the size of the file in bytes ?

821363 bytes

Exactly which ESP32 variant are you using and which partition scheme have you got selected for the ESP32 in the IDE ?

What exactly is the problem you need help fixing?

I found the problem! The Spiffs was full