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?