PTENES
MÓDULO 2.5 · PRÁCTICA

📡 Telemetría y registro de datos

Ajustar un robot "a ojo" es adivinar. Aquí haces que el robot cuente lo que siente y decide: gráfico en vivo en Serial Plotter, archivo CSV en la tarjeta microSD, mensajes MQTT para un panel en la red y, al final, análisis del registro en Python.

6
Temas
Intermedio
Nivel
Práctica
Tipo
ESP32 + microSD
Hardware
0%
0 de 0 temas leídos · Sección 1 de 6

Contenido detallado

1

📏 Qué medir y con qué frecuencia

Telemetría es el robot informando sobre su propio estado mientras trabaja. Regla práctica: registra la entrada (lo que leyó el sensor), la decisión (error, salida del PID, estado de la máquina) y la salida (PWM de cada motor), siempre con el tiempo en milisegundos. Con estas tres columnas descubres si el problema está en percibir, pensar o actuar.

🌱 ¿Eres nuevo aquí?

Frecuencia de muestreo es cuántas mediciones por segundo guardas, en hercios (Hz): 50 Hz = una fila cada 20 ms. Baud es la velocidad del puerto serial en bits por segundo; a 115200 baud caben cerca de 11 500 caracteres por segundo, porque cada carácter ocupa 10 bits.

QuéEjemploRitmo sugerido
Bucle de controlerror, corrección del PID, PWM50 a 200 Hz (el ritmo del PID)
Movimientovelocidad de las ruedas, ángulo de la IMU20 a 100 Hz
Estadovoltaje de la batería, señal Wi-Fi (RSSI)1 a 2 Hz
Eventoscambio de estado, línea perdidacuando ocurren
Panel para las personasresumen de los elementos anteriores5 a 10 Hz (el ojo ya no distingue más)
Serial a 115200 baud ≈ 11,5 kB/s límite 40 B × 50 Hz = 2 kB/s: hay margen 8 kB/s: cabe 40 B × 500 Hz = 20 kB/s: no cabe

Cómo leer: el recuadro punteado de arriba representa la capacidad del puerto serial. Cada barra es una línea de CSV de unos 40 caracteres, enviada a distintos ritmos. Cuando la barra supera el límite (rojo), el envío empieza a retrasar el propio bucle de control. Graba rápido en la tarjeta; envía despacio por la red.

✓ Hazlo

  • ✓Una columna de tiempo (t_ms) en cada fila.
  • ✓Nombres de columna cortos e iguales en todos los registros.
  • ✓Una línea de encabezado con los parámetros de la prueba (Kp, Kd, velocidad).

✗ Evita

  • ✗Frases largas en cada ciclo ("El valor del sensor ahora es...").
  • ✗Imprimir dentro del bucle de control más rápido de lo que el puerto soporta.
  • ✗Registro sin tiempo: no sabes qué ocurrió primero.

Por qué aprender

En el seguidor de línea del 2.6 ajustarás Kp y Kd mirando gráficos del antes y el después. Si el registro no tiene las columnas correctas con el ritmo adecuado, el gráfico no muestra la oscilación que quieres corregir.

Conceptos clave

Telemetría
el robot se informa sobre sí mismo
Hz
mediciones por segundo
Entrada/decisión/salida
las tres columnas
Presupuesto
¿caben los bytes × Hz?
2

📈 Gráfico en vivo con Serial Plotter

Arduino IDE 2.x tiene un Serial Plotter (menú Herramientas o el ícono de gráfico en la esquina superior derecha). Lee cada línea de Serial y dibuja una curva por valor. El formato es una línea por instante, con pares nombre:valor separados por comas. Solo una ventana puede usar el puerto: cierra Serial Monitor antes de abrir Plotter.

40950 pot (crudo)filtradoobjetivo (2000)

Cómo leer: el eje horizontal es el tiempo (las líneas que van llegando) y el vertical es el valor. La curva azul muestra lo que entrega el sensor; la cian es la misma lectura después del filtro, más suave y con un poco de retraso; la línea punteada es el objetivo fijo. Comparar las curvas lado a lado es la forma más rápida de ver el ruido, el retraso y el error.

Objetivo: dibujar tres curvas en vivo: la lectura cruda de un potenciómetro (o sensor IR) en GPIO 34, la versión filtrada y un objetivo fijo. Funciona en Wokwi, que también tiene Serial Plotter.

