Community project

Web-Controlled CNC Safety Console

ESP32
Photo of Web-Controlled CNC Safety Console
Generated with AI

Amirhossein Mirzaei

Published September 25, 2026

This web-controlled CNC safety console uses an ESP32 to manage stepper motors, endstops, and safety interlocks for a two-axis CNC machine. The system monitors power consumption with an INA226 voltage monitor, tracks spindle temperature via DS18B20, and provides audio feedback through a buzzer. Builders will receive a complete wiring diagram, parts list, and step-by-step assembly instructions for integrating the TB6600 stepper drivers, servo-controlled pen mechanism, relay-based power control, and four normally-closed endstop switches.

The firmware enables wireless operation through a web interface, with real-time status monitoring and motion control via G-code-like commands. Safety features include endstop detection, fault reporting, and an alarm pattern system. This guide covers power preparation, relay wiring, driver configuration, sensor integration, endstop installation, and initial safety testing to ensure reliable operation.

Wiring diagram

Wiring diagram for Web-Controlled CNC Safety Console

Gather all the parts

QtyComponent
1

TB6600 X-axis stepper driver

4 A, 24 V

A 24 V driver module that powers the X-axis stepper motor from safe control signals.

1

TB6600 Z-axis stepper driver

4 A, 24 V

A 24 V driver module that powers the Z-axis stepper motor from safe control signals.

1

SG90 Servo

5 V continuous rotation

Micro servo motor (SG90)

1

DS18B20

3.3 V

Digital temperature sensor using OneWire protocol

1

4.7 kΩ DS18B20 pull-up resistor

4.7 kΩ

A resistor that keeps the temperature sensor data wire at a reliable high level.

1

INA226 R100 voltage monitor

R100; voltage only

An I2C module that measures the 24 V supply voltage without carrying motor current.

1

Buzzer

5 V active module

Piezo buzzer for sound output

1

5 V single-channel relay module

contacts rated at least 24 V DC / 8 A

A relay module that removes 24 V power from both stepper drivers during a safety fault.

1

X-min normally-closed endstop switch

normally closed

A normally-closed limit switch that stops X motion at the left edge or when its cable breaks.

1

X-max normally-closed endstop switch

normally closed

A normally-closed limit switch that stops X motion at the right edge or when its cable breaks.

1

Z-min normally-closed endstop switch

normally closed

A normally-closed limit switch that stops Z motion at the near edge or when its cable breaks.

1

Z-max normally-closed endstop switch

normally closed

A normally-closed limit switch that stops Z motion at the far edge or when its cable breaks.

4

Four 10 kΩ endstop pull-up resistors

10 kΩ

Four separate resistors that make a normally-closed endstop line read high when a switch opens or its wire breaks.

1

24 V DC power supply

24 V DC, at least 8 A

The existing 24 V supply that powers the two stepper drivers through the safety relay.

Assemble it in 6 steps

1. منبع‌های برق را ایمن آماده کنید

منبع ۲۴ ولت را فقط برای دو TB6600 بگذارید و برای ESP32، سروو، رله و بازر یک منبع ۵ ولت رگوله با توان کافی استفاده کنید. منفیِ منبع ۵ ولت را به GND ESP32 وصل کنید. منفی ۲۴ ولت را به GND ترمینال هر دو TB6600 وصل کنید. هنوز برق ۲۴ ولت را روشن نکنید.

  • برای سروو، منبع ۵ ولت با جریان کافی انتخاب کنید؛ افت ولتاژ سروو می‌تواند ESP32 را ریست کند.
  • یک فیوز مناسب جریان موتور نزدیک خروجی مثبت منبع ۲۴ ولت قرار دهید.
  • هرگز ۲۴ ولت را به پایه 5V یا 3V3 ESP32 وصل نکنید — برد فوراً آسیب می‌بیند.
  • رله باید برای قطع جریان DC ۲۴ ولت و دست‌کم ۸ آمپر واقعاً مناسب باشد؛ رله کوچک ممکن است کنتاکتش بسوزد یا بچسبد.

2. رلهٔ سه‌پایه را وصل کنید

