PTENES
MÓDULO 2.5 · PRÁTICA

📡 Telemetria e registro de dados

Ajustar um robô "no olho" é adivinhar. Aqui você faz o robô contar o que sente e o que decide: gráfico ao vivo no Serial Plotter, arquivo CSV no cartão microSD, mensagens MQTT para um painel na rede e, no fim, a análise do log em Python.

6
Tópicos
Intermediário
Nível
Prática
Tipo
ESP32 + microSD
Hardware
0%
0 de 0 tópicos lidos · Seção 1 de 6

Conteúdo detalhado

1

📏 O que medir e com que frequência

Telemetria é o robô relatando o próprio estado enquanto trabalha. A regra prática: registre a entrada (o que o sensor leu), a decisão (erro, saída do PID, estado da máquina) e a saída (PWM de cada motor), sempre com o tempo em milissegundos. Com essas três colunas você descobre se o problema está no sentir, no pensar ou no agir.

🌱 Novo aqui?

Frequência de amostragem é quantas medidas por segundo você guarda, em hertz (Hz): 50 Hz = uma linha a cada 20 ms. Baud é a velocidade da porta serial em bits por segundo; a 115200 baud cabem cerca de 11 500 caracteres por segundo, porque cada caractere gasta 10 bits.

O queExemploRitmo sugerido
Laço de controleerro, correção do PID, PWM50 a 200 Hz (o ritmo do PID)
Movimentovelocidade das rodas, ângulo da IMU20 a 100 Hz
Saúdetensão da bateria, sinal Wi-Fi (RSSI)1 a 2 Hz
Eventostroca de estado, linha perdidaquando acontecem
Painel para pessoasresumo dos itens acima5 a 10 Hz (o olho não vê mais)
Serial a 115200 baud ≈ 11,5 kB/s limite 40 B × 50 Hz = 2 kB/s: folga 8 kB/s: cabe 40 B × 500 Hz = 20 kB/s: não cabe

Como ler: a caixa tracejada de cima é a capacidade da porta serial. Cada barra é uma linha de CSV com cerca de 40 caracteres, mandada em ritmos diferentes. Quando a barra passa do limite (vermelho), o envio começa a atrasar o próprio laço de controle. Grave rápido no cartão; mande devagar pela rede.

✓ Faça

  • ✓Uma coluna de tempo (t_ms) em toda linha.
  • ✓Nomes de coluna curtos e iguais em todos os logs.
  • ✓Uma linha de cabeçalho com os parâmetros do teste (Kp, Kd, velocidade).

✗ Evite

  • ✗Frases longas a cada ciclo ("O valor do sensor agora é...").
  • ✗Imprimir dentro do laço de controle mais rápido do que a porta aguenta.
  • ✗Log sem tempo: você não sabe o que veio antes.

Por que aprender

No seguidor de linha do 2.6 você vai ajustar Kp e Kd olhando gráficos de antes e depois. Se o log não tiver as colunas certas no ritmo certo, o gráfico não mostra a oscilação que você quer corrigir.

Conceitos-chave

Telemetria
o robô relata a si mesmo
Hz
medidas por segundo
Entrada/decisão/saída
as três colunas
Orçamento
bytes × Hz cabem?
2

📈 Gráfico ao vivo pelo Serial Plotter

A Arduino IDE 2.x tem um Serial Plotter (menu Ferramentas, ou o ícone de gráfico no canto superior direito). Ele lê cada linha da Serial e desenha uma curva por valor. O formato é uma linha por instante, com pares nome:valor separados por vírgula. Só uma janela pode usar a porta: feche o Serial Monitor antes de abrir o Plotter.

40950 pot (cru)filtradoalvo (2000)

Como ler: o eixo horizontal é o tempo (as linhas chegando), o vertical é o valor. A curva azul é o que o sensor entrega; a ciano é a mesma leitura depois do filtro, mais lisa e um pouco atrasada; o tracejado é o alvo fixo. Comparar as curvas lado a lado é o jeito mais rápido de ver ruído, atraso e erro.

Objetivo: desenhar três curvas ao vivo: a leitura crua de um potenciômetro (ou sensor IR) no GPIO 34, a versão filtrada e um alvo fixo. Funciona no Wokwi, que também tem Serial Plotter.

