← Índice del curso

Robótica y Automatización · Tema 39

Automatización de procesos: secuencias y enclavamientos

Una cinta transportadora que clasifica piezas reúne todo lo anterior: ciclo de scan, máquina de estados sin bloqueos y fallas enclavadas. Lo nuevo es lo que distingue a un proceso de una demostración: saber dónde está cada pieza, ordenar los pasos con garantías y prohibir con enclavamientos lo que nunca debe ocurrir.

01 · Punto de partida

Del ejemplo al proceso

Los temas 35 a 38 enseñaron las herramientas: el ciclo de scan de un PLC, las máquinas de estados, la ejecución sin bloqueos y la jerarquía. Un proceso real las combina y agrega tres cuestiones propias de la automatización: lo que ocurre en el material (las piezas se mueven y hay que saber dónde están), lo que no debe ocurrir nunca (los enclavamientos) y lo que hay que hacer cuando algo sale mal.

El caso es una cinta transportadora que recibe piezas de dos alturas, deja pasar las pequeñas hasta el final y desvía las grandes con un cilindro neumático hacia una tolva lateral. Es simple para entenderla entera y suficiente para que aparezcan los problemas de verdad.

Todo funciona sin hardware: una planta simulada produce los sensores, igual que en los temas anteriores, y el mismo código sirve para conectar sensores y salidas reales.

02 · El sistema

La línea de clasificación

La cinta avanza a 20 cm/s cuando el motor está en marcha. Al comienzo hay un puesto de entrada con dos sensores: S_ENT detecta que hay una pieza y S_ALTO, ubicado más arriba, solo se interrumpe si la pieza es grande. A 60 cm está el expulsor, un cilindro de simple efecto con electroválvula Y1 y dos finales de carrera: FC_REP (replegado) y FC_EXT (extendido). Al final de la cinta, a 100 cm, S_FIN cuenta las piezas que llegan. Las piezas miden 5 cm.

Señales de la línea.
SeñalTipoSignificado
START, STOPentradaPulsadores de arranque y parada ordenada (STOP en contacto normalmente cerrado)
EMERG_OK, GUARDA_OKentradaCadena de seguridad: 1 cuando todo está bien (contactos NC)
ACKentradaReconocimiento de la falla por el operador
S_ENT, S_ALTO, S_FINentradaSensores de pieza
FC_REP, FC_EXTentradaPosiciones del cilindro
MOTORsalidaContactor de la cinta
Y1salidaElectroválvula del expulsor
SIRENAsalidaAviso previo al arranque

El requisito funcional se resume así: las piezas grandes terminan en la tolva, las pequeñas en el final de la cinta, nada se empuja con la cinta detenida y todo se detiene de forma segura ante una emergencia, una guarda abierta o una falla.

03 · El orden de los pasos

La secuencia de la línea

La secuencia es una máquina de estados (tema 36) que se actualiza una vez por ciclo (tema 37). Cada ciclo ejecuta siempre las mismas tareas en este orden, como un PLC (tema 35):

  1. Protecciones primero. Si la cadena de seguridad se abrió, se pasa a EMERGENCIA desde cualquier estado y se apagan las salidas.
  2. Seguimiento de piezas. Se actualiza el recorrido de la cinta y la lista de piezas con los flancos de los sensores.
  3. Secuencia. Las transiciones del estado actual.
  4. Supervisión. Tiempos máximos que generan fallas.
  5. Accionamiento. Se decide cuándo disparar el expulsor y se escriben las salidas, al final y desde un solo lugar.

Los estados de la línea son seis:

Estados y transiciones de la línea.
EstadoSalidasSale cuando…
REPOSOtodo apagadoSTART y cilindro replegado → PREAVISO
PREAVISOsirenapasaron 2 s → MARCHA; STOP → REPOSO
MARCHAmotorSTOP → VACIANDO
VACIANDOmotorno quedan piezas en seguimiento → REPOSO
FALLAtodo apagadoACK con la causa resuelta → REPOSO
EMERGENCIAtodo apagadoACK con la cadena de seguridad cerrada → REPOSO

Tres decisiones de diseño merecen explicación. El preaviso (2 s de sirena) existe porque una persona cerca de la cinta debe poder reaccionar antes de que se mueva. El estado VACIANDO hace que la parada ordenada deje la cinta sin piezas a medio camino: se sigue moviendo hasta clasificar las que ya entraron. Y desde FALLA y EMERGENCIA no se sale por sí solo: hace falta una acción humana (ACK), y solo si la causa desapareció. Un rearme automático al cerrarse la guarda sería un arranque inesperado.

Observá además qué pasa en REPOSO con piezas aún en seguimiento: el arranque no exige que la lista esté vacía. Si la cinta se detuvo por una emergencia con una pieza encima, esa pieza sigue en su lugar y el seguimiento por distancia la retoma al reanudar. Solo una falla reconocida vacía la lista, porque el operador retiró el material.

04 · Dónde está cada pieza

Seguir las piezas

Para empujar una pieza hay que dispararla cuando su centro está frente al expulsor. La pieza se detecta a la entrada, 60 cm antes. ¿Cómo se sabe cuándo llegó? Hay dos formas:

  • Por tiempo. Se anota la hora de entrada y se dispara cuando pasó d / v. Es lo primero que se le ocurre a un programador, y funciona perfecto hasta la primera vez que la cinta se detiene.
  • Por distancia. Se acumula el recorrido de la cinta (un encoder, o aquí la suma de v · Δt mientras el motor está activo) y se anota ese acumulado al detectar la pieza. Se dispara cuando el acumulado avanzó la distancia necesaria. Si la cinta se detiene, el contador también.

