Las CNC grandes tienen mesas que no son planas. No por defecto de fabricación, sino porque la madera se mueve, el aluminio se comba y el sacrificio acumula irregularidades con el uso. Una fresadora de 2.500×1.300 mm con un pandeo de 1-2 mm en diagonal puede arruinar un grabado fino que trabaje a Z=0,2 mm. Este proyecto mide el desnivel real de la mesa usando un BLTouch y un final de carrera en un ESP8266, construye un mapa de alturas con interpolación bilineal o bicúbica, y corrige automáticamente cada línea del G-code de producción antes de enviarlo a la máquina.
01 — El problema: CNC sin feedback de posición y una mesa de 2,5 metros con pandeo
El contexto: una CNC de 2.500×1.300 mm con controlador RichAuto A10. La A10 es una controladora autónoma que ejecuta programas desde USB. No tiene puerto serie accesible para enviar G-code en tiempo real, no tiene API, no hay forma de preguntarle «¿dónde estás?». La CNC es una caja negra que hace lo que tiene en el USB y ya.
Las controladoras modernas (Grbl, Mach3, LinuxCNC) tienen compensación de superficie integrada: mides el mesh en la misma máquina y ella se encarga de corregir Z sobre la marcha. Aquí no hay nada de eso. Si quieres compensar irregularidades, tienes que hacerlo tú antes de generar el G-code definitivo.
El problema concreto: grabados que trabajan a Z=0,1-0,3 mm sobre la superficie. Con 1-2 mm de pandeo en la mesa de sacrificio, la herramienta va a veces enterrada, a veces en el aire, y el resultado es basura. La solución obvia es nivelar la mesa periódicamente, pero en una mesa de 2,5 metros eso es trabajoso y no dura. La solución elegante es medir el desnivel una vez y dejar que el software lo compense en el G-code.
02 — La idea: medir el desnivel de la mesa CNC con timing dual de microsegundos
El truco está en que, aunque la A10 no da feedback de posición, sí ejecuta G-code predecible. Si le das un programa con movimientos Z a velocidad fija y conocida, puedes calcular cuánto bajó midiendo el tiempo entre dos eventos:
- Z-limit switch se abre → Z empieza a bajar (timestamp
t_descent) - BLTouch toca la superficie → sonda hace contacto (timestamp
t_trigger)
La distancia recorrida es simplemente feedrate × (t_trigger − t_descent). Si el feedrate es 120 mm/min (2 mm/s) y el tiempo entre eventos es 1.250 ms, la sonda bajó 2,5 mm. Eso es la altura en ese punto de la mesa.
Lo que hace que esto funcione bien es que el inicio del descenso no se estima (cosa que haría acumular error): se mide directamente con el Z-limit switch instalado en el punto más alto del eje Z. Cuando Z sale de arriba del todo, el switch se abre y el ESP captura el microsegundo exacto. Cada medición es completamente independiente de las anteriores.
A F120 mm/min, un error de ±50 ms en los timestamps da ±0,1 mm de error en la altura. Más que suficiente para compensar mesas con 1-5 mm de irregularidad.
03 — El hardware necesario para auto-leveling: ESP8266, BLTouch y final de carrera Z
El hardware de medición es barato y minimalista. Un Wemos D1 Mini (ESP8266, ~3€) con batería, un BLTouch como sonda de precisión (~10€ el clon) y un microswitch de final de carrera (~1€). El ESP es completamente pasivo durante el escaneo: solo escucha sensores y reporta eventos por WebSocket. La CNC ejecuta su G-code de forma autónoma sin saber que el ESP existe.
| Componente | Función | Pin ESP |
|---|---|---|
| BLTouch servo | Desplegar/retraer la sonda | D5 (GPIO14) |
| BLTouch probe | Detectar contacto con superficie | D6 (GPIO12) |
| Z-limit switch | Detectar cuando Z está arriba del todo | D7 (GPIO13) |
El BLTouch tiene un servo interno que despliega y retrae un pin metálico. Cuando el pin toca la superficie, se activa una señal digital limpia (sin rebote: sensor óptico/hall interno). El ESP controla el servo con señal PWM por D5 y lee el contacto por interrupción en D6.
El Z-limit switch es un microswitch mecánico normally open instalado en el punto más alto del eje Z. Cuando Z llega arriba, el switch se cierra. Cuando Z empieza a bajar, se abre. Esa apertura es el timestamp de inicio del descenso.
04 — Diagrama de conexiones del ESP8266 con BLTouch y Z-limit switch
05 — El G-code de escaneo de superficie: estructura por punto para CNC offline
El programa de escaneo lo genera la propia aplicación. La estructura por cada punto es fija y está diseñada para que el ESP pueda sincronizarse sin ambigüedad:
G0 X[x] Y[y] ; Rapid a posición XY del punto G4 P2000 ; Pausa 2s — el ESP redesplega el BLTouch aquí G1 Z-5.0 F120 ; Descenso lento — Z-limit se abre, BLTouch toca G0 Z50.0 ; Retracción — Z vuelve arriba, Z-limit se cierra G4 P500 ; Pausa 500ms post-retracción
El patrón de escaneo usa boustrophedon (zigzag): las columnas pares van de Y mínimo a Y máximo y las impares al revés, minimizando los rapids entre puntos. En una mesa de 2.500×1.300 mm con espaciado de 50 mm, esto son 50×26 = 1.300 puntos. El G-code de escaneo tiene unos 10.000 líneas y tarda aproximadamente una hora en ejecutarse.
La aplicación permite configurar el espaciado de la grilla, los márgenes (para no llegar al borde de la mesa) y los tiempos de pausa. El único requisito crítico es que la pausa post-retracción sea de al menos 300 ms para que el ESP tenga tiempo de redesplegar el BLTouch.
06 — El firmware del ESP8266: ISRs para BLTouch y Z-limit, debounce y WiFi dual AP+STA
El firmware del ESP8266 tiene dos ISRs (rutinas de interrupción): una para el BLTouch y otra para el Z-limit switch. Necesitan estar en IRAM (memoria de instrucciones, accesible durante interrupciones) y ser lo más rápidas posible.
El BLTouch tiene señal digital limpia (sin rebote mecánico, es un sensor de estado sólido), así que su ISR es directa:
void IRAM_ATTR bltouch_isr() {
bool contact = (digitalRead(PIN_BLTOUCH_PROBE) == cfg.bltouch_triggered);
unsigned long now = micros();
if (contact && !bltouch_contact) {
bltouch_contact = true;
bltouch_trigger_us = now;
bltouch_triggered_flag = true;
} else if (!contact && bltouch_contact) {
bltouch_contact = false;
}
}
El Z-limit switch es mecánico, así que necesita debounce. Se implementa en la propia ISR con 5 lecturas consecutivas a 200 µs de intervalo (~1 ms total). A 120 mm/min, 1 ms de error introduce como máximo 0,002 mm de error en la altura medida. Despreciable:
void IRAM_ATTR zlimit_isr() {
bool pressed = (digitalRead(PIN_ZLIMIT) == cfg.zlimit_triggered);
// Debounce: 5 lecturas consecutivas idénticas
for (int i = 0; i < 4; i++) {
delayMicroseconds(200);
if ((digitalRead(PIN_ZLIMIT) == cfg.zlimit_triggered) != pressed) return;
}
unsigned long now = micros();
if (pressed && !zlimit_pressed) {
zlimit_pressed = true;
zlimit_closed_flag = true; // Z llegó arriba
} else if (!pressed && zlimit_pressed) {
zlimit_pressed = false;
zlimit_open_us = now; // Z empieza a bajar — TIMESTAMP
zlimit_opened_flag = true;
}
}
Hay un problema de interferencia electromagnética entre D5 (servo) y D6 (probe): son pines adyacentes físicamente y el PWM del servo genera EMI que dispara la ISR del probe con falsos positivos. La solución es desactivar la ISR de D6 durante cada movimiento del servo y reactivarla al terminar:
void bltouch_servo_pulse(uint16_t pulse_us) {
detachInterrupt(digitalPinToInterrupt(PIN_BLTOUCH_PROBE)); // desactivar ISR D6
// ... bit-bang del servo ...
attachInterrupt(digitalPinToInterrupt(PIN_BLTOUCH_PROBE), bltouch_isr, CHANGE);
}
El ESP usa WiFi en modo dual AP+STA simultáneo. El SoftAP (192.168.4.1) siempre está activo para acceso de emergencia desde cualquier dispositivo sin configuración. La STA se conecta a la red del taller para comunicarse con el PC. La configuración completa (WiFi, MQTT, tiempos de pausa, parámetros del BLTouch) se guarda en LittleFS y es editable desde el propio portal web del ESP en /config. También tiene OTA: el firmware se puede actualizar subiendo un .bin desde el navegador.
07 — El modelo de timing: calcular la altura de la mesa desde timestamps en microsegundos
El cálculo de altura es sorprendentemente simple una vez que tienes los dos timestamps. El TimingModel convierte el feedrate a mm/µs y multiplica por la duración del descenso:
class TimingModel:
def __init__(self, probe_feedrate_mm_min: float) -> None:
self._feedrate_mm_per_us = probe_feedrate_mm_min / 60.0 / 1_000_000.0
def compute_height(self, descent_us: int, trigger_us: int) -> float:
descent_duration_us = trigger_us - descent_us
if descent_duration_us < 0:
descent_duration_us = 0
return self._feedrate_mm_per_us * descent_duration_us
Ejemplo: a F120 mm/min (2 mm/s = 0,000002 mm/µs), si el descenso dura 1.250.000 µs (1,25 s), la altura es 0,000002 × 1.250.000 = 2,5 mm. Eso es lo que bajó la sonda desde el Z-limit hasta tocar la superficie en ese punto.
Lo importante es que micros() en el ESP8266 tiene resolución de 1 µs. A F120, 1 µs de error → 0,000002 mm. El ruido práctico viene del jitter de la interrupción y el debounce del switch, no de la resolución del timer. Los tests sobre la propia mesa muestran desviación estándar de ±0,03-0,05 mm en mediciones repetidas del mismo punto.
08 — Procesando y visualizando el mapa de alturas 3D de la superficie CNC
Con los 1.300 timestamps procesados, el ScanProcessor construye un HeightMap: una grilla regular de valores Z reales de la superficie. Se guarda en JSON y NPZ (NumPy compressed) para no tener que volver a escanear si solo cambias los parámetros del modificador.
La interfaz permite ver el mapa como superficie 3D interactiva (matplotlib) y como mapa de calor 2D. En el mapa de calor puedes hacer clic en cualquier celda para editar el valor manualmente o promediar con los vecinos ortogonales (útil si un punto dio una medición errónea). Hay undo de 20 niveles. También puedes aplicar un multiplicador de escala (0,5x a 2,0x) para ajustar la agresividad de la corrección sin volver a escanear.
09 — El modificador de G-code: subdivisión de segmentos, arcos adaptativos y corrección Z
El G-code de producción (VCarve genera ficheros de 10-50 MB) se procesa en streaming línea a línea para no cargarlo entero en memoria. El GCodeModifier mantiene el estado modal (posición actual, modo relativo/absoluto, feedrate activo) y aplica la corrección Z de la siguiente forma:
- Movimientos G0 a altura segura: si el Z objetivo supera el umbral (5 mm por defecto), se deja sin modificar. Los rapids de retracción y desplazamiento no se tocan.
- Movimientos G1 cerca de la superficie: se subdividen en segmentos de máximo 5 mm (configurable). Cada subsegmento interpola la corrección Z en su punto medio. Esto evita que un G1 de 200 mm aplique solo una corrección en el extremo y se quede lineal en medio.
- Arcos G2/G3: se linearizan a G1 con error de cuerda máximo de 0,01 mm (10 µm) antes de aplicar las correcciones. El resultado es que los círculos y arcos también siguen la superficie.
La interpolación entre puntos del mapa usa bilineal o bicúbica (scipy RegularGridInterpolator). La bilineal es más rápida y suficiente para la mayoría de trabajos. La bicúbica da bordes más suaves pero puede oscilar en zonas con pocos puntos de medición.
10 — Arquitectura completa: Python, FastAPI, ESP8266, MQTT y app Android
11 — Estado del proyecto: cuándo estará disponible para descargar y cómo conseguirlo antes
El proyecto funciona y está en uso en el taller, pero aún no está empaquetado para distribución fácil. El instalador, los binarios compilados del firmware y la documentación de puesta en marcha están pendientes. Si te interesa antes de que eso pase, usa el formulario de contacto de abajo y te mando lo que hay.
El stack completo es Python 3.11+, PySide6, FastAPI, scipy, matplotlib, y firmware Arduino para ESP8266 con arduino-cli. La app Android (opcional, solo para monitorizar desde el móvil) está en B4A.