ESP32 y Arduino

Localizador GPS con ESP32 y MQTT: posiciones del NEO-6M en un mapa web en directo

Lee un módulo GPS NEO-6M con el ESP32, publica posición, velocidad y calidad del fix por MQTT solo cuando el dispositivo se mueve y muestra el recorrido en directo en un mapa Leaflet mediante WebSocket seguro.

ESP32 con un módulo GPS NEO-6M enviando posiciones a un mapa web en directo en un proyecto de localizador gps esp32 mqtt
Pruébalo en tu propio broker MQTTConsigue un broker MQTT privado y gestionado con el plan Free y empieza con ejemplos de código listos para usar.
Pruébalo gratis Ejemplos de código Sin tarjeta

Un proyecto de localizador GPS con ESP32 y MQTT muestra un dispositivo en movimiento sobre un mapa casi en tiempo real con muy poco código. En este tutorial leerás un módulo NEO-6M con TinyGPSPlus, publicarás posición, velocidad y calidad del fix en broker.connectmq.com, el servidor MQTT público gratuito de ConnectMQ, solo cuando el dispositivo se mueva de verdad, y dibujarás el recorrido en directo en un mapa web Leaflet que se suscribe mediante WebSocket seguro.

Cómo encajan las piezas

El flujo de datos tiene tres partes:

  1. Del NEO-6M al ESP32. El módulo GPS emite sentencias NMEA por una UART a 9600 baudios. TinyGPSPlus las convierte en latitud, longitud, velocidad y más.
  2. Del ESP32 al broker. El ESP32 publica una posición compacta en JSON por MQTT sobre el puerto TCP 1883 (o TLS 8883 en producción).
  3. Del broker al navegador. Una página web estática se conecta con MQTT.js mediante WebSocket seguro en el puerto 8084, ruta /mqtt, y mueve un marcador en el mapa.

Como MQTT desacopla publicador y suscriptor, puedes abrir el mapa en varios dispositivos a la vez, y un mensaje retenido con la última posición hace que un mapa recién abierto muestre el localizador de inmediato en lugar de esperar al siguiente fix. Si MQTT en el navegador es nuevo para ti, MQTT sobre WebSocket explica el transporte.

Componentes y conexiones

  • Una placa de desarrollo ESP32.
  • Un módulo GPS NEO-6M con su antena cerámica de parche (la habitual placa GY-NEO6MV2).
  • Librerías: TinyGPSPlus de Mikal Hart y PubSubClient.
Pin del NEO-6M Pin del ESP32 Notas
VCC 3V3 (o 5V si la placa tiene regulador) Comprueba tu módulo; la mayoría de placas GY-NEO6MV2 aceptan ambos
GND GND
TX GPIO 16 (RX de la UART2) Datos GPS hacia el ESP32
RX GPIO 17 (TX de la UART2) Solo se necesita para enviar comandos de configuración

El ESP32 tiene tres UART por hardware, así que el GPS dispone de su propio puerto y Serial queda libre para depurar. No uses software serial.

Obtener el primer fix

Un NEO-6M en arranque en frío necesita una vista despejada del cielo y puede tardar varios minutos en dar su primer fix. En interiores puede que nunca lo consiga. El LED azul de la mayoría de los módulos parpadea una vez por segundo cuando el fix es válido. Consejos prácticos:

  • Haz la primera prueba al aire libre o junto a una ventana, con la antena hacia arriba.
  • Mantén el ESP32 y su antena Wi-Fi a unos centímetros de la antena GPS.
  • La pequeña batería de respaldo del módulo conserva el almanaque, de modo que el siguiente arranque es mucho más rápido. Si los reinicios son siempre lentos, puede que la batería esté agotada.

Estrategia de publicación: por movimiento, no por reloj

Publicar cada segundo desperdicia datos mientras está aparcado y satura a los suscriptores. El sketch publica cuando:

  • el dispositivo se ha desplazado más de una distancia umbral (25 m por defecto) desde la última publicación, o
  • ha pasado un intervalo de heartbeat (60 s), para que el mapa sepa que el localizador sigue vivo mientras está parado.
Topic Retenido Payload
connectmq/demo/gps6j1v/position Sí {"lat":41.008238,"lng":28.978359,"speed":32.4,"course":118.2,"alt":44.1,"sats":8,"hdop":1.1}
connectmq/demo/gps6j1v/status Sí online, offline, no-fix

El sketch del localizador GPS con ESP32 y 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";   // usa tu propio id aleatorio

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 más rápido que esto

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());   // sigue analizando
  }
  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();
  }
}

La comprobación de gps.location.age() es importante: TinyGPSPlus conserva la última ubicación válida incluso después de perder el fix, así que sin ella seguirías publicando una posición obsoleta como si fuera actual.

El mapa web en directo

Guarda esto como map.html y ábrelo en un navegador. Usa Leaflet con teselas de OpenStreetMap y MQTT.js sobre WebSocket seguro. Cambia el prefijo para que coincida con tu 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>

El mensaje retenido position llega en cuanto la página se suscribe, así que el marcador salta de inmediato a la última ubicación conocida. Después, la polilínea crece a medida que llegan nuevas posiciones. Las opciones de Leaflet para marcadores, ventanas emergentes y polilíneas se describen en la documentación de Leaflet.

Probar el flujo del localizador GPS con ESP32 y MQTT

Observa los mensajes en bruto junto al mapa:

mosquitto_sub -h broker.connectmq.com -p 1883 -i gps-view-6j1v -t "connectmq/demo/gps6j1v/#" -v

