← Índice del curso

Robótica y Automatización · Tema 32

Detección y evasión de obstáculos

Un robot que se mueve por un entorno no controlado va a encontrar cosas en el camino. Evitar el choque exige tres cosas bien hechas: leer la distancia con criterio, decidir a qué distancia hay que actuar y saber qué hacer —y qué hacer si no funciona— una vez que se decide.

01 · Punto de partida

Percibir, decidir, actuar

La evasión de obstáculos es el ejemplo clásico del lazo sentir–decidir–actuar del tema 1. En los temas 6 y 17 vimos los dos extremos: cómo funcionan los sensores de distancia y cuánto espacio necesita un robot para frenar. Este tema los une en un comportamiento completo.

El comportamiento es reactivo: no construye un mapa ni planifica una ruta; reacciona a lo que ve ahora. Es simple, robusto y rápido, y funciona bien en entornos con obstáculos aislados. Tiene límites que conviene conocer: puede quedar atrapado en trampas (un callejón sin salida) y oscilar entre dos obstáculos. Por eso su diseño incluye la recuperación, no solo la evasión.

Todos los números de este tema son de un robot pequeño de ejemplo (16 cm de ancho, 20 cm/s). Medí los tuyos.

02 · Antes de decidir

Del eco al dato confiable

Un sensor de distancia (ultrasonido, infrarrojo, tiempo de vuelo; tema 6) no entrega «la distancia al obstáculo»: entrega una lectura que puede ser incorrecta de varias maneras.

Lecturas problemáticas y qué hacer.
SituaciónQué entrega el sensorTratamiento
Sin eco (nada a la vista, o superficie que no refleja)Tiempo de espera agotado (a veces 0)Distinguirlo de «0 cm»: tratar como «libre» con alcance máximo, sabiendo que una pared de tela o un vidrio también dan sin eco
Obstáculo en la zona ciegaValor erróneo o ausentePrever una distancia mínima fiable; por debajo, asumir «muy cerca»
Lectura espuriaUn valor aislado muy distintoFiltro de mediana de tres lecturas
Rebote en ánguloEco que no vuelve al sensorSensores con distinta orientación; no depender de uno solo
Interferencia entre sensoresEcos de otro sensorMedir de a uno, con una pausa entre disparos

La mediana de las tres últimas lecturas es el filtro más útil aquí: elimina un valor aislado sin retrasar mucho una variación real. Con lecturas 30, 150 y 31 cm, la mediana es 31 cm: el 150 espurio no dispara ni apaga nada. Un promedio habría dado 70 cm, un valor que no corresponde a ninguna realidad.

Para decidir si un obstáculo está «cerca» conviene usar histéresis (tema 7): un umbral para considerar que el frente está bloqueado y otro, algo mayor, para considerarlo libre. Sin eso, un robot que gira hasta pasar justo del umbral lo cruza y lo vuelve a cruzar por el ruido.

Y hay que contar con la latencia: un ultrasonido espera el eco, que a 1 m tarda unos 6 ms, y los sensores suelen leerse de a uno con pausas; entre la lectura, la decisión y la orden a los motores pueden pasar decenas de milisegundos. En ese tiempo el robot sigue avanzando.

03 · ¿A qué distancia reaccionar?

Umbrales y distancia de frenado

El umbral de peligro no es un número arbitrario: debe ser mayor que la distancia que el robot necesita para detenerse. Del tema 17, con velocidad v, latencia total tL y desaceleración a:

dfrenado = v · tL + v² / (2a)    dpeligro ≥ dfrenado + margen

Con v = 20 cm/s, tL = 100 ms y a = 40 cm/s², el robot recorre 2 cm antes de empezar a frenar y 5 cm más frenando: dfrenado = 7 cm. Con 5 cm de margen, el umbral mínimo es de 12 cm. La distancia crece con el cuadrado de la velocidad: a 40 cm/s serían 24 cm de frenado.

La relación inversa también es útil. Si el sensor detecta de forma confiable hasta ddet, la velocidad máxima segura es la que permite frenar dentro de esa distancia:

vmáx = a · ( −tL + √( tL² + 2·(ddet − margen) / a ) )

Con detección confiable hasta 100 cm, 5 cm de margen y los mismos valores, vmáx ≈ 83 cm/s. Nada de esto vale si el robot no puede frenar con la aceleración supuesta (tema 17): el límite lo ponen el agarre y los motores.

