Ensayo 6 min de lectura

De PLC a Python: lo que la automatización industrial me enseñó sobre software

Hay un momento en la programación de PLCs que nunca se olvida realmente. Tienes un programa nuevo cargado en el controlador, la máquina está en reposo en su posición inicial, y pulsas la tecla de habilitación. En ese instante, la física se convierte en software. Las cintas transportadoras giran. Los cilindros se extienden. Los sensores disparan. Si tu código tiene un error lógico, no te enteras con un stack trace en un terminal sino con un brazo metálico moviéndose en la dirección equivocada a máxima velocidad. Aprendes, rápidamente, a pensar de forma diferente sobre lo que significa que el código se ejecute.

Pasé la mayor parte de dos años en Stuttgart trabajando con PLCs industriales — programando sistemas Siemens S7, cableando arrays de sensores, calibrando servomotores, escribiendo lógica ladder para secuencias de líneas de montaje. En aquel momento lo veía puramente como trabajo mecánico: hacer que la cosa se mueva, hacer que pare, hacer que lo repita diez mil veces sin fallo. Solo después de mi transición al desarrollo web — primero JavaScript vanilla, luego React, después sistemas backend distribuidos — me di cuenta de cuánto aquellos años en la planta de producción habían modelado silenciosamente la forma en que escribo software. Los paralelismos no son superficiales.


El ciclo de scan: el tiempo como restricción, no como abstracción

Un PLC no ejecuta código como lo hace un ordenador de propósito general. No dispara eventos, no responde a interrupciones, ni hace malabares con hilos. Ejecuta un solo bucle — el ciclo de scan — de arriba a abajo, una y otra vez, típicamente cada 5 a 20 milisegundos. En cada pasada, lee todos los estados de entrada, evalúa cada escalón de lógica, y escribe todos los estados de salida. Eso es todo. Cada ciclo. Sin excepción.

A esto se le llama determinismo, y es el principio fundacional del control industrial. A la máquina no le importa la latencia de red, las pausas de recolección de basura, ni un planificador del sistema operativo decidiendo que algo más es más importante ahora mismo. El ciclo de scan se completará en un tiempo conocido y acotado, o el temporizador watchdog forzará un fallo en la CPU y detendrá la máquina. No hay ejecución parcial, no hay "estaba funcionando lento ese día." El sistema es predecible por diseño, porque la impredecibilidad en una fábrica mata gente.

Interiorizas esta restricción. Dejas de escribir código que asume "probablemente será lo bastante rápido" y empiezas a escribir código que demuestra el timing. Cada rama importa. Cada condicional que podría extender un ciclo de scan se examina. Es quizás lo más enfocado que he estado nunca en coste computacional — no porque fuera académicamente riguroso, sino porque la alternativa era una alarma de fallo a las 3 de la mañana.


Lógica ladder y la primera máquina de estados

Los programas de PLC se escriben tradicionalmente en lógica ladder: un lenguaje gráfico que parece un esquema eléctrico de relés convertido en código. Los contactos (entradas) y bobinas (salidas) se organizan en escalones. Un escalón se evalúa de izquierda a derecha, y la bobina al final se energiza si el camino lógico a través de los contactos se completa. Es, en su esencia, un lenguaje de dominio específico para expresar transiciones de estado booleanas a lo largo del tiempo.

Considera una secuencia simple: un motor que solo debe funcionar cuando se pulsa un botón de arranque, una protección de seguridad está cerrada, y no hay parada de emergencia activa. En lógica ladder se ve así:

Lógica Ladder — escalones equivalentes en STL de Siemens

// Rung 1: Start latch — motor runs until E-stop or guard opens
|--[ START_PB ]--+--[ MOTOR_RUN ]--+--[/E_STOP]--[GUARD_CLOSED]--( MOTOR_RUN )--|
|                |                 |
|                +--[ MOTOR_RUN ]--+

// Rung 2: Fault output — any safety condition triggers alarm
|--[/E_STOP]--( FAULT_LAMP )--|
|--[/GUARD_CLOSED]--( FAULT_LAMP )--|

Lo que estás escribiendo, aunque quizás no lo llames así, es una máquina de estados. MOTOR_RUN es un estado booleano. Las transiciones de entrada y salida están protegidas por condiciones. El escalón de enganche (usando la propia salida del motor como contacto de retención) es una implementación canónica de un circuito de auto-sellado — una vez arrancado, sostiene su propia condición hasta que un evento externo la rompe. Ese patrón es, estructuralmente, idéntico a lo que escribes en Redux cuando describes un estado de carga que persiste hasta que llega una acción de éxito o error.