plotter-tres-curvas.ino
// Tres curvas en Serial Plotter de Arduino IDE 2.x (ESP32, paquete esp32 3.x)
const int PINO_SENSOR = 34;   // potenciómetro o 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% del valor nuevo, 90% del anterior
  if (millis() - tPlot >= 20) {                // 50 líneas por segundo: Plotter puede seguir el ritmo
    tPlot = millis();
    Serial.printf("pot:%d,filtrado:%.0f,alvo:%d\n", cru, filtrado, ALVO);
  }
}

Cómo verificar

  • ✓Plotter muestra tres curvas con las leyendas pot, filtrado y objetivo.
  • ✓Al girar rápido el potenciómetro, la curva filtrada llega después, sin los picos.
  • ✗¿Solo aparece una curva, llamada "value 1"? Revisa el separador: dos puntos entre el nombre y el valor, y comas entre los pares. Si el formato cambia en tu versión del IDE, la ayuda de Plotter muestra el formato actual.

💡 El mismo formato sirve para el PID

Cambia las variables por erro:%d,corr:%d,esq:%d,dir:%d y verás el PID del 2.3 en acción: la corrección aumenta cuando aparece el error y las ruedas se diferencian. Con el robot sobre la mesa, sujeto al cable, esto ya revela si la señal está invertida o la ganancia es excesiva.

Por qué aprender

Es telemetría sin costo: sin cables adicionales ni programas extra. Para pruebas de banco (sensor, filtro, señal del PID), resuelve la mayoría de las dudas en minutos.

Conceptos clave

nombre:valor
una curva por par
Filtro exponencial
suaviza sin guardar el historial
Puerto ocupado
Monitor o Plotter
Banco de pruebas
robot detenido, conectado por cable
3

💾 Grabar en tarjeta microSD (CSV)

Un robot en movimiento no puede arrastrar un cable USB. La solución es llevar una bitácora: un módulo de tarjeta microSD que graba un archivo CSV con 50 o 100 líneas por segundo. Después de la prueba, sacas la tarjeta y abres el archivo en la computadora, en una hoja de cálculo o en Python. La tarjeta se comunica con el ESP32 mediante el bus SPI.

🌱 ¿Eres nuevo aquí?

CSV (valores separados por comas) es un texto en el que cada línea es un registro y cada coma separa una columna; la primera línea indica los nombres. SPI es un bus de cuatro cables (reloj SCK, datos MOSI y MISO, selección CS), más rápido que I2C, que usan tarjetas, pantallas y algunos sensores.

ESP32 microSD GPIO 5GPIO 18GPIO 19GPIO 233V3GND CSSCKMISOMOSIVCCGND verifica el voltaje de tu módulo

Cómo leer: cada cable conecta el pin del ESP32 (izquierda) con el pin de la misma función en el módulo (derecha). Los cuatro de arriba son los pines SPI estándar del ESP32 (VSPI). El rojo es la alimentación: hay módulos de 3,3 V y módulos con regulador que requieren 5 V en VCC; lee las etiquetas antes de conectar.

Objetivo: crear un archivo nuevo en cada encendido (/log001.csv, /log002.csv...), grabar 50 líneas por segundo y cerrar de forma segura al presionar BOOT. Tarjeta formateada en FAT32.

sd-log.ino
// Registro CSV en tarjeta microSD (ESP32, paquete 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 estándar)
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 no encontrado: comprueba los cables y FAT32"); while (true) delay(1000); }
  for (int i = 1; i < 1000; i++) {                       // primer nombre libre
    snprintf(nome, sizeof nome, "/log%03d.csv", i);   // la barra inicial es obligatoria
    if (!SD.exists(nome)) break;
  }
  arq = SD.open(nome, FILE_WRITE);
  arq.println("t_ms,sensor,filtrado");                 // encabezado, una vez
  Serial.printf("Grabando en %s (BOOT finaliza)\n", nome);
}
void loop() {
  if (!gravando) return;
  if (millis() - tLog >= 20) {                            // 50 líneas 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();                    // realmente escribe cada 0,5 s
  }
  if (digitalRead(BOTAO) == LOW) {                          // finalizar antes de retirar la tarjeta
    arq.close(); gravando = false;
    Serial.printf("Archivo cerrado: %lu líneas. Puedes retirar la tarjeta.\n", linhas);
  }
}

