hardware 2026 · varios meses a ratos · ~15 € (ESP8266 + BLTouch)

CNC Auto-Leveling Mesh: corrigiendo G-code según el desnivel real de la mesa

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.

Mapa de alturas 3D de la mesa CNC mostrando el desnivel real en superficie de 50×26 puntos
Mapa de alturas real: superficie 3D (50×26 puntos) y mapa de calor. Se aprecian variaciones de hasta ±0,4 mm en una mesa de 2,5 metros.
Comparación 3D de trayectorias: G-code original plano en naranja vs G-code corregido siguiendo el desnivel en azul
Visualización 3D de trayectorias: naranja = G-code original (plano), azul = G-code corregido siguiendo el desnivel real de la mesa.

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:

  1. Z-limit switch se abre → Z empieza a bajar (timestamp t_descent)
  2. 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.

ComponenteFunciónPin ESP
BLTouch servoDesplegar/retraer la sondaD5 (GPIO14)
BLTouch probeDetectar contacto con superficieD6 (GPIO12)
Z-limit switchDetectar cuando Z está arriba del todoD7 (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.

Pantalla de configuración de CNC Auto-Leveling Mesh: dimensiones CNC, parámetros de escaneo, sonda BLTouch, final de carrera, WebSocket y configuración de interpolación
Pantalla de configuración: dimensiones de la CNC, espaciado de grilla, parámetros del BLTouch, debounce del final de carrera, tiempos dwell, servidor WebSocket y método de interpolación.

04 — Diagrama de conexiones del ESP8266 con BLTouch y Z-limit switch

CONEXIONES — WEMOS D1 MINI + BLTOUCH + Z-LIMIT WEMOS D1 MINI ESP8266 / 2MB flash 5V GND D5 / GPIO14 D6 / GPIO12 D7 / GPIO13 GND GND WiFi AP+STA · WebSocket · MQTT OTA · HTTP config portal BLTouch sonda de precisión VCC (rojo) GND (marrón+negro) Servo (amarillo) Probe (blanco) pin metálico servo sensor óptico/hall absorbe sobrecarrera Z-Limit Switch microswitch NO COM → D7 NO → GND 5V D7 usa pull-up interno · condensador 100nF recomendado entre COM y NO para eliminar rebote

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.

Pantalla de escaneo: botón generar G-code, estado ESP8266 conectado/desconectado, control de inicio y parada del escaneo con barra de progreso
Pantalla de escaneo: genera el G-code de exploración, muestra el estado de conexión con el ESP8266 y controla el inicio y parada del proceso.

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:

  1. 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.
  2. 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.
  3. 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.

Pantalla del modificador de G-code: selector de archivo .nc, método de interpolación y segmento máximo
Pantalla del modificador: carga el .nc de VCarve, elige interpolación y segmento máximo, y genera el fichero corregido listo para meter en el USB.

10 — Arquitectura completa: Python, FastAPI, ESP8266, MQTT y app Android

ARQUITECTURA DEL SISTEMA VCarve .nc G-code de producción G-code Modifier subdiv · arcos · Z corr .nc corregido → USB → CNC A10 PySide6 Desktop UI Configuración · Escaneo · Mapa de Alturas · Modificador G-code · Visualización FastAPI WebSocket Server thread daemon · asyncio · WS handler HeightMap + Scan Processor TimingModel · bilineal/bicúbica · JSON/NPZ Scanning G-code Generator boustrophedon · G4 dwells · scan.nc ESP8266 Wemos D1 Mini + batería BLTouch + Z-limit switch ISR · debounce · timestamps µs AP+STA · OTA · config portal WiFi · WebSocket Broker MQTT status · probe · scan · cmd retained LWT App Android (B4A) monitor · control remoto estado · log · comandos CNC RichAuto A10 2500×1300 mm ejecuta .nc desde USB sin puerto de datos caja negra total USB Línea continua = datos en tiempo real · Línea discontinua = señal / notificación

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.