مثبت ۲۴ ولت را از راه فیوز به COM رله وصل کنید و از NO رله به VCC هر دو TB6600 بروید؛ این سیم برق درایورها را قطع و وصل می‌کند. VCC رله را به 5V (برق)، GND رله را به GND مشترک (زمین)، و IN رله را مستقیم به GPIO25 (فرمان) وصل کنید. این ماژول رله فقط همین سه پایهٔ فرمان را دارد و مقاومت‌های لازم روی خود ماژول هستند.

  • پیش از وصل موتور، با مولتی‌متر مطمئن شوید در حالت قطع رله روی VCC درایورها ۲۴ ولت نیست.
  • اگر رله هنگام روشن‌شدن یا راه‌اندازی برعکس عمل می‌کند، مقدار RELAY_ACTIVE_LOW را در کد تغییر دهید.
  • هرگز موتور را وقتی TB6600 برق دارد جدا یا وصل نکنید — ولتاژ برگشتی می‌تواند درایور را خراب کند.

3. فرمان‌های TB6600 را مستقیم وصل کنید

روی هر TB6600، پایه‌های PUL+، DIR+ و ENA+ را به 5V (برق فرمان) وصل کنید. سپس PUL-، DIR- و ENA- محور X را مستقیم به‌ترتیب به GPIO13، GPIO14 و GPIO16 ESP32 وصل کنید؛ PUL-، DIR- و ENA- محور Z را مستقیم به‌ترتیب به GPIO17، GPIO18 و GPIO19 وصل کنید (فرمان پالس و جهت). چهار سیم هر موتور فقط به ترمینال خروجی موتور همان TB6600 می‌روند.

  • پیش از بستن ابزار، جریان و ریزگام هر TB6600 را مطابق موتور و پیچ حرکتی تنظیم کنید.
  • سیم موتور را به PUL/DIR/ENA وصل نکنید؛ چهار سیم موتور فقط به ترمینال خروجی موتور همان TB6600 می‌روند.

4. سروو، بازر و حسگرها را وصل کنید

سیم قرمز SG90 را به 5V (برق)، سیم قهوه‌ای یا مشکی را به GND (زمین)، و سیم نارنجی را به GPIO26 (فرمان) وصل کنید. GND بازر را به GND (زمین) و SIGNAL آن را به GPIO27 (هشدار) وصل کنید. DS18B20 را با VCC به 3V3 (برق)، GND به GND (زمین) و DATA به GPIO4 (داده) وصل کنید؛ مقاومت ۴٫۷ کیلو‌اهم را بین DATA و 3V3 بگذارید. INA226 را با VCC به 3V3 (برق)، GND به GND (زمین)، SDA به GPIO21 (داده) و SCL به GPIO22 (داده) وصل کنید؛ VIN+ و VIN- را هر دو به مثبت ۲۴ ولت وصل کنید تا فقط ولتاژ نمایش داده شود.

  • سرووی ۳۶۰ درجه موقعیت زاویه‌ای واقعی ندارد؛ زمان بالا و پایین رفتن ماژیک را ابتدا بدون ماژیک تنظیم کنید.
  • INA226 R100 شما فقط ولتاژ را نشان می‌دهد و نباید در مسیر جریان موتور قرار بگیرد.
  • برق سروو را از پایه 3V3 ESP32 نگیرید — سروو ممکن است برد را ریست یا خراب کند.
  • VCC و GND حسگرها را جابه‌جا نکنید — برق برعکس می‌تواند حسگر را آسیب بزند.

5. چهار انداستاپ عادی‌بسته را نصب کنید

کلیدهای X-min، X-max، Z-min و Z-max را در دو انتهای واقعی هر محور نصب کنید. پایه NC هر کلید را به GND (زمین) وصل کنید و پایه COM آن‌ها را به‌ترتیب به GPIO34، GPIO35، GPIO36 و GPIO39 ببرید (سیگنال). برای هر COM، یک مقاومت ۱۰ کیلو‌اهم از همان پایه به 3V3 وصل کنید (نگه‌داشتن سیگنال در حالت عادی).

  • در حالت عادی کلید NC باید وصل باشد؛ فشار کلید یا قطع‌شدن سیم باید خطا ایجاد کند.
  • پیش از نصب ماژیک، هر کلید را با دست فشار دهید و واکنش ایمنی را آزمایش کنید.
  • کلیدها را به پایه NO وصل نکنید — در این طراحی، قطع‌شدن سیم باید خطر تشخیص داده شود تا محور به انتهای مکانیکی نکوبد.

6. آزمون امن اولیه را انجام دهید