Aquí está la misma lógica en Python, reducida a su esencia:

Python — transición de estado equivalente

from dataclasses import dataclass, replace
from typing import Literal

type MotorState = Literal["idle", "running", "fault"]

@dataclass(frozen=True)
class SystemInputs:
    start_pb: bool
    e_stop_ok: bool
    guard_closed: bool

def next_state(state: MotorState, inputs: SystemInputs) -> MotorState:
    if not inputs.e_stop_ok or not inputs.guard_closed:
        return "fault"
    if state == "running":
        return "running"  # self-seal: sustain until safety clears
    if inputs.start_pb:
        return "running"
    return "idle"

La estructura es la misma: función pura, sin efectos secundarios en la propia transición, salidas determinadas enteramente por el estado actual más las entradas. El escalón ladder es un reducer. No conocía la palabra "reducer" en Stuttgart. Pero los había estado escribiendo durante dos años.


Lo que realmente me enseñó sobre el fallo

El software web falla con gracia, o al menos ese es el objetivo. Un error 500 es molesto; un reintento normalmente lo arregla; un usuario pierde unos segundos. El software de control industrial tiene una relación diferente con el fallo. Un paquete perdido de un sensor de fieldbus significa que un brazo robótico opera a ciegas. Un error lógico que permite que dos cilindros enclavados se extiendan simultáneamente puede destruir utillaje que vale más que un año de salario. Los modos de fallo no son casos extremos en los que piensas después de que la funcionalidad funciona. Son lo primero que diseñas.

La disciplina que esto crea es difícil de replicar artificialmente. Cuando has pasado tiempo mirando un diagrama de cableado preguntándote "¿qué le pasa a esta salida si este cable de entrada se suelta?", desarrollas un instinto para examinar primero el camino infeliz. ¿Cuál es el estado seguro? ¿Qué hace el sistema cuando pierde la señal, no cuando todo está nominal? Esta es la pregunta que ahora hago habitualmente al diseñar APIs o sistemas distribuidos, porque aprendí por las malas que el caso nominal no es el que va a romper producción a las 2 de la mañana.


El espejo sorprendente: event loops, máquinas de estado asíncronas, React

Cuando leí por primera vez sobre el event loop de JavaScript — un solo hilo, procesando una tarea a la vez de una cola, I/O no bloqueante gestionado mediante callbacks — tuve una sensación inmediata de reconocimiento. No intelectualmente. Algo más cercano a la memoria muscular. La estructura era la misma: un bucle de evaluación controlado y secuencial sobre un estado conocido, impulsado por señales externas. Las señales eran eventos de usuario y respuestas de red en lugar de entradas digitales de un array de sensores, pero el modelo era isomorfo.

El modelo de estado de React profundizó el paralelismo. El estado de un componente en el tiempo t es una función pura de su estado en el tiempo t−1 y la acción despachada — exactamente la función next_state de arriba, excepto que el "ciclo de scan" es un render. El DOM virtual es la imagen de salida del PLC: una instantánea de cómo debería verse el mundo, calculada de nuevo en cada ciclo, aplicada atómicamente. He visto a desarrolladores de React sorprendidos por bugs de closures obsoletas en useEffect — están confundidos porque esperan que el tiempo sea continuo. El pensamiento PLC lo reencuadra inmediatamente: estás leyendo la instantánea de estado del ciclo anterior. Por supuesto que el valor está obsoleto. Tu imagen de entrada se capturó al inicio de este scan.

El DOM virtual es la imagen de salida del PLC: una instantánea de cómo debería verse el mundo, calculada de nuevo en cada ciclo, aplicada atómicamente.

XState, la librería de máquinas de estado de JavaScript, bien podría haber sido escrita por un ingeniero de control. Estados explícitos. Transiciones con guardas. Acciones de entrada y salida. Cuando leí su documentación por primera vez sentí, por un momento, como si me hubieran teletransportado de vuelta a un proyecto STEP 7 en Stuttgart. El vocabulario era diferente. La formalización era la misma.


El cambio mental: de deploy-y-listo a despliegue continuo

Hay una discontinuidad profunda entre el control industrial y el software web, y me llevó más tiempo absorberla de lo que esperaba. Un programa de PLC, una vez puesto en marcha y firmado, está esencialmente congelado. No envías un parche a una máquina en funcionamiento en mitad de producción. Programas una ventana de mantenimiento. Pruebas la nueva versión en un entorno de staging — a menudo una réplica física de la línea de producción. Lo validas exhaustivamente. Luego, durante una parada controlada, cargas el nuevo programa y reinicias. El despliegue es un evento. Es planificado, presenciado y registrado.