Cómo verificar

  • ✓El Monitor Serie muestra "Grabando en /log001.csv"; después de presionar BOOT, el total de líneas (unas 50 por segundo grabado).
  • ✓En la computadora, el archivo se abre en la hoja de cálculo con las columnas t_ms, sensor y filtrado.
  • ✗"SD no encontrado": tarjeta de más de 32 GB en exFAT, cable de CS cambiado o módulo sin la alimentación adecuada.

💡 Por qué importa flush()

El ESP32 junta las líneas en la memoria y solo las escribe en la tarjeta cada cierto tiempo. Si la batería se agota antes, se pierde el final de la prueba y el archivo puede dañarse. flush() cada medio segundo limita la pérdida a 0,5 s; close() antes de retirar la tarjeta evita que el archivo se dañe.

Por qué aprender

La tarjeta graba rápido, sin red ni cable, y guarda pruebas completas para compararlas después. Es el registro que vas a usar para ajustar el seguidor de línea.

Conceptos clave

CSV
texto en columnas
SPI
CS, SCK, MOSI, MISO
flush()
graba lo que está pendiente
FAT32
formato de la tarjeta
4

📡 MQTT: publicar telemetría en la red

Para ver el robot en vivo, sin cable, usa MQTT: un protocolo ligero de mensajes, diseñado para dispositivos pequeños. El robot publica en un tópico (robo/telemetria) y no necesita saber quién está escuchando. Un servidor llamado broker entrega el mensaje a todos los que se suscribieron a ese tópico: el panel en el navegador, Node-RED, un script de Python. Los comandos siguen el camino inverso, por otro tópico.

🌱 ¿Eres nuevo aquí?

O broker es el correo central (el más usado es Mosquitto, gratuito). Un tópico es la dirección del mensaje, con niveles separados por barras; robo/# se suscribe a todo lo que está debajo de robo/. Publicar es enviar; suscribirse es pedir recibir.

robotESP32 brokerMosquitto panel web Node-RED Python publica comando robo/telemetria →← robo/comando (línea discontinua)

Cómo leer: las flechas continuas representan la telemetría que sale del robot y el broker copia para cada suscriptor. Las discontinuas representan el comando: el panel publica en robo/comando y el broker se lo entrega al robot. Nadie se comunica directamente con nadie; todos se comunican con el broker.

Paso a paso: broker Mosquitto en tu PC o Raspberry Pi

1

Instala

Linux y Raspberry Pi OS mediante apt (abajo). En Windows y macOS, usa el instalador de mosquitto.org/download.

2

Abre el acceso a la red local

Mosquitto 2.x solo acepta conexiones de la propia máquina hasta que declares un listener. El archivo robo.conf abre el puerto 1883 (ESP32) y el 9001 (navegador, WebSocket).

3

Averigua la IP y escucha

Anota la IP del PC en la red (hostname -I en Linux, ipconfig en Windows) y deja una terminal con mosquitto_sub abierta.

/etc/mosquitto/conf.d/robo.conf
# Broker para la red de casa o de la escuela (Mosquitto 2.x)
listener 1883
listener 9001
protocol websockets
# sin contraseña: solo en una red local de confianza; si está expuesto a Internet, configura usuario y contraseña
allow_anonymous true
terminal
sudo apt install mosquitto mosquitto-clients
sudo nano /etc/mosquitto/conf.d/robo.conf      # pega el bloque de arriba y guarda
sudo systemctl restart mosquitto
mosquitto_sub -h localhost -t 'robo/#' -v         # muestra todo lo que publica el robot
mosquitto_pub -h localhost -t robo/comando -m parar   # en otra terminal: envía un comando

Objetivo: el ESP32 publica un JSON breve 5 veces por segundo y obedece "parar" y "andar" (el LED del GPIO 2 indica que está detenido). Instala la biblioteca PubSubClient (Nick O'Leary) desde el Administrador de Bibliotecas. Cambia los campos entre < >.

mqtt-telemetria.ino
// Telemetría por MQTT (ESP32, paquete esp32 3.x + biblioteca PubSubClient)
#include <WiFi.h>
#include <PubSubClient.h>
const char *WIFI_NOME  = "<seu_wifi>";       // en Wokwi: "Wokwi-GUEST" con contraseña ""
const char *WIFI_SENHA = "<sua_senha>";
const char *BROKER     = "<ip_do_seu_pc>";    // prueba rápida: "test.mosquitto.org" (público: cualquiera puede leer)
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) {   // los comandos llegan aquí
  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 solo en setup
  Serial.printf("\nWi-Fi listo: %s\n", WiFi.localIP().toString().c_str());
  mqtt.setServer(BROKER, 1883);
  mqtt.setCallback(aoReceber);
}
void loop() {
  if (!mqtt.connected() && millis() - tTentativa > 2000) {    // se reconecta sin bloquear el robot
    tTentativa = millis();
    if (mqtt.connect("robo-inema-01")) { mqtt.subscribe("robo/comando"); Serial.println("MQTT conectado"); }
  }
  mqtt.loop();                                                 // envía y recibe; llama en cada vuelta
  if (mqtt.connected() && millis() - tPub >= 200) {           // 5 mensajes por segundo
    tPub = millis();
    char json[96];                                             // breve: el búfer predeterminado es 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);
  }
}

