Um projeto de rastreador GPS com ESP32 e MQTT mostra um dispositivo em movimento em um mapa quase em tempo real com pouquíssimo código. Neste tutorial, você vai ler um módulo NEO-6M com TinyGPSPlus, publicar posição, velocidade e qualidade do fix no broker.connectmq.com apenas quando o dispositivo realmente se mover e desenhar o trajeto ao vivo em um mapa web Leaflet que assina via WebSocket seguro.
Como as peças se encaixam
O fluxo de dados tem três partes:
- NEO-6M para o ESP32. O módulo GPS transmite sentenças NMEA por uma UART a 9600 baud. O TinyGPSPlus as interpreta em latitude, longitude, velocidade e mais.
- ESP32 para o broker. O ESP32 publica uma posição JSON compacta via MQTT na porta TCP 1883 (ou TLS 8883 em produção).
- Broker para o navegador. Uma página web estática se conecta com MQTT.js via WebSocket seguro na porta 8084, caminho
/mqtt, e move um marcador no mapa.
Como o MQTT desacopla publicador e assinante, você pode abrir o mapa em vários dispositivos ao mesmo tempo, e uma mensagem retida com a última posição faz com que um mapa recém-aberto mostre o rastreador imediatamente, sem esperar o próximo fix. Se MQTT no navegador é novidade para você, MQTT sobre WebSocket explica o transporte.
Peças e ligação
- Uma placa de desenvolvimento ESP32.
- Um módulo GPS NEO-6M com sua antena cerâmica (a placa GY-NEO6MV2, a mais comum).
- Bibliotecas: TinyGPSPlus de Mikal Hart e PubSubClient.
| Pino do NEO-6M | Pino do ESP32 | Observações |
|---|---|---|
| VCC | 3V3 (ou 5V se a placa tiver regulador) | Confira seu módulo; a maioria das placas GY-NEO6MV2 aceita os dois |
| GND | GND | |
| TX | GPIO 16 (RX da UART2) | Dados do GPS para o ESP32 |
| RX | GPIO 17 (TX da UART2) | Necessário apenas para enviar comandos de configuração |
O ESP32 tem três UARTs de hardware, então o GPS ganha sua própria porta e o Serial fica livre para depuração. Não use serial por software.
Obtendo o primeiro fix
Um NEO-6M em partida a frio precisa de céu aberto e pode levar vários minutos para obter o primeiro fix. Em ambientes fechados, talvez nunca consiga. O LED azul da maioria dos módulos pisca uma vez por segundo quando o fix é válido. Dicas práticas:
- Faça o primeiro teste ao ar livre ou junto a uma janela, com a antena voltada para cima.
- Mantenha o ESP32 e sua antena Wi-Fi a alguns centímetros da antena do GPS.
- A pequena bateria de backup do módulo guarda os dados do almanaque, para que a próxima partida seja muito mais rápida. Se as reinicializações forem sempre lentas, a bateria pode estar descarregada.
Estratégia de publicação: por movimento, não por relógio
Publicar a cada segundo desperdiça dados quando o dispositivo está parado e inunda os assinantes. O sketch publica quando:
- o dispositivo se moveu mais que uma distância limite (25 m por padrão) desde a última publicação, ou
- passou um intervalo de heartbeat (60 s), para que o mapa saiba que o rastreador está ativo mesmo parado.
| Topic | Retido | Payload |
|---|---|---|
connectmq/demo/gps6j1v/position |
Sim | {"lat":41.008238,"lng":28.978359,"speed":32.4,"course":118.2,"alt":44.1,"sats":8,"hdop":1.1} |
connectmq/demo/gps6j1v/status |
Sim | online, offline, no-fix |
O sketch do rastreador GPS com ESP32 e MQTT
#include <WiFi.h>
#include <PubSubClient.h>
#include <TinyGPSPlus.h>
const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 1883;
const char* PREFIX = "connectmq/demo/gps6j1v"; // use seu próprio id aleatório
const int GPS_RX = 16, GPS_TX = 17;
const double MIN_MOVE_M = 25.0;
const uint32_t HEARTBEAT_MS = 60000;
const uint32_t MIN_GAP_MS = 2000; // nunca mais rápido que isto
TinyGPSPlus gps;
HardwareSerial gpsSerial(2);
WiFiClient net;
PubSubClient mqtt(net);
String clientId, tPosition, tStatus;
double lastLat = 0, lastLng = 0;
bool havePublished = false;
uint32_t lastPublishMs = 0;
bool fixReported = false;
void connectWifi() {
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) {
delay(200);
while (gpsSerial.available()) gps.encode(gpsSerial.read()); // continua interpretando
}
Serial.printf("Wi-Fi OK, IP %s\n", WiFi.localIP().toString().c_str());
}
void connectMqtt() {
while (!mqtt.connected()) {
if (mqtt.connect(clientId.c_str(), tStatus.c_str(), 1, true, "offline")) {
mqtt.publish(tStatus.c_str(), "online", true);
Serial.println("MQTT connected");
} else {
Serial.printf("MQTT failed, state %d\n", mqtt.state());
delay(3000);
}
}
}
void publishPosition() {
char payload[220];
snprintf(payload, sizeof(payload),
"{\"lat\":%.6f,\"lng\":%.6f,\"speed\":%.1f,\"course\":%.1f,"
"\"alt\":%.1f,\"sats\":%lu,\"hdop\":%.1f}",
gps.location.lat(), gps.location.lng(),
gps.speed.isValid() ? gps.speed.kmph() : 0.0,
gps.course.isValid() ? gps.course.deg() : 0.0,
gps.altitude.isValid() ? gps.altitude.meters() : 0.0,
(unsigned long)gps.satellites.value(),
gps.hdop.isValid() ? gps.hdop.hdop() : 99.9);
if (mqtt.publish(tPosition.c_str(), payload, true)) {
lastLat = gps.location.lat();
lastLng = gps.location.lng();
lastPublishMs = millis();
havePublished = true;
Serial.println(payload);
}
}
void setup() {
Serial.begin(115200);
gpsSerial.begin(9600, SERIAL_8N1, GPS_RX, GPS_TX);
clientId = "esp32-gps-" + String((uint32_t)ESP.getEfuseMac(), HEX);
tPosition = String(PREFIX) + "/position";
tStatus = String(PREFIX) + "/status";
connectWifi();
mqtt.setServer(MQTT_HOST, MQTT_PORT);
mqtt.setBufferSize(512);
connectMqtt();
}
void loop() {
while (gpsSerial.available()) gps.encode(gpsSerial.read());
if (WiFi.status() != WL_CONNECTED) connectWifi();
if (!mqtt.connected()) connectMqtt();
mqtt.loop();
bool fixValid = gps.location.isValid() && gps.location.age() < 3000;
if (!fixValid) {
if (fixReported || millis() - lastPublishMs > HEARTBEAT_MS) {
mqtt.publish(tStatus.c_str(), "no-fix", true);
fixReported = false;
lastPublishMs = millis();
}
if (millis() > 10000 && gps.charsProcessed() < 10) {
Serial.println("No NMEA data: check GPS TX -> GPIO 16 wiring");
}
return;
}
if (!fixReported) {
mqtt.publish(tStatus.c_str(), "online", true);
fixReported = true;
}
uint32_t sinceLast = millis() - lastPublishMs;
if (sinceLast < MIN_GAP_MS) return;
double moved = havePublished
? TinyGPSPlus::distanceBetween(lastLat, lastLng, gps.location.lat(), gps.location.lng())
: 1e9;
if (moved >= MIN_MOVE_M || sinceLast >= HEARTBEAT_MS) {
publishPosition();
}
}
A verificação de gps.location.age() é importante: o TinyGPSPlus mantém a última localização válida mesmo depois que o fix é perdido, então sem ela você continuaria publicando uma posição desatualizada como se fosse atual.
O mapa web ao vivo
Salve isto como map.html e abra em um navegador. Ele usa Leaflet com tiles do OpenStreetMap e MQTT.js via WebSocket seguro. Altere o prefixo para corresponder ao seu sketch.
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>ESP32 GPS tracker</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<style>html, body, #map { height: 100%; margin: 0; }</style>
</head>
<body>
<div id="map"></div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://unpkg.com/mqtt@5.10.1/dist/mqtt.min.js"></script>
<script>
const PREFIX = "connectmq/demo/gps6j1v";
const map = L.map("map").setView([0, 0], 2);
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom: 19,
attribution: "© OpenStreetMap contributors"
}).addTo(map);
const marker = L.marker([0, 0]).addTo(map);
const track = L.polyline([], { weight: 4 }).addTo(map);
let first = true;
const client = mqtt.connect("wss://broker.connectmq.com:8084/mqtt", {
clientId: "gps-map-" + Math.random().toString(16).slice(2, 10),
protocolVersion: 5,
clean: true
});
client.on("connect", () => client.subscribe(PREFIX + "/#", { qos: 0 }));
client.on("message", (topic, payload) => {
if (topic === PREFIX + "/status") {
marker.bindTooltip("Status: " + payload.toString()).openTooltip();
return;
}
if (topic !== PREFIX + "/position") return;
const p = JSON.parse(payload.toString());
const pos = [p.lat, p.lng];
marker.setLatLng(pos).bindPopup(
`Speed ${p.speed} km/h<br>Satellites ${p.sats}<br>HDOP ${p.hdop}`);
track.addLatLng(pos);
if (first) { map.setView(pos, 16); first = false; } else { map.panTo(pos); }
});
</script>
</body>
</html>
A mensagem retida position chega assim que a página assina, então o marcador salta imediatamente para a última localização conhecida. A polyline então cresce à medida que novas posições chegam. As opções do Leaflet para marcadores, popups e polylines estão na documentação do Leaflet.
Testando o fluxo do rastreador GPS com ESP32 e MQTT
Acompanhe as mensagens brutas junto com o mapa:
mosquitto_sub -h broker.connectmq.com -p 1883 -i gps-view-6j1v -t "connectmq/demo/gps6j1v/#" -v
Dê uma volta no quarteirão com o rastreador conectado ao hotspot do celular e o trajeto se desenha sozinho. Localização é dado pessoal, e a área pública pode ser lida por qualquer pessoa que conheça ou adivinhe o topic, então trate isto como um experimento curto e nunca rastreie pessoas ali. A página do servidor MQTT público explica para que serve a área pública.
Mantendo os payloads pequenos e úteis
Cada campo da mensagem de posição tem uma finalidade, e vale resistir à tentação de adicionar mais. Seis casas decimais de latitude e longitude correspondem a cerca de dez centímetros, o que já é mais fino do que um NEO-6M consegue entregar, então mais dígitos só acrescentam bytes. Velocidade e rumo permitem que o mapa gire um ícone ou mostre um rótulo de velocidade sem calculá-los a partir de pontos consecutivos. A contagem de satélites e o HDOP descrevem a qualidade do fix: um consumidor pode esmaecer o marcador, ou ignorar um ponto inteiramente, quando o HDOP está alto ou há menos de cinco satélites em uso.
Se mais tarde você migrar para um modem celular em que cada kilobyte custa dinheiro, encurte as chaves (la, lo, sp) ou envie um layout binário fixo, e mantenha a publicação por movimento. Em um rastreador alimentado por bateria, o próprio módulo GPS costuma ser o maior consumidor; o NEO-6M suporta modos de economia de energia configurados por comandos UBX em seu pino RX, e é por isso que esse fio está na tabela, embora o sketch básico nunca envie nada.
Solução de problemas
- Nenhum dado NMEA. TX e RX estão trocados ou o baud rate está errado. O NEO-6M usa 9600 por padrão.
- Dados, mas sem fix. Ambiente fechado ou antena encoberta. Vá para fora e espere vários minutos em uma partida a frio.
- A posição pula enquanto está parado. Jitter normal do GPS. Aumente
MIN_MOVE_Mou descarte fixes com HDOP acima de cerca de 2,5. - O mapa fica em 0,0. O navegador não alcança a porta 8084 ou o prefixo em
map.htmlnão confere. Verifique o console do navegador. - Lacunas ao dirigir. Não há Wi-Fi na estrada. Um rastreador veicular real precisa de um modem celular; o código MQTT continua o mesmo.
Configuração para produção e privacidade
Para rastreamento real, crie um broker em app.connectmq.com, mude o ESP32 para WiFiClientSecure na porta 8883 com o certificado ISRG Root X1 e conecte com YOUR_CLIENT_ID, YOUR_USERNAME e YOUR_PASSWORD. Não coloque credenciais de produção em uma página web pública: qualquer pessoa pode lê-las no código-fonte da página. Sirva o mapa a partir de um local privado ou coloque um pequeno backend entre o broker e os visitantes públicos. No portal do ConnectMQ, o explorador de topics ao vivo mostra cada atualização de posição à medida que chega. Para adicionar um timestamp a cada fix, use o horário do GPS em gps.date e gps.time ou siga o guia de timestamp NTP no ESP32. Para rastreadores que detectam tags BLE próximas, o gateway BLE MQTT com ESP32 é uma extensão natural.
Rastreie sua frota no ConnectMQ
Um rastreador precisa de um broker que dispositivos e navegadores possam alcançar com segurança. O ConnectMQ é um broker MQTT gerenciado que roda como broker MQTT na nuvem, com MQTT sobre TLS na porta 8883 e WebSocket seguro na 8084, mensagens retidas para as últimas posições conhecidas e um plano gratuito Serverless que não exige cartão de crédito. Conecte seu rastreador e seu mapa e depois navegue pela série tutorial de IoT com ESP32 para mais projetos de dispositivos.
Perguntas frequentes
Com que frequência um rastreador GPS com ESP32 deve publicar via MQTT?
Publique por movimento mais um heartbeat, em vez de usar um relógio fixo e rápido. Um limite de 25 m com heartbeat de um minuto serve bem para caminhadas e veículos lentos.
Por que usar uma mensagem retida para a posição?
Um novo assinante, como um mapa que acabou de ser aberto, recebe imediatamente a última posição conhecida em vez de esperar o dispositivo se mover.
O mapa web pode se conectar diretamente ao broker?
Sim. Os navegadores usam MQTT sobre WebSocket seguro, aqui wss://broker.connectmq.com:8084/mqtt. Para dados privados, evite expor credenciais em uma página pública.
O NEO-6M funciona em ambientes fechados?
Normalmente não de forma confiável. Os sinais de GPS são fracos e precisam de céu aberto. Teste ao ar livre ou junto a uma janela.