En la práctica se usan dos umbrales: una zona de alerta, donde el robot reduce la velocidad en proporción a la distancia, y una de peligro, donde inicia la maniobra de escape. Frenar suavemente en la zona de alerta reduce la distancia de frenado necesaria y evita tirones.

También los umbrales se escalan con la velocidad (si el robot acelera, el umbral de peligro debe crecer) y tienen un piso: nunca menor que la zona ciega del sensor más la distancia de frenado.

04 · El hueco entre sensores

Qué ven los sensores y qué no

Cada sensor tiene un haz: un cono de unos grados de semiapertura (un ultrasónico común, del orden de ±15°). Lo que queda fuera de los conos es invisible. Dos consecuencias:

  • El corredor del robot. Para no rozar lo que está a la distancia d, hay que ver todo el ancho del robot más un margen lateral m: el semiángulo necesario es atan((W/2 + m) / d). Con W = 16 cm, m = 3 cm y d = 25 cm, ±24°: más que el haz de un solo ultrasónico. Un sensor frontal único no basta; se agregan sensores en diagonal (±30° a ±45°).
  • Los obstáculos delgados. Un poste, la pata de una silla o un cable pueden quedar entre los haces, y el robot los embiste sin haberlos «visto» nunca. Es el caso más peligroso porque no se detecta. Se mitiga con haces más anchos, más sensores y un parachoques con interruptores (tema 7) como último recurso.

Además hay materiales que fallan: superficies blandas que absorben el ultrasonido, vidrios que dejan pasar el infrarrojo, superficies oscuras o muy inclinadas. Ningún sensor sirve para todo: combinar un ultrasónico con un infrarrojo, o con un parachoques, cubre más casos.

El sensor se monta a una altura y con una inclinación que eviten ver el suelo (eco falso) y dejen pasar los obstáculos más bajos que el robot no puede franquear. Hay que verificarlo con objetos reales.

05 · ¿Qué hacer?

Estrategias de evasión

Cuatro formas de reaccionar a un obstáculo.
EstrategiaCómo funcionaFortaleza y límite
DetenerseFrena y esperaLa más segura; no avanza
Retroceder y girarMáquina de estados: frenar, (retroceder), girar hasta ver libre, avanzarSimple y robusta; maniobra a saltos; exige tiempos y umbrales bien elegidos
Reacción continuaGirar en proporción a la diferencia de distancias izquierda–derecha, sin detenerse (estilo Braitenberg)Movimiento fluido; difícil en pasillos angostos y frente a una pared de frente
Seguir el contornoRodear el obstáculo manteniendo una distancia a su borde (tema 34)Resuelve laberintos; más compleja

Aquí desarrollamos «retroceder y girar» porque es la base de las demás y la más fácil de diagnosticar. Define estados claros, cada uno con una condición de salida, y se combina bien con el resto del robot.

La reacción continua se resume en un par de líneas: ω = K·(dizq − dder) gira hacia el lado con más espacio. Funciona bien con obstáculos laterales, pero frente a una pared de frente ambas distancias son iguales y no se decide: hace falta igualmente el estado de escape.

06 · El comportamiento

La máquina de estados de escape

Con tres sensores —izquierdo y derecho a ±40°, central al frente— el comportamiento tiene cuatro estados:

  • AVANZAR: avanza con velocidad proporcional a la distancia frontal (más lento cerca de los obstáculos). Si el frente está a menos de D_PELIGRO, o una diagonal a menos de D_LAT (por ejemplo 0,6 veces el umbral), inicia el escape.
  • RETRO: si el obstáculo está muy cerca (menos de D_CRITICA), retrocede un tiempo corto para ganar espacio de giro.
  • GIRAR: gira en el lugar. Termina cuando el frente y las diagonales quedan libres (con histéresis: D_LIBRE = D_PELIGRO + 10 cm) y pasó un tiempo mínimo.
  • DETENIDO: se alcanzó un límite de intentos o de tiempo de giro; el robot se detiene y avisa.

Elegir el sentido del giro es la decisión central. Se gira hacia el lado con más espacio: si la distancia izquierda es mayor que la derecha en más de 5 cm, a la izquierda; si no, a la derecha. Cuando las dos son casi iguales (una pared de frente), se alterna respecto del último giro, para no repetir una maniobra que no funcionó. Sin esa alternancia, un robot que siempre elige el mismo lado puede quedar dando vueltas en una esquina.

