ESP32 et Arduino

Traceur GPS ESP32 MQTT : positions NEO-6M sur une carte web en direct

Lisez un module GPS NEO-6M avec l'ESP32, publiez position, vitesse et qualité du fix via MQTT uniquement lorsque l'appareil se déplace, et affichez le trajet en direct sur une carte Leaflet via WebSocket sécurisé.

ESP32 avec un module GPS NEO-6M envoyant ses positions vers une carte web en direct dans un projet de traceur GPS ESP32 MQTT
Essayez-le sur votre propre broker MQTTObtenez un broker MQTT privé et managé avec l’offre Free et partez d’exemples de code prêts à l’emploi.
Essayer gratuitement Exemples de code Aucune carte bancaire requise

Un projet de traceur GPS ESP32 MQTT affiche un appareil en mouvement sur une carte, quasiment en temps réel, avec très peu de code. Dans ce tutoriel, vous allez lire un module NEO-6M avec TinyGPSPlus, publier position, vitesse et qualité du fix vers broker.connectmq.com, le serveur MQTT public gratuit, uniquement lorsque l'appareil se déplace réellement, et tracer le parcours en direct sur une carte web Leaflet qui s'abonne via WebSocket sécurisé.

Comment les éléments s'articulent

Le flux de données comporte trois parties :

  1. Du NEO-6M à l'ESP32. Le module GPS diffuse des trames NMEA sur un UART à 9600 bauds. TinyGPSPlus les analyse pour en extraire latitude, longitude, vitesse et plus encore.
  2. De l'ESP32 au broker. L'ESP32 publie une position JSON compacte via MQTT sur le port TCP 1883 (ou en TLS sur le 8883 en production).
  3. Du broker au navigateur. Une page web statique se connecte avec MQTT.js via WebSocket sécurisé sur le port 8084, chemin /mqtt, et déplace un marqueur sur la carte.

Comme MQTT découple éditeur et abonné, vous pouvez ouvrir la carte sur plusieurs appareils à la fois, et un retained message de dernière position permet à une carte fraîchement ouverte d'afficher immédiatement le traceur au lieu d'attendre le fix suivant. Si vous découvrez MQTT dans le navigateur, MQTT over WebSocket explique ce transport.

Composants et câblage

  • Une carte de développement ESP32.
  • Un module GPS NEO-6M avec son antenne patch céramique (la carte courante GY-NEO6MV2).
  • Bibliothèques : TinyGPSPlus de Mikal Hart et PubSubClient.
Broche NEO-6M Broche ESP32 Remarques
VCC 3V3 (ou 5V si la carte a un régulateur) Vérifiez votre module ; la plupart des cartes GY-NEO6MV2 acceptent les deux
GND GND
TX GPIO 16 (RX de l'UART2) Données GPS vers l'ESP32
RX GPIO 17 (TX de l'UART2) Nécessaire uniquement pour envoyer des commandes de configuration

L'ESP32 dispose de trois UART matériels : le GPS a donc son propre port et Serial reste libre pour le débogage. N'utilisez pas de liaison série logicielle.

Obtenir un premier fix

Un NEO-6M qui démarre à froid a besoin d'une vue dégagée sur le ciel et peut mettre plusieurs minutes à produire son premier fix. En intérieur, il risque de ne jamais en obtenir. La LED bleue de la plupart des modules clignote une fois par seconde lorsque le fix est valide. Conseils pratiques :

  • Faites le premier test à l'extérieur ou près d'une fenêtre, antenne tournée vers le haut.
  • Éloignez de quelques centimètres l'ESP32 et son antenne Wi-Fi de l'antenne GPS.
  • La petite pile de sauvegarde du module conserve les données d'almanach, ce qui accélère nettement le démarrage suivant. Si les redémarrages sont toujours lents, la pile est peut-être vide.

Stratégie de publication : au mouvement, pas à l'horloge

Publier chaque seconde gaspille des données à l'arrêt et inonde les abonnés. Le sketch publie lorsque :

  • l'appareil s'est déplacé de plus d'une distance seuil (25 m par défaut) depuis la dernière publication, ou
  • un intervalle de heartbeat s'est écoulé (60 s), pour que la carte sache que le traceur est vivant même à l'arrêt.
Topic Retained Payload
connectmq/demo/gps6j1v/position Oui {"lat":41.008238,"lng":28.978359,"speed":32.4,"course":118.2,"alt":44.1,"sats":8,"hdop":1.1}
connectmq/demo/gps6j1v/status Oui online, offline, no-fix

Le sketch du traceur GPS ESP32 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";   // utilisez votre propre identifiant aléatoire

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;     // jamais plus rapide que cela

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());   // continuer l'analyse
  }
  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 vérification de gps.location.age() est importante : TinyGPSPlus conserve la dernière position valide même après la perte du fix, et sans elle vous continueriez à publier une position périmée comme si elle était actuelle.

La carte web en direct

Enregistrez ce fichier sous map.html et ouvrez-le dans un navigateur. Il utilise Leaflet avec les tuiles OpenStreetMap et MQTT.js via WebSocket sécurisé. Modifiez le préfixe pour qu'il corresponde à votre 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>

Le message retained position arrive dès que la page s'abonne : le marqueur saute donc immédiatement à la dernière position connue. La polyligne s'allonge ensuite à mesure que de nouvelles positions arrivent. Les options de Leaflet pour les marqueurs, popups et polylignes sont décrites dans la documentation Leaflet.

Tester le flux du traceur GPS ESP32 MQTT

Observez les messages bruts en parallèle de la carte :

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