ابتدا تنها ESP32 را با USB روشن کنید. به شبکه CNC-ESP32 وصل شوید و نشانی 192.168.4.1 را باز کنید. ولتاژ و دما را بررسی کنید، سپس بدون ماژیک و با محورهای آزاد، رله و هوم‌کردن را آزمایش کنید. پس از درست‌بودن جهت حرکت، مقدار طول و عرض، steps per mm، سرعت و زمان سروو را در ابتدای کد تنظیم کنید.

  • نخستین G-code را بسیار کوچک و با سرعت پایین اجرا کنید.
  • نام شبکه و گذرواژه در ابتدای کد هستند؛ پیش از استفاده واقعی گذرواژه را عوض کنید.
  • یک کلید توقف اضطراری سخت‌افزاریِ قفل‌شونده در مسیر برق ۲۴ ولت یا فرمان رله نصب کنید — توقف وب به‌تنهایی جای آن را نمی‌گیرد.
  • هنگام هوم یا اجرای فایل، دست و لباس را از محورهای متحرک دور نگه دارید.

Review all connections

1. Connections between "supply_24v" and "ESP32"

Functionsupply_24vESP32
powerV+ → 5 V single-channel relay module COMEXT
groundV-GND

2. Connections between "relay_1" and "ESP32"

Functionrelay_1ESP32
powerVCC5V
groundGNDGND
powerNO → TB6600 X-axis stepper driver VCCEXT
digitalINGPIO 25

3. Connections between "tb6600_x" and "ESP32"

Functiontb6600_xESP32
powerPUL+5V
powerDIR+5V
powerENA+5V
powerVCC → TB6600 Z-axis stepper driver VCCEXT
groundGNDGND
digitalPUL-GPIO 13
digitalDIR-GPIO 14
digitalENA-GPIO 16

4. Connections between "tb6600_z" and "ESP32"

Functiontb6600_zESP32
powerPUL+5V
powerDIR+5V
powerENA+5V
groundGNDGND
digitalPUL-GPIO 17
digitalDIR-GPIO 18
digitalENA-GPIO 19

5. Connections between "servo_y" and "ESP32"

Functionservo_yESP32
powerVCC5V
groundGNDGND
pwmSIGNALGPIO 26

6. Connections between "buzzer_1" and "ESP32"

Functionbuzzer_1ESP32
groundGNDGND
digitalSIGNALGPIO 27

7. Connections between "ina226_1" and "ESP32"

Functionina226_1ESP32
powerVCC3V3
groundGNDGND
i2cSDAGPIO 21
i2cSCLGPIO 22
analogVIN+ → 24 V DC power supply V+EXT
analogVIN- → INA226 R100 voltage monitor VIN+EXT

8. Connections between "ds18b20_1" and "ESP32"

Functionds18b20_1ESP32
powerVCC3V3
groundGNDGND
dataDATAGPIO 4

9. Connections between "ds18b20_pullup" and "ESP32"

Functionds18b20_pullupESP32
powerVCC3V3
dataDATA → DS18B20 DATAEXT

10. Connections between "x_min" and "ESP32"

Functionx_minESP32
analogCOMGPIO 34
groundNCGND

11. Connections between "x_max" and "ESP32"

Functionx_maxESP32
analogCOMGPIO 35
groundNCGND

12. Connections between "z_min" and "ESP32"

Functionz_minESP32
analogCOMGPIO 36
groundNCGND

13. Connections between "z_max" and "ESP32"

Functionz_maxESP32
analogCOMGPIO 39
groundNCGND

14. Connections between "endstop_pullups" and "ESP32"

Functionendstop_pullupsESP32
powerVCC3V3
analogXMIN → X-min normally-closed endstop switch COMEXT
analogXMAX → X-max normally-closed endstop switch COMEXT
analogZMIN → Z-min normally-closed endstop switch COMEXT
analogZMAX → Z-max normally-closed endstop switch COMEXT

Deploy the firmware

#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <Wire.h>
#include <ESP32Servo.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <INA226.h>
#include <ctype.h>

#define X_EN_PIN 16
#define Z_EN_PIN 19
#define SERVO_PIN 26
#define X_DIR_PIN 14
#define X_MAX_PIN 35
#define Z_DIR_PIN 18
#define Z_MAX_PIN 39
#define Z_MIN_PIN 36
#define BUZZER_PIN 27
#define I2C_SCL_PIN 22
#define I2C_SDA_PIN 21