Los tiempos son parámetros de diseño: el retroceso (0,4 s), el giro mínimo (0,3 s) y el máximo (4 s, unas 460° a 2 rad/s). Un giro de tiempo fijo es de lazo abierto; con un giroscopio (tema 29) o con odometría (tema 27) se puede girar un ángulo medido, que es más robusto.

07 · Cuando el entorno gana

Recuperación y atascos

Un robot reactivo puede caer en tres situaciones sin salida aparente:

  • Callejón sin salida. Entra por un pasillo y al fondo debe dar la vuelta con poco espacio. Se resuelve con el giro en el lugar si hay espacio; si no, el robot gira buscando una abertura que no existe.
  • Oscilación entre dos obstáculos. Esquiva uno y queda frente a otro, y vuelve. La alternancia del sentido de giro y la memoria de los intentos ayudan.
  • Atasco mecánico. Las ruedas giran pero el robot no avanza (una rueda atrapada, un escalón). Los sensores de distancia no lo ven; se detecta comparando lo que ordenan las ruedas con lo que mide la odometría o un giroscopio (temas 27 y 29).

La respuesta general es un contador de intentos con olvido: cada escape suma un intento; si el robot avanza libre durante unos segundos (por ejemplo 3 s), el contador se reinicia, porque ya no está en una trampa sino en un entorno con obstáculos. Si acumula demasiados intentos seguidos (seis, por ejemplo) sin librarse, se detiene. Mejor un robot detenido y avisando que uno que gira para siempre y gasta batería.

Se puede escalar la respuesta: tras dos intentos, retroceder más; tras cuatro, girar un ángulo mayor o aleatorio; tras seis, parar y pedir ayuda. Y todo lo que implique movimiento debe respetar siempre el comando de parada y los límites de velocidad.

Cuando varios comportamientos compiten por las ruedas (seguir una línea, ir a un punto, evitar obstáculos), el de seguridad debe pasar primero: la evasión tiene prioridad sobre la tarea. Cómo organizar esa jerarquía es el tema de la arquitectura de subsunción (tema 33).

08 · Código para la placa

Evasión en Arduino

Supuestos que debés verificar: UNO R3; tres sensores ultrasónicos tipo HC-SR04 con TRIG y ECHO en A0–A5 como pines digitales (izquierdo en A0/A1, central en A2/A3, derecho en A4/A5), montados a +40°, 0° y −40°; el mismo driver de motores del tema 31 (PWM en D5 y D6, sentido en D7 y D8). Los valores de PWM son un punto de partida.

// Evasión de obstáculos con tres sensores ultrasónicos (tipo HC-SR04) y un robot diferencial.
// Sensores: izquierdo (+40°), central (0°) y derecho (-40°). ¡Probalo primero con las ruedas en el aire!
const byte TRIG[3] = {A0, A2, A4}, ECO[3] = {A1, A3, A5};      // índice 0 = izquierda, 1 = centro, 2 = derecha
const float RANGO = 150.0;                      // cm: distancia que se asume cuando no hay eco
const unsigned long ECO_MAX_US = 12000;         // ~2 m de ida y vuelta; limita el bloqueo de pulseIn()

const byte PIN_PWM_IZQ = 5, PIN_DIR_IZQ = 7, PIN_PWM_DER = 6, PIN_DIR_DER = 8;
const int PWM_BASE = 120, PWM_MIN = 70, PWM_RETRO = 90, PWM_GIRO = 110;
const float D_PELIGRO = 25.0;                   // cm: obstáculo al frente que dispara la evasión
const float D_LAT = 0.6 * D_PELIGRO;            // cm: obstáculo en las diagonales
const float D_CRITICA = 10.0;                   // cm: tan cerca que primero se retrocede
const float D_LIBRE = D_PELIGRO + 10.0;         // cm: histéresis para dar por libre el frente
const unsigned long PERIODO_MS = 50, RETRO_MS = 400, GIRO_MIN_MS = 300, GIRO_MAX_MS = 4000, LIBRE_MS = 3000;
const byte MAX_INTENTOS = 6;

enum Estado { DETENIDO, AVANZAR, RETRO, GIRAR };
Estado estado = DETENIDO;
float hist[3][3] = {{RANGO, RANGO, RANGO}, {RANGO, RANGO, RANGO}, {RANGO, RANGO, RANGO}};
int lado = 1, ultimoGiro = 1;                   // +1 = girar a la izquierda, -1 = a la derecha
byte intentos = 0;
unsigned long ultimo = 0, tEstado = 0, libreDesde = 0;