Faites le tour du pâté de maisons avec le traceur connecté au point d'accès d'un téléphone, et le trajet se dessine tout seul. La localisation est une donnée personnelle, et la zone publique est lisible par quiconque connaît ou devine le topic : considérez donc ceci comme une courte expérience et n'y suivez jamais de personnes. Le serveur MQTT public gratuit sert aux tests et aux prototypes, jamais aux données de production.

Des payloads compacts et utiles

Chaque champ du message de position a une raison d'être, et mieux vaut résister à l'envie d'en ajouter. Six décimales de latitude et de longitude correspondent à une résolution d'environ dix centimètres, déjà plus fine que ce qu'un NEO-6M peut fournir : davantage de chiffres n'ajoute que des octets. La vitesse et le cap permettent à la carte d'orienter une icône ou d'afficher une étiquette de vitesse sans les calculer à partir de points successifs. Le nombre de satellites et le HDOP décrivent la qualité du fix : un consommateur peut griser le marqueur, voire ignorer complètement un point, lorsque le HDOP est élevé ou que moins de cinq satellites sont utilisés.

Si vous passez plus tard à un modem cellulaire où chaque kilo-octet coûte de l'argent, raccourcissez les clés (la, lo, sp) ou envoyez un format binaire fixe, et conservez la publication au mouvement. Sur un traceur alimenté par batterie, le module GPS lui-même est souvent le plus gros consommateur ; le NEO-6M prend en charge des modes d'économie d'énergie configurés par des commandes UBX sur sa broche RX, ce qui explique la présence de ce fil dans le tableau même si le sketch de base n'envoie rien.

Dépannage

  1. Aucune donnée NMEA. TX et RX sont inversés, ou le débit est incorrect. Le NEO-6M fonctionne par défaut à 9600 bauds.
  2. Des données mais pas de fix. En intérieur, ou antenne masquée. Sortez et patientez plusieurs minutes lors d'un démarrage à froid.
  3. La position saute à l'arrêt. Gigue GPS normale. Augmentez MIN_MOVE_M, ou ignorez les fix dont le HDOP dépasse environ 2,5.
  4. La carte reste sur 0,0. Le navigateur ne peut pas joindre le port 8084, ou le préfixe dans map.html ne correspond pas. Consultez la console du navigateur.
  5. Des trous en voiture. Le Wi-Fi n'est pas disponible sur la route. Un vrai traceur de véhicule nécessite un modem cellulaire ; le code MQTT reste le même.

Configuration de production et confidentialité

Pour un vrai suivi, créez un broker sur app.connectmq.com, passez l'ESP32 à WiFiClientSecure sur le port 8883 avec le certificat ISRG Root X1 et connectez-vous avec YOUR_CLIENT_ID, YOUR_USERNAME et YOUR_PASSWORD. N'intégrez pas d'identifiants de production dans une page web publique : n'importe qui peut les lire dans le code source de la page. Servez la carte depuis un emplacement privé, ou placez un petit backend entre le broker et les visiteurs publics. Dans le portail ConnectMQ, l'explorateur de topics en direct affiche chaque mise à jour de position à son arrivée. Pour ajouter un horodatage à chaque fix, utilisez l'heure GPS de gps.date et gps.time, ou suivez le guide d'horodatage NTP de l'ESP32. Pour des traceurs qui captent des balises BLE à proximité, la passerelle ESP32 BLE MQTT est une extension naturelle.

Suivez votre flotte avec ConnectMQ

Un traceur a besoin d'un broker que les appareils comme les navigateurs peuvent joindre en toute sécurité. ConnectMQ est un broker MQTT managé : un broker MQTT cloud avec MQTT over TLS sur le port 8883 et WebSocket sécurisé sur le 8084, des retained messages pour les dernières positions connues et une offre gratuite Serverless sans carte bancaire. Connectez votre traceur et votre carte, puis parcourez la série tutoriel IoT ESP32 pour d'autres projets d'appareils.

Questions fréquentes

À quelle fréquence un traceur GPS ESP32 doit-il publier via MQTT ?

Publiez au mouvement avec un heartbeat plutôt qu'à une cadence fixe rapide. Un seuil de 25 m avec un heartbeat d'une minute convient à la marche et aux véhicules lents.

Pourquoi utiliser un retained message pour la position ?

Un nouvel abonné, comme une carte qui vient de s'ouvrir, reçoit immédiatement la dernière position connue au lieu d'attendre que l'appareil se déplace.

La carte web peut-elle se connecter directement au broker ?

Oui. Les navigateurs utilisent MQTT via WebSocket sécurisé, ici wss://broker.connectmq.com:8084/mqtt. Pour des données privées, évitez d'exposer des identifiants dans une page publique.

Le NEO-6M fonctionne-t-il en intérieur ?

Généralement pas de manière fiable. Les signaux GPS sont faibles et nécessitent une vue sur le ciel. Testez à l'extérieur ou près d'une fenêtre.

Articles liés

Exemple MQTT ESP-IDF : construire un client esp-mqtt à partir de zéro

Construisez un projet ESP-IDF à partir d'un dossier vide avec le composant esp-mqtt : configuration du Wi-Fi en mode station, gestionnaire d'événements MQTT, publication et abonnement, propriétés MQTT 5 et TLS avec le bundle de certificats.

9 min de lecture

Horodatage NTP ESP32 MQTT : ajouter une heure précise à chaque message

Synchronisez l'horloge de l'ESP32 avec NTP, formatez des horodatages ISO 8601 et epoch en millisecondes, et joignez-les aux payloads MQTT pour que les mesures conservent leur véritable heure de capture même lorsqu'elles sont publiées en retard.

8 min de lecture

Tous les articles →

Votre broker MQTT privé, à quelques clics

Créez un compte, récupérez les identifiants de votre broker et connectez votre premier appareil — aucun serveur à installer, mettre à jour ou dimensionner.