PTENES
MÓDULO 5.6 · PROYECTO

🤝 Proyecto final: robot asistente

El proyecto que cierra el curso: pides en voz alta «trae el cubo», el robot entiende, prepara un plan con acciones permitidas, lo verifica con la cámara, lo agarra con el brazo y lo lleva a la base. Cada pieza viene de un módulo anterior; aquí las conectas con bloqueos de seguridad y mides si funciona de verdad.

6
Temas
Avanzado
Nivel
Proyecto
Tipo
SO-101
+ base opcional
0%
0 de 0 temas leídos · Sección 1 de 6

Contenido detallado

1

🎯 El robot asistente: alcance y reglas

Un buen proyecto tiene un alcance pequeño y bien medido. Antes de empezar, define por escrito: qué pedidos atiende el robot (por ejemplo, «trae el cubo», «trae la pelota», «detente»), dónde (mesa y sala), y qué cuenta como éxito (el objeto se entrega en la mano en un minuto como máximo). Para todo lo demás, el robot responde: «No sé hacer eso».

VOZWhisper (5.2) PLANLLM local (5.1) VALIDADORlista cerrada CÁMARAverifica BRAZO SO-101políticas (5.4) BASE MÓVILESP32 (T3/T4) PARADA DE EMERGENCIA: corta la energía, fuera del software

Cómo leer: la línea superior piensa; la del medio actúa. El validador (cian) es la puerta entre ambas: nada del modelo de lenguaje llega al motor sin pasar por él. La franja roja no tiene flecha porque no depende de ninguna caja: es un botón que corta la energía de los motores incluso si la computadora se bloquea.

✓ Alcance que se puede completar

  • ✓Versión mínima: solo el brazo sobre la mesa, 2 objetos y comandos por voz.
  • ✓Versión completa: + una base que lleva el objeto hasta la sala.
  • ✓Lista escrita de pedidos aceptados y criterios de éxito.

✗ Alcance imposible de completar

  • ✗«Un asistente que hace cualquier cosa en la casa».
  • ✗El modelo de lenguaje envía ángulos al servo.
  • ✗Empezar por la base antes de que el brazo funcione por sí solo.

Por qué aprender

La integración es donde los proyectos fracasan: cada parte funciona por separado, pero el conjunto no. Un alcance acotado, una arquitectura por capas y un bloqueo fuera del software te permiten llegar al final con algo que funciona frente a otras personas.

Conceptos clave

Alcance
pedidos aceptados, por escrito
Capas
pensar por separado de actuar
Validador
puerta hacia el motor
Parada física
no depende del código
2

🗣️ Voz → plan (LLM) → acciones

El corazón del proyecto es un programa corto: el orquestador. Recibe el texto que Whisper escuchó (5.2), le pide al modelo local un plan en JSON usando solo la lista cerrada de acciones (5.1), valida el plan, te pide confirmación y lo ejecuta paso a paso. Si algún paso falla, envía la orden de detenerse. El modo --simular cambia el modelo y el hardware por versiones simuladas para que pruebes la lógica y los bloqueos en la computadora.

🌱 ¿Eres nuevo aquí?

Un orquestador es el programa que no hace el trabajo pesado, sino que llama a cada parte en el orden correcto y decide qué hacer cuando algo sale mal. Lista cerrada significa que el modelo solo puede elegir entre las acciones que escribiste; cualquier acción inventada se rechaza antes de llegar al robot.

Objetivo: ejecutar el orquestador completo en modo simulado y ver los tres resultados posibles: plan ejecutado, plan rechazado y parada. Python 3 puro; el modo real también usa LeRobot (5.4), pyserial (3.5) y Ollama (5.1).

assistente.py
# Robot asistente: texto (de la voz) -> plan en JSON (LLM) -> validación -> ejecución.
# Modo --simular: sin LLM ni hardware, para probar la lógica y los bloqueos.
import argparse, json, subprocess, sys, time, urllib.request

OBJETOS = {"cubo": "outputs/train/act-cubo/checkpoints/last/pretrained_model",
           "bola": "outputs/train/act-bola/checkpoints/last/pretrained_model"}
SOLTAR = "outputs/train/act-soltar/checkpoints/last/pretrained_model"
LUGARES = ["mesa", "sala", "base"]
ACOES = {  # lista cerrada: nombre -> argumentos permitidos
    "ir_para": {"lugar": LUGARES},
    "pegar": {"objeto": list(OBJETOS)},
    "entregar": {},
    "falar": {"texto": None},
    "parar": {},
}
MAX_PASSOS = 6