void mover(int izq, int der) {                  // -255..255
  digitalWrite(PIN_DIR_IZQ, izq >= 0 ? HIGH : LOW);
  digitalWrite(PIN_DIR_DER, der >= 0 ? HIGH : LOW);
  analogWrite(PIN_PWM_IZQ, constrain(abs(izq), 0, 255));
  analogWrite(PIN_PWM_DER, constrain(abs(der), 0, 255));
}

void detener(const char *motivo) { mover(0, 0); estado = DETENIDO; Serial.println(motivo); }

float medirCm(byte i) {
  digitalWrite(TRIG[i], LOW); delayMicroseconds(2);
  digitalWrite(TRIG[i], HIGH); delayMicroseconds(10);
  digitalWrite(TRIG[i], LOW);
  const unsigned long us = pulseIn(ECO[i], HIGH, ECO_MAX_US);
  if (us == 0) return RANGO;                    // sin eco: se asume libre (cuidado con superficies que no reflejan)
  return constrain(us * 0.0343 / 2.0, 2.0, RANGO);
}

float mediana3(float a, float b, float c) {
  return max(min(a, b), min(max(a, b), c));
}

float filtrado(byte i) {                        // mediana de las tres últimas lecturas
  hist[i][0] = hist[i][1]; hist[i][1] = hist[i][2]; hist[i][2] = medirCm(i);
  return mediana3(hist[i][0], hist[i][1], hist[i][2]);
}

void setup() {
  const byte motores[] = {PIN_PWM_IZQ, PIN_DIR_IZQ, PIN_PWM_DER, PIN_DIR_DER};
  for (byte i = 0; i < 4; i++) { digitalWrite(motores[i], LOW); pinMode(motores[i], OUTPUT); }
  for (byte i = 0; i < 3; i++) { pinMode(TRIG[i], OUTPUT); pinMode(ECO[i], INPUT); }
  Serial.begin(115200);
  Serial.println("G: avanzar evitando obstaculos; S: detener");
}

void loop() {
  if (Serial.available() > 0) {
    const char c = toupper(Serial.read());
    if (c == 'S') detener("Detenido por comando");
    else if (c == 'G' && estado == DETENIDO) { estado = AVANZAR; intentos = 0; libreDesde = millis(); Serial.println("Avanzando"); }
  }
  const unsigned long ahora = millis();
  if (ahora - ultimo < PERIODO_MS) return;
  ultimo = ahora;
  if (estado == DETENIDO) return;

  const float dIzq = filtrado(0), dCen = filtrado(1), dDer = filtrado(2);   // hasta ~36 ms de bloqueo

  if (estado == AVANZAR) {
    if (dCen < D_PELIGRO || dIzq < D_LAT || dDer < D_LAT) {
      intentos++;
      libreDesde = ahora;
      lado = (fabs(dIzq - dDer) > 5.0) ? ((dIzq > dDer) ? 1 : -1) : -ultimoGiro;   // hacia el lado con más espacio
      ultimoGiro = lado;
      estado = (dCen < D_CRITICA) ? RETRO : GIRAR;
      tEstado = ahora;
      Serial.print("Obstaculo: giro a la "); Serial.println(lado > 0 ? "izquierda" : "derecha");
    } else {
      if (ahora - libreDesde > LIBRE_MS) intentos = 0;                          // terreno libre: se olvidan los intentos
      const int pwm = constrain(map((long)dCen, (long)D_PELIGRO, (long)(D_PELIGRO + 40), PWM_MIN, PWM_BASE), PWM_MIN, PWM_BASE);
      mover(pwm, pwm);                                                         // más lento cerca de los obstáculos
    }
  } else if (estado == RETRO) {
    mover(-PWM_RETRO, -PWM_RETRO);
    if (ahora - tEstado > RETRO_MS) { estado = GIRAR; tEstado = ahora; }
  } else if (estado == GIRAR) {
    mover(-lado * PWM_GIRO, lado * PWM_GIRO);                                   // giro en el lugar
    if (ahora - tEstado > GIRO_MIN_MS && dCen > D_LIBRE && dIzq > D_LAT + 5 && dDer > D_LAT + 5) {
      estado = AVANZAR; libreDesde = ahora;
    } else if (ahora - tEstado > GIRO_MAX_MS) {
      detener("Sin salida: detenido");
    }
  }
  if (intentos >= MAX_INTENTOS) detener("Demasiados intentos seguidos: detenido");
}