El tiempo transcurre aunque la cinta esté parada; la distancia recorrida no. Por eso el seguimiento por tiempo falla con cada parada de emergencia, guarda abierta o falla, y lo hace de la peor manera: empuja en el lugar equivocado y la pieza grande sigue hasta el final con el sistema en apariencia sano. Es un defecto que no aparece en la puesta en marcha, porque nadie para la cinta mientras prueba.

La distancia de disparo se compensa por la demora mecánica. El cilindro tarda 0,15 s en llegar a la pieza, durante los cuales la cinta avanza 20 · 0,15 = 3 cm; como el empuje actúa a mitad de carrera, se adelanta la orden la mitad: D_CMD = X_EXP + L/2 − v · T_EXT / 2 = 60 + 2,5 − 1,5 = 61 cm. La pieza se mide entre los flancos del sensor: el sistema anota el acumulado en el flanco de subida (borde delantero) y espera 61 cm más.

La clase de la pieza se conoce al terminar de pasar: si S_ALTO se interrumpió en algún momento, es grande. La orden de expulsión se emite 61 cm después, con la clase ya decidida. Cada pieza es un registro (acumulado inicial, clase, bandera de orden emitida) en una lista pequeña; las piezas salen de la lista al expulsarse o al pasar el final de la cinta.

05 · Lo que está prohibido

Enclavamientos

Un enclavamiento (interlock) es una condición que impide una acción mientras no se cumpla, sin importar qué pida la secuencia. La secuencia dice qué se quiere hacer; los enclavamientos dicen qué está permitido. Son el último filtro antes de cada salida. En la línea:

Enclavamientos de la línea.
AcciónCondición para permitirlaSi no se cumple
Arrancar la cintacadena de seguridad cerrada y cilindro replegado, tras el preavisono arranca
Extender el cilindrocinta en marcha, cilindro replegado y válvula libreel pedido se descarta o genera falla
Mantener salidascadena de seguridad cerradaEMERGENCIA: todo apagado

La regla «nada se empuja con la cinta detenida» tiene una razón: una pieza empujada contra una cinta quieta se atasca o se daña. Pero hay otra consecuencia, no obvia: si la secuencia ordenó una expulsión y el cilindro no está replegado (quedó a medias de un ciclo anterior), disparar de nuevo no es seguro, porque no se conoce su posición. El programa lo trata como una falla («el expulsor no está replegado») y no como un reintento silencioso.

Los enclavamientos se escriben como condiciones sobre las salidas, en un solo lugar, no repartidos por la secuencia. Así, aunque se agregue un modo manual o un paso nuevo que pida extender el cilindro, la restricción sigue valiendo. Es la misma idea de la acción de salida de un superestado del tema 38: las garantías de seguridad no dependen de que cada camino se acuerde de cumplirlas.

Una advertencia: un enclavamiento en el programa es una ayuda al proceso, no una protección de personas. Si un fallo de software puede lastimar a alguien, la protección tiene que existir también fuera del programa (sección siguiente).

06 · Cuando algo no sucede

Supervisión y fallas

Un programa que ordena una acción y da por hecho que ocurrió está ciego. La supervisión compara lo ordenado con lo observado y declara una falla si no coinciden dentro de un tiempo razonable. La línea tiene dos:

  • Atasco en la entrada. Si S_ENT permanece activo 3 s con el motor en marcha, una pieza de 5 cm que avanza a 20 cm/s la habría liberado en 0,25 s, de modo que algo bloquea el sensor o la cinta. El margen es de 12 veces el tiempo normal: suficiente para no disparar en falso.
  • Final de carrera del expulsor. Después de activar Y1 se espera que FC_EXT se active en 1 s (el cilindro tarda 0,15 s). Si no, el cilindro está trabado, falta aire o el sensor falló. La supervisión sigue vigente aunque el pulso de Y1 ya haya terminado: es un error clásico vigilar solo mientras la salida está activa.

Los tiempos de supervisión se eligen con un margen de varias veces el tiempo normal: lo bastante grande para no disparar por pequeñas variaciones y lo bastante chico para detectar rápido un problema real. Conviene medir el tiempo real en la máquina y agregar el factor, no usar el valor de catálogo.

Una falla se enclava: se anota la causa, se apagan las salidas y el sistema queda en FALLA hasta un reconocimiento. El ACK se acepta solo si la causa desapareció (sensor de entrada libre, cilindro replegado); si no, la falla persiste. Al reconocerla, el operador retiró físicamente las piezas, de modo que la lista de seguimiento se vacía: es la única forma en que el programa vuelve a un estado conocido después de intervención manual.

07 · Proteger a las personas

Condiciones de seguridad