PROMPT = ("Controlas un robot con brazo y base móvil. Responde SOLO con JSON en el formato "
          ' {"passos": [{"acao": "...", ...}]}. Acciones permitidas: ' + json.dumps(ACOES, ensure_ascii=False) +
          ". Si la solicitud no se puede resolver con estas acciones, responde con una sola acción hablar explicando.")

def planejar_simulado(texto):
    t = texto.lower()
    if "pare" in t or "parar" in t:
        return {"passos": [{"acao": "parar"}]}
    for obj in OBJETOS:
        if obj in t:
            return {"passos": [{"acao": "ir_para", "lugar": "mesa"}, {"acao": "pegar", "objeto": obj},
                               {"acao": "ir_para", "lugar": "sala"}, {"acao": "entregar"},
                               {"acao": "falar", "texto": f"Listo, traje: {obj}."}]}
    if "janela" in t:  # simula un LLM que inventa una acción fuera de la lista
        return {"passos": [{"acao": "abrir_janela"}]}
    return {"passos": [{"acao": "falar", "texto": "No sé hacer eso."}]}

def planejar_ollama(texto, modelo):
    corpo = json.dumps({"model": modelo, "stream": False, "format": "json", "options": {"temperature": 0},
                        "messages": [{"role": "system", "content": PROMPT}, {"role": "user", "content": texto}]})
    req = urllib.request.Request("http://localhost:11434/api/chat", corpo.encode(), {"Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=60) as r:
        return json.loads(json.loads(r.read())["message"]["content"])

def validar(plano):
    passos = plano.get("passos") if isinstance(plano, dict) else None
    if not isinstance(passos, list) or not 1 <= len(passos) <= MAX_PASSOS:
        return None, "plan vacío o demasiado largo"
    for p in passos:
        regra = ACOES.get(p.get("acao"))
        if regra is None:
            return None, f"acción fuera de la lista: {p.get('acao')}"
        for arg, permitidos in regra.items():
            if arg not in p:
                return None, f"falta '{arg}' en {p['acao']}"
            if permitidos is not None and p[arg] not in permitidos:
                return None, f"valor no permitido: {arg}={p[arg]}"
    return passos, "ok"

def executar(passo, simular, porta_base):
    a = passo["acao"]
    if simular:
        print("  [SIM]", json.dumps(passo, ensure_ascii=False)); time.sleep(0.2); return True
    if a in ("pegar", "entregar"):  # brazo: políticas entrenadas en el 5.4
        politica = OBJETOS[passo["objeto"]] if a == "pegar" else SOLTAR
        tarefa = f"Tomar el {passo['objeto']}." if a == "pegar" else "Soltar el objeto en la mano de la persona."
        cmd = ["lerobot-rollout", "--strategy.type=base", f"--policy.path={politica}",
               "--robot.type=so101_follower", "--robot.port=/dev/ttyACM0", "--robot.id=seguidor",
               f"--task={tarefa}", "--duration=20"]
        return subprocess.run(cmd).returncode == 0
    if a in ("ir_para", "parar"):  # base móvil: ESP32 de la Trilha 3 por serial
        import serial
        with serial.Serial(porta_base, 115200, timeout=30) as s:
            s.write((f"{a.upper()} {passo.get('lugar', '')}".strip() + "\n").encode())
            return s.readline().decode().strip() == "OK"
    if a == "falar":
        print("  ROBÔ:", passo["texto"]); return True  # cambia por el Piper del 5.2
    return False

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--texto", required=True, help="solicitud (viene de Whisper en el 5.2)")
    ap.add_argument("--simular", action="store_true")
    ap.add_argument("--modelo", default="<seu_modelo_ollama>")
    ap.add_argument("--porta-base", default="/dev/ttyUSB0")
    args = ap.parse_args()
    print("PEDIDO:", args.texto)
    plano = planejar_simulado(args.texto) if args.simular else planejar_ollama(args.texto, args.modelo)
    passos, motivo = validar(plano)
    if passos is None:
        print("RECUSADO:", motivo); sys.exit(2)
    print(f"PLAN ({len(passos)} pasos):", [p["acao"] for p in passos])
    if not args.simular and input("¿Confirmar ejecución? (s/n) ").strip().lower() != "s":
        print("CANCELADO por el operador"); sys.exit(0)
    for i, p in enumerate(passos, 1):
        print(f"paso {i}/{len(passos)}: {p['acao']}")
        if not executar(p, args.simular, args.porta_base):
            print("FALLÓ en el paso", i, "- parando"); executar({"acao": "parar"}, args.simular, args.porta_base); sys.exit(1)
    print("COMPLETADO")

if __name__ == "__main__":
    main()

Cómo verificar

  • ✓python3 assistente.py --simular --texto "trae el cubo a la sala" muestra PLAN (5 pasos), cinco líneas [SIM] e COMPLETADO.
  • ✓Con --texto "abre la ventana" sale RECHAZADO: acción fuera de la lista: abrir_janela y no se ejecuta nada.
  • ✓Con --texto "detente ahora" el plan tiene un solo paso: detenerse.
  • ✗En el modo real, cambia <seu_modelo_ollama> por el modelo que descargaste en el 5.1 y prueba primero solo con hablar, sin el brazo conectado.

Por qué aprender

Es el estándar que usa la industria para poner modelos de lenguaje en máquinas físicas: el modelo sugiere, el código verifica, la persona confirma y el hardware tiene sus propios límites. Así, cuando un modelo inventa una acción, el resultado es una línea de "RECHAZADO", no un brazo que golpea la ventana.

Conceptos clave

Orquestador
llama a las partes
Plan JSON
pasos con argumentos
Confirmación
la persona aprueba
Si falla, se detiene
nunca continúa a ciegas
3

👁️ Ver y tomar un objeto (visión + brazo)

Tomar tiene tres momentos: verificar antes (¿el objeto está realmente sobre la mesa?), tomar (la política del 5.4 realiza el movimiento) y verificar después (¿el objeto salió de la mesa?). Las verificaciones usan la cámara: el detector YOLO26 de la Trilha 3 o la pregunta «¿qué ves?» al VLM del 5.1. Sin ellas, el robot «entrega» la mano vacía y dice que salió bien.

está en lamesa? TOMARpolítica 5.4 salió de lamesa? síhecho avisa y se detiene sí: sigue el plan nono: intenta 1 vez

Cómo leer: los rombos cian son preguntas a la cámara; el rectángulo verde es el único paso en el que se mueve el brazo. Las flechas rojas muestran los caminos de falla: cada uno tiene un final definido (avisar y detenerse, o hacer un solo intento más), nunca un ciclo infinito.

Paso a paso para preparar el brazo

1

Una política por tarea (ruta ACT)

Graba y entrena «tomar el cubo», «tomar la pelota» y «soltar en la mano» como en el 5.4. Son las tres rutas de OBJETOS e SOLTAR en el orquestador.

2

O una política con texto (ruta SmolVLA)

Con suficiente GPU, un SmolVLA ajustado recibe la solicitud en --task; las tres rutas del diccionario pasan a ser la misma.

3

Comprueba primero en el gemelo y luego avanza despacio en el robot real

Prueba el alcance y la posición de la cámara en el gemelo (5.5) y realiza 20 intentos de cada política en el robot real antes de conectarlo al orquestador.

💡 Entregar en la mano requiere cuidado adicional

Soltar en la mano de una persona es la parte más delicada: el brazo se mueve cerca de los dedos. En la demostración, prefiere «soltar en una bandeja junto a la persona». Si lo haces en la mano, usa solo objetos ligeros y blandos, y avísale a la persona dónde se detendrá el brazo.

Por qué aprender

Verificar con la cámara convierte a un robot que «cree» que lo hizo en uno que sabe si lo hizo. Este hábito de comprobar el resultado de la propia acción es el lazo cerrado del 1.1, ahora con visión e IA.

Conceptos clave

Precondición
verificar antes
Poscondición
verificar después
Un intento más
nunca un ciclo infinito
ACT o VLA
según la GPU
4

🚗 Base móvil + brazo: integración

En la versión completa, el brazo va sobre el rover de las trilhas anteriores. La computadora (Pi 5, Jetson o notebook) ejecuta el orquestador y se comunica con el brazo por USB y con el ESP32 de la base por serial, con el mismo protocolo de líneas de texto del módulo 3.5. Si hiciste la Trilha 4, puedes cambiar «ir_para sala» por un punto de Nav2; el orquestador no cambia.

COMPUTADORAassistente.py SO-101placa de los servos ESP32base (T3/T4) puente H + motores fuente del brazo EMERGENCIAcorta la alimentación de ambas USBserial

Cómo leer: las líneas cian son datos (USB y serial); las grises y verdes son alimentación y control del motor. La caja roja está en el centro a propósito: un solo botón corta la alimentación del brazo y la de los motores de la base. Las baterías separadas evitan que el pico del motor reinicie la placa de los servos.

Objetivo: el lado de la base que responde al orquestador: recibe IR_PARA <lugar> o PARAR, responde OK o ERROR, y se detiene sola si el recorrido supera el tiempo límite. El recorrido está simulado aquí; cambia chegou() e pararMotores() por el código de tu rover.

base-assistente.ino
// Base del robot asistente (ESP32, paquete esp32 3.x): protocolo de líneas con el orquestador.
const unsigned long LIMITE_MS = 15000;   // ningún recorrido supera este tiempo: se detiene solo

void pararMotores() { /* llama aquí a la parada de tu rover (velocidad 0 en las dos ruedas) */ }
void andarRumo(const String &lugar) { /* ruta fija, seguidor de línea o punto de Nav2 */ }
bool chegou(unsigned long t0) { return millis() - t0 > 2000; }  // simulación: 2 s de recorrido

bool irPara(const String &lugar) {
  if (lugar != "mesa" && lugar != "sala" && lugar != "base") return false;
  unsigned long t0 = millis();
  andarRumo(lugar);
  while (!chegou(t0)) {
    if (Serial.available() && Serial.readStringUntil('\n').startsWith("PARAR")) { pararMotores(); return false; }
    if (millis() - t0 > LIMITE_MS) { pararMotores(); return false; }
    delay(10);
  }
  pararMotores();
  return true;
}

void setup() { Serial.begin(115200); pararMotores(); Serial.println("BASE LISTA"); }

void loop() {
  if (!Serial.available()) return;
  String linha = Serial.readStringUntil('\n');
  linha.trim();
  if (linha.startsWith("IR_PARA ")) Serial.println(irPara(linha.substring(8)) ? "OK" : "ERRO");
  else if (linha == "PARAR") { pararMotores(); Serial.println("OK"); }
  else Serial.println("ERROR comando desconocido");
}

Cómo verificar

  • ✓En el Monitor Serial (115200, «Nueva línea»), escribe IR_PARA sala: después de 2 segundos aparece OK. También funciona en Wokwi.
  • ✓IR_PARA cocina responde ERROR: lugar fuera de la lista; la base ni siquiera arranca.
  • ✓Conectado a la computadora, assistente.py sin --simular recibe el OK y sigue con el siguiente paso.

✓ Reglas de integración

  • ✓El brazo solo se mueve con la base detenida, y viceversa.
  • ✓Brazo replegado (posición de reposo) antes de que la base avance.
  • ✓Velocidad baja de la base con el brazo encima.

✗ Problemas comunes

  • ✗Brazo demasiado alto: el robot se vuelca al frenar.
  • ✗La misma batería para motores y servos: la placa se reinicia.
  • ✗Política entrenada con el brazo sobre la mesa y usada en la base: la cámara ve otra escena.

Por qué aprender

Los robots reales de almacén y del hogar son exactamente esto: una base que navega y un brazo que manipula, coordinados por un programa que evita que ambos se muevan al mismo tiempo por accidente.

Conceptos clave

Manipulador móvil
base + brazo
Protocolo
comando → OK/ERROR
Límite de tiempo
se detiene solo
Uno a la vez
brazo o base
5

📊 Pruebas y métricas de éxito

En un robot con varias etapas, las tasas de éxito se multiplican. Si la voz acierta 9 de cada 10 veces, el plan 9 de cada 10, el agarre 8 de cada 10 y la entrega 9 de cada 10, el conjunto solo acierta unas 6 de cada 10 veces. Por eso mides cada etapa por separado y descubres cuál hace caer el resultado total.

20pedidos 18escuchó 16plan correcto 13agarró 12entregó

Cómo leer: cada barra indica cuántos de los 20 pedidos llegaron a esa etapa (números de ejemplo). Busca la mayor caída entre dos barras vecinas: aquí es el agarre, señalado con un contorno rojo (de 16 a 13). Es ahí donde conviene grabar más episodios, no en el modelo de lenguaje.

Batería de pruebasCuántosQué se espera
Pedidos del alcance, expresados de distintas maneras10plan correcto y objeto entregado
Pedidos fuera del alcance ("abre la ventana")5rechazo amable, nada se mueve
Pedidos peligrosos ("lánzame el cubo")3rechazo; no existe ninguna acción de "lanzar"
"Detente" en medio de una tarea2todo se detiene en menos de 1 segundo

🌱 ¿Eres nuevo aquí?

Una batería de pruebas es una lista fija de casos que ejecutas de la misma manera con cada versión del proyecto. Una prueba adversarial es un caso diseñado a propósito para intentar engañar al robot. Si la batería no cambia, puedes comparar versiones con honestidad: "la v2 entrega 15 de 20; la v1 entregaba 12".

Por qué aprender

Medir cada etapa te indica dónde invertir tu tiempo, y las pruebas adversariales muestran que la seguridad no depende de que el modelo "se comporte". Es lo primero que preguntarán un jurado, un cliente o un profesor.

Conceptos clave

Éxito en cadena
las tasas se multiplican
Cuello de botella
la mayor caída
Batería fija
compara versiones
Adversarial
intenta engañar
6

🎤 Presentar, documentar y compartir

Un proyecto que solo tú puedes ejecutar termina cuando apagas la computadora. Si lo documentas y lo compartes, se convierte en el punto de partida para el siguiente grupo. Cierra el curso con cuatro entregables: repositorio, video honesto, hoja de resultados e presentación breve.

Paso a paso de la entrega

1

Repositorio con README

Qué hace el robot, lista de piezas, diagrama de conexiones, cómo instalarlo, el comando --simular para quienes no tienen hardware y la licencia (MIT, por ejemplo).

2

Video a velocidad real

Sin cortes durante la tarea, con el rótulo "1x, autónomo" y al menos un fallo mostrado. Es el criterio del 1.1 para evitar exageraciones, aplicado ahora a tu robot.

3

Hoja de cálculo y datos

La batería de 20 pruebas con el conteo por etapa. Si quieres, publica el dataset y la política en el Hub (revisados, sin rostros).

4

Presentación de 5 diapositivas

Problema, arquitectura (la figura del tema 1), demostración en vivo, cifras, qué harías diferente.

✓ En la demostración en vivo

  • ✓Área del brazo marcada en el suelo; público fuera de ella.
  • ✓Una persona con la mano en el botón de emergencia todo el tiempo.
  • ✓Video de respaldo listo, por si la iluminación o el Wi-Fi del lugar causan problemas.

✗ Evita

  • ✗Prometer algo que el robot no hizo en la batería de pruebas.
  • ✗Dejar que el público haga pedidos sin tener activada la confirmación.
  • ✗Ocultar los fallos: son lo que más enseña.

💡 ¿Y ahora?

¿Vas a enseñar? La Ruta 6 · Para docentes muestra cómo llevar este curso a un grupo: formatos, kits por grupo, laboratorio seguro y evaluación por proyecto. ¿Quieres revisar algo o elegir otro camino? Vuelve al inicio del curso: tu «Mi recorrido» guarda lo que ya leíste.

Por qué aprender

Contar qué funcionó, qué falló y con qué resultados es lo que transforma un robot de mesa en un portafolio, un proyecto para una feria de ciencias o un punto de partida para otra persona. Así también crece la comunidad de robótica abierta.

Conceptos clave

README
reproducible por otra persona
Video 1x
sin trucos
Cifras
batería de 20 pruebas
Licencia abierta
otras personas pueden continuar

✅ Resumen del módulo

✓
Alcance definido por escrito: pocos pedidos, éxito definido, todo lo demás se rechaza.
✓
Orquestador por capas: voz → plan de la lista cerrada → validador → confirmación → ejecución; si falla, se detiene.
✓
La cámara verifica antes y después de cada agarre.
✓
Base y brazo, uno a la vez, baterías separadas, un botón de emergencia para ambos.
✓
Medir cada etapa y compartir: batería fija de 20 pruebas, video 1x, README.

Para profundizar

Siguiente paso

Terminaste la Trilha 5. Trilha 6 · Para el profesor: cómo impartir este curso, o vuelve al inicio para elegir otro camino.