Cómo funciona: con G avanza; cada 50 ms lee los tres sensores en secuencia (para que no se interfieran), filtra con mediana de tres y ejecuta la máquina de estados. Imprime cada maniobra («Obstaculo: giro a la izquierda»), y se detiene con S, tras seis intentos seguidos o si un giro dura más de 4 s.

Dos limitaciones de este sketch, a propósito: pulseIn() bloquea hasta 12 ms por sensor (hasta unos 36 ms por ciclo), tolerable a 20 cm/s pero a considerar a mayor velocidad (una alternativa es medir con interrupciones o con sensores de interfaz serie); y el giro es de tiempo, no de ángulo. Probalo primero con las ruedas en el aire, acercando la mano a cada sensor, y después en un recinto libre con un obstáculo grande, con la mano cerca del interruptor.

Un detalle de seguridad: nunca hay que depender solo de los sensores de distancia. Un parachoques con interruptores que detenga los motores ante un choque cubre el poste delgado, el vidrio y la falla de un sensor (tema 7), y es parte de un diseño responsable.

09 · Exploración interactiva

Laboratorios: umbrales y evasión

1. Cuánto antes hay que reaccionar

Elegí la velocidad, la latencia total, la desaceleración y los márgenes. El laboratorio calcula la distancia de frenado, el umbral de peligro mínimo, la velocidad máxima compatible con el alcance del sensor y el semiángulo que hay que cubrir para ver el ancho del robot.

Distancia de frenado—
Umbral de peligro mínimo—
Velocidad máxima segura—
Semiángulo a cubrir (margen lateral 3 cm)—

Elegí los parámetros.

Con los valores iniciales: 7 cm de frenado, 12 cm de umbral mínimo, 83 cm/s de velocidad máxima segura y ±43° a cubrir (porque el umbral es corto). Subí la velocidad a 40 cm/s: el frenado pasa a 24 cm. Subí la latencia a 300 ms a 20 cm/s: 6 cm solo en reaccionar. Con 90 cm/s y alcance de 100 cm, la velocidad deja de ser segura (el máximo es de 83 cm/s).

2. Un robot que esquiva

Vista desde arriba de una sala de 2 × 1,5 m. El robot (círculo de 8 cm de radio) tiene tres sensores a +40°, 0° y −40° y ejecuta la máquina de estados del texto durante 40 s. La línea lima es su recorrido; el círculo se pone rojo si choca.

Recorrido del robot en la salaVista superior de una sala rectangular con obstáculos, el recorrido del robot en lima y su posición final como un círculo.
Estado—
Colisiones—
Maniobras de evasión—
Recorrido—

Elegí el escenario.

Con el obstáculo único, el robot hace 9 evasiones en 40 s y recorre 497 cm sin chocar. Con el poste delgado y haz de ±0°, choca sin detectarlo nunca (cero maniobras); con ±15° lo esquiva. Con el umbral de peligro en 5, 10 o 15 cm choca contra el obstáculo; con 25 no. En el callejón sin salida con un umbral de 15 cm se queda sin salida y se detiene a los 24 s tras seis intentos. Con ruido de ±6 cm el filtro de mediana lo sostiene; con ±10 cm acumula demasiadas maniobras y se detiene a los 34 s.

10 · Código ejecutable

Simular la evasión en Python

La simulación del laboratorio, en Python: lanzamiento de rayos contra las paredes y los obstáculos, haz en cono, filtro de mediana, la máquina de estados completa y el recuento de colisiones. Sirve para ajustar los umbrales y los tiempos antes de probar con el 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

DT, R_ROBOT, RANGO, SALA = 0.05, 8.0, 150.0, (200.0, 150.0)
V_BASE, V_MIN, V_RETRO, W_GIRO = 20.0, 8.0, -10.0, 2.0
ESCENARIOS = {
    "obstaculo": ([(90, 50, 110, 100)], (20, 75, 0.0)),
    "poste": ([(110, 78, 113, 81)], (20, 75, 0.0)),
    "pasillo": ([(60, 0, 80, 100), (120, 50, 140, 150)], (20, 75, 0.0)),
    "callejon": ([(100, 40, 180, 50), (100, 100, 180, 110), (170, 40, 180, 110)], (20, 75, 0.0)),
}

def generador(semilla):
    a = [semilla & 0xFFFFFFFF]
    def siguiente():
        a[0] = (a[0] + 0x6D2B79F5) & 0xFFFFFFFF
        t = a[0]
        t = ((t ^ (t >> 15)) * (t | 1)) & 0xFFFFFFFF
        t ^= (t + (((t ^ (t >> 7)) * (t | 61)) & 0xFFFFFFFF)) & 0xFFFFFFFF
        return ((t ^ (t >> 14)) & 0xFFFFFFFF) / 4294967296
    return siguiente

