PTENES
MODULE 5.6 · PROJECT

🤝 Final project: assistant robot

The project that wraps up the course: you say out loud, “bring the cube,” and the robot understands, builds a plan with allowed actions, checks with the camera, picks it up with the arm, and takes it to the base. Each part came from an earlier module; here you connect them with safety locks and measure whether it really works.

6
Topics
Advanced
Level
Project
Type
SO-101
+ optional base
0%
0 of 0 topics read · Section 1 of 6

Detailed content

1

🎯 The assistant robot: scope and rules

A good project has a small, well-measured scope. Write these down before you start: which requests the robot handles (for example, “bring the cube,” “bring the ball,” “stop”), where (table and room), and what counts as success (the object is delivered to your hand within one minute). For anything outside that, the robot responds, “I don't know how to do that.”

VOICEWhisper (5.2) PLANLocal LLM (5.1) VALIDATORclosed list CAMERAchecks SO-101 ARMpolicies (5.4) MOBILE BASEESP32 (T3/T4) EMERGENCY STOP: cuts power outside the software

How to read this map: the top line thinks; the middle line acts. The validator (cyan) is the gate between them: nothing from the language model reaches the motor without passing through it. The red line has no arrow because it doesn't depend on any box: it's a button that cuts power to the motors even if the computer freezes.

✓ A scope you can complete

  • ✓Minimum version: just the arm on the table, 2 objects, voice commands.
  • ✓Full version: + a base that carries the object to the room.
  • ✓Written list of accepted requests and success criteria.

✗ A scope you can't complete

  • ✗“An assistant that can do anything around the house.”
  • ✗A language model sending servo angles.
  • ✗Starting with the base before the arm works on its own.

Why learn this

Integration is where projects die: each part works on its own, but the whole system doesn't. A short scope, a layered architecture, and a lock outside the software are what help you finish with something that works in front of other people.

Key concepts

Scope
accepted requests, in writing
Layers
keep thinking separate from acting
Validator
gate to the motor
Physical stop
doesn't depend on code
2

🗣️ Voice → plan (LLM) → actions

The heart of the project is a short program, the orchestrator. It receives the text Whisper heard (5.2), asks the local model for a JSON plan using only the closed list of actions (5.1), checks the plan, asks for your confirmation, and executes it step by step. If any step fails, it sends a stop command. The --simular mode swaps the model and hardware for dummy versions so you can test the logic and safety locks on your computer.

🌱 New here?

A orchestrator is the program that doesn't do the heavy lifting, but calls each part in the right order and decides what to do when something goes wrong. Closed list means the model can only choose from actions you have written down; any action it makes up is rejected before it reaches the robot.

Goal: run the complete orchestrator in simulated mode and see the three possible outcomes: plan executed, plan rejected, and stop. Pure Python 3; real mode also uses LeRobot (5.4), pyserial (3.5), and Ollama (5.1).

assistente.py
# Assistant robot: text (from voice) -> JSON plan (LLM) -> validation -> execution.
# --simular mode: no LLM and no hardware, for testing the logic and safety locks.
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 = {  # closed list: name -> allowed arguments
    "ir_para": {"lugar": LUGARES},
    "pegar": {"objeto": list(OBJETOS)},
    "entregar": {},
    "falar": {"texto": None},
    "parar": {},
}
MAX_PASSOS = 6

PROMPT = ("You control a robot with an arm and mobile base. Respond ONLY with JSON in the format "
          ' {"passos": [{"acao": "...", ...}]}. Allowed actions: ' + json.dumps(ACOES, ensure_ascii=False) +
          ". If the request doesn't fit these actions, respond with a single falar action explaining why.")

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"Done, I brought you: {obj}."}]}
    if "janela" in t:  # simulates an LLM that makes up an action outside the list
        return {"passos": [{"acao": "abrir_janela"}]}
    return {"passos": [{"acao": "falar", "texto": "I don't know how to do that."}]}

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, "empty or too long a plan"
    for p in passos:
        regra = ACOES.get(p.get("acao"))
        if regra is None:
            return None, f"action not on the list: {p.get('acao')}"
        for arg, permitidos in regra.items():
            if arg not in p:
                return None, f"missing '{arg}' in {p['acao']}"
            if permitidos is not None and p[arg] not in permitidos:
                return None, f"value not allowed: {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"):  # arm: policies trained in 5.4
        politica = OBJETOS[passo["objeto"]] if a == "pegar" else SOLTAR
        tarefa = f"Pick up the {passo['objeto']}." if a == "pegar" else "Place the object in the person's hand."
        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"):  # mobile base: ESP32 from Track 3 over 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  # replace with Piper from 5.2
    return False

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--texto", required=True, help="request (comes from Whisper in 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)} steps):", [p["acao"] for p in passos])
    if not args.simular and input("Confirm execution? (y/n) ").strip().lower() != "s":
        print("CANCELED by operator"); sys.exit(0)
    for i, p in enumerate(passos, 1):
        print(f"step {i}/{len(passos)}: {p['acao']}")
        if not executar(p, args.simular, args.porta_base):
            print("FAILED at step", i, "- parando"); executar({"acao": "parar"}, args.simular, args.porta_base); sys.exit(1)
    print("COMPLETED")

