ESP32 e Arduino

Rastreador GPS com ESP32 e MQTT: Posições do NEO-6M em um Mapa Web ao Vivo

Leia um módulo GPS NEO-6M com o ESP32, publique posição, velocidade e qualidade do fix via MQTT apenas quando o dispositivo se move e exiba o trajeto ao vivo em um mapa Leaflet via WebSocket seguro.

ESP32 com módulo GPS NEO-6M enviando posições para um mapa web ao vivo em um projeto de rastreador GPS com MQTT
Rode isso no seu próprio broker MQTTTenha um broker MQTT privado e gerenciado no plano Free e comece com exemplos de código prontos.
Teste grátis Exemplos de código Sem cartão de crédito

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:

  1. 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.
  2. ESP32 para o broker. O ESP32 publica uma posição JSON compacta via MQTT na porta TCP 1883 (ou TLS 8883 em produção).
  3. 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: "&copy; 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

  1. Nenhum dado NMEA. TX e RX estão trocados ou o baud rate está errado. O NEO-6M usa 9600 por padrão.
  2. Dados, mas sem fix. Ambiente fechado ou antena encoberta. Vá para fora e espere vários minutos em uma partida a frio.
  3. A posição pula enquanto está parado. Jitter normal do GPS. Aumente MIN_MOVE_M ou descarte fixes com HDOP acima de cerca de 2,5.
  4. O mapa fica em 0,0. O navegador não alcança a porta 8084 ou o prefixo em map.html não confere. Verifique o console do navegador.
  5. 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.

Artigos relacionados

Exemplo de MQTT com ESP-IDF: Monte um Cliente esp-mqtt do Zero

Monte um projeto ESP-IDF a partir de uma pasta vazia com o componente esp-mqtt: configuração do Wi-Fi em modo station, o handler de eventos MQTT, publicação e inscrição, propriedades do MQTT 5 e TLS com o bundle de certificados.

8 min de leitura

Usando os Recursos do MQTT 5 do esp-mqtt no ESP32 com ESP-IDF

Um projeto ESP-IDF prático que ativa o MQTT 5.0 no esp-mqtt e usa user properties, expiração de mensagens, topic aliases, expiração de sessão e response topics com um broker MQTT 5 real.

8 min de leitura

Todos os artigos →

Seu broker MQTT privado está a poucos cliques de distância

Crie uma conta, receba as credenciais do seu broker e conecte seu primeiro dispositivo — sem servidores para instalar, atualizar ou escalar.