def rayo(x, y, ang, obst):
    dx, dy = math.cos(ang), math.sin(ang)
    mejor = RANGO
    for p, d, lim in ((x, dx, SALA[0]), (y, dy, SALA[1])):
        if d > 1e-12: mejor = min(mejor, (lim - p) / d)
        elif d < -1e-12: mejor = min(mejor, (0 - p) / d)
    for x1, y1, x2, y2 in obst:
        t0, t1 = -1e18, 1e18
        ok = True
        for p, d, lo, hi in ((x, dx, x1, x2), (y, dy, y1, y2)):
            if abs(d) < 1e-12:
                if p < lo or p > hi: ok = False
            else:
                a, b = (lo - p) / d, (hi - p) / d
                t0, t1 = max(t0, min(a, b)), min(t1, max(a, b))
        if ok and t1 >= max(t0, 0.0) and t0 > 0:
            mejor = min(mejor, t0)
    return mejor

def medir(x, y, th, rel, semi_haz, obst):
    angulos = [th + rel + k * math.radians(semi_haz) for k in ((-1, 0, 1) if semi_haz > 0 else (0,))]
    return min(rayo(x, y, a, obst) for a in angulos)

def colision(x, y, obst):
    if x - R_ROBOT < 0 or y - R_ROBOT < 0 or x + R_ROBOT > SALA[0] or y + R_ROBOT > SALA[1]:
        return True
    for x1, y1, x2, y2 in obst:
        cx, cy = min(max(x, x1), x2), min(max(y, y1), y2)
        if math.hypot(x - cx, y - cy) < R_ROBOT:
            return True
    return False

def mediana(v):
    return sorted(v)[1]

def simular(escenario, d_peligro=25.0, semi_haz=15.0, ruido=0.0, duracion=40.0, semilla=3):
    obst, (x, y, th) = ESCENARIOS[escenario]
    azar = generador(semilla)
    d_lat, d_critica, d_libre = 0.6 * d_peligro, 10.0, d_peligro + 10.0
    historial = [[RANGO] * 3 for _ in range(3)]
    estado, t_estado, ultimo_giro, lado = "AVANZAR", 0.0, 1, 1
    intentos, libre_desde, colisiones, escapes, largo, enchoque = 0, 0.0, 0, 0, 0.0, False
    t, recorrido = 0.0, [(x, y)]
    while t < duracion and estado != "DETENIDO":
        crudo = [medir(x, y, th, rel, semi_haz, obst) for rel in (math.radians(40), 0.0, -math.radians(40))]
        lect = []
        for i, c in enumerate(crudo):
            c = min(RANGO, max(2.0, c + ruido * (2 * azar() - 1)))
            historial[i] = historial[i][1:] + [c]
            lect.append(mediana(historial[i]))
        d_izq, d_cen, d_der = lect
        v = w = 0.0
        if estado == "AVANZAR":
            v = max(V_MIN, min(V_BASE, V_MIN + (V_BASE - V_MIN) * (d_cen - d_peligro) / 40.0))
            if d_cen < d_peligro or d_izq < d_lat or d_der < d_lat:
                intentos += 1; escapes += 1; libre_desde = t
                lado = (1 if d_izq > d_der else -1) if abs(d_izq - d_der) > 5 else -ultimo_giro
                ultimo_giro = lado
                estado = "RETRO" if d_cen < d_critica else "GIRAR"
                t_estado = t
            elif t - libre_desde > 3.0:
                intentos = 0
        elif estado == "RETRO":
            v = V_RETRO
            if t - t_estado > 0.4:
                estado, t_estado = "GIRAR", t
        elif estado == "GIRAR":
            w = lado * W_GIRO
            if t - t_estado > 0.3 and d_cen > d_libre and d_izq > d_lat + 5 and d_der > d_lat + 5:
                estado, libre_desde = "AVANZAR", t
            elif t - t_estado > 4.0:
                estado = "DETENIDO"
        if intentos >= 6:
            estado = "DETENIDO"
        nx, ny = x + v * math.cos(th) * DT, y + v * math.sin(th) * DT
        if colision(nx, ny, obst):
            if not enchoque:
                colisiones += 1
            enchoque = True
        else:
            enchoque = False
            largo += abs(v) * DT
            x, y = nx, ny
        th += w * DT
        t += DT
        recorrido.append((x, y))
    return {"estado": estado, "t": t, "colisiones": colisiones, "escapes": escapes, "largo": largo, "recorrido": recorrido}

