PTENES
MODULE 2.5 · PRACTICE

📡 Telemetry and data logging

Tuning a robot "by eye" is guesswork. Here, you'll make the robot report what it senses and decides: a live graph in Serial Plotter, a CSV file on the microSD card, MQTT messages over the network to a dashboard, and finally, log analysis in Python.

6
Topics
Intermediate
Level
Practice
Type
ESP32 + microSD
Hardware
0%
0 of 0 topics read · Section 1 of 6

Detailed content

1

📏 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.

WhatExampleSuggested rate
Control loopPID error, correction, PWM50 to 200 Hz (the PID rate)
Motionwheel speed, IMU angle20 to 100 Hz
Healthbattery voltage, Wi-Fi signal (RSSI)1 to 2 Hz
Eventsstate change, line lostwhen they happen
Dashboard for peoplesummary of the items above5 to 10 Hz (the eye can’t see any faster)
Serial at 115200 baud ≈ 11.5 kB/s limit 40 B × 50 Hz = 2 kB/s: plenty of headroom 8 kB/s: fits 40 B × 500 Hz = 20 kB/s: doesn’t fit

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

Telemetry
the robot reports its own state
Hz
measurements per second
Input/decision/output
the three columns
Budget
do bytes × Hz fit?
2

📈 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.

40950 pot (raw)filteredtarget (2000)

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.

plotter-tres-curvas.ino
// 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

name:value
one curve per pair
Exponential filter
smooths without storing history
Port in use
Monitor or Plotter
Bench
robot stopped, tethered by its cable
3

💾 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.

ESP32 microSD GPIO 5GPIO 18GPIO 19GPIO 233V3GND CSSCKMISOMOSIVCCGND check your module’s voltage

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.

sd-log.ino
// 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

CSV
columnar text
SPI
CS, SCK, MOSI, MISO
flush()
writes what’s pending
FAT32
card format
4

📡 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.

robotESP32 brokerMosquitto web dashboard Node-RED Python publishes command robo/telemetria →← robo/comando (dashed)

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

1

Install

On Linux and Raspberry Pi OS, use apt (below). On Windows and macOS, use the installer from mosquitto.org/download.

2

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).

3

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.

/etc/mosquitto/conf.d/robo.conf
# 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
terminal
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 < >.

mqtt-telemetria.ino
// 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

Broker
central mailroom
Topic
address with slashes
Publish/subscribe
send / ask to receive
mqtt.loop()
on every loop iteration
5

🖥️ 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.

🌐 HTML page

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.

🧩 Node-RED

Build flows by dragging blocks: mqtt in → json → chart. With the FlowFuse dashboard package, it becomes a dashboard with gauges and buttons.

📊 Grafana

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).

painel.html
<!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

1

Install and open

With Node.js installed: npm install -g --unsafe-perm node-red, then node-red. The editor opens at http://localhost:1880.

2

Add the dashboard

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

3

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

WebSocket
MQTT in the browser
Flow
blocks connected in Node-RED
Time series
value + time, for Grafana
5 to 10 Hz
is enough for the human eye
6

🐍 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.

log001.csvmicroSD read_csvtable describetime diff chartlog001.png an interval longer than expected = the robot stalled at that moment

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
# 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

DataFrame
pandas table
describe()
average, minimum, maximum
diff()
difference between rows
venv
project libraries

✅ Module summary

✓
Input, decision, and output, always with t_ms, at the phenomenon’s pace and within the port’s budget.
✓
Serial Plotter: name:value pairs, one per line; ideal for the workbench.
✓
microSD: fast CSV without a cable; call flush() periodically and close() before removing it.
✓
MQTT: the robot publishes, the broker delivers; commands come back on another topic.
✓
Pythonpandas summarizes, matplotlib draws; compare tests by changing one thing at a time.

To go further

Next module

2.6 · Project: PID line follower, tuned with the telemetry you just set up.