plotter-tres-curvas.ino
// Três curvas no Serial Plotter da Arduino IDE 2.x (ESP32, pacote esp32 3.x)
const int PINO_SENSOR = 34;   // potenciômetro ou sensor IR analógico (ADC1)
const int ALVO = 2000;
float filtrado = 0;
unsigned long tPlot = 0;
void setup() { Serial.begin(115200); }
void loop() {
  int cru = analogRead(PINO_SENSOR);           // 0 a 4095
  filtrado = 0.9 * filtrado + 0.1 * cru;        // filtro exponencial: 10% do novo, 90% do antigo
  if (millis() - tPlot >= 20) {                // 50 linhas por segundo: o Plotter acompanha
    tPlot = millis();
    Serial.printf("pot:%d,filtrado:%.0f,alvo:%d\n", cru, filtrado, ALVO);
  }
}

Como verificar

  • ✓O Plotter mostra três curvas com a legenda pot, filtrado e alvo.
  • ✓Girando o potenciômetro rápido, a curva filtrada chega depois, sem os picos.
  • ✗Uma curva só, chamada "value 1"? Confira o separador: dois-pontos entre nome e valor, vírgula entre pares. Se a sua versão da IDE mudar o formato, a ajuda do Plotter mostra o atual.

💡 O mesmo formato serve para o PID

Troque as variáveis por erro:%d,corr:%d,esq:%d,dir:%d e você vê o PID do 2.3 trabalhando: a correção sobe quando o erro aparece e as rodas se separam. Com o robô sobre a mesa, preso ao cabo, isso já revela sinal trocado e ganho exagerado.

Por que aprender

É a telemetria de custo zero: nenhum fio extra, nenhum programa extra. Para testes de bancada (sensor, filtro, sinal do PID) resolve a maior parte das dúvidas em minutos.

Conceitos-chave

nome:valor
uma curva por par
Filtro exponencial
alisa sem guardar histórico
Porta ocupada
Monitor ou Plotter
Bancada
robô parado, no cabo
3

💾 Gravar em cartão microSD (CSV)

Robô andando não pode arrastar cabo USB. A solução é levar um caderno de bordo: um módulo de cartão microSD gravando um arquivo CSV a 50 ou 100 linhas por segundo. Depois do teste, você tira o cartão e abre no computador, na planilha ou no Python. O cartão conversa com o ESP32 pelo barramento SPI.

🌱 Novo aqui?

CSV (valores separados por vírgula) é um texto em que cada linha é um registro e cada vírgula separa uma coluna; a primeira linha dá os nomes. SPI é um barramento de quatro fios (relógio SCK, dados MOSI e MISO, seleção CS) mais rápido que o I2C, usado por cartões, telas e alguns sensores.

ESP32 microSD GPIO 5GPIO 18GPIO 19GPIO 233V3GND CSSCKMISOMOSIVCCGND confira a tensão do seu módulo

Como ler: cada fio liga o pino do ESP32 (esquerda) ao pino de mesmo papel no módulo (direita). Os quatro de cima são o SPI padrão do ESP32 (VSPI). O vermelho é a alimentação: há módulos de 3,3 V e módulos com regulador que pedem 5 V no VCC; leia a serigrafia antes de ligar.

Objetivo: criar um arquivo novo a cada ligação (/log001.csv, /log002.csv...), gravar 50 linhas por segundo e fechar com segurança ao apertar BOOT. Cartão formatado em FAT32.