El despliegue continuo — donde un merge a main puede enviar código a millones de usuarios en minutos — fue, inicialmente, genuinamente alarmante para mí. El concepto de un feature flag como un toggle incremental, la idea de despliegues progresivos que actualizan el 5% de los nodos a la vez, la normalización de "monitorizaremos y haremos rollback si es necesario" — todo esto se sentía, desde un trasfondo de control, como una abdicación temeraria de la disciplina de testing pre-comisionado que me habían entrenado para tomar en serio.

Con el tiempo llegué a ver que la respuesta de la web a esto no es temeridad sino un tipo diferente de rigor. La observabilidad — métricas, trazas distribuidas, logging estructurado — es el panel de monitorización de la línea de producción. Los despliegues canary son la prueba funcional a rendimiento reducido antes de la transición completa. Los feature flags son la anulación manual que te permite desenergizar un escalón sin modificar el programa. Los principios mapean. Las herramientas son completamente diferentes. Aprender a confiar en las herramientas requirió un cambio deliberado de instinto.


Donde las habilidades divergen: concurrencia y no determinismo

El lugar donde el pensamiento PLC te lleva activamente por mal camino es la concurrencia. Un ciclo de scan es, por definición, de un solo hilo. No hay condiciones de carrera en un PLC porque solo hay un hilo de ejecución y siempre se ejecuta hasta completarse antes de que comience el siguiente scan. El modelo mental es limpio, simple, y completamente erróneo para sistemas distribuidos.

Cuando me encontré por primera vez con los niveles de aislamiento de transacciones en bases de datos, me parecieron extraños y sobreingeniados. Cuando debugeé por primera vez una condición de carrera en un servicio Node.js — dos peticiones concurrentes leyendo un contador, ambas incrementándolo, ambas escribiéndolo de vuelta — estaba genuinamente confundido. Esto no ocurre en la planta de producción. No puede ocurrir, por diseño. Entender que la concurrencia es el problema central de los sistemas en red, no un caso extremo patológico, requirió desaprender el confortable determinismo del pensamiento del ciclo de scan.

Lo mismo aplica a la consistencia eventual. Un PLC sabe, con certeza, el estado de cada entrada al inicio de cada scan. Un sistema distribuido a menudo no sabe el estado actual de un nodo que no ha reportado en 200 milisegundos. ¿Está caído? ¿Está lento? ¿La partición de red está entre tú y él, o entre él y el mundo exterior? La respuesta es: no lo sabes, y tu sistema debe funcionar razonablemente en las tres posibilidades. No hay equivalente PLC de esto. La suposición de la planta de producción — que el fieldbus está funcionando o está en fallo — es un lujo que los sistemas distribuidos no disfrutan.


Por qué las restricciones generan claridad

La lección más profunda que saqué de la automatización industrial no es sobre ningún patrón técnico específico. Es sobre lo que los entornos restringidos hacen a tu pensamiento. Cuando un error en tu código puede ser físicamente peligroso, dejas de tolerar la ambigüedad en los requisitos. Cuando tu programa se ejecuta en una CPU con 512 KB de memoria y un presupuesto de 10 ms por scan, dejas de escribir código que "debería estar bien." Cuando la persona que va a mantener tu programa es un electricista leyendo un diagrama ladder, no un desarrollador leyendo TypeScript, dejas de escribir código ingenioso.

La ingeniería de software ha resuelto en gran medida el problema de la seguridad física — un bug en una app web no lesiona a nadie. Pero no ha resuelto el problema de la disciplina. Las restricciones que hacían rigurosa la programación de PLCs eran externas e inevitables. En el desarrollo web, el rigor análogo es opcional. Hay que elegirlo. Agradezco haber pasado tiempo en un entorno donde no era opcional. Hizo la elección más fácil.

Todavía pienso en ciclos de scan a veces. Cuando estoy revisando un componente React y algo se siente mal en sus actualizaciones de estado, me encuentro preguntando: ¿cuál es la imagen de entrada al inicio de este render? Cuando estoy diseñando un endpoint de API, pregunto: ¿cuál es el estado seguro si esta llamada nunca recibe respuesta? Las preguntas son diferentes. El hábito de hacerlas vino de una planta de producción en Stuttgart, y no creo que lo cambiaría.