Todo lo anterior protege a la máquina y al proceso. La seguridad de las personas se rige por otras reglas, más estrictas, que ya adelantamos en el tema 35 y aquí conviene fijar:

  • Parada de emergencia cableada. El hongo de emergencia corta la potencia del contactor del motor por hardware, con un relé de seguridad, independiente del programa. El programa solo lee el estado de la cadena para informar y para no intentar volver a arrancar.
  • Contactos normalmente cerrados. Una emergencia, una guarda o un final de seguridad se cablean como NC: si un cable se corta o un contacto se suelda abierto, el sistema lo interpreta como peligro. En el programa, EMERG_OK y GUARDA_OK valen 1 en condición normal.
  • Guardas enclavadas. Abrir una guarda detiene la línea (en el programa, EMERGENCIA) y cerrarla no reanuda: se exige un reconocimiento y un nuevo arranque con preaviso.
  • Sin rearme automático. Al volver la energía, el sistema inicia en REPOSO y espera START.
  • Fallas en dirección segura. Ante cualquier duda (sensor desconectado, señal imposible) se pasa al estado seguro: salidas apagadas.
  • Separar funciones de seguridad y de proceso. Los componentes de seguridad certificados (relés de seguridad, PLC de seguridad) se encargan de lo que protege a las personas; el controlador común se ocupa del proceso. Las normas aplicables, como la ISO 13849, definen niveles de desempeño según el riesgo.

Este curso usa una planta simulada, y la emergencia del ejemplo es una señal de entrada más. En una máquina real, lo anterior se resuelve en el diseño eléctrico antes de escribir una línea de código; el programa lo respeta y lo complementa.

08 · Código para la placa

El proceso en Arduino

El sketch implementa la función scan() con los cinco bloques en orden, una lista de piezas de tamaño fijo y una planta simulada que genera los sensores (con hardware, se reemplaza por lecturas y escrituras de pines). Ejecuta tres escenarios: el funcionamiento normal con seis piezas, y la guarda abierta con una pieza grande en tránsito, con seguimiento por distancia y por tiempo.

// Cinta transportadora con clasificador: secuencia, seguimiento de piezas y enclavamientos.
// Cada ciclo (10 ms simulados) hace: proteger -> seguir piezas -> secuencia -> supervisar -> expulsar.
// Sin hardware: una "planta" simulada genera los sensores. Con hardware, reemplazá leerSensores() y escribirSalidas().
const float DT = 0.01, V = 20.0, L_PIEZA = 5.0, X_EXP = 60.0, X_FIN = 100.0;
const float T_EXT = 0.15, T_RET = 0.15, T_PULSO = 0.30, T_FC = 1.0, T_PREAVISO = 2.0, T_ATASCO = 3.0;
const float D_CMD = X_EXP + L_PIEZA / 2 - V * T_EXT / 2;
enum Estado : byte { REPOSO, PREAVISO, MARCHA, VACIANDO, FALLA, EMERGENCIA };
const char *NOMBRE[] = {"REPOSO", "PREAVISO", "MARCHA", "VACIANDO", "FALLA", "EMERGENCIA"};

struct Entradas { bool start, stopOk, ack, guardaOk, fcRep, fcExt, sEnt, sAlto, sFin, motorReal; };
struct Pieza { float D0, t0; bool grande, alto, orden, clasificada; };
const byte MAX_P = 8;
Pieza cola[MAX_P]; byte nCola = 0;

Estado estado = REPOSO; float tEstado = 0, D = 0, tY1 = 0, tEntAlto = -1;
bool y1 = false, vigilaFc = false, sEntPrev = false, tiempoModo = false;
int entradasCont = 0, expulsiones = 0;

void entrar(Estado nuevo, float t, const char *causa) {
  Serial.print("   t="); Serial.print(t, 2); Serial.print(" s: ");
  Serial.print(NOMBRE[estado]); Serial.print(" -> "); Serial.print(NOMBRE[nuevo]);
  if (causa[0]) { Serial.print("  ("); Serial.print(causa); Serial.print(")"); }
  Serial.println();
  estado = nuevo; tEstado = t;
}
bool motor() { return estado == MARCHA || estado == VACIANDO; }
void quitar(byte i) { for (byte k = i; k + 1 < nCola; k++) cola[k] = cola[k + 1]; nCola--; }