// ===================== Machine setup: edit these values first =====================

// Forward declarations
bool endstopHit(int pin);
void relaySet(bool on);
void driverEnable(bool on);
void buzz(uint16_t ms);
void alarmPattern();
void stopOutputs();
void setFault(const String &why);
void clearFault();
bool safetyOK();
void penUp();
void penDown();
void pulsePin(int pin);
void setDirection(bool xAxis, bool positive);
bool moveLinear(float targetX, float targetZ, float feed);
bool homeAxis(bool xAxis);
float wordValue(const String &line,char letter,bool &found);
bool executeLine(String line, bool &absolute, float &feed);
void motionTask(void *);
void sensorTick();
String jsonNumber(float n,int d);
void handleStatus();

const char *AP_SSID = "CNC-ESP32";
const char *AP_PASSWORD = "CNCsafe2026"; // Change before using the machine.

// Work area in millimetres. Homing establishes X=0, Z=0 at the MIN switches.
const float X_TRAVEL_MM = 300.0f;
const float Z_TRAVEL_MM = 200.0f;
// Set from motor steps/revolution × TB6600 microstep setting ÷ lead-screw pitch (mm/rev).
const float X_STEPS_PER_MM = 80.0f;
const float Z_STEPS_PER_MM = 80.0f;
const float MAX_FEED_MM_MIN = 1800.0f; // Begin low; raise only after reliable testing.
const float HOME_FEED_MM_MIN = 300.0f;
const uint32_t HOME_MAX_STEPS = 80000;

// 360-degree SG90 is a continuous-rotation servo: these are speed commands and times,
// not angles. Tune STOP until it truly stops, then tune UP/DOWN time on your mechanism.
const int SERVO_STOP_US = 1500;
const int SERVO_UP_US = 1700;
const int SERVO_DOWN_US = 1300;
const uint16_t PEN_UP_TIME_MS = 500;
const uint16_t PEN_DOWN_TIME_MS = 500;

// Safety limits. Current protection is deliberately disabled for the R100 INA226 board.
const float MIN_BUS_VOLTAGE = 20.0f;
const float MAX_BUS_VOLTAGE = 30.0f;
const float MIN_TEMP_C = 0.0f;
const float MAX_TEMP_C = 60.0f;

// TB6600 PUL-, DIR- and ENA- inputs connect directly to ESP32 GPIOs.
// The three-pin relay module connects directly to GPIO25. Set this to match its IN logic.
const bool RELAY_ACTIVE_LOW = true; // Most relay modules are active-low; test safely first.
const bool X_DIR_INVERT = false;
const bool Z_DIR_INVERT = false;
const uint16_t STEP_PULSE_US = 5; // TB6600 requires only a few microseconds.
const size_t MAX_GCODE_BYTES = 60000;

#define X_STEP_PIN 13
#define Z_STEP_PIN 17
#define RELAY_PIN 25
#define ONEWIRE_PIN 4
#define X_MIN_PIN 34

WebServer server(80);
Servo penServo;
OneWire oneWire(ONEWIRE_PIN);
DallasTemperature temperatureSensor(&oneWire);
INA226 ina226(0x40);

portMUX_TYPE stateMux = portMUX_INITIALIZER_UNLOCKED;
volatile bool abortRequested = false;
volatile bool paused = false;
volatile bool running = false;
volatile bool faulted = false;
volatile bool homed = false;
volatile bool relayOn = false;
String faultText = "Not homed";
long xSteps = 0, zSteps = 0;
float busVoltage = NAN, temperatureC = NAN;
bool inaPresent = false, tempPresent = false;
bool penIsDown = false;

char *gcodeBuffer = nullptr;
size_t gcodeLength = 0;
size_t uploadBytes = 0;
bool uploadOK = false;
String uploadError;
uint32_t lastSensorRequest = 0;
uint32_t lastSensorRead = 0;