Cómo verificar

  • ✓La terminal de mosquitto_sub muestra robo/telemetria {"t":12345,"sensor":2048,...} 5 veces por segundo.
  • ✓O mosquitto_pub ... -m parar enciende el LED; "andar" lo apaga.
  • ✗¿Nunca se conecta? El firewall del PC bloquea el puerto 1883, la IP es incorrecta o el PC y el ESP32 están en redes diferentes (la red de invitados aísla los dispositivos). En un broker público, usa un tópico solo tuyo, como inema/seunombre/telemetria.

Por qué aprender

MQTT es el idioma común de Internet de las cosas: el mismo robot se comunica con un panel, una hoja de cálculo, Home Assistant u otro robot sin cambiar una sola línea. Y es la misma idea de publicar y suscribirse a tópicos que volverás a encontrar en ROS 2, en la Trilha 4.

Conceptos clave

Broker
correo central
Tópico
dirección con barras
Publicar/suscribirse
enviar / pedir recibir
mqtt.loop()
en cada vuelta del loop
5

🖥️ Panel en el navegador o en Node-RED/Grafana

Con la telemetría en el broker, cualquier programa puede dibujar el panel. Hay tres opciones, de la más simple a la más completa. Empieza por la primera y pasa a la siguiente solo cuando la necesites.

🌐 Página HTML

Un solo archivo, se abre en el navegador y se comunica con el broker por el puerto WebSocket (9001). Es útil para la mesa de trabajo y para mostrarlo en clase.

🧩 Node-RED

Arma flujos arrastrando bloques: mqtt in → json → gráfico. Con el paquete de dashboard de FlowFuse, se convierte en un panel con medidores y botones.

📊 Grafana

Gráficos históricos y de largo plazo. En general, guarda los datos en una base de datos de series temporales (por ejemplo, InfluxDB), alimentada por Node-RED o Telegraf.

Objetivo: una página que muestra el sensor y la señal Wi-Fi en tiempo real, dibuja el gráfico de los últimos 120 valores y tiene botones Parar y Avanzar. Guárdala como painel.html y ábrela en el navegador de la PC donde se ejecuta el broker (o cambia la dirección).

painel.html
<!doctype html>
<html lang="pt-BR"><head><meta charset="utf-8"><title>Panel del robot</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>Robot: <span id="estado">desconectado</span></h1>
<p>sensor: <b id="sensor">-</b> · Wi-Fi: <b id="rssi">-</b> dBm · detenido: <b id="parado">-</b></p>
<canvas id="g" width="600" height="200" style="background:#000;max-width:100%"></canvas>
<p><button onclick="cmd('parar')">Detener</button> <button onclick="cmd('andar')">Avanzar</button></p>
<script>
const cli = mqtt.connect('ws://localhost:9001');   // en otro dispositivo: 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 ? 'sí' : 'no';
  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>

Cómo verificar

  • ✓El título cambia a «Robot: conectado» y la línea azul se mueve cuando giras el sensor.
  • ✓«Parar» enciende el LED del ESP32 y el campo «parado» cambia a «sí».
  • ✗¿Sigue «desconectado»? Falta el puerto 9001 con protocol websockets en robo.conf. Abre la consola del navegador (F12) para ver el error.

Paso a paso: el mismo panel en Node-RED

1

Instala y abre

Con Node.js instalado: npm install -g --unsafe-perm node-red, después node-red. El editor se abre en http://localhost:1880.

2

Agrega el dashboard

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

3

Conecta los bloques

mqtt in (servidor localhost:1883, tópico robo/telemetria) → json → change (msg.payload = msg.payload.sensor) → ui-chart. Un ui-button con el payload «parar» conectado a un mqtt out en robo/comando. Haz clic en Deploy y abre /dashboard.