# Distancia de frenado y umbral recomendado
v, latencia, a, margen = 20.0, 0.10, 40.0, 5.0
parada = v * latencia + v * v / (2 * a)
print(f"Distancia de frenado a {v:.0f} cm/s: {parada:.1f} cm -> umbral de peligro recomendado {parada + margen:.1f} cm")
d_det = 100.0
v_max = a * (-latencia + math.sqrt(latencia ** 2 + 2 * (d_det - margen) / a))
print(f"Velocidad maxima para frenar dentro de {d_det:.0f} cm de deteccion: {v_max:.1f} cm/s")

# Comportamiento en cuatro escenarios, con haz ideal y con haz de 15 grados
for esc in ("obstaculo", "poste", "pasillo", "callejon"):
    for haz in (0.0, 15.0):
        r = simular(esc, semi_haz=haz)
        print(f"{esc:10s} haz={haz:4.1f}: {r['estado']:9s} colisiones={r['colisiones']} escapes={r['escapes']:2d} recorrido={r['largo']:6.1f} cm")

# Umbral de peligro demasiado chico y ruido del sensor
for dp in (5, 10, 15, 25):
    r = simular("obstaculo", d_peligro=dp)
    print(f"umbral {dp:2d} cm: colisiones={r['colisiones']} escapes={r['escapes']:2d}")
for ru in (0, 6, 10):
    r = simular("obstaculo", ruido=ru)
    print(f"ruido +-{ru} cm: {r['estado']} a los {r['t']:.1f} s, escapes={r['escapes']}")

La distancia de frenado a 20 cm/s es de 7 cm (12 cm con el margen) y la velocidad máxima segura con 100 cm de alcance, 83 cm/s. En el obstáculo único: 9 evasiones y 497 cm con haz de ±15°. El poste delgado: con haz ideal, una colisión y cero maniobras; con ±15°, cero colisiones y 9 maniobras. Con umbrales de 5, 10 y 15 cm hay una colisión; con 25, ninguna. Con ruido de ±10 cm se detiene a los 33,8 s.

Probá d_peligro=40 en el callejón, semi_haz=30 en el pasillo, y agregá un cuarto escenario con un obstáculo móvil. Un buen ejercicio: cambiá la regla de elección del sentido de giro para que sea siempre a la izquierda y compará.

11 · Método y diagnóstico

Pruebas y diagnóstico

  1. Con las ruedas en el aire, acercá un objeto a cada sensor y verificá que reacciona el correcto, en el sentido correcto.
  2. Medí cada sensor contra una regla (30, 60 y 100 cm) y registrá el error, el ruido y la distancia mínima fiable.
  3. Medí la distancia real de frenado a tu velocidad de trabajo y compará con la calculada.
  4. Probá un obstáculo grande de frente, en diagonal y de costado, a distintas velocidades.
  5. Probá obstáculos difíciles: un poste delgado, un vidrio, una tela, una superficie inclinada.
  6. Probá una esquina y un callejón sin salida; verificá que se detiene o escapa y que no gira sin fin.
  7. Probá el comando de parada y el parachoques en plena maniobra.
«Con un sensor al frente alcanza»

Solo si el obstáculo cae dentro de su haz. Un obstáculo en diagonal o un poste fuera del eje se choca sin haberlo detectado. Hacen falta sensores laterales o un parachoques.

«Si no hay eco, no hay obstáculo»

Puede ser un vidrio, una tela o una superficie muy inclinada. «Sin eco» es información incompleta, no una garantía de libre.

«Promedio las lecturas para filtrarlas»

Un valor espurio contamina el promedio y da una distancia que no es real. La mediana descarta el valor aislado.

«El umbral de peligro se elige a ojo»

Debe ser mayor que la distancia de frenado más un margen. A mayor velocidad, mayor umbral.

«El robot gira hacia el lado de siempre y no sale de la esquina»

Falta la alternancia: si las distancias son parecidas, el sentido debe cambiar respecto del último giro.

«Gira mientras haya obstáculo, no hace falta límite de tiempo»

En un callejón o con un sensor roto gira para siempre. Todo giro y todo conjunto de intentos necesitan un tope y una acción final (detenerse).

12 · Práctica guiada

Predecí antes de comprobar

1. Distancia de frenado