void scan(float t, const Entradas &e, bool &salMotor, bool &salY1) {
  if (estado != EMERGENCIA && !e.guardaOk) { entrar(EMERGENCIA, t, "emergencia o guarda abierta"); y1 = false; }
  if (e.motorReal) D += V * DT;
  if (e.sEnt && !sEntPrev && nCola < MAX_P) {
    cola[nCola++] = {D, t, false, false, false, false}; entradasCont++; tEntAlto = t;
  }
  if (e.sEnt && e.sAlto && nCola) cola[nCola - 1].alto = true;
  if (!e.sEnt && sEntPrev && nCola) { cola[nCola - 1].grande = cola[nCola - 1].alto; cola[nCola - 1].clasificada = true; }
  if (!e.sEnt) tEntAlto = -1;
  sEntPrev = e.sEnt;
  float en = t - tEstado;
  switch (estado) {
    case REPOSO:   if (e.start && e.fcRep) entrar(PREAVISO, t, ""); break;
    case PREAVISO: if (!e.stopOk) entrar(REPOSO, t, "STOP durante el preaviso"); else if (en >= T_PREAVISO) entrar(MARCHA, t, ""); break;
    case MARCHA:   if (!e.stopOk) entrar(VACIANDO, t, "parada ordenada"); break;
    case VACIANDO: if (nCola == 0 && !e.sEnt) entrar(REPOSO, t, "cola vacia"); break;
    case FALLA:    if (e.ack && !e.sEnt && e.fcRep) { nCola = 0; entrar(REPOSO, t, "falla reconocida"); } break;
    case EMERGENCIA: if (e.ack && e.guardaOk) entrar(REPOSO, t, "emergencia rearmada"); break;
  }
  if (motor() && tEntAlto >= 0 && t - tEntAlto >= T_ATASCO) { entrar(FALLA, t, "ATASCO en la entrada"); y1 = false; }
  if (vigilaFc && e.fcExt) vigilaFc = false;
  if (vigilaFc && t - tY1 >= T_FC) { vigilaFc = false; entrar(FALLA, t, "EXPULSOR no llega al final de carrera"); y1 = false; }
  if (y1 && t - tY1 >= T_PULSO) y1 = false;
  for (int i = nCola - 1; i >= 0; i--) {                         // de atrás hacia adelante para poder quitar
    Pieza &p = cola[i];
    float avance = tiempoModo ? (t - p.t0) * V : D - p.D0;      // distancia recorrida: medida (encoder) o supuesta (reloj)
    bool pasada = avance >= X_FIN + 3 * L_PIEZA, listo = avance >= D_CMD;
    if (p.grande && listo && !p.orden && motor() && !y1) {
      if (e.fcRep) { y1 = true; tY1 = t; p.orden = true; vigilaFc = true; }
      else entrar(FALLA, t, "EXPULSOR no esta replegado");
    }
    if (p.orden && !y1) { quitar(i); expulsiones++; }
    else if (pasada) quitar(i);
  }
  salMotor = motor(); salY1 = y1;
}

// ---- Planta simulada ----
struct Caja { float p; bool alta, viva; };
Caja cajas[MAX_P]; byte nCajas = 0;
float cil = 0;
int gOk, gMal, pOk, pMal;

void ejecutar(const char *titulo, bool modoTiempo, const float *tPiezas, const bool *esGrande, byte n, float duracion,
              float guardaIni, float guardaFin, float ackT, float start2, float stopT) {
  Serial.println(titulo);
  estado = REPOSO; tEstado = 0; D = 0; y1 = false; vigilaFc = false; sEntPrev = false; tEntAlto = -1; tiempoModo = modoTiempo;
  nCola = 0; nCajas = 0; cil = 0; gOk = gMal = pOk = pMal = 0; entradasCont = expulsiones = 0;
  bool salMotor = false, salY1 = false, empuje = false; byte sig = 0;
  for (long k = 0; k < lround(duracion / DT); k++) {
    float t = k * DT;
    while (sig < n && tPiezas[sig] <= t) { cajas[nCajas++] = {-8.0, esGrande[sig], true}; sig++; }
    cil = salY1 ? min(1.0f, cil + DT / T_EXT) : max(0.0f, cil - DT / T_RET);
    if (cil >= 0.5 && !empuje && salY1) {
      empuje = true;
      for (byte i = 0; i < nCajas; i++)
        if (cajas[i].viva && fabs((cajas[i].p - L_PIEZA / 2) - X_EXP) <= 3.0) { cajas[i].viva = false; if (cajas[i].alta) gOk++; else pMal++; }
    }
    if (!salY1) empuje = false;
    if (salMotor) for (byte i = 0; i < nCajas; i++) cajas[i].p += V * DT;
    Entradas e = {};
    for (byte i = 0; i < nCajas; i++) {
      Caja &c = cajas[i];
      if (c.viva && c.p - L_PIEZA <= 0 && 0 <= c.p) { e.sEnt = true; if (c.alta) e.sAlto = true; }
      if (c.viva && c.p - L_PIEZA <= X_FIN && X_FIN <= c.p) e.sFin = true;
      if (c.viva && c.p - L_PIEZA > X_FIN) { c.viva = false; if (c.alta) gMal++; else pOk++; }
    }
    bool enGuarda = t >= guardaIni && t < guardaFin;
    e.start = (t >= 1.0 && t < 1.2) || (start2 > 0 && t >= start2 && t < start2 + 0.2);
    e.stopOk = !(t >= stopT && t < stopT + 0.2);
    e.ack = t >= ackT && t < ackT + 0.2;
    e.guardaOk = !enGuarda; e.fcRep = (cil == 0.0); e.fcExt = (cil == 1.0); e.motorReal = salMotor;
    scan(t, e, salMotor, salY1);
  }
  Serial.print("   grandes a la tolva: "); Serial.print(gOk);
  Serial.print("; pequenas al final: "); Serial.print(pOk);
  Serial.print("; clasificadas mal: "); Serial.println(gMal + pMal);
}

void setup() {
  Serial.begin(115200);
  const float tA[] = {4, 7, 10, 13, 16, 19}; const bool gA[] = {true, false, false, true, false, true};
  ejecutar("A. Funcionamiento normal con parada ordenada a los 22 s", false, tA, gA, 6, 40, -1, -1, -1, -1, 22.0);
  const float tC[] = {7}; const bool gC[] = {true};
  ejecutar("C. Guarda abierta de 9 a 17 s, seguimiento por distancia", false, tC, gC, 1, 50, 9.0, 17.0, 17.5, 18.0, 40.0);
  ejecutar("C'. El mismo caso con seguimiento por tiempo", true, tC, gC, 1, 50, 9.0, 17.0, 17.5, 18.0, 40.0);
}