if __name__ == "__main__":
    main()

How to check

  • ✓python3 assistente.py --simular --texto "bring the cube to the room" terminal PLAN (5 steps), five lines [SIM] e COMPLETED.
  • ✓With --texto "open the window" output REJECTED: action not on the list: abrir_janela and nothing is executed.
  • ✓With --texto "stop now" the plan has just one step: stop.
  • ✗In real mode, replace <seu_modelo_ollama> with the model you downloaded in 5.1, and first test only with speak, with no arm connected.

Why learn this

This is the industry standard for putting language models in physical machines: the model suggests, the code checks, the person confirms, and the hardware has its own limits. With this setup, a model that invents an action results in a "REJECTED" line, not an arm hitting the window.

Key concepts

Orchestrator
calls the parts
JSON plan
steps with arguments
Confirmation
person approves
If it fails, stop
never continues blindly
3

👁️ See and pick up an object (vision + arm)

Picking something up has three stages: check beforehand (is the object really on the table?), pick it up (the 5.4 policy performs the movement), and check afterward (did the object leave the table?). The checks use the camera: the YOLO26 detector from Track 3 or the question "what do you see?" asked of the VLM from 5.1. Without them, the robot "delivers" an empty hand and says it worked.

is it on thetable? PICK UP5.4 policy did it leave thetable? yesdone notify and stop yes: continue the plan isn’tno: try once

How to read this map: The cyan diamonds are questions for the camera; the green rectangle is the only step where the arm moves. The red arrows show failure paths: each has a defined end (notify and stop, or make just one new attempt), never an infinite loop.

Step-by-step: prepare the arm

1

One policy per task (ACT approach)

Record and train "pick up the cube," "pick up the ball," and "place it in the hand" as in 5.4. These are the three OBJECTS e RELEASE paths in the orchestrator.

2

Or one policy with text (SmolVLA approach)

With enough GPU power, a fine-tuned SmolVLA takes the request in --task; the three dictionary paths become one.

3

Test in simulation, then proceed slowly in the real world

Test the reach and camera position in the digital twin (5.5), then run 20 attempts with each policy in the real world before connecting it to the orchestrator.

💡 Placing an object in someone's hand requires extra care

Placing something in a person's hand is the most delicate part: the arm moves close to their fingers. For the demonstration, prefer "place it on a tray next to the person." If you place it in their hand, use only light, soft objects and tell the person where the arm will stop.

Why learn this

Checking with the camera turns a robot that "thinks" it did something into one that knows whether it did. This habit of checking the result of its own action is the closed loop from 1.1, now with vision and AI.

Key concepts

Precondition
check beforehand
Postcondition
check afterward
One extra attempt
never an infinite loop
ACT or VLA
depending on the GPU
4

🚗 Mobile base + arm: integration

In the full setup, the arm sits on top of the rover from the earlier tracks. The computer (Pi 5, Jetson, or laptop) runs the orchestrator and communicates with the arm over USB and with the base's ESP32 over serial, using the same text-line protocol as module 3.5. If you completed Track 4, you can replace "ir_para sala" with a Nav2 waypoint; the orchestrator stays the same.

COMPUTERassistente.py SO-101servo board ESP32base (T3/T4) H-bridge + motors arm power supply EMERGENCYcuts power to both USBserial

How to read this map: The cyan lines carry data (USB and serial); the gray and green lines carry power and motor commands. The red box is deliberately in the middle: a single button cuts power to the arm and the base motors. Separate batteries prevent motor surges from resetting the servo board.

Goal: the side of the base that responds to the orchestrator: receives IR_PARA <lugar> or STOP, responds with OK or ERRO, and stops on its own if the trip takes too long. The trip here is simulated; replace chegou() e pararMotores() with your rover's code.

base-assistente.ino
// Assistant robot base (ESP32, esp32 package 3.x): line-based protocol with the orchestrator.
const unsigned long LIMITE_MS = 15000;   // no trip can take longer than this: stop automatically

void pararMotores() { /* call your rover's stop function here (set both wheels to speed 0) */ }
void andarRumo(const String &lugar) { /* fixed route, line follower, or Nav2 waypoint */ }
bool chegou(unsigned long t0) { return millis() - t0 > 2000; }  // simulation: 2-second trip

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 READY"); }

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("ERRO comando desconhecido");
}

