01 · Punto de partida
Cuando la máquina plana se queda corta
En los temas 36 y 37 construimos máquinas de estados con una decena de estados como máximo: un portón, un semáforo. Un robot o una máquina de verdad tiene más: modos (automático, manual, mantenimiento), etapas dentro de cada modo, comportamientos que se interrumpen y se reanudan, y fallas que pueden ocurrir en cualquier momento. Si cada una de esas combinaciones es un estado de una máquina plana, el diagrama pasa a ser ilegible y, peor, incompleto: es muy fácil olvidar una flecha.
La respuesta, introducida por David Harel en los años 80 con los statecharts, es organizar los estados en jerarquía: unos contienen a otros, y lo que vale para el contenedor vale para todos los contenidos. El tema trabaja con un caso concreto: un robot de limpieza con operación normal, regreso a la base, evasión de obstáculos y fallas.
Este tema cierra la parte de máquinas de estados. El siguiente (tema 39) las aplica a un proyecto de automatización completo.
02 · El problema
La explosión de transiciones
El robot tiene estos estados «hoja»: ESPERA (en la base), LIMPIANDO, BUSCAR_BASE, ACOPLAR (alineándose con la base), EVADIENDO (esquivando un obstáculo) y FALLA. Hay tres eventos que se atienden igual en cualquier etapa de la operación:
PARAR, que el usuario envía y que debe llevar a ESPERA;ACANTILADO, un sensor de borde de escalón, que lleva a FALLA;ATASCO, una rueda trabada, que lleva a FALLA.
En una máquina plana, las cuatro etapas operativas (LIMPIANDO, BUSCAR_BASE, ACOPLAR, EVADIENDO) necesitan una flecha por evento: 4 × 3 = 12 transiciones que son la misma regla repetida. Todas deben mantenerse iguales; si se cambia el destino de ACANTILADO, hay que acordarse de las cuatro. Y si se agrega una quinta etapa, hay que acordarse de agregar tres flechas. Olvidar una significa que en esa etapa el robot ignora un acantilado: un error de seguridad que ni el compilador ni una revisión rápida del diagrama detectan.
Además hay un problema de memoria. EVADIENDO se activa desde LIMPIANDO y también desde BUSCAR_BASE, y al terminar debe volver a donde estaba. En una máquina plana eso exige duplicar el estado (EVADIENDO desde limpiar y EVADIENDO desde volver), cada uno con sus tres flechas de emergencia. Resultado: 7 estados hoja y 25 transiciones, para un robot con 6 comportamientos distintos.
El fondo del problema es que la máquina plana trata a todos los estados como iguales. En realidad, algunos son «de la misma familia»: todos los operativos comparten reglas de seguridad. La jerarquía expresa esa estructura.
03 · La idea central
Superestados y herencia
Un superestado (o estado compuesto) contiene subestados. Estar en un subestado implica estar también en su superestado. La regla de oro del despacho es la herencia:
- Un evento se ofrece primero al estado hoja actual.
- Si no tiene una transición para ese evento, sube al superestado, y así hasta la raíz.
- El primer estado (de abajo hacia arriba) que maneja el evento ejecuta la transición. Si ninguno lo maneja, se ignora.
Así, PARAR, ACANTILADO y ATASCO se escriben una vez, en el superestado OPERANDO, que contiene LIMPIANDO, REGRESANDO (con BUSCAR_BASE y ACOPLAR) y EVADIENDO. Cualquier estado nuevo que se agregue dentro de OPERANDO hereda gratis esas tres reglas, y no se pueden olvidar. Una regla específica de una etapa se escribe en la etapa, y tiene prioridad sobre la heredada: ese es el mecanismo de «excepción».
La jerarquía del robot:
| Estado | Contenido o padre | Transiciones propias |
|---|---|---|
| ESPERA | raíz | INICIO → LIMPIANDO |
| OPERANDO | contiene LIMPIANDO (inicial), REGRESANDO, EVADIENDO | PARAR → ESPERA; ACANTILADO → FALLA; ATASCO → FALLA |
| LIMPIANDO | en OPERANDO | BATERIA_BAJA → REGRESANDO; TIEMPO_FIN → REGRESANDO; OBSTACULO → EVADIENDO |
| REGRESANDO | en OPERANDO; contiene BUSCAR_BASE (inicial) y ACOPLAR | (ninguna propia) |
| BUSCAR_BASE | en REGRESANDO | BASE_VISTA → ACOPLAR; OBSTACULO → EVADIENDO |
| ACOPLAR | en REGRESANDO | ACOPLADO → ESPERA |
| EVADIENDO | en OPERANDO | LIBRE → historia |
| FALLA | raíz | ACK → ESPERA |
El total es de 12 transiciones escritas, frente a las 21 que habría en una máquina plana con los mismos 6 estados hoja (25 si se duplica EVADIENDO por la historia). Más importante que el número es la garantía: la propiedad «en cualquier etapa operativa, un acantilado lleva a FALLA» está escrita en un solo lugar.
Cuando una transición llega a un estado compuesto, se entra en su subestado inicial: BATERIA_BAJA lleva a REGRESANDO, y el robot queda en BUSCAR_BASE; INICIO lleva a LIMPIANDO, que es el inicial de OPERANDO. El estado inicial de cada compuesto es parte del diseño.
04 · Qué se ejecuta al cambiar
Acciones de entrada y de salida
Con superestados, cambiar de estado no es un solo paso: se sale de varios niveles y se entra a otros. Cada estado puede tener una acción de entrada (qué preparar al comenzar: encender el cepillo, iniciar la alarma) y una acción de salida (qué dejar en orden al terminar: apagar el cepillo, detener los motores). El orden es el que mantiene todo coherente:
- Al salir: primero el estado más interno, luego sus contenedores, de adentro hacia afuera, hasta el ancestro común con el destino.
- Al entrar: primero el contenedor más externo y luego los internos, de afuera hacia adentro, hasta la hoja de destino (y sus subestados iniciales).
El motivo es el mismo que el de abrir y cerrar recursos: lo que se abrió primero se cierra último. En el robot, al transitar de LIMPIANDO a FALLA por un atasco ocurre: sale LIMPIANDO (apagar cepillo); sale OPERANDO (apagar motores); entra FALLA (alarma). El cepillo se apaga antes que los motores, y la alarma se enciende al final, con todo ya detenido. Nadie escribió esa secuencia como una lista: surge de la jerarquía.
Las acciones de entrada y salida son también el lugar natural para garantizar la seguridad: al salir de OPERANDO, por cualquier camino, los motores se apagan. Aunque se agreguen cinco caminos nuevos hacia fuera de la operación (parar, falla, acoplado, tiempo agotado…), todos pasan por esa salida. Es una propiedad que una máquina plana solo puede cumplir repitiendo la acción en cada transición.
Una advertencia: las acciones deben ser breves y no bloquear (tema 37). Si arrancar o detener algo lleva tiempo, la acción inicia el proceso y un estado intermedio espera a que termine.
05 · Volver a donde estaba
La historia
EVADIENDO puede interrumpir LIMPIANDO o BUSCAR_BASE. Al terminar (evento LIBRE), el robot debe retomar lo que hacía. Un pseudoestado de historia guarda el último subestado activo del superestado y permite regresar a él. En nuestro código es una variable: al entrar en EVADIENDO se anota la hoja de origen, y LIBRE transita a ese valor.
Con la historia, EVADIENDO es un único estado reutilizable por todas las etapas operativas. Sin ella, habría uno por cada origen posible. Observá que al volver se ejecutan de nuevo las acciones de entrada del estado retomado: si volvemos a BUSCAR_BASE, el robot vuelve a «apagar el cepillo» y «buscar la baliza». Eso es lo deseable (el estado queda en una condición conocida) siempre que las acciones de entrada sean idempotentes.
La historia tiene un costo conceptual: el comportamiento depende de algo que no se ve en el diagrama (qué hoja se guardó). Conviene registrarla y mostrarla en depuración. Y hay que decidir qué ocurre con los eventos que ocurrieron durante la interrupción: por ejemplo, si la batería baja mientras se evade, el evento BATERIA_BAJA se ignora (EVADIENDO no lo maneja), y al volver a LIMPIANDO nadie lo repite. Los eventos puntuales se pierden; las condiciones persistentes (una bandera «batería baja») se pueden volver a consultar al entrar. Es el mismo criterio de eventos y condiciones del tema 36.
06 · Cómo opera la máquina
Modos de operación
Un modo es un superestado que cambia las reglas del juego: automático (la máquina decide), manual (decide el operador), mantenimiento (se permiten movimientos lentos con protecciones reducidas), emergencia. Cada modo contiene sus propias etapas. Las buenas prácticas:
- Un modo por vez. Los modos son excluyentes: cada uno es un superestado, y el sistema está exactamente en uno.
- Cambiar de modo siempre pasa por un estado seguro. Al salir de un modo, la acción de salida deja los actuadores apagados. Pasar de automático a manual con el motor en marcha es una fuente clásica de accidentes.
- Las órdenes de un modo no se ejecutan en otro. El botón de avance manual no hace nada en automático; la jerarquía lo garantiza porque solo el modo manual maneja ese evento.
- La emergencia vive en la raíz. Un evento de emergencia definido en el estado más externo se hereda en todos los modos y etapas, incluidos los de espera. Nada debe poder ignorarla.
- La selección de modo es una entrada controlada (un selector con llave, un código), no un botón cualquiera.
En nuestro robot, OPERANDO es el modo automático. Un modo manual agregaría un superestado MANUAL con sus propias reglas (avanzar, girar) y las mismas protecciones: el acantilado y el atasco se podrían definir una vez más en la raíz y valer para ambos.
07 · Lo inesperado
Manejo de fallos
Un diseño robusto trata las fallas como ciudadanos de primera clase de la máquina, no como un caso aparte. Una clasificación útil:
| Tipo | Ejemplo | Respuesta |
|---|---|---|
| Advertencia | Batería baja | Seguir, registrar y cambiar de comportamiento (volver a la base) |
| Recuperable | Una rueda patina una vez | Reintentar una cantidad limitada de veces, con una acción correctiva (retroceder); si persiste, subir a crítica |
| Crítica | Acantilado, rueda atascada, motor en sobrecorriente | Detener los actuadores, enclavar la falla, avisar, y esperar el reconocimiento humano |
Las fallas críticas comparten tres propiedades en la máquina:
- Se detectan en el superestado. ACANTILADO y ATASCO se definen una vez en OPERANDO: no se pueden olvidar en una etapa.
- Se enclavan. FALLA solo se abandona con un reconocimiento (ACK). Idealmente, el ACK se acepta solo si la causa desapareció (una guarda: «ACK [sensor de borde despejado]»); si no, la falla se re-dispara.
- Dejan todo en estado seguro. Las acciones de salida de OPERANDO apagan los motores; la entrada de FALLA enciende la alarma y registra la causa.
Dos mecanismos complementarios: los temporizadores de supervisión (un estado que lleva demasiado tiempo sin avanzar genera un evento de falla, como T_MAX del portón) y un vigilante (watchdog), un temporizador de hardware que reinicia el microcontrolador si el programa deja de responder, útil contra bloqueos que ni siquiera la máquina de estados puede detectar. Después de un reinicio, el sistema vuelve al estado inicial seguro, no al que estaba.
También conviene la degradación: ante una falla no crítica, seguir con funciones reducidas en lugar de parar todo (por ejemplo, seguir limpiando sin el sensor de piso, a baja velocidad). Eso es otro modo, DEGRADADO, con sus reglas más restrictivas. Y siempre se registra la causa: «falla» sin motivo no ayuda a nadie.
08 · Más de una cosa a la vez
Varias máquinas a la vez
A veces un sistema hace dos cosas independientes: el robot se mueve y, a la vez, una luz de estado parpadea y un monitor de batería vigila el nivel. Meter todo en una sola jerarquía produce productos de estados (movimiento × luz × batería). Los statecharts permiten regiones ortogonales: dentro de un estado, varias submáquinas corren en paralelo, cada una con sus estados y eventos. En la práctica de un microcontrolador se implementa con varias máquinas independientes, cada una con su estado, todas actualizadas una vez por vuelta de loop():
- La máquina de movimiento (la jerárquica de este tema).
- La máquina de indicadores, que lee el estado de la primera y decide los patrones de luz.
- El monitor de batería, que emite eventos (BATERIA_BAJA) hacia la máquina de movimiento.
Se comunican por eventos (una máquina emite, otra recibe) o leyendo el estado de la otra, nunca escribiendo en sus variables internas. Si dos de ellas compiten por el mismo actuador, hace falta un árbitro.
Y aquí se cierra el círculo con el tema 33. Un comportamiento de la arquitectura de subsunción (seguir la línea, evitar obstáculos) es, por dentro, una pequeña máquina de estados. La subsunción ordena los comportamientos por prioridad y deja ganar a uno; la jerarquía de estados los agrupa en etapas y modos con transiciones explícitas. Se pueden combinar: modos y etapas con una máquina jerárquica, y dentro de una etapa como LIMPIANDO, comportamientos reactivos arbitrados por prioridad. Una regla de oro: usar lo más simple que alcance, y recurrir a la jerarquía cuando aparezca la repetición de reglas.
09 · Código para la placa
La jerarquía en Arduino
El sketch representa la jerarquía con un arreglo PADRE[] (el árbol), un arreglo INICIAL[] (subestado inicial de cada compuesto) y una tabla de transiciones donde el «manejador» puede ser un superestado. La función despachar() busca el manejador subiendo por el árbol, calcula qué estados salen (de adentro hacia afuera hasta el ancestro común con el destino) y cuáles entran (de afuera hacia adentro, más el descenso por los subestados iniciales), y aplica la historia. Ejecuta un escenario guionado y muestra las acciones.
// Máquina de estados jerárquica de un robot de limpieza.
// Los estados forman un árbol (PADRE[]). Un evento se busca primero en el estado actual y, si no lo maneja,
// se sube al superestado: las transiciones comunes (PARAR, ACANTILADO, ATASCO) se escriben una sola vez en OPERANDO.
// Al transitar: salen los estados de adentro hacia afuera y entran de afuera hacia adentro; EVADIENDO recuerda de dónde viene (historia).
enum Estado : byte { RAIZ, ESPERA, OPERANDO, LIMPIANDO, REGRESANDO, BUSCAR_BASE, ACOPLAR, EVADIENDO, FALLA, N_ESTADOS, NINGUNO = 254, HISTORIA = 255 };
enum Evento : byte { INICIO, PARAR, OBSTACULO, LIBRE, BATERIA_BAJA, TIEMPO_FIN, BASE_VISTA, ACOPLADO, ACANTILADO, ATASCO, ACK, N_EVENTOS };
const char *NOMBRE[N_ESTADOS] = {"RAIZ", "ESPERA", "OPERANDO", "LIMPIANDO", "REGRESANDO", "BUSCAR_BASE", "ACOPLAR", "EVADIENDO", "FALLA"};
const char *NOMBRE_EV[N_EVENTOS] = {"INICIO", "PARAR", "OBSTACULO", "LIBRE", "BATERIA_BAJA", "TIEMPO_FIN", "BASE_VISTA", "ACOPLADO", "ACANTILADO", "ATASCO", "ACK"};
const char *ACCION_ENTRADA[N_ESTADOS] = {nullptr, "detener motores, cargar", "LED de actividad", "cepillo y avance", "apagar cepillo", "buscar la baliza", "avance lento", "detener y girar", "alarma"};
const char *ACCION_SALIDA[N_ESTADOS] = {nullptr, nullptr, "apagar motores", "apagar cepillo", nullptr, nullptr, nullptr, "dejar de girar", "apagar alarma"};
const Estado PADRE[N_ESTADOS] = {NINGUNO, RAIZ, RAIZ, OPERANDO, OPERANDO, REGRESANDO, REGRESANDO, OPERANDO, RAIZ};
const Estado INICIAL[N_ESTADOS] = {NINGUNO, NINGUNO, LIMPIANDO, NINGUNO, BUSCAR_BASE, NINGUNO, NINGUNO, NINGUNO, NINGUNO};
struct Transicion { Estado manejador; Evento evento; Estado destino; };
const Transicion TABLA[] = {
{ESPERA, INICIO, LIMPIANDO},
{OPERANDO, PARAR, ESPERA}, {OPERANDO, ACANTILADO, FALLA}, {OPERANDO, ATASCO, FALLA},
{LIMPIANDO, BATERIA_BAJA, REGRESANDO}, {LIMPIANDO, TIEMPO_FIN, REGRESANDO}, {LIMPIANDO, OBSTACULO, EVADIENDO},
{BUSCAR_BASE, BASE_VISTA, ACOPLAR}, {BUSCAR_BASE, OBSTACULO, EVADIENDO},
{ACOPLAR, ACOPLADO, ESPERA},
{EVADIENDO, LIBRE, HISTORIA},
{FALLA, ACK, ESPERA}
};
const byte N_TRANS = sizeof(TABLA) / sizeof(TABLA[0]);
Estado actual = ESPERA, historia = NINGUNO;
bool enCadena(Estado inicio, Estado buscado) { // ¿buscado está entre inicio y la raíz?
for (Estado s = inicio; s != NINGUNO; s = PADRE[s]) if (s == buscado) return true;
return false;
}
Estado hoja(Estado s) { while (INICIAL[s] != NINGUNO) s = INICIAL[s]; return s; }
Estado buscarManejador(Estado desde, Evento ev, Estado &destino) { // sube por la jerarquía hasta hallar quien lo maneje
for (Estado s = desde; s != NINGUNO; s = PADRE[s])
for (byte i = 0; i < N_TRANS; i++)
if (TABLA[i].manejador == s && TABLA[i].evento == ev) { destino = TABLA[i].destino; return s; }
return NINGUNO;
}
void despachar(Evento ev) {
Estado destino = NINGUNO;
const Estado origen = actual, manejador = buscarManejador(actual, ev, destino);
Serial.print(" "); Serial.print(NOMBRE_EV[ev]);
if (manejador == NINGUNO) { Serial.print(": ignorado en "); Serial.println(NOMBRE[actual]); return; }
if (destino == HISTORIA) destino = historia;
Estado salen[5]; byte ns = 0; // de la hoja al manejador y luego hasta el ancestro común
for (Estado s = actual;; s = PADRE[s]) { salen[ns++] = s; if (s == manejador) break; }
for (Estado s = PADRE[manejador]; !enCadena(PADRE[destino], s); s = PADRE[s]) salen[ns++] = s;
Estado entran[6]; byte ne = 0; // del destino hacia arriba (se imprimen al revés)
for (Estado s = destino; s != RAIZ; s = PADRE[s]) {
bool comun = enCadena(origen, s);
for (byte i = 0; i < ns; i++) if (salen[i] == s) comun = false;
if (!comun) entran[ne++] = s;
}
Serial.print(": "); Serial.print(NOMBRE[origen]); Serial.print(" -> ");
if (destino == EVADIENDO) historia = origen;
actual = hoja(destino);
Serial.print(NOMBRE[actual]);
if (manejador != origen) { Serial.print(" [manejado por "); Serial.print(NOMBRE[manejador]); Serial.print("]"); }
Serial.print(" | ");
for (byte i = 0; i < ns; i++) {
if (i) Serial.print("; ");
Serial.print("sale "); Serial.print(NOMBRE[salen[i]]);
if (ACCION_SALIDA[salen[i]]) { Serial.print(" ("); Serial.print(ACCION_SALIDA[salen[i]]); Serial.print(")"); }
}
for (int i = ne - 1; i >= 0; i--) {
Serial.print("; entra "); Serial.print(NOMBRE[entran[i]]);
if (ACCION_ENTRADA[entran[i]]) { Serial.print(" ("); Serial.print(ACCION_ENTRADA[entran[i]]); Serial.print(")"); }
}
for (Estado s = destino; INICIAL[s] != NINGUNO;) { // descenso por los subestados iniciales
s = INICIAL[s];
Serial.print("; entra "); Serial.print(NOMBRE[s]);
if (ACCION_ENTRADA[s]) { Serial.print(" ("); Serial.print(ACCION_ENTRADA[s]); Serial.print(")"); }
}
Serial.println();
}
void setup() {
Serial.begin(115200);
Serial.println("Escenario guionado del robot:");
const Evento guion[] = {INICIO, OBSTACULO, LIBRE, BATERIA_BAJA, OBSTACULO, LIBRE, BASE_VISTA, OBSTACULO, ACOPLADO, INICIO, ATASCO, PARAR, ACK};
for (byte i = 0; i < sizeof(guion) / sizeof(guion[0]); i++) despachar(guion[i]);
Serial.print("Transiciones escritas con jerarquia: "); Serial.println(N_TRANS);
}
void loop() {}
Recorrido de la salida: INICIO lleva de ESPERA a LIMPIANDO entrando primero en OPERANDO; OBSTACULO lleva a EVADIENDO; LIBRE regresa a LIMPIANDO por la historia; BATERIA_BAJA lleva a BUSCAR_BASE (entra en REGRESANDO y baja a su subestado inicial); un nuevo OBSTACULO sale de BUSCAR_BASE y de REGRESANDO; LIBRE vuelve a BUSCAR_BASE, no a LIMPIANDO; BASE_VISTA pasa a ACOPLAR; OBSTACULO se ignora en ACOPLAR; ACOPLADO sale de tres niveles hacia ESPERA; después, ATASCO desde LIMPIANDO se atiende en OPERANDO ([manejado por OPERANDO]) y lleva a FALLA; PARAR se ignora en FALLA y ACK vuelve a ESPERA. Al final informa Transiciones escritas con jerarquia: 12.
Para usarlo en un robot, la capa impura convierte sensores en eventos (con antirrebote y flancos, tema 37), llama a despachar() una vez por evento y aplica las salidas por estado. Los eventos de seguridad (ACANTILADO, ATASCO) se atienden antes que los demás, y el tiempo de cada etapa se mide con millis() y genera eventos como TIEMPO_FIN.
10 · Exploración interactiva
Laboratorios del robot de limpieza
1. Recorrer la jerarquía
El diagrama anida los estados: los contenedores son superestados. El estado actual se resalta en lima, y también sus contenedores. Los botones que tienen un manejador (propio o heredado) desde el estado actual aparecen resaltados. La última transición se marca; las flechas punteadas son la historia. Cada paso muestra los estados que salen y los que entran, con sus acciones.
Probá INICIO.
Una secuencia reveladora: INICIO, OBSTACULO, LIBRE (vuelve a LIMPIANDO por la historia), BATERIA_BAJA, OBSTACULO, LIBRE (ahora vuelve a BUSCAR_BASE), BASE_VISTA, ACOPLADO. Luego INICIO y ATASCO: en el registro verás «[manejado por OPERANDO]», porque LIMPIANDO no define ATASCO y el evento sube. Probá también ACANTILADO desde BUSCAR_BASE o desde ACOPLAR: la misma regla, escrita una vez, protege todas las etapas.
2. El costo de repetir reglas
Un modelo simple: dentro de un superestado hay n estados hoja, g eventos comunes que se atienden igual en todos y l transiciones propias por estado. Se cuentan las transiciones que hay que escribir en una máquina plana y en una jerárquica (más 2 transiciones fuera del superestado: ESPERA y FALLA).
Elegí los parámetros.
Con los valores iniciales (4 estados, 3 eventos comunes, 1 transición propia): 18 transiciones en la plana y 9 en la jerárquica. Subí los eventos comunes a 6 y los estados a 8: 58 contra 16. La diferencia crece como el producto n · g. El robot real (21 contra 12, o 25 si se duplica EVADIENDO por la historia) es un caso con valores parecidos a los iniciales.
11 · Código ejecutable
Despachar eventos en Python
El motor jerárquico en pocas líneas: el árbol de estados, el manejador que se busca hacia arriba, el cálculo de las salidas y entradas, la historia y el conteo de transiciones equivalentes en una máquina plana.
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 robot.
PADRE = {"ESPERA": "RAIZ", "OPERANDO": "RAIZ", "LIMPIANDO": "OPERANDO", "REGRESANDO": "OPERANDO", "BUSCAR_BASE": "REGRESANDO",
"ACOPLAR": "REGRESANDO", "EVADIENDO": "OPERANDO", "FALLA": "RAIZ", "RAIZ": None}
INICIAL = {"OPERANDO": "LIMPIANDO", "REGRESANDO": "BUSCAR_BASE"} # subestado inicial de cada estado compuesto
TRANSICIONES = { # (estado que la maneja, evento) -> destino
("ESPERA", "INICIO"): "LIMPIANDO",
("OPERANDO", "PARAR"): "ESPERA", ("OPERANDO", "ACANTILADO"): "FALLA", ("OPERANDO", "ATASCO"): "FALLA",
("LIMPIANDO", "BATERIA_BAJA"): "REGRESANDO", ("LIMPIANDO", "TIEMPO_FIN"): "REGRESANDO", ("LIMPIANDO", "OBSTACULO"): "EVADIENDO",
("BUSCAR_BASE", "BASE_VISTA"): "ACOPLAR", ("BUSCAR_BASE", "OBSTACULO"): "EVADIENDO",
("ACOPLAR", "ACOPLADO"): "ESPERA",
("EVADIENDO", "LIBRE"): "HISTORIA",
("FALLA", "ACK"): "ESPERA",
}
ENTRADA = {"ESPERA": "detener motores, cargar", "OPERANDO": "LED de actividad", "LIMPIANDO": "cepillo y avance", "REGRESANDO": "apagar cepillo",
"BUSCAR_BASE": "buscar la baliza", "ACOPLAR": "avance lento", "EVADIENDO": "detener y girar", "FALLA": "alarma"}
SALIDA = {"OPERANDO": "apagar motores", "LIMPIANDO": "apagar cepillo", "EVADIENDO": "dejar de girar", "FALLA": "apagar alarma"}
SALIDAS = { # máquina de Moore sobre las hojas: (motor, cepillo, alarma)
"ESPERA": ("parado", 0, 0), "LIMPIANDO": ("avanza", 1, 0), "BUSCAR_BASE": ("avanza", 0, 0), "ACOPLAR": ("lento", 0, 0),
"EVADIENDO": ("gira", 0, 0), "FALLA": ("parado", 0, 1),
}
def cadena(estado):
"""Del estado hasta la raíz, ambos incluidos."""
c = []
while estado is not None:
c.append(estado); estado = PADRE[estado]
return c
def hoja(estado):
while estado in INICIAL:
estado = INICIAL[estado]
return estado
class Robot:
def __init__(self):
self.actual = "ESPERA"
self.historia = None
def despachar(self, evento):
"""Busca el manejador subiendo por la jerarquía; ejecuta las salidas y entradas necesarias."""
manejador = next((e for e in cadena(self.actual) if (e, evento) in TRANSICIONES), None)
if manejador is None:
return None, [], f"{evento}: ignorado en {self.actual}"
destino = TRANSICIONES[(manejador, evento)]
origen = self.actual
if destino == "HISTORIA":
destino = self.historia
# estados que se abandonan: de la hoja actual hasta el manejador (inclusive) y luego los ancestros
# que no contienen al destino
propios = set(cadena(destino)[1:])
salen, e = [], self.actual
while True:
salen.append(e)
if e == manejador:
break
e = PADRE[e]
e = PADRE[manejador]
while e not in propios:
salen.append(e)
e = PADRE[e]
# estados que se inician: desde debajo del ancestro común hasta el destino y sus subestados iniciales
comunes = set(cadena(self.actual)) - set(salen)
entran = [x for x in reversed(cadena(destino)) if x not in comunes and x != "RAIZ"]
e = destino
while e in INICIAL:
e = INICIAL[e]; entran.append(e)
if destino == "EVADIENDO":
self.historia = origen # se recuerda desde dónde se evade
self.actual = hoja(destino)
return manejador, (salen, entran), None
def formato(salen, entran):
partes = [f"sale {s}" + (f" ({SALIDA[s]})" if s in SALIDA else "") for s in salen]
partes += [f"entra {e}" + (f" ({ENTRADA[e]})" if e in ENTRADA else "") for e in entran]
return "; ".join(partes)
def contar_transiciones_planas():
hojas = [e for e in PADRE if e != "RAIZ" and e not in PADRE.values()]
return sum(len({ev for (st, ev) in TRANSICIONES if st in cadena(h)}) for h in hojas), len(hojas)
print("Escenario guionado del robot:")
r = Robot()
guion = ["INICIO", "OBSTACULO", "LIBRE", "BATERIA_BAJA", "OBSTACULO", "LIBRE", "BASE_VISTA", "OBSTACULO", "ACOPLADO",
"INICIO", "ATASCO", "PARAR", "ACK"]
for ev in guion:
antes = r.actual
manejador, acciones, aviso = r.despachar(ev)
if aviso:
print(f" {aviso}")
else:
heredada = f" [manejado por {manejador}]" if manejador != antes else ""
print(f" {ev}: {antes} -> {r.actual}{heredada} | {formato(*acciones)}")
planas, n_hojas = contar_transiciones_planas()
print(f"Transiciones escritas con jerarquia: {len(TRANSICIONES)}; equivalentes en una maquina plana: {planas} sobre {n_hojas} estados hoja")
print("Con la historia hay que duplicar EVADIENDO (uno por cada estado de origen): 25 transiciones y 7 estados hoja")El escenario de 13 eventos reproduce lo descrito: la historia lleva a LIMPIANDO la primera vez y a BUSCAR_BASE la segunda, OBSTACULO se ignora en ACOPLAR, ATASCO se atiende en OPERANDO desde LIMPIANDO y PARAR se ignora en FALLA. Las 12 transiciones escritas equivalen a 21 en una máquina plana de 6 estados hoja.
Agregá un estado MANUAL con sus propias transiciones, o un evento EMERGENCIA definido en la raíz, y comprobá que se hereda en todos los estados. Probá cambiar el destino de ACANTILADO en OPERANDO: es un solo cambio.
12 · Método y diagnóstico
Pruebas y diagnóstico
- Dibujá primero la jerarquía y marcá qué eventos son comunes a cada grupo; solo después escribí la tabla.
- Probá el despacho con escenarios sin hardware: cada evento desde cada estado hoja, comparando contra el resultado esperado.
- Verificá la herencia: un evento del superestado debe funcionar desde todos sus subestados, incluidos los agregados después.
- Verificá el orden de las acciones: registrá salidas y entradas con sus nombres y comprobá que van de adentro hacia afuera y de afuera hacia adentro.
- Probá la historia con interrupciones desde cada origen posible.
- Provocá cada falla crítica en cada etapa y comprobá que todo queda en estado seguro y la alarma se enciende.
- Registrá las transiciones con la hora y el motivo: con una jerarquía, el «por qué» es tan importante como el «a dónde».
«Más niveles de jerarquía es siempre mejor»
Cada nivel agrega abstracción y también indirección: hay que saber de dónde viene una regla. Dos o tres niveles suelen bastar; más, y el diagrama se vuelve un laberinto.
«Si una etapa no maneja un evento, el sistema lo ignora»
No: el evento sube al superestado. Es la herencia, y es fuente de sorpresas si no se recuerda. Una etapa que quiere ignorar un evento heredado debe definirlo explícitamente como «no hacer nada».
«La historia es gratis»
Esconde una variable fuera del diagrama. Hay que registrarla, probarla y decidir qué pasa con los eventos que ocurrieron durante la interrupción.
«Las acciones de entrada pueden bloquear un poco, es solo un delay corto»
Congelan la máquina entera durante la transición y rompen las garantías de reacción. Se inician y se esperan con un estado intermedio.
«Con una falla enclavada alcanza, no hace falta registrar la causa»
Sin la causa, el operador reinicia a ciegas y el problema vuelve. Hay que guardar qué falla fue, en qué estado y a qué hora.
«Un comportamiento reactivo y un estado son lo mismo»
Se parecen, pero un comportamiento compite por prioridad con otros y se evalúa siempre; un estado tiene transiciones explícitas y solo está activo uno por región. Se usan juntos.
13 · Práctica guiada
Predecí antes de comprobar
1. Quién maneja el evento
El robot está en ACOPLAR y llega ATASCO. ¿Quién lo maneja, a dónde va y qué acciones se ejecutan?
Ver solución
ACOPLAR no lo maneja; REGRESANDO tampoco; lo maneja OPERANDO. Va a FALLA. Acciones: sale ACOPLAR, sale REGRESANDO, sale OPERANDO (apagar motores), entra FALLA (alarma).
2. Historia
Desde LIMPIANDO llegan BATERIA_BAJA, OBSTACULO y LIBRE. ¿En qué estado termina y por qué?
Ver solución
BATERIA_BAJA lleva a REGRESANDO y de allí a BUSCAR_BASE. OBSTACULO desde BUSCAR_BASE lleva a EVADIENDO y guarda BUSCAR_BASE. LIBRE vuelve por la historia a BUSCAR_BASE: no a LIMPIANDO.
3. Un evento puntual perdido
El robot evade un obstáculo mientras limpia y, durante esa evasión, la batería baja y se emite BATERIA_BAJA. ¿Qué pasa y cómo se corrige?
Ver solución
EVADIENDO no maneja BATERIA_BAJA ni lo hereda: se ignora. Al volver a LIMPIANDO por la historia, el evento no se repite y el robot seguiría limpiando. Se corrige guardando una condición persistente (una bandera «batería baja») y consultándola al entrar en LIMPIANDO, o cambiando el diseño para que EVADIENDO vuelva a REGRESANDO si la bandera está activa.
4. Un estado nuevo
Se agrega dentro de OPERANDO un estado BARRIDO_ESPIRAL. ¿Cuántas transiciones de emergencia hay que escribir en la versión plana y en la jerárquica?
Ver solución
En la plana, tres (PARAR, ACANTILADO, ATASCO). En la jerárquica, ninguna: las hereda de OPERANDO. Solo hay que escribir las transiciones propias del estado nuevo.
5. El orden de las acciones
¿Por qué se sale de adentro hacia afuera y se entra de afuera hacia adentro?
Ver solución
Es la regla de recursos anidados: lo que abrió el contenedor, lo cierra el contenedor, y el hijo depende del contexto que preparó el padre. Al salir, el hijo limpia lo suyo antes de que el padre apague el contexto; al entrar, el padre prepara el contexto antes de que el hijo lo use.
6. Clasificar fallas
Clasificá: batería baja, rueda que patina una vez, rueda que no gira.
Ver solución
Batería baja: advertencia (volver a la base). Rueda que patina una vez: recuperable (retroceder y reintentar un número limitado de veces; si persiste, falla crítica). Rueda que no gira: crítica (parar, enclavar, esperar el reconocimiento).
7. La emergencia
Se agrega un modo MANUAL y un pulsador de emergencia que debe funcionar siempre. ¿Dónde se define la transición?
Ver solución
En la raíz: así se hereda en ESPERA, OPERANDO, MANUAL y cualquier estado futuro. La entrada a FALLA (o a un estado de EMERGENCIA) apaga los actuadores. Además, la emergencia real sigue siendo un circuito cableado independiente (tema 35); la del programa es complementaria.
14 · Síntesis
Ideas para recordar
- Una máquina plana repite las reglas comunes en cada estado; la jerarquía las escribe una vez en un superestado.
- El despacho busca el manejador subiendo desde la hoja actual hasta la raíz; el primero que lo maneja actúa, y si ninguno, se ignora.
- Un estado compuesto tiene un subestado inicial; al entrar en él se baja hasta una hoja.
- Se sale de adentro hacia afuera y se entra de afuera hacia adentro, ejecutando las acciones de salida y de entrada en ese orden.
- La acción de salida de un superestado garantiza propiedades de seguridad (motores apagados) para todos los caminos que salen de él.
- La historia permite retomar el subestado interrumpido; esconde una variable y no recuerda los eventos puntuales.
- Los modos son superestados excluyentes; los cambios de modo pasan por un estado seguro; la emergencia se define en la raíz.
- Las fallas se clasifican (advertencia, recuperable, crítica), se enclavan, se reconocen y se registran; hay supervisión por tiempo y vigilante.
¿Listo para el tema 39?
Antes de avanzar, tomá un sistema con modos (una impresora, un ascensor, una cinta) y dibujá su jerarquía: modos, etapas, eventos comunes y fallas. Contá cuántas transiciones tendría en forma plana y en forma jerárquica, y marcá dónde van las acciones de seguridad.
En el próximo tema aplicamos todo en un proyecto de automatización: la automatización de procesos con secuencias y enlaces sobre una cinta transportadora y un clasificador, con sus condiciones de seguridad.