Un robot va a 30 cm/s con 150 ms de latencia total y frena a 60 cm/s². ¿Cuánto vale la distancia de frenado y qué umbral mínimo conviene con 5 cm de margen?

Ver solución

30·0,15 = 4,5 cm de reacción y 30²/(2·60) = 7,5 cm frenando: 12 cm. Umbral mínimo: 12 + 5 = 17 cm.

2. Velocidad máxima

El sensor detecta de forma confiable hasta 80 cm; latencia 100 ms, desaceleración 40 cm/s², margen 5 cm. ¿Qué velocidad máxima permite frenar a tiempo?

Ver solución

v = 40·(−0,1 + √(0,01 + 2·75/40)) = 40·(−0,1 + √3,76) ≈ 40·1,839 ≈ 73,6 cm/s. Con estos números la distancia de frenado coincide con los 75 cm disponibles.

3. Cobertura

El robot mide 20 cm de ancho y se pide un margen lateral de 3 cm. ¿Qué semiángulo hay que cubrir para un obstáculo a 30 cm?

Ver solución

atan((10 + 3)/30) = atan(0,433) ≈ 23,4°. Un único ultrasónico de ±15° no alcanza: hay que sumar sensores en diagonal.

4. Mediana

Las tres últimas lecturas son 30, 150 y 31 cm. ¿Cuál es la mediana? ¿Y el promedio? ¿Cuál conviene y por qué?

Ver solución

Mediana: 31 cm. Promedio: 70,3 cm. Conviene la mediana: el 150 es un valor espurio (sin eco o rebote) y el promedio lo propaga como una distancia inexistente.

5. Sentido de giro

Las distancias son 40 cm a la izquierda y 25 cm a la derecha. ¿Hacia dónde gira? ¿Y con 40 y 37 cm si el último giro fue a la izquierda?

Ver solución

Diferencia 15 cm, mayor que 5: gira a la izquierda (más espacio). Con 40 y 37 la diferencia (3 cm) es menor que 5: se alterna respecto del último giro y gira a la derecha.

6. Olvido de intentos

¿Por qué el contador de intentos se reinicia tras 3 s de avance libre en vez de contar todos los escapes de la misión?

Ver solución

En un entorno con muchos obstáculos el robot hace muchos escapes legítimos. Lo que indica una trampa es una racha de intentos sin librarse. Si pasó un tiempo avanzando libre, la situación se resolvió y el contador debe volver a cero; si no, un robot sano terminaría deteniéndose por acumulación.

7. Histéresis

Con D_PELIGRO = 25 cm y D_LIBRE = 35 cm, ¿qué problema evita ese margen de 10 cm?

Ver solución

Si ambos umbrales fueran 25 cm, el robot que gira hasta que la distancia frontal supere 25 cm pasaría a avanzar justo en el borde y, con una variación mínima o ruido, volvería a considerarlo bloqueado: alternaría entre avanzar y girar. Con 35 cm sale con un margen real.

13 · Síntesis

Ideas para recordar

  • La evasión reactiva es el lazo sentir–decidir–actuar: simple y robusto, pero sin memoria del mapa; puede atraparse.
  • Una lectura de distancia no es verdad: puede faltar el eco, estar en la zona ciega o ser espuria. La mediana de tres y la histéresis la hacen utilizable.
  • El umbral de peligro sale de la distancia de frenado: v·tL + v²/(2a) más un margen; crece con el cuadrado de la velocidad.
  • La cobertura importa: hay que ver todo el ancho del robot a la distancia de peligro, y los obstáculos delgados pueden quedar entre los haces.
  • La máquina de estados AVANZAR–RETRO–GIRAR–DETENIDO se elige girar hacia el lado con más espacio y alternar cuando hay empate.
  • Hace falta un contador de intentos con olvido y un tope de tiempo en cada giro para no quedar atrapado.
  • Los sensores de distancia no bastan: un parachoques y un comando de parada completan la seguridad.
  • La evasión tiene prioridad sobre cualquier otra tarea del robot.

¿Listo para el tema 33?

Antes de avanzar, calculá el umbral de peligro de tu robot a su velocidad de trabajo y comprobá qué obstáculos quedarían entre tus sensores. Pensá cómo harías para que ese comportamiento conviva con un seguidor de línea: ¿quién manda cuando los dos piden cosas distintas?

En el próximo tema estudiamos los comportamientos reactivos y la arquitectura de subsunción: prioridades entre comportamientos simples que, combinados, forman uno complejo.