Por qué aprender

Un panel convierte al robot en una caja transparente para quienes lo rodean: todo el grupo ve el error y la batería mientras se mueve. En el aula, esto permite analizar el comportamiento del robot con datos, no con impresiones.

Conceptos clave

WebSocket
MQTT dentro del navegador
Flujo
bloques conectados en Node-RED
Serie temporal
valor + hora, para Grafana
5 a 10 Hz
es suficiente para la vista
6

🐍 Analizar el registro en Python (pandas + gráfico)

La hoja de cálculo abre el CSV, pero se bloquea con 50 000 filas y no permite comparar dos pruebas con facilidad. pandas lee el archivo en una tabla con una línea de código, calcula promedios e intervalos, y matplotlib dibuja los gráficos. El siguiente script sirve para cualquier registro de este curso: tú indicas el archivo y las columnas.

log001.csvmicroSD read_csvtabla describediferencia de tiempo gráficolog001.png intervalo mayor de lo esperado = el robot se trabó en ese instante

Cómo leer: sigue los cuadros de izquierda a derecha. El paso del medio es el que más enseña: la diferencia entre un t_ms y el siguiente debería ser siempre de 20 ms; un pico ahí revela cuándo se retrasó el bucle (tarjeta lenta, Wi-Fi, un delay olvidado).

Objetivo: leer el CSV del tema 3, imprimir un resumen y guardar un PNG con las curvas y el intervalo entre filas. Prepara el entorno una vez: python3 -m venv .venv, source .venv/bin/activate (en Windows, .venv\Scripts\activate) e pip install pandas matplotlib.

analisa_log.py
# analisa_log.py: resumen y gráfico de un registro CSV del robot
# Uso: python analisa_log.py log001.csv [columna1 columna2 ...]
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="#")                    # las filas con # son anotaciones
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 el inicio
dt = df["t_ms"].diff()                                       # intervalo entre filas

print(f"{len(df)} filas, {df['t_s'].iloc[-1]:.1f} s registrados")
print(f"intervalo: promedio {dt.mean():.1f} ms, máximo {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("tiempo (s)"); a2.grid(alpha=0.3)
plt.tight_layout()
plt.savefig(arquivo.replace(".csv", ".png"), dpi=120)
plt.show()

Cómo verificar

  • ✓python analisa_log.py log001.csv imprime algo como «1500 filas, 30.0 s registrados» e «intervalo: promedio 20.0 ms».
  • ✓Aparece una ventana con dos gráficos y el archivo log001.png queda guardado junto al CSV.
  • ✗¿Aparece «externally-managed-environment» en pip? El entorno virtual no está activo: ejecuta activate de nuevo.

✓ Buenas preguntas para el registro

  • ✓¿El intervalo se mantuvo constante o tuvo picos?
  • ✓¿El error oscila? ¿Cada cuánto tiempo?
  • ✓¿El PWM llegó a 255 (se saturó) en algún tramo?

✗ Conclusiones apresuradas

  • ✗Comparar dos pruebas con la batería en distintos niveles de carga.
  • ✗Cambiar tres parámetros a la vez y no saber cuál ayudó.
  • ✗Mirar solo el promedio: oculta el pico que hizo caer al robot.

Por qué aprender

Este script es la herramienta de ajuste del proyecto 2.6: graba una prueba, ejecuta el script, cambia un parámetro, graba de nuevo y compara los PNG. También es la puerta a la Trilha 3, donde Python es el lenguaje principal.

Conceptos clave

DataFrame
tabla de pandas
describe()
promedio, mínimo, máximo
diff()
diferencia entre líneas
venv
bibliotecas del proyecto

✅ Resumen del módulo

✓
Entrada, decisión y salida, siempre con t_ms, al ritmo del fenómeno y dentro del presupuesto del puerto.
✓
Serial Plotter: pares nombre:valor por línea; ideal para la mesa de trabajo.
✓
microSD: CSV rápido y sin cables; flush() periódico y close() antes de retirar la tarjeta.
✓
MQTT: el robot publica, el broker entrega; los comandos vuelven por otro tópico.
✓
Python: pandas resume, matplotlib dibuja; compara las pruebas cambiando una cosa a la vez.

Para profundizar

Siguiente módulo

2.6 · Proyecto: seguidor de línea con PID, ajustado con la telemetría que acabas de preparar.