Para enviar órdenes desde una base de datos a un PLC, guardas cada orden como una fila con un id y un estado, llevas las filas nuevas hasta el PLC con un pequeño servicio (MQTT, Modbus TCP o una API web) y el PLC escribe el resultado en esa misma fila. La base de datos decide qué tiene que pasar. El PLC lo ejecuta, lo confirma con una señal real y mantiene un estado conocido cuando se cae la red.
Nuestros ejemplos usan cilindros neumáticos y un ESP32 PLC. Puedes aplicar la misma lógica a actuadores eléctricos y a cualquier PLC con puerto de red.
¿Cómo puede una base de datos controlar actuadores neumáticos a través de un PLC?
Una base de datos no puede conmutar una electroválvula. Un PLC sí. Tu trabajo consiste en montar un camino fiable desde una fila de una tabla hasta una salida del PLC, y de vuelta con una confirmación. La cadena tiene cinco eslabones:
- Se escribe una fila. Tu ERP, una aplicación web o un operador inserta una orden: qué actuador, qué acción y el estado
PENDING. - La orden llega al PLC. Un servicio envía las filas nuevas por MQTT o Modbus TCP, o el PLC se las pide a una API web.
- El PLC activa una salida. La salida excita la bobina de una válvula, la válvula manda aire al cilindro y el cilindro mueve la compuerta, la puerta o la trampilla.
- El PLC comprueba el resultado. Un final de carrera le indica que el cilindro ha llegado al final de su recorrido.
- El estado vuelve a la fila.
SENTcuando el PLC acepta la orden,DONEcuando la posición está confirmada, yFAULToSKIPPEDcon un motivo cuando no.
Cada eslabón puede fallar por separado. Con un buen diseño, verás cada fallo en tu tabla.
Diseña la tabla de órdenes antes que el programa del PLC
La tabla de órdenes es el contrato entre IT y OT. Necesitas un id único y creciente, el destino, la acción, un estado y unas marcas de tiempo que escriba el propio servidor de base de datos.
Ejemplo Una tabla MySQL mínima:
CREATE TABLE gate_command (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
gate_id SMALLINT UNSIGNED NOT NULL,
command ENUM('OPEN','CLOSE') NOT NULL,
status ENUM('PENDING','SENT','DONE','FAULT','SKIPPED')
NOT NULL DEFAULT 'PENDING',
reason VARCHAR(64) NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NULL,
INDEX idx_pending (status, id)
);
INSERT INTO gate_command (gate_id, command) VALUES (3, 'OPEN');
Cuatro reglas que aplicaríamos desde el primer día:
- Ordena por id, no por la hora del cliente. Dos aplicaciones pueden tener relojes distintos. El id autoincremental da a todos el mismo orden.
- Solo el lado del PLC escribe el estado final. Tu aplicación escribe
PENDING.DONEsignifica que el PLC ha visto el final de carrera, nada menos. - Guarda un motivo en cada fila que no se ha ejecutado. Un motivo como "default state active" o "position timeout" te ahorra horas de diagnóstico.
- Nunca borres filas para cancelarlas. Márcalas como
SKIPPED, porque la tabla también es tu registro de órdenes.
Tres formas de llevar una orden de MySQL al PLC
En las tres opciones ponemos un pequeño programa entre la base de datos y los PLC. Así las credenciales de la base de datos y el tráfico SQL se quedan fuera de la red de control. Puedes ejecutarlo en un PC industrial o en un controlador basado en Linux cerca de los PLC.
Servicio de órdenes con MQTT
El servicio lee las filas PENDING y publica cada una en un topic por PLC, por ejemplo plc-a/cmd. El PLC se suscribe, ejecuta y publica la confirmación en plc-a/ack, y el servicio la escribe en la fila. Necesitarás un broker. Encaja bien cuando hay muchos PLC o un enlace inestable.
Servicio de órdenes con Modbus TCP
Aquí el PLC es el servidor Modbus TCP y el servicio es el cliente. El servicio escribe el id de la orden, el número de compuerta y la acción en holding registers. El PLC deja el último id ejecutado y su estado en otros registros, y el servicio los vuelve a leer. Tu SCADA o tu HMI pueden leer esos mismos registros.
API web consultada por el PLC
Una pequeña API web delante de la base de datos responde a GET /next-command?plc=a con la siguiente fila para ese PLC y recibe la confirmación con un POST. Es el PLC quien inicia siempre el intercambio, y eso ayuda detrás de un cortafuegos.
| Opción | Quién inicia el intercambio | Componente adicional | Retardo |
|---|---|---|---|
| Servicio con MQTT | El servicio publica, el PLC se suscribe | Broker MQTT | Ciclo del servicio más tiempo de red |
| Servicio con Modbus TCP | El servicio (cliente Modbus) | Ninguno | Ciclo del servicio |
| API web consultada por el PLC | El PLC | API web | Intervalo de consulta del PLC |
Polling o push: el intervalo marca el peor retardo
Salvo que uses triggers o captura de cambios (CDC) sobre el binlog de MySQL, algo tiene que consultar la base de datos. Si el servicio consulta cada 1 s, una fila nueva espera hasta 1 s, más el transporte y la carrera del cilindro. Con 5 s espera hasta 5 s, que suele bastar para una compuerta y se queda corto para un pulsador de avance manual. Un servicio que lee cada segundo una columna status indexada es una carga ligera. Veinte PLC con sus propias conexiones SQL ya no lo son.
El push (MQTT) elimina la espera entre el servicio y el PLC, pero no entre la base de datos y el servicio. Para recortar ese tramo, haz que tu aplicación avise al servicio y mantén el polling como respaldo.
Confirmación de órdenes e idempotencia en el PLC
Una orden es idempotente cuando recibirla dos veces tiene el mismo efecto que recibirla una. Un mensaje puede llegar dos veces: MQTT con QoS 1 puede volver a entregarlo, y un servicio puede reiniciarse y reenviarlo.
Nuestra regla más sencilla: el PLC recuerda el id de orden más alto que ha ejecutado e ignora cualquier id igual o menor. Como los ids solo crecen, también se rechazan las órdenes antiguas que lleguen tarde. Guarda ese valor en memoria no volátil, o deja que la tabla te proteja después de un reinicio.
Confirma la posición, no la salida. Una salida a ON solo demuestra que el PLC ha dado corriente a una bobina. No demuestra que hubiera aire ni que el cilindro se moviera. Usa un final de carrera al final del recorrido y un tiempo límite: si el final de carrera no cambia a tiempo, la fila pasa a FAULT.
También te toca decidir qué pasa cuando llegan dos órdenes para la misma compuerta:
- Gana la más reciente. El servicio marca como
SKIPPEDlas filasPENDINGanteriores de esa compuerta, con el motivo "newer command". Sirve cuando solo importa la posición final. - Ejecución en orden. Cada orden espera a que la anterior de esa compuerta esté en
DONE. Sirve cuando cuenta cada movimiento.
Pérdida de red: un estado conocido en el lado del PLC
Un PLC no sabe por sí solo que la base de datos ha desaparecido. Sus salidas se quedan como están hasta que el programa las cambie. Por eso necesitas un watchdog: el servicio envía una señal de vida (un mensaje MQTT, o un registro Modbus que se incrementa cada segundo) solo mientras puede leer la base de datos. Si el PLC no recibe nada durante un tiempo fijado, por ejemplo 5 s, lleva todos los actuadores a su estado por defecto, enciende una lámpara de alarma y deja de aceptar órdenes.
El estado por defecto se define actuador por actuador. Para una compuerta es "cerrar". Para otra es mejor "mantener la posición actual", porque cerrarla podría bloquear un paso. Déjalo por escrito, junto con la válvula que lo hace posible.
La reconexión también necesita una regla. El servicio lee primero las posiciones reales del PLC, actualiza la tabla y solo después envía órdenes nuevas. Ten en cuenta que perder la red y perder la alimentación son cosas distintas: si cae la red, el PLC ejecuta el estado por defecto; si cae la alimentación, no se ejecuta nada y solo deciden la válvula y la mecánica. Comprueba los dos casos en cada actuador.
Pruébalo en el simulador. Escribe filas OPEN y CLOSE, corta la red con compuertas abiertas y mira cómo los PLC pasan al estado por defecto y qué escriben en la tabla.
Ejecuta el escenario de caída de redUn estado por defecto no es una función de seguridad
A menudo se le llama "estado seguro", pero es una función de control. Hace que el proceso sea predecible, y eso es útil. No protege a las personas. Ni un programa de PLC estándar, ni un watchdog, ni una base de datos están validados como partes del sistema de mando relativas a la seguridad.
Si una compuerta o un cilindro en movimiento pueden herir a alguien, la máquina necesita una evaluación de riesgos (ISO 12100) y un circuito de seguridad independiente, diseñado para el nivel de prestaciones requerido (ISO 13849-1). En neumática, ese circuito suele cortar la presión con una válvula de descarga de seguridad, gobernada por un relé o un controlador de seguridad, sin depender del PLC ni de la red. El PLC puede leer el estado de ese circuito, pero no debe formar parte de él.
Electroválvula 5/2 monoestable, 5/2 biestable o 5/3 de centro cerrado
La válvula decide qué hace el cilindro cuando la salida se desactiva o falla la alimentación. Elígela a la vez que el estado por defecto, no después.
| Válvula | Salidas del PLC por cilindro | Salida a OFF o pérdida de alimentación | Uso típico |
|---|---|---|---|
| 5/2 monoestable, retorno por muelle | 1 | El muelle lleva la válvula a su posición de reposo. El cilindro va al extremo de reposo (por ejemplo, cerrado). | Quieres una posición por defecto clara, un cableado sencillo y un estado por defecto simple. |
| 5/2 biestable (doble bobina) | 2 | La válvula se queda en la última posición conmutada. El cilindro sigue hasta ese extremo si hay aire. | Hay que mantener la última posición tras una señal breve. El estado por defecto tiene que activar la otra bobina. |
| 5/3 de centro cerrado | 2 | Los muelles centran la válvula y bloquean todas las vías. El cilindro se para donde está. | Parar a mitad de carrera. Las fugas y la carga pueden seguir moviendo el cilindro poco a poco, así que no es un bloqueo mecánico. |
Una nota práctica: el PLC nunca debe activar a la vez las dos bobinas de una válvula biestable. Comprueba el símbolo de la válvula en la hoja de datos, no solo el nombre.
Cableado de las bobinas de electroválvula a las salidas del PLC
Hay tres formas habituales de conectar las bobinas, que mostramos como ejemplo. La adecuada depende de la tensión de la bobina, del número de válvulas y de la distancia al armario.
- Salidas directas. Una salida de transistor por cada bobina de 24 Vdc. Compara la corriente de la bobina con la que admite la salida, y añade un diodo de supresión si la bobina no lo lleva.
- Relés de interfaz. Una salida gobierna un relé y el relé conmuta la bobina. Te conviene cuando la tensión o la corriente de la bobina no encajan con la salida, o cuando quieres un punto de aislamiento claro.
- Isla de válvulas (terminal de válvulas) con interfaz de bus. Muchas válvulas en un mismo bloque, y el PLC escribe bits por una red o un bus de campo. Antes de elegir el PLC, comprueba qué protocolo admite la isla, por ejemplo Modbus TCP.
Ejemplo Dimensionado de E/S con un ESP32 PLC 42. Según su página de producto, tiene 16 salidas (10 digitales optoaisladas y 6 analógicas/digitales/PWM, de 5 a 24 Vdc, sin salidas de relé) y 26 entradas (14 digitales optoaisladas y 12 analógicas/digitales). Supón que tienes 6 compuertas con válvulas monoestables y dos finales de carrera en cada una: necesitas 6 salidas y 12 entradas, y te quedan 10 salidas libres para pilotos. Con válvulas biestables, las mismas compuertas ocupan 12 salidas. Consulta los valores nominales de las salidas en la hoja de datos de la familia ESP32 PLC. Si necesitas más E/S, puedes añadir módulos de expansión por I2C (hasta 127) o un segundo PLC.
Código de ejemplo: gestor de órdenes MQTT para un ESP32 PLC
Ejemplo Este código ilustra el patrón. No es código de ninguna instalación de cliente y no está listo para producción. Muestra el rechazo de duplicados por id, los estados de confirmación, un tiempo límite de posición y un watchdog.
Primero, el servicio de órdenes. Lee las filas PENDING, las publica, escribe las confirmaciones en la tabla y envía la señal de vida solo después de una consulta correcta. Usa las librerías de Python PyMySQL y paho-mqtt 2.x.
# EXAMPLE ONLY: command service between MySQL and the PLCs
import json, time
import pymysql
import paho.mqtt.client as mqtt
db = pymysql.connect(host="localhost", user="plc_service", password="change-me",
database="plant", autocommit=True)
GATE_TO_PLC = {1: ("plc-a", 1), 2: ("plc-a", 2), 3: ("plc-a", 3)} # example map
def on_ack(client, userdata, msg):
ack = json.loads(msg.payload)
if ack["status"] == "DUPLICATE":
return # already handled once
with db.cursor() as cur:
cur.execute("UPDATE gate_command SET status=%s, reason=%s, "
"updated_at=NOW(3) WHERE id=%s",
(ack["status"], ack.get("reason") or None, ack["id"]))
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="db-service")
client.on_message = on_ack
client.connect("192.168.1.10", 1883)
client.subscribe("+/ack", qos=1)
while True:
with db.cursor() as cur:
cur.execute("SELECT id, gate_id, command FROM gate_command "
"WHERE status='PENDING' ORDER BY id LIMIT 20")
rows = cur.fetchall()
for cid, gate, cmd in rows:
plc, local_gate = GATE_TO_PLC[gate]
payload = json.dumps({"id": cid, "gate": local_gate, "cmd": cmd},
separators=(",", ":"))
client.publish(plc + "/cmd", payload, qos=1)
client.publish("plc-a/heartbeat", "1") # only reached if the query worked
client.loop(timeout=1.0) # handles acks in this same thread
Una fila sigue en PENDING hasta que el PLC responde SENT, así que el servicio puede publicarla más de una vez. Es intencionado: el PLC rechaza las repeticiones por id. Un servicio real necesita además reconexión y gestión de errores.
Después, el lado del PLC, para el IDE de Arduino con el paquete de placas ESP32 de Industrial Shields y la librería PubSubClient para MQTT. Los nombres de los pines dependen del modelo, así que compruébalos en el paquete de placas.
// EXAMPLE ONLY: minimal pattern, not production code.
// Three gates, 5/2 single solenoid valves with spring return (output ON = open),
// one "open" and one "closed" limit switch per gate.
#include <Ethernet.h>
#include <PubSubClient.h>
const int N = 3;
const int VALVE[N] = {Q0_0, Q0_1, Q0_2}; // valve coil outputs
const int IS_OPEN[N] = {I0_0, I0_1, I0_2}; // limit switch: gate open
const int IS_SHUT[N] = {I0_3, I0_4, I0_5}; // limit switch: gate closed
const int ALARM_LAMP = Q0_7;
const unsigned long LINK_TIMEOUT_MS = 5000; // no message for 5 s: default state
const unsigned long MOVE_TIMEOUT_MS = 8000; // no limit switch after 8 s: FAULT
byte mac[] = {0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x42};
EthernetClient eth;
PubSubClient mqtt(eth);
long lastId = 0; // highest command id already executed
unsigned long lastContact = 0; // last message from the service
bool defaultState = false;
struct Move { long id; bool open; unsigned long start; bool busy; };
Move moves[N];
void ack(long id, const char* status, const char* reason = "") {
char msg[96];
snprintf(msg, sizeof(msg), "{\"id\":%ld,\"status\":\"%s\",\"reason\":\"%s\"}",
id, status, reason);
mqtt.publish("plc-a/ack", msg);
}
void enterDefaultState() {
for (int g = 0; g < N; g++) {
digitalWrite(VALVE[g], LOW); // spring returns every gate to closed
moves[g].busy = false;
}
digitalWrite(ALARM_LAMP, HIGH);
defaultState = true;
}
// Payload example: {"id":1042,"gate":2,"cmd":"OPEN"} (gate 1..N)
void onMessage(char* topic, byte* payload, unsigned int len) {
lastContact = millis();
if (strcmp(topic, "plc-a/heartbeat") == 0) return;
char buf[64];
len = len < sizeof(buf) - 1 ? len : sizeof(buf) - 1;
memcpy(buf, payload, len);
buf[len] = 0;
long id = 0; int gate = 0; char cmd[8] = "";
if (sscanf(buf, "{\"id\":%ld,\"gate\":%d,\"cmd\":\"%7[A-Z]\"}", &id, &gate, cmd) != 3) return;
if (id <= lastId) { ack(id, "DUPLICATE"); return; } // never run a row twice
if (gate < 1 || gate > N) { ack(id, "SKIPPED", "unknown gate"); return; }
if (defaultState) { ack(id, "SKIPPED", "default state active"); return; }
lastId = id;
int g = gate - 1;
bool open = strcmp(cmd, "OPEN") == 0;
digitalWrite(VALVE[g], open ? HIGH : LOW);
moves[g] = {id, open, millis(), true};
ack(id, "SENT");
}
void checkMoves() {
for (int g = 0; g < N; g++) {
if (!moves[g].busy) continue;
bool arrived = digitalRead(moves[g].open ? IS_OPEN[g] : IS_SHUT[g]) == HIGH;
if (arrived) {
ack(moves[g].id, "DONE");
moves[g].busy = false;
} else if (millis() - moves[g].start > MOVE_TIMEOUT_MS) {
ack(moves[g].id, "FAULT", "position timeout");
moves[g].busy = false;
}
}
}
void setup() {
for (int g = 0; g < N; g++) pinMode(VALVE[g], OUTPUT);
pinMode(ALARM_LAMP, OUTPUT);
Ethernet.begin(mac);
mqtt.setServer(IPAddress(192, 168, 1, 10), 1883);
mqtt.setCallback(onMessage);
enterDefaultState(); // start in a known state and wait for the service
}
void loop() {
if (!mqtt.connected()) {
if (mqtt.connect("plc-a")) {
mqtt.subscribe("plc-a/cmd", 1);
mqtt.subscribe("plc-a/heartbeat");
mqtt.publish("plc-a/status", "ONLINE");
}
}
mqtt.loop();
if (millis() - lastContact > LINK_TIMEOUT_MS) {
if (!defaultState) enterDefaultState();
} else if (defaultState) {
defaultState = false; // link is back: accept new commands
digitalWrite(ALARM_LAMP, LOW);
}
checkMoves();
}
Aquí el estado por defecto es "cerrar", porque una válvula 5/2 monoestable con retorno por muelle cierra la compuerta cuando la salida se desactiva. Si alguna de tus compuertas tiene que mantener la posición, necesita otra válvula (por ejemplo, una 5/3 de centro cerrado) y otra línea en enterDefaultState(). Con Modbus TCP, sustituyes el callback de MQTT por una comprobación de registros.
El patrón en una instalación real
Este patrón ya funciona en una instalación real. Un cliente de Industrial Shields controla 18 compuertas neumáticas y 9 pilotos de señalización con 2 unidades ESP32 PLC 42, y las órdenes de abrir y cerrar llegan por la red desde una base de datos MySQL. El propio equipo del cliente programó y puso en marcha el sistema con la documentación de Industrial Shields y los cursos de MQTT y Modbus de esta web, y funciona sin problemas. Las opciones anteriores son decisiones generales de ingeniería para tu propio diseño. Lee el caso de éxito de control de compuertas neumáticas para conocer la historia completa.
Tutoriales de MQTT y Modbus para ESP32 PLC
Tienes una guía paso a paso para cada parte de esta arquitectura:
- Órdenes remotas por MQTT con un topic de respuesta (PLC Solution Library)
- Conexión MQTT sobre TLS: guía para ESP32 PLC
- Modbus TCP con PLC basados en Arduino y ESP32
- Modbus RTU con PLC basados en Arduino y ESP32
- RS-485 con PLC basados en Arduino y ESP32
- Primeros pasos con Modbus (curso)
- Cómo usar SQLite con ESP32 PLC, para guardar en el propio PLC un registro local de órdenes
- Librería MSSQL para PLC industriales Arduino y ESP32
- ESP32 PLC 42 (página de producto) y la familia ESP32 PLC
Preguntas frecuentes sobre órdenes de una base de datos a un PLC
¿El PLC debe consultar la base de datos, o es mejor enviarle las órdenes?
Te recomendamos un único servicio que consulte la tabla y envíe cada orden al PLC por MQTT, o la escriba por Modbus TCP. El PLC nunca abre una conexión a la base de datos, y el intervalo de ese servicio marca el peor retardo.
¿Cómo evito que la misma orden mueva una compuerta dos veces?
Da a cada orden un id único que solo crezca, como una columna autoincremental de MySQL. El PLC guarda el id más alto que ha ejecutado e ignora cualquier id igual o menor.
¿Un estado por defecto ante una pérdida de red es una función de seguridad?
No. Es una función de control del programa del PLC y no protege a las personas. Para eso hacen falta una evaluación de riesgos (ISO 12100) y un circuito de seguridad independiente diseñado según ISO 13849-1, al margen del PLC y de la red.
¿Qué válvula mantiene un cilindro neumático en su sitio cuando se pierde la señal?
Una válvula 5/2 biestable mantiene la última posición conmutada, así que el cilindro completa esa carrera. Para pararlo a mitad de carrera, usa una válvula 5/3 de centro cerrado. Una 5/2 monoestable vuelve a reposo.
¿Puede un ESP32 PLC leer órdenes de MySQL?
Sí, con un pequeño servicio intermedio. El servicio lee la tabla y envía cada orden por MQTT o Modbus TCP, o bien el PLC pide la siguiente fila a una API web. Las credenciales de la base de datos se quedan fuera de la red de control.
¿Puedo usar Modbus TCP en lugar de MQTT para las órdenes de la base de datos?
Sí. El PLC es el servidor Modbus TCP, y el servicio escribe cada orden en holding registers y lee el resultado en otros registros. No necesitas broker.