void loop() {}

Salida esperada, idéntica a la del ejemplo Python: en el escenario A, REPOSO -> PREAVISO a 1,00 s, PREAVISO -> MARCHA a 3,00 s, MARCHA -> VACIANDO a 22,00 s y VACIANDO -> REPOSO a 22,76 s, con 3 piezas grandes en la tolva, 3 pequeñas al final y ninguna mal clasificada. En C, la guarda abierta a 9 s lleva a EMERGENCIA, el rearme a 17,50 s a REPOSO, el arranque a 18 s a PREAVISO y a 20 s a MARCHA; con seguimiento por distancia la pieza grande llega a la tolva, y en C′ (por tiempo) pasa de largo y queda mal clasificada.

Para usarlo con hardware: leerSensores() arma la estructura Entradas con antirrebote (tema 37), el encoder reemplaza la suma v · Δt para el recorrido y las salidas del scan() se escriben una sola vez al final del ciclo. Los pulsadores START y STOP se leen por flancos.

09 · Exploración interactiva

Laboratorios de la línea

1. Recorrer los escenarios

Elegí un escenario y movés el tiempo con el deslizador. Las piezas lima son grandes y las violetas pequeñas; el cilindro baja desde arriba para empujar hacia la tolva. Abajo aparecen el estado, las salidas y el registro de transiciones hasta ese momento.

Cinta transportadora con clasificadorUna cinta horizontal con sensores a la entrada y al final, un cilindro expulsor sobre la cinta a 60 cm y una tolva debajo. Las piezas grandes y pequeñas se mueven según el escenario elegido.
Estado—
Cinta—
Expulsor—
Piezas—

