Detailed content
📏 What to measure and how often
Telemetry is the robot reporting its own state while it works. A practical rule: record the input (what the sensor read), the decision (error, PID output, machine state), and the output (PWM for each motor), always with the time in milliseconds. With these three columns, you can find out whether the problem is in sensing, thinking, or acting.
🌱 New here?
Sampling rate is how many measurements per second you save, in hertz (Hz): 50 Hz = one row every 20 ms. Baud is the serial port's speed in bits per second; at 115200 baud, about 11,500 characters per second fit, because each character uses 10 bits.
| What | Example | Suggested rate |
|---|---|---|
| Control loop | PID error, correction, PWM | 50 to 200 Hz (the PID rate) |
| Motion | wheel speed, IMU angle | 20 to 100 Hz |
| Health | battery voltage, Wi-Fi signal (RSSI) | 1 to 2 Hz |
| Events | state change, line lost | when they happen |
| Dashboard for people | summary of the items above | 5 to 10 Hz (the eye can’t see any faster) |
How to read this map: the dashed box at the top shows the serial port’s capacity. Each bar is a CSV line with about 40 characters, sent at different rates. When a bar exceeds the limit (red), sending starts to slow down the control loop itself. Record quickly to the card; send slowly over the network.
✓ Do
- ✓A time column (t_ms) on every line.
- ✓Short column names that stay the same in every log.
- ✓One header line with the test parameters (Kp, Kd, speed).
✗ Avoid
- ✗Long sentences on every cycle ("The sensor value right now is...").
- ✗Printing inside the control loop faster than the port can handle.
- ✗Log without timestamps: you don’t know what came first.
Why learn this
In the line follower in 2.6, you’ll tune Kp and Kd by looking at before-and-after graphs. If the log doesn’t have the right columns at the right rate, the graph won’t show the oscillation you want to correct.
Key concepts
📈 Live graph with the Serial Plotter
Arduino IDE 2.x has a Serial Plotter (Tools menu, or the graph icon in the upper-right corner). It reads each Serial line and draws one curve per value. The format is one line per instant, with pairs name:value separated by commas. Only one window can use the port: close the Serial Monitor before opening the Plotter.
How to read this map: the horizontal axis is time (the arriving lines), and the vertical axis is the value. The blue curve is what the sensor reports; the cyan curve is the same reading after filtering, smoother and slightly delayed; the dashed line is the fixed target. Comparing the curves side by side is the quickest way to spot noise, delay, and error.
Goal: draw three live curves: the raw reading from a potentiometer (or IR sensor) on GPIO 34, the filtered version, and a fixed target. It works in Wokwi, which also has a Serial Plotter.
// Three curves in the Arduino IDE 2.x Serial Plotter (ESP32, esp32 3.x package)
const int PINO_SENSOR = 34; // potentiometer or analog IR sensor (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; // exponential filter: 10% new, 90% old
if (millis() - tPlot >= 20) { // 50 lines per second: the Plotter can keep up
tPlot = millis();
Serial.printf("pot:%d,filtrado:%.0f,alvo:%d\n", cru, filtrado, ALVO);
}
}
How to check
- ✓The Plotter shows three curves labeled pot, filtered, and target.
- ✓When you turn the potentiometer quickly, the filtered curve lags behind without the spikes.
- ✗Only one curve, called "value 1"? Check the separators: a colon between the name and value, and a comma between pairs. If your IDE version changes the format, the Plotter help shows the current one.
💡 The same format works for PID
Replace the variables with erro:%d,corr:%d,esq:%d,dir:%d and you’ll see the PID from 2.3 at work: the correction increases when an error appears, and the wheels diverge. With the robot on the table and tethered by its cable, this already reveals reversed signs and excessive gain.
Why learn this
It’s zero-cost telemetry: no extra wires, no extra software. For bench tests (sensor, filter, PID signal), it answers most questions in minutes.
Key concepts
💾 Record to a microSD card (CSV)
A moving robot can’t drag a USB cable. The solution is to bring a logbook: a microSD card module that records a CSV file at 50 or 100 lines per second. After the test, remove the card and open it on your computer, in a spreadsheet, or in Python. The card communicates with the ESP32 over the SPI bus.
🌱 New here?
CSV (comma-separated values) is text where each line is a record and each comma separates a column; the first line gives the names. SPI is a four-wire bus (clock SCK, data MOSI and MISO, chip select CS) that is faster than I2C and used by cards, displays, and some sensors.
How to read this map: each wire connects the ESP32 pin (left) to the corresponding pin on the module (right). The top four are the ESP32’s standard SPI pins (VSPI). The red wire is the power supply: some modules use 3.3 V, while modules with a regulator require 5 V at VCC; check the label before connecting.
Goal: create a new file each time it powers on (/log001.csv, /log002.csv...), record 50 lines per second, and close the file safely when you press BOOT. The card must be formatted as FAT32.
// CSV log on a microSD card (ESP32, esp32 3.x package, microSD module over SPI)
#include <SPI.h>
#include <SD.h>
const int PINO_CS = 5, PINO_SENSOR = 34, BOTAO = 0; // SCK 18, MISO 19, MOSI 23 (default VSPI)
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++) { // first available name
snprintf(nome, sizeof nome, "/log%03d.csv", i); // the leading slash is required
if (!SD.exists(nome)) break;
}
arq = SD.open(nome, FILE_WRITE);
arq.println("t_ms,sensor,filtrado"); // header, once
Serial.printf("Gravando em %s (BOOT encerra)\n", nome);
}
void loop() {
if (!gravando) return;
if (millis() - tLog >= 20) { // 50 lines per second
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(); // actually writes every 0.5 s
}
if (digitalRead(BOTAO) == LOW) { // close before removing the card
arq.close(); gravando = false;
Serial.printf("Arquivo fechado: %lu linhas. Pode tirar o cartao.\n", linhas);
}
}
How to check
- ✓The Serial Monitor displays "Gravando em /log001.csv"; after you press BOOT, it shows the total number of lines (about 50 recorded per second).
- ✓On your computer, the file opens in a spreadsheet with the columns t_ms, sensor, and filtered.
- ✗"SD nao encontrado": a card larger than 32 GB formatted as exFAT, a swapped CS wire, or a module without the correct power supply.
💡 Why flush() matters
The ESP32 buffers the lines in memory and writes them to the card every so often. If the battery dies first, the end of the test is lost and the file may be corrupted. flush() every half second limits data loss to 0.5 s; close() before removing the card, prevents a damaged file.
Why learn this
The card records quickly, without a network or cable, and stores complete tests for later comparison. You’ll use this record to tune the line follower.
Key concepts
📡 MQTT: publish telemetry over the network
To see the robot live, without a cable, use MQTT: a lightweight messaging protocol designed for small devices. The robot publishes to a topic (robo/telemetria) and doesn’t need to know who is listening. A server called the broker delivers the message to everyone who has subscribed to that topic: the browser dashboard, Node-RED, a Python script. Commands travel in the opposite direction, through another topic.
🌱 New here?
O broker is the central mailroom (the most widely used one is Mosquitto, which is free). A topic is the message address, with levels separated by slashes; robo/# subscribes to everything under robo/. Publishing means sending; subscribing means asking to receive.
How to read this map: The solid arrows show telemetry leaving the robot and being copied by the broker to each subscriber. The dashed arrows show the command: the dashboard publishes to robo/comando and the broker delivers it to the robot. No one talks directly to anyone else; everyone talks to the broker.
Step by step: Mosquitto broker on your PC or Raspberry Pi
Install
On Linux and Raspberry Pi OS, use apt (below). On Windows and macOS, use the installer from mosquitto.org/download.
Allow connections from your local network
Mosquitto 2.x only accepts connections from the same machine until you define a listener. The file robo.conf opens ports 1883 (ESP32) and 9001 (browser, WebSocket).
Find the IP address and listen
Note your PC’s IP address on the network (hostname -I on Linux, ipconfig on Windows) and leave a terminal open with mosquitto_sub running.
# Broker for your home or school network (Mosquitto 2.x)
listener 1883
listener 9001
protocol websockets
# no password: use only on a trusted local network; if exposed to the internet, configure a username and password
allow_anonymous true
sudo apt install mosquitto mosquitto-clients
sudo nano /etc/mosquitto/conf.d/robo.conf # paste the block above and save
sudo systemctl restart mosquitto
mosquitto_sub -h localhost -t 'robo/#' -v # shows everything the robot publishes
mosquitto_pub -h localhost -t robo/comando -m parar # in another terminal: send a command
Goal: The ESP32 publishes a short JSON message 5 times per second and responds to "parar" and "andar" (the GPIO 2 LED indicates that it has stopped). Install the PubSubClient (Nick O'Leary) library through the Library Manager. Replace the fields between < >.
// Telemetry over MQTT (ESP32, esp32 3.x package + PubSubClient library)
#include <WiFi.h>
#include <PubSubClient.h>
const char *WIFI_NOME = "<seu_wifi>"; // in Wokwi: "Wokwi-GUEST" with password ""
const char *WIFI_SENHA = "<sua_senha>";
const char *BROKER = "<ip_do_seu_pc>"; // quick test: "test.mosquitto.org" (public: anyone can read)
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) { // commands arrive here
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("command: %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("."); } // wait only in 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) { // reconnect without blocking the robot
tTentativa = millis();
if (mqtt.connect("robo-inema-01")) { mqtt.subscribe("robo/comando"); Serial.println("MQTT connected"); }
}
mqtt.loop(); // send and receive; call on every loop iteration
if (mqtt.connected() && millis() - tPub >= 200) { // 5 messages per second
tPub = millis();
char json[96]; // short: the default buffer is 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);
}
}
How to check
- ✓The mosquitto_sub terminal shows 5 times per second.
- ✓O mosquitto_pub ... -m parar lights the LED; "andar" turns it off.
- ✗Can’t connect? Your PC firewall may be blocking port 1883, the IP address may be wrong, or the PC and ESP32 may be on different networks (guest networks isolate devices). On a public broker, use a topic of your own, such as inema/seunome/telemetria.
Why learn this
MQTT is the common language of the Internet of Things: the same robot can communicate with a dashboard, spreadsheet, Home Assistant, or another robot without changing a line of code. You’ll encounter the same idea of publishing to and subscribing to topics again in ROS 2, in Track 4.
Key concepts
🖥️ Dashboard in a browser or in Node-RED/Grafana
Once telemetry is on the broker, any program can display it on a dashboard. There are three options, from simplest to most complete. Start with the first and move on only when you need to.
Just one file. Open it in a browser and connect to the broker through the WebSocket port (9001). Good for the workbench and classroom demonstrations.
Build flows by dragging blocks: mqtt in → json → chart. With the FlowFuse dashboard package, it becomes a dashboard with gauges and buttons.
Long-term charts and history. It usually stores data in a time-series database (InfluxDB, for example), fed by Node-RED or Telegraf.
Goal: a page that shows the sensor and Wi-Fi signal live, plots the last 120 values, and has Stop and Go buttons. Save it as painel.html and open it in the browser on the PC running the broker (or change the address).
<!doctype html>
<html lang="pt-BR"><head><meta charset="utf-8"><title>Robot dashboard</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">disconnected</span></h1>
<p>sensor: <b id="sensor">-</b> · Wi-Fi: <b id="rssi">-</b> dBm · stopped: <b id="parado">-</b></p>
<canvas id="g" width="600" height="200" style="background:#000;max-width:100%"></canvas>
<p><button onclick="cmd('parar')">Stop</button> <button onclick="cmd('andar')">Move</button></p>
<script>
const cli = mqtt.connect('ws://localhost:9001'); // on another device: ws://<ip_do_seu_pc>:9001
const pts = [], g = document.getElementById('g').getContext('2d');
const $ = id => document.getElementById(id);
cli.on('connect', () => { $('estado').textContent = 'connected'; 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 ? 'yes' : '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>
How to check
- ✓The title changes to "Robot: connected" and the blue line moves as you turn the sensor.
- ✓"Stop" turns on the ESP32 LED, and the "stopped" field changes to "yes".
- ✗Still "disconnected"? The port 9001 is missing protocol websockets in robo.conf. Open the browser console (F12) to see the error.
Step by step: the same dashboard in Node-RED
Install and open
With Node.js installed: npm install -g --unsafe-perm node-red, then node-red. The editor opens at http://localhost:1880.
Add the dashboard
Menu → Manage palette → Install → search for @flowfuse/node-red-dashboard.
Connect the blocks
mqtt in (localhost server:1883, topic robo/telemetria) → json → change (msg.payload = msg.payload.sensor) → ui-chart. A ui-button with payload "parar" connected to an mqtt out on robo/comando. Click Deploy and open /dashboard.
Why learn this
A dashboard turns the robot into a transparent box for everyone around it: the whole group can see the error and the battery level while it moves. In the classroom, this makes it possible to discuss the robot’s behavior using data, not impressions.
Key concepts
🐍 Analyze the log in Python (pandas + chart)
A spreadsheet opens the CSV, but bogs down with 50 000 rows and makes it hard to compare two tests. pandas reads the file into a table in one line of code, calculates averages and ranges, and matplotlib draws the charts. The script below works with any log from this course: you specify the file and the columns.
How to read this map: follow the boxes from left to right. The middle step teaches you the most: the difference between one t_ms and the next should always be 20 ms; a spike there shows where the loop fell behind (slow card, Wi-Fi, a forgotten delay).
Goal: read the CSV from topic 3, print a summary, and save a PNG with the curves and the interval between rows. Set up the environment once: python3 -m venv .venv, source .venv/bin/activate (on Windows, .venv\Scripts\activate) e pip install pandas matplotlib.
# analisa_log.py: summary and chart of a robot CSV log
# Usage: python analisa_log.py log001.csv [column1 column2 ...]
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="#") # lines with # are annotations
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 # seconds since the start
dt = df["t_ms"].diff() # interval between rows
print(f"{len(df)} rows, {df['t_s'].iloc[-1]:.1f} s recorded")
print(f"interval: average {dt.mean():.1f} ms, maximum {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("interval (ms)"); a2.set_xlabel("time (s)"); a2.grid(alpha=0.3)
plt.tight_layout()
plt.savefig(arquivo.replace(".csv", ".png"), dpi=120)
plt.show()
How to check
- ✓python analisa_log.py log001.csv prints something like "1500 rows, 30.0 s recorded" and "interval: average 20.0 ms".
- ✓A window with two charts appears, and the file log001.png is saved next to the CSV.
- ✗Seeing "externally-managed-environment" in pip? The virtual environment isn’t active: run activate again.
✓ Good questions to ask about the log
- ✓Was the interval constant, or were there spikes?
- ✓Does the error oscillate? How often?
- ✓Did the PWM hit 255 (saturate) at any point?
✗ Hasty conclusions
- ✗Comparing two tests with different battery loads.
- ✗Changing three parameters at once and not knowing which one helped.
- ✗Looking only at the average: it hides the spike that brought the robot down.
Why learn this
This script is the tuning tool for project 2.6: record a test, run the script, change one parameter, record again, and compare the PNGs. It’s also the gateway to Track 3, where Python is the main language.
Key concepts
✅ Module summary
To go further
- Serial Plotter in Arduino IDE 2 — format and options
- Arduino-ESP32 SD library — official examples
- PubSubClient — client API
- Mosquitto — broker, listeners, and security
- Node-RED e FlowFuse Dashboard
- Grafana — time-series dashboards
- pandas: getting started
Next module
2.6 · Project: PID line follower, tuned with the telemetry you just set up.