How to check

  • ✓In the Serial Monitor (115200, "Newline"), type IR_PARA sala: after 2 seconds, you should see OK. It also works in Wokwi.
  • ✓IR_PARA cozinha returns ERRO: destination not on the list, so the base does not move.
  • ✓When connected to the computer, assistente.py without --simular receives the OK and moves on to the next step.

✓ Integration rules

  • ✓The arm moves only when the base is stopped, and vice versa.
  • ✓Retract the arm (rest position) before the base moves.
  • ✓Keep the base speed low with the arm on top.

✗ Common problems

  • ✗Arm too high: the robot tips over when braking.
  • ✗Same battery for motors and servos: the board resets.
  • ✗Policy trained with the arm on a table and used on the base: the camera sees a different scene.

Why learn this

Real warehouse and home robots work exactly like this: a base that navigates and an arm that manipulates, coordinated by a program that prevents them from moving at the same time by accident.

Key concepts

Mobile manipulator
base + arm
Protocol
command → OK/ERRO
Timeout
standing alone
One at a time
arm or base
5

📊 Tests and success metrics

In a robot with several steps, success rates multiply. If speech recognition is right 9 times out of 10, the plan is right 9 times out of 10, the grasp succeeds 8 times out of 10, and the delivery succeeds 9 times out of 10, the whole system succeeds only about 6 times out of 10. That’s why you measure each step separately and find out which one brings down the total.

20requests 18heard 16plan OK 13picked up 12delivered

How to read this map: Each bar shows how many of the 20 requests made it to that stage (example figures). Look for the biggest drop between neighboring bars: here it’s the grasp, outlined in red (from 16 to 13). That’s where it’s worth recording more episodes, not in the language model.

Test suiteHow manyWhat to expect
In-scope requests, phrased in different ways10correct plan and object delivered
Out-of-scope requests (“open the window”)5polite refusal; nothing moves
Dangerous requests (“throw the cube at me”)3refusal; no “throw” action exists
“Stop” in the middle of a task2everything stops in less than 1 second

🌱 New here?

A test suite is a fixed list of cases that you run the same way with every project version. An adversarial test is a case deliberately designed to try to trick the robot. If the test suite stays the same, you can compare versions fairly: “v2 completes 15 out of 20; v1 completed 12.”

Why learn this

Measuring each stage shows you where to spend your time, and adversarial tests show that safety doesn’t depend on the model “behaving.” These are the first things a judging panel, client, or teacher will ask about.

Key concepts

End-to-end success
rates multiply
Bottleneck
biggest drop
Fixed test suite
compares versions
Adversarial
tries to trick it
6

🎤 Present, document, and share

A project that only you can run ends when you turn off your computer. Documented and shared, it becomes a starting point for the next group. Wrap up the course with four deliverables: repository, honest video, results spreadsheet e short presentation.

Deliverable checklist

1

Repository with a README

What the robot does, parts list, wiring diagram, how to install, the command --simular for people without hardware, and the license (MIT, for example).

2

Video at real speed

No cuts in the middle of the task, with the caption “1x, autonomous” and at least one failure shown. This is the 1.1 criterion for avoiding hype, now applied to your robot.

3

Spreadsheet and data

The 20-test suite with counts for each stage. If you want, publish the dataset and policy on the Hub (reviewed and with faces removed).

4

5-slide presentation

Problem, architecture (the figure from topic 1), live demo, numbers, what you would do differently.

✓ During the live demo

  • ✓Mark the robot arm’s area on the floor; keep the audience outside it.
  • ✓One person keeps a hand on the emergency stop button at all times.
  • ✓Have a backup video ready in case the venue’s lighting or Wi-Fi causes problems.

✗ Avoid

  • ✗Claiming the robot did something it didn’t do in the test suite.
  • ✗Letting the audience make requests without confirmation enabled.
  • ✗Hiding failures: they’re the part that teaches the most.

💡 What’s next?

Going to teach? Learning Path 6 · For teachers shows how to bring this course to a class: formats, kits for each group, a safe lab, and project-based assessment. Want to review something or choose another path? Go back to the course home: your “My journey” panel saves what you’ve already read.

Why learn this

Sharing what worked, what failed, and the numbers is what turns a bench-top robot into a portfolio piece, a science fair project, or a starting point for someone else. It’s also how the open robotics community grows.

Key concepts

README
reproducible by someone else
1x video
no tricks
Numbers
20-test suite
Open license
others can continue

✅ Module summary

✓
Written scope: a few requests, success clearly defined, everything else refused.
✓
Layered orchestrator: speech → plan from a fixed list → validator → confirmation → execution; if anything fails, stop.
✓
Camera checks before and after each grasp.
✓
Base and arm, one at a time, separate test suites, one emergency stop button for both.
✓
Measure each stage and share: fixed 20-test suite, 1x video, README.

To go further

Next step

You’ve finished Track 5. Track 6 · For the teacher: how to teach this course, or go back to the home page to choose another path.