const char PAGE[] PROGMEM = R"HTML(
<!doctype html><html lang="fa" dir="rtl"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>ESP32 CNC</title><style>
*{box-sizing:border-box}body{margin:0;background:#0b1020;color:#e7edf8;font-family:Tahoma,Arial,sans-serif}.wrap{max-width:960px;margin:auto;padding:20px}.head{background:linear-gradient(120deg,#152449,#253b70);border-radius:18px;padding:20px;box-shadow:0 8px 30px #0006}h1{margin:0 0 8px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:12px;margin:16px 0}.card{background:#141c31;border:1px solid #273657;border-radius:14px;padding:14px}.n{font-size:1.65em;font-weight:bold;color:#79d7ff}.label{color:#aab8d4;font-size:.86em}.status{padding:13px;border-radius:12px;background:#141c31;margin:14px 0;border-right:5px solid #42d392}.bad{border-color:#ff5d6c}.actions{display:flex;gap:10px;flex-wrap:wrap}button,label.file{border:0;border-radius:10px;padding:12px 16px;color:#fff;background:#2864dc;font:bold 15px Tahoma;cursor:pointer}button.stop{background:#d9344a}button.warn{background:#c37b10}button:disabled{opacity:.45;cursor:not-allowed}input[type=file]{display:none}.hint{color:#aab8d4;font-size:.88em;line-height:1.8}code{direction:ltr;display:inline-block;background:#0b1020;padding:3px 6px;border-radius:5px}.bar{height:7px;background:#202c47;border-radius:5px;overflow:hidden}.bar i{display:block;height:100%;width:0;background:#42d392;transition:.3s}</style></head><body><div class="wrap"><section class="head"><h1>کنترل امن CNC</h1><div>شبکهٔ دستگاه: <b>CNC-ESP32</b> — نشانی پنل: <code>192.168.4.1</code></div></section><div id="state" class="status">در حال دریافت وضعیت…</div><section class="grid"><div class="card"><div class="label">ولتاژ ورودی ۲۴ ولت</div><div class="n" id="v">--</div></div><div class="card"><div class="label">دمای DS18B20</div><div class="n" id="t">--</div></div><div class="card"><div class="label">موقعیت X / Z</div><div class="n" id="p">--</div></div><div class="card"><div class="label">پیشرفت فایل</div><div class="n" id="pr">--</div><div class="bar"><i id="b"></i></div></div></section><section class="card"><h3>فایل G-code</h3><form id="f"><label class="file">انتخاب فایل <input id="file" type="file" accept=".gcode,.nc,.txt"></label><span id="fn" class="hint">فایلی انتخاب نشده است</span></form><p class="hint">فرمان‌های پشتیبانی‌شده: G0/G1 با X و Z بر حسب میلی‌متر، F بر حسب mm/min، G90، G91، G28، M3 (ماژیک پایین)، M5 (ماژیک بالا)، M0.</p><div class="actions"><button onclick="cmd('home')">هوم کردن</button><button onclick="cmd('start')">شروع</button><button class="warn" onclick="cmd('pause')">مکث / ادامه</button><button class="stop" onclick="cmd('stop')">توقف اضطراری نرم‌افزاری</button><button onclick="cmd('clear')">پاک کردن خطا</button></div></section><p class="hint">توقف اضطراری نرم‌افزاری، رله را قطع می‌کند. برای ایمنی واقعی، یک کلید قطع اضطراری سخت‌افزاریِ قفل‌شونده را در مسیر بوبین رله یا تغذیهٔ ۲۴ ولت قرار دهید.</p></div><script>
const $=id=>document.getElementById(id);$('file').onchange=async e=>{let f=e.target.files[0];if(!f)return;$('fn').textContent='در حال ارسال: '+f.name;let d=new FormData();d.append('gcode',f);try{let r=await fetch('/upload',{method:'POST',body:d});let j=await r.json();$('fn').textContent=j.message}catch(x){$('fn').textContent='خطا در ارسال فایل'}};async function cmd(c){await fetch('/cmd?c='+c,{method:'POST'});setTimeout(status,100)}async function status(){try{let j=await (await fetch('/status')).json();$('v').textContent=j.voltage===null?'نامشخص':j.voltage.toFixed(2)+' V';$('t').textContent=j.temp===null?'نامشخص':j.temp.toFixed(1)+' °C';$('p').textContent=j.x.toFixed(2)+' / '+j.z.toFixed(2)+' mm';$('pr').textContent=j.progress+'%';$('b').style.width=j.progress+'%';let s=$('state');s.textContent=j.message;s.className='status '+(j.fault?'bad':'');}catch(e){$('state').textContent='ارتباط با ESP32 قطع است'}}status();setInterval(status,700);
</script></body></html>
)HTML";