sd-log.ino
// Log CSV no cartão microSD (ESP32, pacote esp32 3.x, módulo microSD por SPI)
#include <SPI.h>
#include <SD.h>
const int PINO_CS = 5, PINO_SENSOR = 34, BOTAO = 0;   // SCK 18, MISO 19, MOSI 23 (VSPI padrão)
File arq;
char nome[16];
unsigned long tLog = 0, linhas = 0;
float filtrado = 0;
bool gravando = true;
void setup() {
  Serial.begin(115200);
  pinMode(BOTAO, INPUT_PULLUP);
  if (!SD.begin(PINO_CS)) { Serial.println("SD nao encontrado: confira fios e FAT32"); while (true) delay(1000); }
  for (int i = 1; i < 1000; i++) {                       // primeiro nome livre
    snprintf(nome, sizeof nome, "/log%03d.csv", i);   // a barra inicial é obrigatória
    if (!SD.exists(nome)) break;
  }
  arq = SD.open(nome, FILE_WRITE);
  arq.println("t_ms,sensor,filtrado");                 // cabeçalho, uma vez
  Serial.printf("Gravando em %s (BOOT encerra)\n", nome);
}
void loop() {
  if (!gravando) return;
  if (millis() - tLog >= 20) {                            // 50 linhas por segundo
    tLog = millis();
    int cru = analogRead(PINO_SENSOR);
    filtrado = 0.9 * filtrado + 0.1 * cru;
    arq.printf("%lu,%d,%.1f\n", tLog, cru, filtrado);
    if (++linhas % 25 == 0) arq.flush();                    // grava de fato a cada 0,5 s
  }
  if (digitalRead(BOTAO) == LOW) {                          // encerrar antes de tirar o cartão
    arq.close(); gravando = false;
    Serial.printf("Arquivo fechado: %lu linhas. Pode tirar o cartao.\n", linhas);
  }
}

Como verificar

  • ✓O Serial Monitor mostra "Gravando em /log001.csv"; depois de apertar BOOT, o total de linhas (cerca de 50 por segundo gravado).
  • ✓No computador, o arquivo abre na planilha com as colunas t_ms, sensor e filtrado.
  • ✗"SD nao encontrado": cartão maior que 32 GB em exFAT, fio de CS trocado ou módulo sem alimentação certa.

💡 Por que o flush() importa

O ESP32 junta as linhas na memória e só escreve no cartão de tempos em tempos. Se a bateria cair antes, o fim do teste se perde e o arquivo pode corromper. flush() a cada meio segundo limita a perda a 0,5 s; close() antes de tirar o cartão evita arquivo quebrado.

Por que aprender

O cartão grava rápido, sem rede e sem cabo, e guarda testes inteiros para comparar depois. É o registro que você vai usar no ajuste do seguidor de linha.

Conceitos-chave

CSV
texto em colunas
SPI
CS, SCK, MOSI, MISO
flush()
grava o que está pendente
FAT32
formato do cartão
4

📡 MQTT: publicar telemetria na rede

Para ver o robô ao vivo, sem cabo, use MQTT: um protocolo leve de mensagens, feito para aparelhos pequenos. O robô publica num tópico (robo/telemetria) e não precisa saber quem está ouvindo. Um servidor chamado broker entrega a mensagem a todo mundo que assinou aquele tópico: o painel no navegador, o Node-RED, um script Python. Comandos fazem o caminho inverso, por outro tópico.

🌱 Novo aqui?

O broker é o correio central (o mais usado é o Mosquitto, gratuito). Um tópico é o endereço da mensagem, com níveis separados por barra; robo/# assina tudo abaixo de robo/. Publicar é enviar; assinar é pedir para receber.

robôESP32 brokerMosquitto painel web Node-RED Python publica comando robo/telemetria →← robo/comando (tracejado)

Como ler: as setas cheias são a telemetria saindo do robô e sendo copiada pelo broker para cada assinante. As tracejadas são o comando: o painel publica em robo/comando e o broker entrega ao robô. Ninguém fala direto com ninguém; todos falam com o broker.

Passo a passo: broker Mosquitto no seu PC ou Raspberry Pi

1

Instale

Linux e Raspberry Pi OS pelo apt (abaixo). No Windows e no macOS, use o instalador de mosquitto.org/download.

2

Abra para a rede local

O Mosquitto 2.x só aceita conexões da própria máquina até você declarar um listener. O arquivo robo.conf abre a porta 1883 (ESP32) e a 9001 (navegador, WebSocket).

3

Descubra o IP e escute

Anote o IP do PC na rede (hostname -I no Linux, ipconfig no Windows) e deixe um terminal com mosquitto_sub aberto.

