Conteúdo detalhado
🧩 LLM, VLM e VLA: o que cada um faz no robô
Três siglas aparecem em todo projeto de robô com IA, e cada uma ocupa um lugar diferente. O LLM (modelo de linguagem grande) recebe texto e devolve texto: entende o pedido e decide qual ação faz sentido. O VLM (modelo de visão e linguagem) recebe uma imagem junto com o texto e responde sobre ela: "tem um copo azul à esquerda". O VLA (visão, linguagem e ação) vai um passo além: recebe imagem e instrução e devolve o próprio movimento das juntas, várias vezes por segundo.
🌱 Novo aqui?
Um modelo é um arquivo de números (os parâmetros, ou pesos) aprendidos a partir de muitos exemplos. Rodar o modelo para obter uma resposta se chama inferência. O texto entra e sai em pedaços chamados tokens (partes de palavras). Quanto mais parâmetros, em geral, melhor a resposta e maior a memória e o tempo que a inferência exige.
Como ler: cada linha é um tipo de modelo. À esquerda (ciano) está o que ele recebe; à direita, o que devolve. O LLM e o VLM param em palavras ou numa escolha; o VLA, com brilho, devolve movimento direto. Este módulo usa os dois primeiros como planejadores; o VLA aparece no módulo 5.4, com o SmolVLA.
Traduz "vai um pouco para a frente" em andar 20 cm. Bom para entender pedidos, ruim para medir distância.
Descreve a cena da câmera. Ajuda o LLM a saber o que existe na mesa, mas pode errar cor, quantidade e posição.
Treinado com demonstrações do próprio braço. Controla as juntas, por isso precisa de dados, testes e limites ainda mais rígidos.
Por que aprender
Saber qual modelo faz o quê evita o erro mais comum de quem começa: pedir a um modelo de texto que controle um motor em tempo real. Cada tipo tem um lugar na pilha do robô, e escolher o lugar certo é metade da segurança.
Conceitos-chave
🛡️ Arquitetura segura: o modelo escolhe, o robô executa
O modelo de linguagem fica no alto nível: ele lê o pedido e escolhe uma ação. Tudo que vem depois é código comum, que você escreveu e testou. Um validador no Raspberry Pi ou no PC confere se a ação existe e se os números estão dentro dos limites. Só então uma linha de texto vai pela serial até o ESP32, que tem os próprios limites e a parada de emergência. Se o modelo inventar, a resposta morre no validador.
Como ler: a caixa ciano tracejada é a parte que pode errar (o modelo); a caixa teal é código determinístico, que sempre faz a mesma coisa. A seta vermelha tracejada mostra o caminho da recusa. O botão PARE age direto no ESP32, sem passar pelo modelo nem pelo Pi.
✓ Faça
- ✓Deixe o modelo escolher entre poucas ações com nome claro.
- ✓Confira tudo no código antes de mandar à serial.
- ✓Repita os limites no ESP32: ele é a última barreira.
- ✓Registre cada pedido, resposta e decisão num log.
✗ Não faça
- ✗Deixar o modelo gerar código e executar sem revisão.
- ✗Mandar o texto do modelo direto para a serial.
- ✗Pedir ao modelo valores de PWM ou ângulos crus de motor.
- ✗Depender do modelo para parar o robô numa emergência.
Por que aprender
Modelos de linguagem às vezes respondem com confiança algo errado (a isso se dá o nome de alucinação). Num chat, isso gera uma frase ruim; num robô, pode gerar uma batida. A separação entre "escolher" e "executar" deixa você trocar de modelo, local ou na nuvem, sem mexer no que garante a segurança.
Conceitos-chave
📋 Lista fechada de ações em JSON (function calling)
Em vez de pedir uma resposta livre, você entrega ao modelo um cardápio: as ações que o robô sabe fazer, os parâmetros de cada uma e o formato exato da resposta. Essa ideia tem o nome de function calling (chamada de função) ou tool use. O Ollama aceita um JSON Schema no campo format e força a resposta a seguir esse molde; o campo enum lista os únicos nomes de ação permitidos.
🌱 Novo aqui?
JSON é um formato de texto para dados, com chaves e valores: {"acao": "girar", "parametros": {"graus": 90}}. Um JSON Schema é a regra que diz quais chaves existem e de que tipo é cada valor. O schema ajuda o modelo a acertar o formato, mas não garante que o valor seja seguro: 9000 graus ainda é um número válido. Por isso o validador confere os limites depois.
Como ler: à esquerda está o cardápio que você escreve no código. À direita, três respostas possíveis do modelo. Só a primeira vira comando; as vermelhas são recusadas pelo validador, e o robô fica parado e explica o motivo.
Objetivo: ver como fica uma resposta válida do modelo. Este é o formato que o script do tópico 4 exige; repare que motivo é só texto para o log e nunca é executado.
{
"acao": "girar",
"parametros": { "graus": -90 },
"motivo": "o usuário pediu para virar à esquerda"
}
Como verificar: cole o bloco num validador de JSON (ou rode python3 -m json.tool resposta-exemplo.json); ele deve ser aceito sem erro. Troque -90 por "esquerda" e veja que o tipo deixa de ser número: o validador do tópico 4 recusaria.
Por que aprender
A lista fechada transforma um problema aberto ("o que o modelo vai responder?") num problema fechado e testável ("qual das quatro ações ele escolheu, e o número está no limite?"). É o mesmo princípio que assistentes comerciais usam para chamar ferramentas, aplicado ao robô.
Conceitos-chave
💻 Rodar um modelo local (Ollama no Pi 5 ou no PC)
O Ollama baixa modelos abertos e os serve na sua máquina, numa API HTTP em http://localhost:11434. O seu script conversa com ele como conversaria com um serviço na internet, só que nada sai do computador. Modelos na nuvem também servem para esta arquitetura (o validador é o mesmo); o curso mostra o caminho local porque funciona sem internet, sem custo por pedido e sem mandar a voz ou a imagem da sua casa para fora.
Como ler: tudo dentro da moldura teal roda na sua máquina. As duas setas entre o script e o Ollama são a pergunta e a resposta pela rede local (localhost). O cabo tracejado ciano é a serial até o ESP32, a mesma do módulo 3.5.
| Onde roda | Tamanho de modelo que cabe | Na prática |
|---|---|---|
| Raspberry Pi 5 (8 GB), só CPU | pequenos, de 1 a 3 bilhões de parâmetros | alguns segundos por resposta curta |
| Pi 5 + AI HAT+ 2 | LLM e VLM pequenos no acelerador | confira os modelos suportados na doc do Pi |
| Jetson Orin Nano Super | pequenos e médios, com GPU | mais rápido que o Pi, mais caro |
| PC com GPU NVIDIA | médios (7 a 14 bilhões) com folga | resposta quase imediata |
Passo a passo
Instale o Ollama
Siga o instalador do seu sistema em ollama.com/download (no Linux e no Pi é um comando só). Ele sobe um serviço que fica ouvindo na porta 11434.
Escolha e baixe um modelo
Em ollama.com/library, filtre por modelos com suporte a "tools" e tamanho que caiba na sua memória. Baixe com ollama pull <modelo>. Os nomes e versões mudam rápido: confira a lista do dia.
Teste pelo terminal e instale o pyserial
Rode os comandos abaixo. Se o curl devolver um JSON com "message", o servidor está pronto para o script.
ollama pull <modelo> # troque pelo nome escolhido
ollama list # o modelo deve aparecer aqui
curl http://localhost:11434/api/chat -d '{"model": "<modelo>", "stream": false,
"messages": [{"role": "user", "content": "Responda só: ok"}]}'
python3 -m venv ~/robo-ia && source ~/robo-ia/bin/activate
pip install pyserial
Objetivo: o script completo da arquitetura segura. Ele manda o pedido e o schema ao modelo local, valida a resposta (ação na lista, parâmetros dentro dos limites, nada a mais), pede confirmação para movimentos grandes e só então envia uma linha de texto ao ESP32. Com --simular, imprime a linha em vez de enviar, para testar sem hardware.
#!/usr/bin/env python3
"""Cérebro com modelo local: o modelo ESCOLHE uma ação da lista fechada,
o código VALIDA e só então manda uma linha de texto ao ESP32 pela serial.
Uso: python3 cerebro_llm.py (com ESP32)
python3 cerebro_llm.py --simular (sem hardware: só imprime)"""
import json, sys, time, urllib.request, urllib.error
OLLAMA_URL = "http://localhost:11434/api/chat"
MODELO = "<modelo>" # troque pelo nome que aparece em: ollama list
PORTA = "/dev/ttyUSB0" # USB: /dev/ttyUSB0 ou /dev/ttyACM0 · UART do Pi: /dev/serial0 · Windows: COM3
BAUD = 115200
CONFIRMAR_ACIMA_CM = 50 # andar mais que isso pede "s" no teclado
# Lista fechada: ação -> {parâmetro: (mínimo, máximo)} ou {parâmetro: [valores permitidos]}
ACOES = {
"andar": {"cm": (5, 100)},
"girar": {"graus": (-180, 180)},
"garra": {"estado": ["abrir", "fechar"]},
"parar": {},
}
ESQUEMA = {
"type": "object",
"properties": {
"acao": {"type": "string", "enum": list(ACOES)},
"parametros": {"type": "object"},
"motivo": {"type": "string"},
},
"required": ["acao", "parametros"],
}
SISTEMA = (
"Você controla um robô pequeno. Escolha UMA ação e responda só em JSON.\n"
"Ações: andar {cm: 5 a 100, negativo não existe}; girar {graus: -180 a 180, "
"negativo = esquerda}; garra {estado: abrir ou fechar}; parar {}.\n"
"Se o pedido não couber em nenhuma ação, ou for perigoso, use parar e explique no motivo."
)
def perguntar_modelo(pedido):
"""Manda o pedido ao Ollama e devolve o dicionário da resposta (ou levanta erro)."""
corpo = {
"model": MODELO,
"messages": [{"role": "system", "content": SISTEMA},
{"role": "user", "content": pedido}],
"stream": False, # uma resposta inteira, não pedaços
"format": ESQUEMA, # força o molde JSON
"options": {"temperature": 0},
}
req = urllib.request.Request(OLLAMA_URL, data=json.dumps(corpo).encode(),
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=120) as r:
dados = json.loads(r.read())
return json.loads(dados["message"]["content"])
def validar(resp):
"""Devolve (ok, linha_ou_motivo, precisa_confirmar). Nunca confia no modelo."""
if not isinstance(resp, dict):
return False, "resposta não é um objeto JSON", False
acao = resp.get("acao")
if acao not in ACOES:
return False, f"ação '{acao}' não está na lista", False
params = resp.get("parametros") or {}
if not isinstance(params, dict):
return False, "parâmetros não são um objeto", False
sobra = set(params) - set(ACOES[acao])
if sobra:
return False, f"parâmetro desconhecido: {', '.join(sorted(sobra))}", False
valores = []
for nome, regra in ACOES[acao].items():
if nome not in params:
return False, f"falta o parâmetro '{nome}'", False
v = params[nome]
if isinstance(regra, list): # valores permitidos
if v not in regra:
return False, f"'{v}' não é permitido para {nome}", False
valores.append(str(v).upper())
else: # faixa numérica
if isinstance(v, bool) or not isinstance(v, (int, float)):
return False, f"{nome} precisa ser número", False
minimo, maximo = regra
if not (minimo <= v <= maximo):
return False, f"{nome}={v} fora do limite ({minimo} a {maximo})", False
valores.append(str(int(round(v))))
linha = " ".join([acao.upper()] + valores) # ex.: ANDAR 30 · GARRA ABRIR · PARAR
confirmar = acao == "andar" and params["cm"] > CONFIRMAR_ACIMA_CM
return True, linha, confirmar
def abrir_serial(simular):
if simular:
return None
import serial # pip install pyserial
s = serial.Serial(PORTA, BAUD, timeout=2)
time.sleep(2) # abrir a porta reinicia o ESP32: espere
s.reset_input_buffer()
return s
def enviar(s, linha):
if s is None:
return f"(simulado) {linha}"
s.write((linha + "\n").encode())
return s.readline().decode(errors="replace").strip() or "(sem resposta do ESP32)"
def main():
simular = "--simular" in sys.argv
s = abrir_serial(simular)
print("Cérebro pronto. Escreva um pedido (ou 'sair').")
while True:
pedido = input("Você: ").strip()
if pedido.lower() in ("sair", "exit"):
break
if not pedido:
continue
try:
resp = perguntar_modelo(pedido)
except urllib.error.URLError as e:
print(f"Ollama não respondeu ({e}). Ele está rodando? Teste: ollama list"); continue
except (json.JSONDecodeError, KeyError) as e:
print(f"Recusado: resposta ilegível ({e})."); continue
print("Modelo:", json.dumps(resp, ensure_ascii=False))
ok, linha, confirmar = validar(resp)
if not ok:
print("Recusado:", linha); continue
if confirmar and input(f"Confirma '{linha}'? (s/n) ").strip().lower() != "s":
print("Cancelado por você."); continue
print("ESP32:", enviar(s, linha))
if s:
s.write(b"PARAR\n"); s.close()
if __name__ == "__main__":
main()
Como verificar
- ✓Com --simular, "anda trinta centímetros" mostra o JSON do modelo e "ESP32: (simulado) ANDAR 30".
- ✓"anda dois metros" é recusado (cm fora do limite) ou vira parar; "anda 80 cm" pede confirmação antes.
- ✓"faz um café" termina em recusa ou em PARAR, nunca numa ação inventada.
- ✗"Ollama não respondeu"? O serviço está parado ou o nome do modelo não bate com ollama list.
💡 Teste o validador sem modelo
No Python interativo, rode from cerebro_llm import validar e chame validar({"acao": "girar", "parametros": {"graus": 900}}). Escreva uns dez casos ruins (ação inexistente, texto no lugar de número, parâmetro sobrando). Se todos forem recusados, o seu robô está protegido contra qualquer modelo que você plugar depois.
Por que aprender
Com o modelo local você testa à vontade, sem cota nem conta, e o robô continua funcionando sem Wi-Fi. E, como o validador fica no seu código, trocar o Ollama por um modelo na nuvem é trocar só a função perguntar_modelo.
Conceitos-chave
👁️ VLM: "o que você vê?" pela câmera
Um VLM recebe uma foto e uma pergunta. No Ollama, a imagem vai em base64 no campo images da mensagem. O uso seguro é o mesmo de antes: o VLM descreve, e essa descrição entra como contexto no pedido ao LLM (ou no mesmo modelo, se ele aceitar imagem e ferramentas). Ele não manda em motor. Para posição exata, o detector do módulo 3.3 continua mais confiável; o VLM ajuda quando a pergunta é aberta: "tem alguma coisa no caminho?", "qual objeto é vermelho?".
Objetivo: tirar uma foto da câmera USB (ou da Camera Module 3 via OpenCV) e perguntar ao VLM local o que há na cena. Precisa de um modelo com visão: na biblioteca do Ollama, filtre por "vision".
#!/usr/bin/env python3
"""Tira uma foto e pergunta ao VLM local o que há nela. pip install opencv-python"""
import base64, json, sys, urllib.request
import cv2
MODELO_VISAO = "<modelo-de-visao>" # um modelo com 'vision' em ollama.com/library
PERGUNTA = sys.argv[1] if len(sys.argv) > 1 else "Liste os objetos sobre a mesa, com cor e posição (esquerda, centro, direita)."
cam = cv2.VideoCapture(0) # 0 = primeira câmera
for _ in range(5): # descarta quadros escuros do começo
ok, quadro = cam.read()
cam.release()
if not ok:
sys.exit("Câmera não abriu: confira o índice (0, 1...) e o cabo.")
quadro = cv2.resize(quadro, (640, 480)) # imagem menor = resposta mais rápida
cv2.imwrite("foto.jpg", quadro)
_, jpg = cv2.imencode(".jpg", quadro)
corpo = {
"model": MODELO_VISAO, "stream": False, "options": {"temperature": 0},
"messages": [{"role": "user", "content": PERGUNTA + " Responda em português, em até 3 frases.",
"images": [base64.b64encode(jpg.tobytes()).decode()]}],
}
req = urllib.request.Request("http://localhost:11434/api/chat", data=json.dumps(corpo).encode(),
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=180) as r:
print(json.loads(r.read())["message"]["content"])
Como verificar: o arquivo foto.jpg aparece na pasta e o terminal mostra uma descrição curta da cena. Ponha dois objetos de cores diferentes na mesa e troque-os de lado: a descrição deve acompanhar. Se ela errar, você acabou de ver por que o VLM não pode mandar no motor.
✓ Bom uso do VLM
- ✓Dar contexto ao LLM: "na mesa há um copo azul e uma bola".
- ✓Responder perguntas abertas do usuário sobre a cena.
- ✓Conferir depois da ação: "a garra está segurando algo?".
✗ Mau uso do VLM
- ✗Pedir coordenadas em milímetros e mandar o braço até lá.
- ✗Usar como sensor de segurança ("tem gente perto?").
- ✗Chamar a cada quadro de vídeo: é lento demais para isso.
Por que aprender
Juntar visão e linguagem deixa o robô responder a pedidos que citam o mundo ("pega o vermelho"). Saber onde o VLM é forte e onde ele erra prepara você para o VLA do módulo 5.4, que leva a mesma ideia até o movimento.
Conceitos-chave
🚨 Travas: limites, confirmação e parada de emergência
Segurança em robô com IA é feita em camadas, e cada camada não confia na anterior. O validador do Pi é a primeira. O ESP32 repete os limites e recusa qualquer linha que não entenda. Um watchdog de comando para os motores se nenhuma ordem chegar em pouco tempo. E um botão de parada físico corta o movimento sem depender de software algum.
Como ler: o motor está no centro; para que ele se mova, nenhuma das quatro camadas ao redor pode ter barrado o comando. A camada vermelha, por fora de tudo, é a única que funciona mesmo com o programa travado: é física. Se uma camada falhar, a de fora ainda segura.
Objetivo: o lado ESP32 do protocolo de linhas de texto (o mesmo do módulo 3.5), com limites próprios, resposta OK/ERRO, watchdog de 2 segundos e um botão de parada no GPIO 13. As funções de motor só imprimem; troque pelo código dos seus motores (Trilha 1).
// Executor seguro: recebe "ANDAR 30", "GIRAR -90", "GARRA ABRIR", "PARAR" (ESP32, pacote esp32 3.x)
const int PINO_PARADA = 13; // botão para o GND (usa pull-up interno)
const unsigned long WATCHDOG_MS = 2000; // sem comando por 2 s: para
unsigned long ultimoComando = 0;
bool movendo = false;
void pararMotores() { movendo = false; Serial.println("# motores parados"); } // troque pelo seu driver
void andar(int cm) { movendo = true; Serial.printf("# andando %d cm\n", cm); }
void girar(int graus) { movendo = true; Serial.printf("# girando %d graus\n", graus); }
void garra(bool abrir) { Serial.println(abrir ? "# garra abrindo" : "# garra fechando"); }
void setup() {
Serial.begin(115200);
pinMode(PINO_PARADA, INPUT_PULLUP);
pararMotores();
Serial.println("PRONTO");
}
void loop() {
if (digitalRead(PINO_PARADA) == LOW) { pararMotores(); while (digitalRead(PINO_PARADA) == LOW) delay(10); }
if (movendo && millis() - ultimoComando > WATCHDOG_MS) pararMotores();
if (!Serial.available()) return;
String linha = Serial.readStringUntil('\n'); linha.trim();
if (linha.length() == 0 || linha.length() > 32) { Serial.println("ERRO linha invalida"); return; }
ultimoComando = millis();
int esp = linha.indexOf(' ');
String cmd = esp < 0 ? linha : linha.substring(0, esp);
String arg = esp < 0 ? "" : linha.substring(esp + 1);
if (cmd == "PARAR") { pararMotores(); Serial.println("OK PARAR"); }
else if (cmd == "ANDAR") { int v = arg.toInt(); if (v < 5 || v > 100) Serial.println("ERRO limite"); else { andar(v); Serial.println("OK ANDAR"); } }
else if (cmd == "GIRAR") { int v = arg.toInt(); if (v < -180 || v > 180) Serial.println("ERRO limite"); else { girar(v); Serial.println("OK GIRAR"); } }
else if (cmd == "GARRA" && (arg == "ABRIR" || arg == "FECHAR")) { garra(arg == "ABRIR"); Serial.println("OK GARRA"); }
else { pararMotores(); Serial.println("ERRO comando desconhecido"); }
}
Como verificar
- ✓No Serial Monitor (115200, fim de linha "Nova linha"), "ANDAR 30" responde "OK ANDAR" e, 2 s depois, "# motores parados".
- ✓"ANDAR 500" responde "ERRO limite" mesmo que o Pi tenha falhado em barrar.
- ✓Com o botão do GPIO 13 apertado, qualquer movimento para na hora. Teste também no Wokwi.
💡 Parada de verdade é física
O botão no GPIO depende do programa estar rodando. Em robô com força (braço, base pesada), coloque também uma chave em série com a alimentação dos motores, ao alcance da mão. Antes de cada teste, aperte-a uma vez para ter certeza de que funciona.
Por que aprender
Os módulos seguintes põem um braço que pega objetos e uma voz que dá ordens. Com as quatro camadas prontas, qualquer modelo que você testar, local ou na nuvem, grande ou pequeno, opera dentro de uma caixa que você controla.
Conceitos-chave
✅ Resumo do módulo
Para ir além
- Ollama — instalação e biblioteca de modelos
- API do Ollama — /api/chat, format, images, tools
- JSON Schema passo a passo — o molde da resposta
- IA no Raspberry Pi (AI HAT+ e AI HAT+ 2) — aceleradores para o Pi 5
- pySerial — a serial do lado Python
Próximo módulo
5.2 · Voz local: ouvir com o Whisper e falar com o Piper, ligando a voz ao cérebro deste módulo.