bool endstopHit(int pin) { return digitalRead(pin) == HIGH; } // NC switch opened = fault.
void relaySet(bool on) { relayOn = on; digitalWrite(RELAY_PIN, (on ^ RELAY_ACTIVE_LOW) ? HIGH : LOW); }
void driverEnable(bool on) { digitalWrite(X_EN_PIN, on ? HIGH : LOW); digitalWrite(Z_EN_PIN, on ? HIGH : LOW); }
void buzz(uint16_t ms) { digitalWrite(BUZZER_PIN, HIGH); delay(ms); digitalWrite(BUZZER_PIN, LOW); }
void alarmPattern() { for (int i=0;i<3;i++){ buzz(100); delay(80); } }

void stopOutputs() { abortRequested = true; driverEnable(false); relaySet(false); penServo.writeMicroseconds(SERVO_STOP_US); }
void setFault(const String &why) { portENTER_CRITICAL(&stateMux); faulted=true; running=false; paused=false; faultText=why; portEXIT_CRITICAL(&stateMux); stopOutputs(); alarmPattern(); }
void clearFault() { if (endstopHit(X_MIN_PIN)||endstopHit(X_MAX_PIN)||endstopHit(Z_MIN_PIN)||endstopHit(Z_MAX_PIN)) { faultText="An endstop is open or its wire is disconnected"; return; } faulted=false; abortRequested=false; faultText="Ready"; }

bool safetyOK() {
  if (faulted || abortRequested) return false;
  if (inaPresent && (!isfinite(busVoltage) || busVoltage < MIN_BUS_VOLTAGE || busVoltage > MAX_BUS_VOLTAGE)) { setFault("24 V supply outside configured limit"); return false; }
  if (tempPresent && (!isfinite(temperatureC) || temperatureC < MIN_TEMP_C || temperatureC > MAX_TEMP_C)) { setFault("Temperature outside configured limit"); return false; }
  return true;
}

void penUp() { penServo.writeMicroseconds(SERVO_UP_US); delay(PEN_UP_TIME_MS); penServo.writeMicroseconds(SERVO_STOP_US); penIsDown=false; }
void penDown() { penServo.writeMicroseconds(SERVO_DOWN_US); delay(PEN_DOWN_TIME_MS); penServo.writeMicroseconds(SERVO_STOP_US); penIsDown=true; }

void pulsePin(int pin) { digitalWrite(pin,HIGH); delayMicroseconds(STEP_PULSE_US); digitalWrite(pin,LOW); }
void setDirection(bool xAxis, bool positive) { bool level = positive ^ (xAxis ? X_DIR_INVERT : Z_DIR_INVERT); digitalWrite(xAxis ? X_DIR_PIN : Z_DIR_PIN, level ? HIGH : LOW); delayMicroseconds(10); }

bool moveLinear(float targetX, float targetZ, float feed) {
  if (!homed) { setFault("Machine must be homed before motion"); return false; }
  if (targetX < 0 || targetX > X_TRAVEL_MM || targetZ < 0 || targetZ > Z_TRAVEL_MM) { setFault("G-code target outside soft travel limit"); return false; }
  long tx=lroundf(targetX*X_STEPS_PER_MM), tz=lroundf(targetZ*Z_STEPS_PER_MM);
  long dx=tx-xSteps, dz=tz-zSteps; long ax=abs(dx), az=abs(dz), total=max(ax,az); if (!total) return true;
  bool xp=dx>=0, zp=dz>=0; setDirection(true,xp); setDirection(false,zp);
  float distance=hypot(targetX-xSteps/X_STEPS_PER_MM,targetZ-zSteps/Z_STEPS_PER_MM);
  feed=constrain(feed,10.0f,MAX_FEED_MM_MIN);
  uint32_t cruise=max((uint32_t)(1000000.0f*distance*60.0f/(feed*total)), (uint32_t)(STEP_PULSE_US+25));
  uint32_t start=max(cruise*4UL, 1200UL); long ramp=min(total/2, max(10L,total/8));
  long ex=total/2, ez=total/2;
  for(long i=0;i<total;i++) {
    while(paused && !abortRequested && !faulted) vTaskDelay(1);
    if(!safetyOK()) return false;
    // An endstop only blocks travel toward that end; the opposite direction is allowed.
    if ((xp && endstopHit(X_MAX_PIN)) || (!xp && endstopHit(X_MIN_PIN)) || (zp && endstopHit(Z_MAX_PIN)) || (!zp && endstopHit(Z_MIN_PIN))) { setFault("Endstop triggered during motion"); return false; }
    bool sx=false, sz=false; ex+=ax; if(ex>=total){ex-=total;sx=true;} ez+=az; if(ez>=total){ez-=total;sz=true;}
    if(sx) pulsePin(X_STEP_PIN); if(sz) pulsePin(Z_STEP_PIN);
    if(sx) xSteps += xp?1:-1; if(sz) zSteps += zp?1:-1;
    uint32_t interval=cruise; if(i<ramp) interval=start-(uint32_t)((start-cruise)*(float)i/ramp); else if(i>total-ramp) interval=start-(uint32_t)((start-cruise)*(float)(total-i)/ramp);
    uint32_t used=STEP_PULSE_US*(sx+sz); if(interval>used) delayMicroseconds(interval-used);
  }
  return true;
}