/etc/mosquitto/conf.d/robo.conf
# Broker para a rede de casa ou da escola (Mosquitto 2.x)
listener 1883
listener 9001
protocol websockets
# sem senha: só em rede local de confiança; exposto na internet, configure usuário e senha
allow_anonymous true
terminal
sudo apt install mosquitto mosquitto-clients
sudo nano /etc/mosquitto/conf.d/robo.conf      # cole o bloco acima e salve
sudo systemctl restart mosquitto
mosquitto_sub -h localhost -t 'robo/#' -v         # mostra tudo que o robô publica
mosquitto_pub -h localhost -t robo/comando -m parar   # em outro terminal: manda um comando

Objetivo: o ESP32 publica um JSON curto 5 vezes por segundo e obedece a "parar" e "andar" (o LED do GPIO 2 indica parado). Instale a biblioteca PubSubClient (Nick O'Leary) pelo Gerenciador de Bibliotecas. Troque os campos entre < >.

mqtt-telemetria.ino
// Telemetria por MQTT (ESP32, pacote esp32 3.x + biblioteca PubSubClient)
#include <WiFi.h>
#include <PubSubClient.h>
const char *WIFI_NOME  = "<seu_wifi>";       // no Wokwi: "Wokwi-GUEST" com senha ""
const char *WIFI_SENHA = "<sua_senha>";
const char *BROKER     = "<ip_do_seu_pc>";    // teste rápido: "test.mosquitto.org" (público: qualquer um lê)
const int PINO_SENSOR = 34, PINO_LED = 2;
WiFiClient wifi;
PubSubClient mqtt(wifi);
unsigned long tPub = 0, tTentativa = 0;
bool parado = false;
void aoReceber(char *topico, byte *dados, unsigned int n) {   // comandos chegam aqui
  String msg; for (unsigned int i = 0; i < n; i++) msg += (char)dados[i];
  if (msg == "parar") parado = true;
  if (msg == "andar") parado = false;
  digitalWrite(PINO_LED, parado);
  Serial.printf("comando: %s\n", msg.c_str());
}
void setup() {
  Serial.begin(115200);
  pinMode(PINO_LED, OUTPUT);
  WiFi.begin(WIFI_NOME, WIFI_SENHA);
  while (WiFi.status() != WL_CONNECTED) { delay(300); Serial.print("."); }   // espera só no setup
  Serial.printf("\nWi-Fi ok: %s\n", WiFi.localIP().toString().c_str());
  mqtt.setServer(BROKER, 1883);
  mqtt.setCallback(aoReceber);
}
void loop() {
  if (!mqtt.connected() && millis() - tTentativa > 2000) {    // reconecta sem travar o robô
    tTentativa = millis();
    if (mqtt.connect("robo-inema-01")) { mqtt.subscribe("robo/comando"); Serial.println("MQTT conectado"); }
  }
  mqtt.loop();                                                 // envia e recebe; chame em toda volta
  if (mqtt.connected() && millis() - tPub >= 200) {           // 5 mensagens por segundo
    tPub = millis();
    char json[96];                                             // curto: o buffer padrão é de 256 bytes
    snprintf(json, sizeof json, "{\"t\":%lu,\"sensor\":%d,\"rssi\":%d,\"parado\":%d}", tPub, analogRead(PINO_SENSOR), WiFi.RSSI(), parado);
    mqtt.publish("robo/telemetria", json);
  }
}

Como verificar

  • ✓O terminal do mosquitto_sub mostra robo/telemetria {"t":12345,"sensor":2048,...} 5 vezes por segundo.
  • ✓O mosquitto_pub ... -m parar acende o LED; "andar" apaga.
  • ✗Nunca conecta? Firewall do PC bloqueando a porta 1883, IP errado ou PC e ESP32 em redes diferentes (rede de convidados isola aparelhos). Em broker público, use um tópico só seu, como inema/seunome/telemetria.

Por que aprender

MQTT é o idioma comum da internet das coisas: o mesmo robô fala com painel, planilha, Home Assistant ou outro robô sem mudar uma linha. E é a mesma ideia de publicar e assinar tópicos que você vai reencontrar no ROS 2, na Trilha 4.

Conceitos-chave

Broker
correio central
Tópico
endereço com barras
Publicar/assinar
enviar / pedir para receber
mqtt.loop()
em toda volta do loop
5

🖥️ Painel no navegador ou no Node-RED/Grafana

Com a telemetria no broker, qualquer programa pode desenhar o painel. Há três caminhos, do mais simples ao mais completo. Comece pelo primeiro e só suba quando precisar.

🌐 Página HTML

Um arquivo só, abre no navegador e fala com o broker pela porta WebSocket (9001). Bom para a bancada e para mostrar em aula.

🧩 Node-RED

Monta fluxos arrastando blocos: mqtt in → json → gráfico. Com o pacote de dashboard da FlowFuse, vira painel com medidores e botões.

📊 Grafana

Gráficos de longo prazo e históricos. Em geral guarda os dados num banco de séries temporais (InfluxDB, por exemplo) alimentado pelo Node-RED ou pelo Telegraf.

Objetivo: uma página que mostra o sensor e o sinal Wi-Fi ao vivo, desenha o gráfico dos últimos 120 valores e tem botões Parar e Andar. Salve como painel.html e abra no navegador do PC que roda o broker (ou troque o endereço).

painel.html
<!doctype html>
<html lang="pt-BR"><head><meta charset="utf-8"><title>Painel do robô</title>
<script src="https://unpkg.com/mqtt@5.10.1/dist/mqtt.min.js"></script></head>
<body style="font-family:sans-serif;background:#111;color:#eee;padding:16px">
<h1>Robô: <span id="estado">desconectado</span></h1>
<p>sensor: <b id="sensor">-</b> · Wi-Fi: <b id="rssi">-</b> dBm · parado: <b id="parado">-</b></p>
<canvas id="g" width="600" height="200" style="background:#000;max-width:100%"></canvas>
<p><button onclick="cmd('parar')">Parar</button> <button onclick="cmd('andar')">Andar</button></p>
<script>
const cli = mqtt.connect('ws://localhost:9001');   // em outro aparelho: ws://<ip_do_seu_pc>:9001
const pts = [], g = document.getElementById('g').getContext('2d');
const $ = id => document.getElementById(id);
cli.on('connect', () => { $('estado').textContent = 'conectado'; cli.subscribe('robo/telemetria'); });
cli.on('message', (topico, m) => {
  const d = JSON.parse(m.toString());
  $('sensor').textContent = d.sensor; $('rssi').textContent = d.rssi; $('parado').textContent = d.parado ? 'sim' : 'não';
  pts.push(d.sensor); if (pts.length > 120) pts.shift();
  g.clearRect(0, 0, 600, 200); g.strokeStyle = '#60a5fa'; g.lineWidth = 2; g.beginPath();
  pts.forEach((v, i) => g.lineTo(i * 5, 200 - v / 4095 * 200)); g.stroke();
});
function cmd(c) { cli.publish('robo/comando', c); }
</script></body></html>

Como verificar

  • ✓O título muda para "Robô: conectado" e a linha azul anda conforme você gira o sensor.
  • ✓"Parar" acende o LED do ESP32 e o campo "parado" vira "sim".
  • ✗Continua "desconectado"? Falta a porta 9001 com protocol websockets no robo.conf. Abra o console do navegador (F12) para ver o erro.

Passo a passo: o mesmo painel no Node-RED

1

Instale e abra

Com o Node.js instalado: npm install -g --unsafe-perm node-red, depois node-red. O editor abre em http://localhost:1880.

2

Adicione o dashboard

Menu → Manage palette → Install → procure @flowfuse/node-red-dashboard.

3

Ligue os blocos

mqtt in (servidor localhost:1883, tópico robo/telemetria) → json → change (msg.payload = msg.payload.sensor) → ui-chart. Um ui-button com payload "parar" ligado a um mqtt out em robo/comando. Clique em Deploy e abra /dashboard.

Por que aprender

Um painel transforma o robô numa caixa transparente para quem está em volta: o grupo inteiro vê o erro e a bateria enquanto ele anda. Em sala de aula, é o que permite discutir o comportamento do robô com dados, não com impressão.

Conceitos-chave

WebSocket
MQTT dentro do navegador
Fluxo
blocos ligados no Node-RED
Série temporal
valor + hora, para o Grafana
5 a 10 Hz
basta para os olhos
6

🐍 Analisar o log em Python (pandas + gráfico)

A planilha abre o CSV, mas trava com 50 000 linhas e não compara dois testes com facilidade. O pandas lê o arquivo numa tabela em uma linha de código, calcula médias e intervalos, e o matplotlib desenha os gráficos. O script abaixo serve para qualquer log deste curso: você diz o arquivo e as colunas.

log001.csvmicroSD read_csvtabela describediff do tempo gráficolog001.png intervalo maior que o esperado = o robô travou naquele instante

Como ler: siga as caixas da esquerda para a direita. O passo do meio é o que mais ensina: a diferença entre um t_ms e o seguinte deveria ser sempre 20 ms; um pico ali revela onde o laço atrasou (cartão lento, Wi-Fi, um delay esquecido).

Objetivo: ler o CSV do tópico 3, imprimir um resumo e salvar um PNG com as curvas e o intervalo entre linhas. Prepare o ambiente uma vez: python3 -m venv .venv, source .venv/bin/activate (no Windows, .venv\Scripts\activate) e pip install pandas matplotlib.

analisa_log.py
# analisa_log.py: resumo e gráfico de um log CSV do robô
# Uso: python analisa_log.py log001.csv [coluna1 coluna2 ...]
import sys
import pandas as pd
import matplotlib.pyplot as plt

arquivo = sys.argv[1] if len(sys.argv) > 1 else "log001.csv"
df = pd.read_csv(arquivo, comment="#")                    # linhas com # são anotações
colunas = sys.argv[2:] or [c for c in df.columns if c != "t_ms"]
df["t_s"] = (df["t_ms"] - df["t_ms"].iloc[0]) / 1000      # segundos desde o início
dt = df["t_ms"].diff()                                       # intervalo entre linhas

print(f"{len(df)} linhas, {df['t_s'].iloc[-1]:.1f} s gravados")
print(f"intervalo: médio {dt.mean():.1f} ms, maior {dt.max():.0f} ms")
print(df[colunas].describe().round(1))

fig, (a1, a2) = plt.subplots(2, 1, sharex=True, figsize=(10, 6))
df.plot(x="t_s", y=colunas, ax=a1, title=arquivo)
a1.set_ylabel("valor"); a1.grid(alpha=0.3)
a2.plot(df["t_s"], dt, color="tab:red")
a2.set_ylabel("intervalo (ms)"); a2.set_xlabel("tempo (s)"); a2.grid(alpha=0.3)
plt.tight_layout()
plt.savefig(arquivo.replace(".csv", ".png"), dpi=120)
plt.show()

Como verificar

  • ✓python analisa_log.py log001.csv imprime algo como "1500 linhas, 30.0 s gravados" e "intervalo: médio 20.0 ms".
  • ✓Aparece uma janela com dois gráficos e o arquivo log001.png fica salvo ao lado do CSV.
  • ✗"externally-managed-environment" no pip? O ambiente virtual não está ativo: rode o activate de novo.

✓ Boas perguntas para o log

  • ✓O intervalo ficou constante ou teve picos?
  • ✓O erro oscila? De quanto em quanto tempo?
  • ✓O PWM bateu em 255 (saturou) em algum trecho?

✗ Conclusões apressadas

  • ✗Comparar dois testes com bateria em cargas diferentes.
  • ✗Mudar três parâmetros de uma vez e não saber qual ajudou.
  • ✗Olhar só a média: ela esconde o pico que derrubou o robô.

Por que aprender

Este script é a ferramenta de ajuste do projeto 2.6: grave um teste, rode o script, mude um parâmetro, grave de novo e compare os PNG. É também a porta para a Trilha 3, onde Python é a língua principal.

Conceitos-chave

DataFrame
tabela do pandas
describe()
média, mínimo, máximo
diff()
diferença entre linhas
venv
bibliotecas do projeto

✅ Resumo do módulo

✓
Entrada, decisão e saída, sempre com t_ms, no ritmo do fenômeno e dentro do orçamento da porta.
✓
Serial Plotter: pares nome:valor por linha; ideal para a bancada.
✓
microSD: CSV rápido e sem cabo; flush() periódico e close() antes de tirar.
✓
MQTT: o robô publica, o broker entrega; comandos voltam por outro tópico.
✓
Python: pandas resume, matplotlib desenha; compare testes mudando uma coisa por vez.

Para ir além

Próximo módulo

2.6 · Projeto: seguidor de linha com PID, ajustado com a telemetria que você acabou de montar.