Elegí un escenario.

    Qué mirar en cada uno. A: tres piezas grandes a la tolva, tres pequeñas al final, y la parada ordenada a los 22 s espera a que se vacíe la lista. B: el sensor de entrada bloqueado entre los 7 y los 12 s genera la falla a los 10 s, tres segundos después; el ACK a los 14 s se acepta porque el sensor ya está libre. C: la guarda abierta detiene todo en el acto; la pieza grande se queda donde estaba, y al reanudar el seguimiento por distancia la lleva a la tolva. D: el cilindro trabado a partir de los 8 s hace que la segunda orden de extensión no se cumpla, y a los 11,45 s (1 s después de la orden) la supervisión declara la falla.

    2. Seguimiento por distancia contra seguimiento por tiempo

    Una pieza grande entra a los 7 s. La guarda se abre a los 9 s y se mantiene abierta el tiempo que elijas; luego se rearma (0,5 s después), se arranca (1 s después del cierre) y la línea pasa por el preaviso de 2 s. Con 0 s no hay parada.

    Resultado de la clasificación según el método de seguimientoDos barras: verde cuando la pieza grande llegó a la tolva y roja cuando pasó al final de la cinta.
    Por distancia—
    Por tiempo—

    Elegí la duración de la parada.

    Sin parada, los dos métodos coinciden. Con cualquier parada (probá 0,5 s), el método por tiempo falla: el reloj siguió corriendo mientras la cinta no se movía, la orden sale con la pieza a mitad de camino y el cilindro empuja aire. El método por distancia acierta siempre. Es la mejor razón para medir el movimiento y no el tiempo.

    10 · Código ejecutable

    La línea en Python

    El mismo controlador, con la planta simulada y los cuatro escenarios, más la variante del seguimiento por tiempo. El resultado de cada escenario incluye las transiciones con su hora y causa, y el conteo de piezas.

    Probalo en el navegador. Python se descarga con Pyodide la primera vez y requiere conexión. El código es editable y no controla ningún equipo real.

    DT, V, L_PIEZA = 0.01, 20.0, 5.0                     # s, cm/s, cm
    X_EXP, X_FIN = 60.0, 100.0                           # posición del expulsor y del final de la cinta (cm)
    T_EXT, T_RET, T_PULSO, T_FC = 0.15, 0.15, 0.30, 1.0  # carrera del cilindro y supervisión de su final de carrera (s)
    T_PREAVISO, T_ATASCO = 2.0, 3.0
    D_CMD = X_EXP + L_PIEZA / 2 - V * T_EXT / 2          # recorrido de la pieza desde el sensor hasta dar la orden al expulsor
    
    class Linea:
        """Controlador: una función scan() por ciclo (proteger, seguir piezas, secuencia, supervisar, accionar)."""
        def __init__(self, seguimiento="distancia"):
            self.estado, self.t_estado = "REPOSO", 0.0
            self.seguimiento, self.cola = seguimiento, []
            self.D = 0.0                                  # recorrido acumulado de la cinta (lo daría un encoder)
            self.s_ent_prev, self.s_fin_prev, self.t_ent_alto = False, False, None
            self.y1, self.t_y1, self.vigila_fc = False, 0.0, False
            self.cuenta = {"entradas": 0, "expulsiones": 0, "al_final": 0}
            self.registro = []
    
        def entrar(self, nuevo, t, causa=""):
            self.registro.append((round(t, 2), self.estado, nuevo, causa))
            self.estado, self.t_estado = nuevo, t
    
        def motor(self):
            return self.estado in ("MARCHA", "VACIANDO")
    
        def scan(self, t, e):
            # 1) Protecciones: siempre primero y desde cualquier estado
            if self.estado != "EMERGENCIA" and not (e["emerg_ok"] and e["guarda_ok"]):
                self.entrar("EMERGENCIA", t, "emergencia o guarda abierta")
                self.y1 = False
            # 2) Recorrido de la cinta y seguimiento de las piezas
            if e["motor_real"]:
                self.D += V * DT
            if e["s_ent"] and not self.s_ent_prev:
                self.cola.append({"D0": self.D, "t0": t, "clase": None, "alto": False, "orden": False})
                self.cuenta["entradas"] += 1
                self.t_ent_alto = t
            if e["s_ent"] and e["s_alto"] and self.cola:
                self.cola[-1]["alto"] = True
            if not e["s_ent"] and self.s_ent_prev and self.cola:
                self.cola[-1]["clase"] = "GRANDE" if self.cola[-1]["alto"] else "PEQUENA"
            if not e["s_ent"]:
                self.t_ent_alto = None
            self.s_ent_prev = e["s_ent"]
            if e["s_fin"] and not self.s_fin_prev:
                self.cuenta["al_final"] += 1
            self.s_fin_prev = e["s_fin"]
            # 3) Secuencia de la línea
            t_en = t - self.t_estado
            if self.estado == "REPOSO":
                if e["start"] and e["fc_rep"]:
                    self.entrar("PREAVISO", t)
            elif self.estado == "PREAVISO":
                if not e["stop_ok"]:
                    self.entrar("REPOSO", t, "STOP durante el preaviso")
                elif t_en >= T_PREAVISO:
                    self.entrar("MARCHA", t)
            elif self.estado == "MARCHA":
                if not e["stop_ok"]:
                    self.entrar("VACIANDO", t, "parada ordenada")
            elif self.estado == "VACIANDO":
                if not self.cola and not e["s_ent"]:
                    self.entrar("REPOSO", t, "cola vacia")
            elif self.estado in ("FALLA", "EMERGENCIA") and e["ack"]:
                if self.estado == "FALLA" and not e["s_ent"] and e["fc_rep"]:
                    self.cola.clear()
                    self.entrar("REPOSO", t, "falla reconocida")
                elif self.estado == "EMERGENCIA" and e["emerg_ok"] and e["guarda_ok"]:
                    self.entrar("REPOSO", t, "emergencia rearmada")
            # 4) Supervisión: atasco en la entrada y finales de carrera del expulsor
            if self.motor() and self.t_ent_alto is not None and t - self.t_ent_alto >= T_ATASCO:
                self.entrar("FALLA", t, "ATASCO en la entrada")
                self.y1 = False
            if self.vigila_fc and e["fc_ext"]:
                self.vigila_fc = False
            if self.vigila_fc and t - self.t_y1 >= T_FC:
                self.vigila_fc = False
                self.entrar("FALLA", t, "EXPULSOR no llega al final de carrera")
                self.y1 = False
            # 5) Expulsión de las piezas grandes en el momento justo
            if self.y1 and t - self.t_y1 >= T_PULSO:
                self.y1 = False
            for p in list(self.cola):
                if self.seguimiento == "distancia":
                    pasada, listo = self.D - p["D0"] >= X_FIN + 3 * L_PIEZA, self.D - p["D0"] >= D_CMD
                else:
                    pasada, listo = t - p["t0"] >= (X_FIN + 3 * L_PIEZA) / V, t - p["t0"] >= D_CMD / V
                if p["clase"] == "GRANDE" and listo and not p["orden"]:
                    if self.motor() and not self.y1:                  # enclavamiento: cinta en marcha y expulsor libre
                        if e["fc_rep"]:
                            self.y1, self.t_y1, p["orden"], self.vigila_fc = True, t, True, True
                        else:
                            self.entrar("FALLA", t, "EXPULSOR no esta replegado")
                if p["orden"] and not self.y1:
                    self.cola.remove(p)
                    self.cuenta["expulsiones"] += 1
                elif pasada:
                    self.cola.remove(p)
            return {"motor": self.motor(), "y1": self.y1}
    
    def simular(piezas, eventos, duracion, seguimiento="distancia", expulsor_trabado_desde=None, atasco=None):
        """piezas: lista de (t_alimentacion, 'G' o 'P'); eventos: intervalos de start, stop, ack y guarda abierta."""
        linea = Linea(seguimiento)
        pos, cil, empuje_hecho = [], 0.0, False
        res = {"G_ok": 0, "G_mal": 0, "P_ok": 0, "P_mal": 0}
        salidas = {"motor": False, "y1": False}
        pendientes = sorted(piezas)
        en = lambda nombre, t: any(a <= t < b for a, b in eventos.get(nombre, []))
        for k in range(round(duracion / DT)):
            t = k * DT
            while pendientes and pendientes[0][0] <= t:
                _, clase = pendientes.pop(0)
                pos.append({"p": -8.0, "alta": clase == "G", "viva": True})
            trabado = expulsor_trabado_desde is not None and t >= expulsor_trabado_desde
            if not trabado:
                cil = min(1.0, cil + DT / T_EXT) if salidas["y1"] else max(0.0, cil - DT / T_RET)
            if cil >= 0.5 and not empuje_hecho and salidas["y1"]:
                empuje_hecho = True
                for q in pos:
                    if q["viva"] and abs((q["p"] - L_PIEZA / 2) - X_EXP) <= 3.0:
                        q["viva"] = False
                        res["G_ok" if q["alta"] else "P_mal"] += 1
            if not salidas["y1"]:
                empuje_hecho = False
            if salidas["motor"]:
                for q in pos:
                    q["p"] += V * DT
            cubre = lambda q, x: q["viva"] and q["p"] - L_PIEZA <= x <= q["p"]
            s_ent = any(cubre(q, 0) for q in pos) or (atasco is not None and atasco[0] <= t < atasco[1])
            s_alto = any(cubre(q, 0) and q["alta"] for q in pos)
            s_fin = any(cubre(q, X_FIN) for q in pos)
            for q in pos:
                if q["viva"] and q["p"] - L_PIEZA > X_FIN:
                    q["viva"] = False
                    res["G_mal" if q["alta"] else "P_ok"] += 1
            e = {"start": en("start", t), "stop_ok": not en("stop", t), "ack": en("ack", t), "guarda_ok": not en("guarda", t),
                 "emerg_ok": True, "fc_rep": cil == 0.0, "fc_ext": cil == 1.0, "s_ent": s_ent, "s_alto": s_alto, "s_fin": s_fin,
                 "motor_real": salidas["motor"]}
            salidas = linea.scan(t, e)
        return linea, res
    
    def informe(titulo, **kw):
        linea, r = simular(**kw)
        print(titulo)
        for t, a, b, causa in linea.registro:
            print(f"   t={t:5.2f} s: {a} -> {b}" + (f"  ({causa})" if causa else ""))
        print(f"   grandes a la tolva: {r['G_ok']}; pequenas al final: {r['P_ok']}; clasificadas mal: {r['G_mal'] + r['P_mal']} "
              f"(grandes al final: {r['G_mal']}, pequenas expulsadas: {r['P_mal']}); piezas contadas en la entrada: {linea.cuenta['entradas']}")
    
    PIEZAS_A = [(4, "G"), (7, "P"), (10, "P"), (13, "G"), (16, "P"), (19, "G")]
    informe("A. Funcionamiento normal con parada ordenada a los 22 s", piezas=PIEZAS_A,
            eventos={"start": [(1.0, 1.2)], "stop": [(22.0, 22.2)]}, duracion=40)
    informe("B. Atasco en la entrada entre los 7 y los 12 s", piezas=[(4, "G"), (7, "P")],
            eventos={"start": [(1.0, 1.2)], "ack": [(14.0, 14.2)]}, duracion=20, atasco=(7.0, 12.0))
    EV_C = {"start": [(1.0, 1.2), (18.0, 18.2)], "guarda": [(9.0, 17.0)], "ack": [(17.5, 17.7)], "stop": [(40.0, 40.2)]}
    informe("C. Guarda abierta de los 9 a los 17 s con una pieza grande en transito (seguimiento por distancia)",
            piezas=[(7, "G")], eventos=EV_C, duracion=50)
    informe("C'. El mismo caso con seguimiento por tiempo", piezas=[(7, "G")], eventos=EV_C, duracion=50, seguimiento="tiempo")
    informe("D. Expulsor trabado desde los 8 s", piezas=[(4, "G"), (7, "G")],
            eventos={"start": [(1.0, 1.2)]}, duracion=30, expulsor_trabado_desde=8.0)

    La salida muestra: A, 3 grandes a la tolva y 3 pequeñas al final; B, falla por atasco a los 10,00 s y rearme a los 14,00 s con 1 grande ya clasificada; C, EMERGENCIA a los 9,00 s y, por distancia, la pieza llega a la tolva, mientras que en C′ (por tiempo) pasa al final; D, falla del expulsor a los 11,45 s.

    Probá modificar la velocidad V o el tiempo de extensión T_EXT y observá cómo cambia D_CMD. Después agregá una guarda abierta a los 12 s en el escenario A y verificá que el conteo final sigue siendo correcto.

    11 · Método y diagnóstico

    Pruebas y diagnóstico

    1. Definí el orden de las tareas del ciclo y respetalo siempre: protecciones, seguimiento, secuencia, supervisión, salidas.
    2. Probá cada estado con cada evento posible, en especial STOP, la emergencia y la guarda en todos los estados.
    3. Probá paradas en cualquier momento con piezas en tránsito: el sistema tiene que reanudar clasificando bien.
    4. Provocá cada falla (atasco, cilindro trabado, cilindro sin replegar) y comprobá que las salidas se apagan y que el ACK solo se acepta con la causa resuelta.
    5. Verificá los enclavamientos como invariantes: en ningún instante el cilindro se extiende con la cinta detenida, ni la cinta se mueve con la guarda abierta.
    6. Registrá cada transición con la hora y la causa.
    7. En una máquina real, probá la cadena de seguridad cortando cables y soltando contactos: debe pasar a estado seguro.
    «Si la cinta va a velocidad constante, contar el tiempo es suficiente»

    Solo mientras no se detenga. Cada parada rompe la relación entre tiempo y posición. La distancia (un encoder, o la integral de la velocidad con el motor activo) es la medida correcta.

    «Si la guarda se cierra, la línea puede seguir sola»

    No. Reanudar sin una acción deliberada es un arranque inesperado, la causa clásica de accidentes. Se exige ACK y un nuevo arranque con preaviso.

    «Con el pulso a Y1 terminado ya no hay nada que vigilar»

    Justamente allí falla el cilindro trabado. La supervisión espera el final de carrera después de terminar el pulso, durante un tiempo propio.

    «Un enclavamiento en el programa protege a las personas»

    Protege el proceso. Para las personas hace falta un circuito cableado de seguridad independiente del software, con contactos NC y rearme manual.

    «Tras una falla, lo más rápido es continuar con la lista de piezas que había»

    Después de una intervención manual no se sabe qué hay en la cinta. Se vacía la lista al reconocer la falla y se retira el material.

    «Un tiempo de supervisión justo detecta antes las fallas»

    También produce falsas alarmas con la menor variación. Se elige un margen sobre el tiempo medido, de varias veces el nominal.

    12 · Práctica guiada

    Predecí antes de comprobar

    1. Distancia de disparo

    Si el cilindro tarda 0,30 s en llegar y la cinta va a 20 cm/s, ¿cuál es la distancia de disparo con las demás medidas iguales?

    Ver solución

    D_CMD = 60 + 2,5 − 20 · 0,30 / 2 = 62,5 − 3 = 59,5 cm. Un cilindro más lento obliga a dar la orden antes.

    2. Otra velocidad

    La cinta pasa a 30 cm/s con el mismo cilindro (0,15 s). ¿Cuánto vale D_CMD y cuánto tiempo transcurre entre la entrada y la orden?

    Ver solución

    D_CMD = 62,5 − 30 · 0,15 / 2 = 62,5 − 2,25 = 60,25 cm. Tiempo: 60,25 / 30 ≈ 2,01 s. Con el seguimiento por distancia no hace falta recalcular el tiempo: basta con el recorrido.

    3. Parada con pieza en tránsito

    Se aprieta la emergencia cuando una pieza grande lleva 30 cm de recorrido desde la entrada, y se reanuda más tarde. ¿Cuánto debe avanzar todavía la cinta para que se emita la orden?

    Ver solución

    Faltan 61 − 30 = 31 cm, es decir, 1,55 s de cinta en marcha, sin importar cuánto duró la parada. El contador de distancia no avanzó durante la parada.

    4. El tiempo de atasco

    Una pieza de 5 cm a 20 cm/s libera el sensor en 0,25 s. Si el tiempo de atasco es de 3 s, ¿qué razón hay entre ambos y qué riesgo habría con 0,3 s?

    Ver solución

    La razón es 12 (3 / 0,25). Con 0,3 s, casi cualquier variación (una pieza algo más larga o una cinta algo más lenta) dispararía falsas fallas.

    5. Cilindro sin replegar

    Llega la orden de expulsión y FC_REP está en 0. ¿Qué debe hacer el programa y por qué no simplemente esperar?

    Ver solución

    Declarar la falla «el expulsor no está replegado». Esperar no es seguro: la posición real es desconocida y, mientras tanto, la pieza sigue avanzando y se pierde su momento. La falla obliga a revisar el cilindro.

    6. Rearme tras emergencia

    La guarda se abrió y se cerró. ¿Qué condiciones deben cumplirse para volver a producir?

    Ver solución

    La cadena de seguridad debe estar cerrada, el operador debe dar el ACK (pasa a REPOSO), luego START con el cilindro replegado, los 2 s de preaviso y recién entonces la cinta se mueve. Son tres acciones humanas deliberadas, no una reanudación automática.

    7. Seguridad del contacto

    ¿Por qué la señal GUARDA_OK usa un contacto normalmente cerrado y no uno normalmente abierto?

    Ver solución

    Porque con NC un cable cortado o un contacto que falla abierto se lee como guarda abierta: el sistema falla hacia el lado seguro. Con NA, el mismo cable cortado parecería «guarda cerrada» y la línea arrancaría sin protección.

    13 · Síntesis

    Ideas para recordar

    • Un proceso automatizado ordena el ciclo en tareas fijas: protecciones, seguimiento, secuencia, supervisión y salidas.
    • La secuencia es una máquina de estados con preaviso, marcha, vaciado y estados enclavados de falla y emergencia.
    • Las piezas se siguen por distancia recorrida, no por tiempo: el tiempo sigue corriendo cuando la cinta se detiene.
    • La orden de empuje se adelanta para compensar la demora del cilindro: D_CMD = X_EXP + L/2 − v · T_EXT / 2.
    • Los enclavamientos son condiciones sobre las salidas, escritas en un solo lugar, independientes de la secuencia.
    • Se supervisa lo ordenado contra lo observado, con tiempos de margen amplio, incluso después de terminar el pulso.
    • Las fallas se enclavan y se reconocen con ACK solo si la causa desapareció; el reconocimiento vacía el seguimiento.
    • La seguridad de las personas es un circuito cableado, con contactos NC y rearme manual; el programa lo complementa.

    ¿Listo para el tema 40?

    Antes de avanzar, elegí un proceso con material en movimiento (un ascensor de carga, un lavado de vehículos, un envasado) y escribí sus estados, sus enclavamientos y las tres fallas que supervisarías, con los tiempos de supervisión.

    En el próximo tema empezamos la parte de control: la diferencia entre el lazo abierto y el lazo cerrado, base para ajustar cualquier sistema que deba alcanzar y mantener un valor.