01 · Punto de partida
Muchos comportamientos, un solo robot
En los temas 28 a 32 desarrollamos varios comportamientos: ir a un punto, seguir una línea, buscarla cuando se pierde, evitar un obstáculo. Cada uno se prueba bien por separado. Pero un robot en el mundo real los necesita todos a la vez: sigue una línea, aparece una caja encima, debe esquivarla, y al terminar tiene que volver a encontrar la línea. Si cada comportamiento escribe a las ruedas cuando quiere, el resultado es un caos.
Hay tres formas clásicas de resolverlo. Una es el planificador deliberativo (sentir, construir un modelo, planificar, actuar): potente, pero lento y frágil si el modelo falla. Otra es una máquina de estados explícita, que desarrollamos en los temas 36 a 38. La tercera, que vemos aquí, es una arquitectura reactiva en capas, propuesta por Rodney Brooks a mediados de los años 80 con el nombre de subsunción.
La idea de fondo es que una conducta compleja puede surgir de comportamientos simples, cada uno con su sensor y su acción, sin un modelo central del mundo.
02 · La pieza básica
Qué es un comportamiento reactivo
Un comportamiento reactivo es una función corta que mira los sensores y propone una acción. En términos de este curso:
Si las condiciones no son las suyas, no propone nada. Si lo son, propone una orden de velocidad lineal y angular (tema 26); el paso a velocidades de rueda es común a todos. Algunas propiedades que conviene respetar:
- Simple y con un solo propósito. «Seguir la línea» no sabe nada de obstáculos; «escapar» no sabe de líneas.
- Poca memoria. Puede guardar un dato mínimo (el último lado visto, si está escapando), pero no un mapa.
- No bloqueante. Se evalúa en cada ciclo de control, en microsegundos, sin
delay()ni esperas (temas 16 y 19). - Sin escribir a los motores. Propone; no ordena. Solo el árbitro habla con las ruedas, para que haya un único responsable.
Son reactivos porque la acción depende de lo que se percibe ahora (más un poco de estado propio). Esto les da rapidez y robustez: si el entorno cambia, el robot cambia de acción en el siguiente ciclo.
03 · Capas y prioridades
La idea de la subsunción
En la arquitectura de subsunción, los comportamientos se organizan en niveles de competencia. Los niveles más bajos son las funciones básicas y de supervivencia (no chocar, no caerse); los más altos, las tareas (seguir una línea, explorar). Cada capa funciona sola, y una capa superior puede intervenir sobre las inferiores de dos maneras:
- Supresión: la capa superior reemplaza la señal de salida de una inferior por la suya.
- Inhibición: la capa superior bloquea la señal, sin reemplazarla.
Una advertencia sobre el nombre: en el planteo original de Brooks, las capas superiores (más ambiciosas) pueden suprimir a las inferiores, pero la seguridad se logra porque las capas básicas siguen corriendo debajo. En la formulación práctica que usan muchos robots educativos, y que adoptamos aquí, es más simple pensarlo como una lista de comportamientos ordenada por prioridad: el de mayor prioridad que quiere actuar gana, y los demás quedan suprimidos. La seguridad (parar, escapar) va en lo más alto de la lista, y las tareas, por debajo.
Dos propiedades hacen valiosa esta organización. Una: se construye de abajo hacia arriba. Se programa y prueba la capa más básica; después se agrega la siguiente, sin tocar las anteriores. Otra: se degrada con elegancia. Si se estropea un sensor y una capa deja de actuar, las demás siguen funcionando.
04 · Quién manda
El árbitro
El árbitro es una función de unas pocas líneas: evalúa todos los comportamientos (cada uno actualiza su estado) y toma la orden del primero activo en el orden de prioridad:
Es importante que todos se evalúen en cada ciclo, aunque no gobiernen: así mantienen su estado actualizado (por ejemplo, ESCAPE sigue detectando si el frente quedó libre) y, al ganar, ya están al día.
La prioridad fija no es la única forma de combinar comportamientos:
| Método | Cómo combina | Cuándo conviene |
|---|---|---|
| Prioridad fija (subsunción) | Gana uno solo: el más prioritario que está activo | Seguridad primero; comportamientos que se excluyen; fácil de depurar |
| Suma ponderada o de vectores | Cada comportamiento aporta un vector y se suman (campos de potencial) | Cuando las acciones se pueden mezclar suavemente, como atracción al objetivo y repulsión de obstáculos |
| Máquina de estados | Un estado explícito decide quién actúa y las transiciones | Secuencias y modos (temas 36 a 38) |
La prioridad fija tiene una ventaja decisiva en un robot pequeño: es predecible y fácil de razonar. Su costo es que no mezcla: cuando gana un comportamiento, los demás quedan en silencio, y a veces una mezcla daría un movimiento más natural. Para la mayoría de las tareas de este curso, la prioridad fija alcanza.
05 · Un ejemplo completo
Diseñar las capas
Para un seguidor de línea que además evita obstáculos y puede recibir una orden de emergencia, estas son las capas, de mayor a menor prioridad:
| Prioridad | Comportamiento | Se activa cuando… | Qué ordena | Estado propio |
|---|---|---|---|---|
| 1 | PARADA | Llega la orden de emergencia | v = 0, ω = 0 | Ninguno |
| 2 | ESCAPE | Hay un obstáculo al frente o en las diagonales (tema 32) | Retroceder si está muy cerca; si no, girar hacia el lado libre | Si está escapando, y el último sentido de giro |
| 3 | SEGUIR_LINEA | El arreglo ve la línea | v constante y ω = K·p (tema 31) | Ninguno |
| 4 | BUSCAR_LINEA | No se ve la línea y hace menos de 3 s que se perdió | Girar en el lugar hacia el último lado visto | Último lado y tiempo sin línea |
Algunas decisiones de diseño que conviene justificar:
- La seguridad arriba. PARADA y ESCAPE son más prioritarios que la tarea: nunca se sigue la línea a costa de chocar.
- ESCAPE retiene el control. Una vez activado, no lo suelta hasta que el frente supera 35 cm y las diagonales quedan libres (tema 32). Sin esa retención el control saltaría entre ESCAPE y SEGUIR_LINEA con cada lectura ruidosa.
- BUSCAR_LINEA se rinde. Después de 3 s sin hallar la línea, deja de proponer una orden. Con la arquitectura de arriba, el robot queda parado: es una decisión explícita (es mejor parar que girar para siempre) y se puede reemplazar por otra capa (avisar, volver a una base).
- Agregar es fácil. Una capa «ESTACIÓN_DE_CARGA» (batería baja: ir a la base) iría entre ESCAPE y SEGUIR_LINEA: es más importante que la tarea y menos que la seguridad.
Entre el tema 31 y este, hay una diferencia de enfoque. Allí vimos una máquina de estados dentro del seguidor (SIGUIENDO, BUSCANDO, DETENIDO). Aquí cada estado es un comportamiento independiente y la decisión de quién actúa está fuera, en el árbitro. Se pueden agregar o quitar comportamientos sin reescribir la máquina.
06 · Lo que sale mal
Problemas típicos
- Oscilación entre comportamientos. Si el disparador de ESCAPE es «distancia < 25 cm» y la lectura fluctúa entre 24 y 28, el control salta de ESCAPE a SEGUIR_LINEA y de vuelta en cada ciclo: el robot tiembla. Se corrige con retención: una condición de entrada y otra, más exigente, de salida (histéresis), y a veces un tiempo mínimo de permanencia.
- Inanición (starvation). Un comportamiento de baja prioridad no actúa nunca porque otro de mayor prioridad está siempre activo (por ejemplo, un sensor atascado que siempre dice «obstáculo»). Hay que registrar quién gana y detectar ganadores que no cambian.
- Nadie actúa. Ningún comportamiento propone una orden, y el robot queda parado sin explicación. Es legítimo como política (mejor parar que improvisar), pero debe ser una decisión consciente y avisarse. Una capa de menor prioridad, «por defecto», evita sorpresas.
- Comportamientos que se anulan. Dos comportamientos que se alternan y se deshacen mutuamente la acción (uno gira a la izquierda, el otro se corrige a la derecha). El síntoma es movimiento sin progreso. Se revisan las condiciones y se agrega retención.
- Prioridades mal ordenadas. Un comportamiento de tarea por encima de uno de seguridad es una falla de diseño. La orden de parada debe poder pisar a todos.
- Escribir a los motores desde varios lugares. Anula la arquitectura. Un solo punto de salida, con tope de velocidad y de PWM.
La mejor herramienta de depuración es un registro del ganador: cada ciclo, o cada vez que cambia, imprimir cuál comportamiento controló y qué orden dio. La mayoría de las fallas se ven de inmediato en ese registro. También sirve contar cuántas veces cambia de ganador por segundo.
07 · Honestidad sobre el método
Hasta dónde llega
Los comportamientos reactivos son rápidos, baratos y robustos, y por eso se usan tanto. Pero tienen límites:
- Sin memoria del mundo. No saben dónde estuvieron, ni dónde están respecto de un mapa. Una trampa que requiera «haber estado ya acá» no se detecta sin agregar estado.
- Conducta emergente. Lo que hace el conjunto no está escrito en ninguna línea; surge de la interacción con el entorno. Eso es una virtud y a la vez una dificultad: es difícil predecir el comportamiento en situaciones nuevas y hay que probar mucho.
- No escala indefinidamente. Con muchas capas, las interacciones se vuelven difíciles de razonar. Para tareas con secuencias y modos (explorar, volver a la base, cargar), se combina con máquinas de estados jerárquicas (tema 38).
- Sin planificación. No elige la mejor ruta; reacciona. Para eso se necesitan mapas y planificadores, que escapan de este curso.
Para el robot que construimos —sigue una línea, esquiva y recupera la línea— la subsunción es una buena herramienta. Y como práctica de diseño, vale más allá: separar lo que el robot quiere hacer de quién decide cuándo, y mantener un único canal hacia los actuadores.
08 · Código para la placa
Un árbitro en Arduino
El sketch contiene los cuatro comportamientos y el árbitro del ejemplo. Para probar la lógica sin hardware, setup() la alimenta con una tabla de nueve lecturas guionadas y muestra el ganador y la orden resultante en cada caso. En el robot real se reemplazan dos funciones: la lectura de los sensores (arreglo de línea del tema 30 y ultrasonidos del tema 32) y la aplicación de la orden (cinemática inversa y motores, temas 26 y 31), y se llama al árbitro cada 20 ms.
// Arquitectura de subsunción: cada comportamiento propone una orden (v, w) o nada; el árbitro elige el de mayor prioridad.
// Para probar la lógica sin hardware, setup() la alimenta con una tabla de lecturas guionadas.
// En un robot real: reemplazá leerSensores() (temas 30 y 32) y aplicarOrden() (tema 31) y llamá a ciclo() cada 20 ms.
const float B = 15.0; // cm entre ruedas
const float D_PELIGRO = 25.0, D_LAT = 15.0, D_CRITICA = 10.0, D_LIBRE = 35.0;
const float V_LINEA = 20.0, K_LINEA = 0.1, W_MAX = 1.33;
const float W_ESCAPE = 2.0, V_RETRO = -10.0, W_BUSQUEDA = 1.2, BUSQUEDA_MAX_S = 3.0;
struct Sensores {
bool parada; // orden de emergencia
float dIzq, dCen, dDer; // cm
bool linea; // ¿el arreglo ve la línea?
float p; // mm, positivo a la izquierda
float sinLineaS; // s desde que se perdió la línea
int ultimoLado; // +1 = izquierda, -1 = derecha
};
struct Orden { bool activa; float v, w; };
// ---- Comportamientos: cada uno evalúa en cada ciclo y puede guardar su propio estado ----
Orden paradaDeEmergencia(const Sensores &s) { return { s.parada, 0.0, 0.0 }; }
bool escapando = false;
int ladoEscape = 1, ultimoGiro = 1;
Orden escape(const Sensores &s) {
const bool peligro = s.dCen < D_PELIGRO || s.dIzq < D_LAT || s.dDer < D_LAT;
const bool libre = s.dCen > D_LIBRE && s.dIzq > D_LAT + 5 && s.dDer > D_LAT + 5;
if (escapando && libre) escapando = false; // retención: no suelta hasta quedar libre
else if (!escapando && peligro) {
escapando = true;
ladoEscape = (fabs(s.dIzq - s.dDer) > 5.0) ? ((s.dIzq > s.dDer) ? 1 : -1) : -ultimoGiro;
ultimoGiro = ladoEscape;
}
if (!escapando) return { false, 0, 0 };
if (s.dCen < D_CRITICA) return { true, V_RETRO, 0.0 };
return { true, 0.0, ladoEscape * W_ESCAPE };
}
Orden seguirLinea(const Sensores &s) {
if (!s.linea) return { false, 0, 0 };
return { true, V_LINEA, constrain(K_LINEA * s.p, -W_MAX, W_MAX) };
}
Orden buscarLinea(const Sensores &s) {
if (s.linea || s.sinLineaS > BUSQUEDA_MAX_S) return { false, 0, 0 };
return { true, 0.0, s.ultimoLado * W_BUSQUEDA }; // girar hacia donde se vio la línea
}
// ---- Árbitro: prioridad fija, de mayor a menor ----
const char *NOMBRES[4] = {"PARADA", "ESCAPE", "SEGUIR_LINEA", "BUSCAR_LINEA"};
const char *arbitrar(const Sensores &s, float &v, float &w) {
const Orden o[4] = { paradaDeEmergencia(s), escape(s), seguirLinea(s), buscarLinea(s) }; // todos se evalúan siempre
for (byte i = 0; i < 4; i++) {
if (o[i].activa) { v = o[i].v; w = o[i].w; return NOMBRES[i]; }
}
v = 0; w = 0;
return "NINGUNO"; // nadie quiere actuar: el robot se detiene
}
void aplicarOrden(float v, float w) { // aquí irían la cinemática inversa (tema 26) y los motores
Serial.print(" v="); Serial.print(v, 1); Serial.print(" cm/s w="); Serial.print(w, 2);
Serial.print(" rad/s (izq="); Serial.print(v - w * B / 2.0, 1);
Serial.print(" der="); Serial.print(v + w * B / 2.0, 1); Serial.println(")");
}
// ---- Prueba guionada ----
struct Caso { const char *titulo; Sensores s; };
const Caso CASOS[] = {
{"Linea 6 mm a la izquierda, camino libre", {false, 40, 150, 40, true, 6.0, 0.0, 1}},
{"Linea 10 mm a la derecha, frente a 40 cm", {false, 40, 40, 40, true, -10.0, 0.0, 1}},
{"Obstaculo al frente a 22 cm", {false, 40, 22, 25, true, 0.0, 0.0, 1}},
{"Obstaculo a 8 cm", {false, 40, 8, 25, true, 0.0, 0.0, 1}},
{"Tras girar: frente a 30 cm, sin linea", {false, 40, 30, 25, false, 0.0, 0.5, 1}},
{"Frente libre (60 cm), sin linea, lado izq", {false, 40, 60, 40, false, 0.0, 1.0, 1}},
{"Linea recuperada (2 mm)", {false, 40, 150, 40, true, 2.0, 0.0, 1}},
{"Parada de emergencia con obstaculo a 10 cm",{true, 40, 10, 40, true, 0.0, 0.0, 1}},
{"Sin linea hace 4 s (la busqueda se rinde)", {false, 40, 150, 40, false, 0.0, 4.0, 1}}
};
void setup() {
Serial.begin(115200);
Serial.println("Prueba del arbitro de subsuncion:");
for (byte i = 0; i < sizeof(CASOS) / sizeof(CASOS[0]); i++) {
float v, w;
const char *ganador = arbitrar(CASOS[i].s, v, w);
Serial.print(CASOS[i].titulo); Serial.print(" -> "); Serial.println(ganador);
aplicarOrden(v, w);
}
}
void loop() {}
Salida esperada, en el mismo orden: el caso 1 (línea a 6 mm, camino libre) lo gana SEGUIR_LINEA con v=20.0 w=0.60 (izq=15.5 der=24.5); el caso 2, SEGUIR_LINEA con w=-1.00; el caso 3 (obstáculo a 22 cm), ESCAPE con v=0.0 w=2.00 (izq=-15.0 der=15.0); el 4 (obstáculo a 8 cm), ESCAPE retrocediendo a v=-10.0; el 5 (frente a 30 cm sin línea), sigue ganando ESCAPE por su retención; el 6 (frente libre, línea perdida), BUSCAR_LINEA con w=1.20; el 7, SEGUIR_LINEA; el 8 (emergencia), PARADA; y el 9 (4 s sin línea), NINGUNO.
Cosas a notar en el código: cada comportamiento devuelve una Orden con un indicador de actividad; ESCAPE tiene variables propias (escapando, ladoEscape); el árbitro evalúa todos antes de elegir; solo aplicarOrden() toca los motores. Las cadenas y la tabla ocupan una parte de los 2 KB de RAM del UNO; para una tabla más grande conviene guardarlas en memoria de programa.
09 · Exploración interactiva
Laboratorios: el árbitro y una misión
1. Quién gana ahora
Armá una situación con los sensores y mirá qué comportamientos quieren actuar, cuál gana y cuáles quedan suprimidos. Podés desactivar capas para ver qué pasa cuando faltan. Las distancias son del tema 32 y el seguimiento de línea, del 31 (b = 15 cm).
| Prioridad | Comportamiento | Estado | Orden que propone |
|---|
Armá una situación.
Con la línea a 6 mm y camino libre gana SEGUIR_LINEA (v = 20, ω = 0,6). Poné el frente a 22 cm: gana ESCAPE y SEGUIR_LINEA queda suprimido, aunque la línea siga a la vista. Poné el frente a 30 cm con «ESCAPE ya estaba activo»: sigue ESCAPE (retención); desmarcalo y SEGUIR_LINEA retoma. Apagá la línea y poné 1 s sin verla: gana BUSCAR_LINEA hacia el último lado. A 4 s sin línea, ninguno actúa: «ninguno». Marcá la emergencia: gana PARADA siempre. Desactivá la capa ESCAPE con un obstáculo a 8 cm: el robot seguirá la línea hacia él.
2. Una misión de 30 segundos
Una misión guionada: el robot sigue la línea; entre los 6 y los 13 s un obstáculo se acerca y se queda a unos 26 cm, con lecturas ruidosas (±3 cm); entre los 13 y los 16,5 s se pierde la línea; entre los 24 y los 26 s llega una orden de emergencia. La franja de color muestra qué comportamiento controló las ruedas en cada instante. SEGUIR_LINEA siempre está habilitado.
Elegí las capas.
Con todas las capas y retención hay 6 cambios de comportamiento: SEGUIR_LINEA 20,4 s, ESCAPE 4,1 s, BUSCAR_LINEA 3,0 s, PARADA 2,0 s y 0,4 s sin orden (cuando la búsqueda se rinde). Cambiá a «sin retención»: ESCAPE oscila con el ruido y los cambios suben a 17. Sin la capa ESCAPE, el robot ignora el obstáculo (SEGUIR_LINEA dura 24,5 s). Sin BUSCAR_LINEA, queda 3,5 s sin ninguna orden cuando se pierde la línea. Sin PARADA, la emergencia se ignora y el robot sigue.
10 · Código ejecutable
Arbitrar en Python
El mismo árbitro, con los cuatro comportamientos y su estado, aplicado a las lecturas guionadas y a la misión de 30 s con distintas combinaciones de capas. Es una buena forma de ensayar nuevas capas y prioridades antes de llevarlas al robot.
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.
import math
B = 15.0 # distancia entre ruedas (cm)
D_PELIGRO, D_LAT, D_CRITICA, D_LIBRE = 25.0, 15.0, 10.0, 35.0
V_LINEA, K_LINEA, W_MAX = 20.0, 0.1, 1.33 # cm/s, rad/s por mm, rad/s
W_ESCAPE, V_RETRO, W_BUSQUEDA, BUSQUEDA_MAX = 2.0, -10.0, 1.2, 3.0
ORDEN = ("PARADA", "ESCAPE", "SEGUIR_LINEA", "BUSCAR_LINEA") # de mayor a menor prioridad
class Estado:
def __init__(self):
self.escapando, self.lado, self.ultimo_giro = False, 1, 1
def comportamientos(s, est, retencion=True):
"""Cada comportamiento devuelve (v, w) si quiere actuar, o None. Todos se evalúan en cada ciclo."""
res = {}
res["PARADA"] = (0.0, 0.0) if s["parada"] else None
peligro = s["dCen"] < D_PELIGRO or s["dIzq"] < D_LAT or s["dDer"] < D_LAT
libre = s["dCen"] > D_LIBRE and s["dIzq"] > D_LAT + 5 and s["dDer"] > D_LAT + 5
if retencion:
if est.escapando and libre:
est.escapando = False
elif not est.escapando and peligro:
est.escapando = True
est.lado = (1 if s["dIzq"] > s["dDer"] else -1) if abs(s["dIzq"] - s["dDer"]) > 5 else -est.ultimo_giro
est.ultimo_giro = est.lado
activo = est.escapando
else:
activo = peligro
if activo:
est.lado = (1 if s["dIzq"] > s["dDer"] else -1) if abs(s["dIzq"] - s["dDer"]) > 5 else -est.ultimo_giro
res["ESCAPE"] = None if not activo else ((V_RETRO, 0.0) if s["dCen"] < D_CRITICA else (0.0, est.lado * W_ESCAPE))
res["SEGUIR_LINEA"] = (V_LINEA, max(-W_MAX, min(W_MAX, K_LINEA * s["p"]))) if s["linea"] else None
res["BUSCAR_LINEA"] = (0.0, s["ultimo_lado"] * W_BUSQUEDA) if (not s["linea"] and s["sin_linea"] <= BUSQUEDA_MAX) else None
return res
def arbitrar(res, capas):
for nombre in ORDEN:
if nombre in capas and res[nombre] is not None:
return nombre, res[nombre]
return "NINGUNO", (0.0, 0.0)
def mision(capas, retencion=True, dt=0.05, duracion=30.0):
est = Estado()
tiempos = {n: 0.0 for n in ORDEN + ("NINGUNO",)}
previo, cambios, ultimo_lado, perdida_desde = None, 0, 1, None
for k in range(round(duracion / dt)):
t = k * dt
linea = not (13.0 <= t < 16.5)
p = 6.0 * math.sin(1.5 * t)
if linea:
ultimo_lado, perdida_desde = (1 if p >= 0 else -1), None
elif perdida_desde is None:
perdida_desde = t
if 6.0 <= t < 9.0:
base_d = 60.0 - 34.0 / 3.0 * (t - 6.0) # el obstaculo se acerca...
else:
base_d = 26.0 # ...y se queda a unos 26 cm
d_cen = 150.0 if not (6.0 <= t < 13.0) else max(2.0, base_d + 3.0 * math.sin(9.0 * t))
s = {"parada": 24.0 <= t < 26.0, "dIzq": 40.0, "dCen": d_cen, "dDer": 25.0, "linea": linea, "p": p,
"sin_linea": 0.0 if perdida_desde is None else t - perdida_desde, "ultimo_lado": ultimo_lado}
nombre, _ = arbitrar(comportamientos(s, est, retencion), capas)
tiempos[nombre] += dt
if previo is not None and nombre != previo:
cambios += 1
previo = nombre
return tiempos, cambios
def instantanea(titulo, s, est, capas=ORDEN):
nombre, (v, w) = arbitrar(comportamientos(s, est), capas)
print(f"{titulo:44s} -> {nombre:12s} v={v:5.1f} cm/s w={w:5.2f} rad/s (vl={v - w * B / 2:6.1f}, vr={v + w * B / 2:6.1f})")
base = {"parada": False, "dIzq": 40.0, "dCen": 150.0, "dDer": 40.0, "linea": True, "p": 0.0, "sin_linea": 0.0, "ultimo_lado": 1}
est = Estado()
casos = [
("Linea 6 mm a la izquierda, camino libre", dict(base, p=6.0)),
("Linea 10 mm a la derecha, frente a 40 cm", dict(base, p=-10.0, dCen=40.0)),
("Obstaculo al frente a 22 cm", dict(base, dCen=22.0, dIzq=40.0, dDer=25.0)),
("Obstaculo a 8 cm", dict(base, dCen=8.0, dIzq=40.0, dDer=25.0)),
("Tras girar: frente a 30 cm, sin linea", dict(base, dCen=30.0, dIzq=40.0, dDer=25.0, linea=False, sin_linea=0.5)),
("Frente libre (60 cm) pero sin linea, ultimo lado izq.", dict(base, dCen=60.0, linea=False, sin_linea=1.0)),
("Linea recuperada (2 mm)", dict(base, p=2.0)),
("Parada de emergencia con obstaculo a 10 cm", dict(base, parada=True, dCen=10.0)),
("Sin linea hace 4 s (la busqueda se rinde)", dict(base, linea=False, sin_linea=4.0)),
]
for titulo, s in casos:
instantanea(titulo, s, est)
print("Mision de 30 s (linea, obstaculo de 6 a 13 s (queda a 26 cm), linea perdida de 13 a 16,5 s, parada de 24 a 26 s):")
configuraciones = [
("todas las capas, con retencion", ORDEN, True),
("todas las capas, sin retencion", ORDEN, False),
("sin ESCAPE", ("PARADA", "SEGUIR_LINEA", "BUSCAR_LINEA"), True),
("sin BUSCAR_LINEA", ("PARADA", "ESCAPE", "SEGUIR_LINEA"), True),
("sin PARADA", ("ESCAPE", "SEGUIR_LINEA", "BUSCAR_LINEA"), True),
]
for nombre, capas, ret in configuraciones:
tiempos, cambios = mision(capas, ret)
detalle = ", ".join(f"{k}={v:.1f}" for k, v in tiempos.items() if v > 0)
print(f" {nombre:32s}: cambios={cambios:3d}; {detalle}")Las nueve lecturas dan SEGUIR_LINEA, SEGUIR_LINEA, ESCAPE (giro de 2 rad/s), ESCAPE (retroceso a −10 cm/s), ESCAPE (por retención), BUSCAR_LINEA (1,2 rad/s), SEGUIR_LINEA, PARADA y NINGUNO. En la misión: con todas las capas y retención, 6 cambios (SEGUIR_LINEA 20,4 s, ESCAPE 4,1 s, BUSCAR_LINEA 3,0 s, PARADA 2,0 s y 0,4 s sin orden); sin retención, 17 cambios; sin ESCAPE, el obstáculo se ignora; sin BUSCAR_LINEA, 3,5 s sin orden; sin PARADA, la emergencia se ignora.
Agregá una capa ESTACION_DE_CARGA (batería baja) entre ESCAPE y SEGUIR_LINEA y una lectura de prueba. Probá poner SEGUIR_LINEA por encima de ESCAPE en ORDEN y mirá cómo el robot ignora el obstáculo. Cambiá el tiempo de búsqueda a 6 s.
11 · Método y diagnóstico
Pruebas y diagnóstico
- Agregá las capas de abajo hacia arriba: primero la más básica sola, después la siguiente, probando cada paso con el robot.
- Registrá el ganador en cada cambio y el tiempo que gobierna cada comportamiento; revisá que cada uno gobierne cuando corresponde.
- Con las ruedas en el aire, provocá cada disparador por separado (emergencia, obstáculo, pérdida de línea) y comprobá que el ganador es el esperado.
- Provocá dos disparadores a la vez y verificá la prioridad.
- Probá valores en el borde de los umbrales para detectar oscilación, y verificá la retención.
- Anulá un sensor (tapalo o desconectalo) y comprobá qué hace el robot: si se degrada o hace algo peligroso.
- Verificá que la parada gana siempre, también durante una maniobra de escape o de búsqueda.
«El robot tiembla cerca de los obstáculos»
Casi seguro oscila entre ESCAPE y SEGUIR_LINEA porque el disparador es un único umbral. Agregá retención: salir solo cuando la distancia supere un umbral mayor.
«Una capa nunca actúa»
Mirá el registro: probablemente otra de mayor prioridad está siempre activa (un sensor atascado, un umbral demasiado amplio). Revisá sus condiciones.
«Se queda parado sin motivo»
Ningún comportamiento propone una orden: la línea perdida con la búsqueda ya rendida, por ejemplo. Agregá una capa por defecto o un aviso.
«Puedo dar a la tarea más prioridad que a la seguridad si es urgente»
No. Las capas de seguridad deben ir siempre arriba; si una tarea es urgente, se resuelve con su propia condición, no saltando la parada.
«Cada comportamiento debería mover los motores directamente»
Eso rompe la arquitectura: dos comportamientos escriben y gana el último. Solo el árbitro habla con los motores.
«Si un comportamiento no gana, no hace falta evaluarlo»
Hay que evaluarlos todos para mantener su estado actualizado. Un ESCAPE que no se evalúa mientras gobierna otro no se entera de que el frente quedó libre.
12 · Práctica guiada
Predecí antes de comprobar
1. Quién gana
La línea está a la vista (p = +6 mm) y el obstáculo frontal está a 20 cm, con la diagonal izquierda a 40 cm y la derecha a 25 cm. ¿Quién gana y qué ordena?
Ver solución
Gana ESCAPE (prioridad 2), que supera a SEGUIR_LINEA (3). Las diagonales difieren en 15 cm (más de 5): gira hacia el lado con más espacio, la izquierda: v = 0, ω = +2 rad/s; ruedas −15 y +15 cm/s.
2. Retención
El frente está a 30 cm y las diagonales libres. ¿Qué diferencia hay si ESCAPE ya estaba activo o no?
Ver solución
Si ya estaba activo, 30 cm es menor que el umbral de salida (35 cm): sigue ESCAPE. Si no, 30 cm es mayor que el de entrada (25 cm): no hay peligro y gobierna SEGUIR_LINEA.
3. Nadie actúa
No se ve la línea hace 4 s y no hay obstáculos ni emergencia. ¿Qué hace el robot? ¿Qué cambiarías?
Ver solución
BUSCAR_LINEA se rindió a los 3 s, así que nadie propone una orden y el robot se detiene. Es seguro, pero conviene avisar (un LED, un mensaje) o agregar una capa por defecto que decida explícitamente qué hacer.
4. Oscilación
En la misión del laboratorio hay 17 cambios sin retención y 6 con ella. ¿Por qué baja tanto?
Ver solución
El obstáculo está a unos 26 cm con ±3 cm de ruido: la lectura cruza el umbral de 25 cm una y otra vez. Sin retención, ESCAPE y SEGUIR_LINEA se turnan en cada cruce. Con retención, ESCAPE entra una vez y sale cuando el frente supera 35 cm.
5. Una capa nueva
Se agrega ESTACION_DE_CARGA (ir a la base cuando la batería está baja). ¿Dónde la ubicás en la lista y por qué?
Ver solución
Entre ESCAPE y SEGUIR_LINEA: es más importante que la tarea (sin batería no hay tarea) pero menos que la seguridad (no se debe chocar mientras se va a cargar). PARADA sigue arriba de todo.
6. Velocidades de rueda
Con b = 15 cm, ¿qué velocidades de rueda salen de SEGUIR_LINEA con p = +6 mm (K = 0,1) y de ESCAPE girando?
Ver solución
SEGUIR_LINEA: ω = 0,6 rad/s; vL = 20 − 0,6·7,5 = 15,5 y vR = 24,5 cm/s. ESCAPE girando: v = 0, ω = 2: vL = −15 y vR = +15 cm/s.
7. Un solo canal
¿Por qué solo el árbitro debe escribir a los motores?
Ver solución
Si varios comportamientos escriben, gana el último en ejecutarse, sin relación con la prioridad. Con un único canal la prioridad se aplica de forma explícita, y los límites de velocidad y PWM se hacen cumplir en un solo lugar.
13 · Síntesis
Ideas para recordar
- Un comportamiento reactivo es una función corta que mira los sensores y propone
(v, ω)o nada; es no bloqueante y casi sin memoria. - La subsunción organiza los comportamientos en capas con prioridades; se construye de abajo hacia arriba y se degrada con elegancia.
- El árbitro de prioridad fija evalúa todos los comportamientos y toma la orden del primero activo; la seguridad va arriba.
- Solo el árbitro escribe a los motores: un único canal, con topes de velocidad y de PWM.
- Los comportamientos con estado propio necesitan retención (histéresis) para no oscilar con el ruido.
- Hay que prever el caso «nadie actúa»: parar con aviso es una decisión, no un descuido.
- El registro del ganador es la mejor herramienta de depuración.
- La subsunción no tiene mapas ni planificación; para secuencias y modos se combina con máquinas de estados.
¿Listo para el tema 34?
Antes de avanzar, escribí la lista de capas de tu robot con su prioridad, su disparador y su estado propio. Para cada par de capas contiguas, pensá qué pasa cuando ambas se activan al mismo tiempo, y qué ocurre si falla un sensor.
En el próximo tema aplicamos estas ideas a un problema nuevo: el seguimiento de pared y la exploración de laberintos, con distancia lateral y estrategias de decisión en las intersecciones.