Da una vuelta a la manzana con el localizador conectado al punto de acceso de un teléfono y el recorrido se dibujará solo. La ubicación es un dato personal, y el área pública puede leerla cualquiera que conozca o adivine el topic, así que trátalo como un experimento breve y nunca rastrees a personas allí. El servidor MQTT público gratuito está pensado para pruebas y prototipos, no para datos de producción; su página, enlazada al principio, explica para qué sirve el área pública.

Payloads pequeños y útiles

Cada campo del mensaje de posición tiene un propósito, y merece la pena resistir la tentación de añadir más. Seis decimales de latitud y longitud equivalen a unos diez centímetros, una resolución ya más fina de la que puede ofrecer un NEO-6M, así que más dígitos solo añaden bytes. La velocidad y el rumbo permiten al mapa girar un icono o mostrar una etiqueta de velocidad sin calcularlos a partir de puntos consecutivos. El número de satélites y el HDOP describen la calidad del fix: un consumidor puede atenuar el marcador, o ignorar un punto por completo, cuando el HDOP es alto o hay menos de cinco satélites en uso.

Si más adelante pasas a un módem celular donde cada kilobyte cuesta dinero, acorta las claves (la, lo, sp) o envía un formato binario fijo, y mantén la publicación por movimiento. En un localizador con batería, el propio módulo GPS suele ser el mayor consumidor; el NEO-6M admite modos de ahorro de energía configurados mediante comandos UBX por su pin RX, y por eso ese cable figura en la tabla aunque el sketch básico nunca envíe nada.

Solución de problemas

  1. No llegan datos NMEA. TX y RX están intercambiados o la velocidad en baudios es incorrecta. El NEO-6M trabaja por defecto a 9600.
  2. Hay datos pero no fix. Estás en interiores o la antena está tapada. Sal al exterior y espera varios minutos en un arranque en frío.
  3. La posición salta mientras está aparcado. Es la variación normal del GPS. Aumenta MIN_MOVE_M o descarta los fixes con un HDOP superior a 2,5 aproximadamente.
  4. El mapa se queda en 0,0. El navegador no llega al puerto 8084, o el prefijo de map.html no coincide. Revisa la consola del navegador.
  5. Huecos al conducir. No hay Wi-Fi en la carretera. Un localizador de vehículos real necesita un módem celular; el código MQTT no cambia.

Configuración para producción y privacidad

Para un seguimiento real, crea un broker MQTT gestionado en app.connectmq.com, cambia el ESP32 a WiFiClientSecure en el puerto 8883 con el certificado ISRG Root X1 y conéctate con YOUR_CLIENT_ID, YOUR_USERNAME y YOUR_PASSWORD. No incrustes credenciales de producción en una página web pública: cualquiera puede leerlas en el código fuente de la página. Sirve el mapa desde una ubicación privada o coloca un pequeño backend entre el broker y los visitantes públicos. En el portal de ConnectMQ, el explorador de topics en vivo muestra cada actualización de posición a medida que llega. Para añadir una marca de tiempo a cada fix, usa la hora GPS de gps.date y gps.time o sigue la guía de timestamp NTP con ESP32. Para localizadores que detectan etiquetas BLE cercanas, la pasarela BLE MQTT con ESP32 es una ampliación natural.

Sigue tu flota con ConnectMQ

Un localizador, ya siga una furgoneta de reparto en Bogotá o una flota de servicio en Valencia, necesita un broker al que puedan llegar de forma segura tanto los dispositivos como los navegadores. ConnectMQ es un broker MQTT gestionado, un broker MQTT en la nube con MQTT sobre TLS en el 8883 y WebSocket seguro en el 8084, retained messages para las últimas posiciones conocidas y un plan gratuito Serverless que no requiere tarjeta de crédito. Conecta tu localizador y tu mapa, y después recorre la serie del tutorial de IoT con ESP32 para ver más proyectos con dispositivos.

Preguntas frecuentes

¿Con qué frecuencia debe publicar por MQTT un localizador GPS con ESP32?

Publica por movimiento más un heartbeat en lugar de con un reloj fijo y rápido. Un umbral de 25 m con un heartbeat de un minuto va bien para desplazamientos a pie y vehículos lentos.

¿Por qué usar un mensaje retenido para la posición?

Un nuevo suscriptor, como un mapa que se acaba de abrir, recibe de inmediato la última posición conocida en lugar de esperar a que el dispositivo se mueva.

¿Puede el mapa web conectarse directamente al broker?

Sí. Los navegadores usan MQTT sobre WebSocket seguro, aquí wss://broker.connectmq.com:8084/mqtt. Para datos privados, evita exponer credenciales en una página pública.

¿Funciona el NEO-6M en interiores?

Normalmente no de forma fiable. Las señales GPS son débiles y necesitan ver el cielo. Haz las pruebas al aire libre o junto a una ventana.

Artículos relacionados

Ejemplo de ESP-IDF MQTT: crea un cliente esp-mqtt desde cero

Construye un proyecto ESP-IDF desde una carpeta vacía con el componente esp-mqtt: configuración de Wi-Fi en modo estación, el manejador de eventos MQTT, publicación y suscripción, propiedades de MQTT 5 y TLS con el bundle de certificados.

9 min de lectura

Todos los artículos →

Tu broker MQTT privado, a unos pocos clics

Crea una cuenta, obtén las credenciales de tu broker y conecta tu primer dispositivo, sin servidores que instalar, parchear ni escalar.