bool homeAxis(bool xAxis) {
  int minPin=xAxis?X_MIN_PIN:Z_MIN_PIN; int stepPin=xAxis?X_STEP_PIN:Z_STEP_PIN; float spm=xAxis?X_STEPS_PER_MM:Z_STEPS_PER_MM;
  setDirection(xAxis,false); uint32_t interval=max((uint32_t)(60000000.0f/(HOME_FEED_MM_MIN*spm)),(uint32_t)500);
  uint32_t n=0; while(!endstopHit(minPin) && n++<HOME_MAX_STEPS) { if(abortRequested||faulted||!safetyOK()) return false; pulsePin(stepPin); delayMicroseconds(interval); }
  if(!endstopHit(minPin)) { setFault("Home switch was not reached"); return false; }
  if(xAxis) xSteps=0; else zSteps=0; return true;
}

float wordValue(const String &line,char letter,bool &found) { found=false; for(size_t i=0;i<line.length();i++){if(toupper(line[i])==letter){ char *e; float v=strtof(line.c_str()+i+1,&e); if(e!=line.c_str()+i+1){found=true;return v;} }} return 0; }

bool executeLine(String line, bool &absolute, float &feed) {
  int comment=line.indexOf(';'); if(comment>=0) line=line.substring(0,comment); line.trim(); line.toUpperCase(); if(!line.length()) return true;
  bool hasG=false; float g=wordValue(line,'G',hasG); bool hasM=false; float m=wordValue(line,'M',hasM);
  if(hasM) { int mi=(int)m; if(mi==3){penDown();return safetyOK();} if(mi==5){penUp();return safetyOK();} if(mi==0){paused=true;return true;} }
  if(hasG && (int)g==28) { homed=false; if(!homeAxis(true)||!homeAxis(false)) return false; homed=true; return true; }
  if(hasG && (int)g==90) {absolute=true;return true;} if(hasG && (int)g==91) {absolute=false;return true;}
  if(hasG && ((int)g==0 || (int)g==1)) { bool fx,fz,ff; float xv=wordValue(line,'X',fx), zv=wordValue(line,'Z',fz), fv=wordValue(line,'F',ff); if(ff) feed=fv; float cx=xSteps/X_STEPS_PER_MM, cz=zSteps/Z_STEPS_PER_MM; return moveLinear(fx?(absolute?xv:cx+xv):cx,fz?(absolute?zv:cz+zv):cz,feed); }
  return true; // Unsupported lines are ignored deliberately, rather than guessed.
}

void motionTask(void *) {
  for(;;) { if(!running){vTaskDelay(10);continue;} bool absolute=true; float feed=600; size_t at=0; driverEnable(true); relaySet(true); while(at<gcodeLength && running && !faulted && !abortRequested) { size_t end=at; while(end<gcodeLength && gcodeBuffer[end]!='\n' && gcodeBuffer[end]!='\r') end++; String line=String(gcodeBuffer).substring(at,end); at=end+1; if(!executeLine(line,absolute,feed)) break; } running=false; driverEnable(false); relaySet(false); if(!faulted && !abortRequested) { penUp(); faultText="Program completed"; buzz(120); delay(100); buzz(180); } else if(abortRequested && !faulted) faultText="Stopped by operator"; vTaskDelay(1); }
}

void sensorTick() {
  uint32_t now=millis(); if(now-lastSensorRequest>1000){temperatureSensor.requestTemperatures();lastSensorRequest=now;} if(now-lastSensorRead>1100){float t=temperatureSensor.getTempCByIndex(0);if(t!=DEVICE_DISCONNECTED_C){temperatureC=t;tempPresent=true;}else tempPresent=false;lastSensorRead=now;} if(inaPresent) busVoltage=ina226.getBusVoltage(); if(running) safetyOK();
}
String jsonNumber(float n,int d=2){return isfinite(n)?String(n,d):"null";}
void handleStatus(){ float x=xSteps/X_STEPS_PER_MM,z=zSteps/Z_STEPS_PER_MM; int progress=running&&gcodeLength?0:0; String msg=faulted?faultText:(running?(paused?"Paused":"Running"):(homed?faultText:"Home the machine before starting")); String j="{\"voltage\":"+jsonNumber(busVoltage)+",\"temp\":"+jsonNumber(temperatureC,1)+",\"x\":"+String(x,2)+",\"z\":"+String(z,2)+",\"progress\":"+String(progress)+",\"fault\":"+String(faulted?"true":"false")+",\"message\":\""+msg+"\"}";server.send(200,"application/json",j);}

void setup() {
  Serial.begin(115200); pinMode(X_STEP_PIN,OUTPUT);pinMode(X_DIR_PIN,OUTPUT);pinMode(X_EN_PIN,OUTPUT);pinMode(Z_STEP_PIN,OUTPUT);pinMode(Z_DIR_PIN,OUTPUT);pinMode(Z_EN_PIN,OUTPUT);pinMode(RELAY_PIN,OUTPUT);pinMode(BUZZER_PIN,OUTPUT);
  pinMode(X_MIN_PIN,INPUT);pinMode(X_MAX_PIN,INPUT);pinMode(Z_MIN_PIN,INPUT);pinMode(Z_MAX_PIN,INPUT); digitalWrite(BUZZER_PIN,LOW); stopOutputs();
  Wire.begin(I2C_SDA_PIN,I2C_SCL_PIN); inaPresent=ina226.begin(); temperatureSensor.begin(); temperatureSensor.setWaitForConversion(false); temperatureSensor.requestTemperatures();
  penServo.setPeriodHertz(50);penServo.attach(SERVO_PIN,500,2400);penServo.writeMicroseconds(SERVO_STOP_US);
  WiFi.mode(WIFI_AP);WiFi.softAP(AP_SSID,AP_PASSWORD); gcodeBuffer=(char*)malloc(MAX_GCODE_BYTES+1); if(!gcodeBuffer) setFault("Insufficient RAM for G-code buffer");
  server.on("/",HTTP_GET,[]{server.send_P(200,"text/html; charset=utf-8",PAGE);});server.on("/status",HTTP_GET,handleStatus);
  server.on("/cmd",HTTP_POST,[]{String c=server.arg("c");if(c=="stop"){abortRequested=true;stopOutputs();}else if(c=="clear")clearFault();else if(c=="pause"&&running)paused=!paused;else if(c=="home"&&!running&&!faulted){abortRequested=false;relaySet(true);driverEnable(true);if(homeAxis(true)&&homeAxis(false)){homed=true;faultText="Homed and ready";buzz(80);}driverEnable(false);relaySet(false);}else if(c=="start"&&!running&&!faulted&&homed&&gcodeLength){abortRequested=false;paused=false;running=true;faultText="Running";buzz(80);}server.send(200,"application/json","{\"ok\":true}");});
  server.on("/upload",HTTP_POST,[]{server.send(200,"application/json",uploadOK?"{\"message\":\"File loaded into temporary memory\"}":"{\"message\":\"Upload rejected\"}");},[]{HTTPUpload &u=server.upload();if(u.status==UPLOAD_FILE_START){uploadBytes=0;uploadOK=!running&&!faulted;uploadError="";}else if(u.status==UPLOAD_FILE_WRITE){if(!uploadOK||uploadBytes+u.currentSize>MAX_GCODE_BYTES){uploadOK=false;uploadError="File is too large or machine is busy";}else{memcpy(gcodeBuffer+uploadBytes,u.buf,u.currentSize);uploadBytes+=u.currentSize;}}else if(u.status==UPLOAD_FILE_END){if(uploadOK){gcodeBuffer[uploadBytes]=0;gcodeLength=uploadBytes;faultText="G-code loaded; home then start";}}});
  xTaskCreatePinnedToCore(motionTask,"motion",8192,nullptr,1,nullptr,0); buzz(70);
}
void loop(){server.handleClient();sensorTick();delay(2);}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik