Uygulamalı Eğitimler

MQTT ile GPS Takip Cihazı Yapın: Cihaz, Broker ve Canlı Harita

MQTT ile GPS takip cihazı yapın: OwnTracks formatında JSON yayınlayan ESP32 ve GPS modülü, test için Python simülatörü ve WebSocket üzerinden canlı Leaflet haritası.

Güncellendi · 10 dk okuma

MQTT ile GPS Takip Cihazı Yapın: Cihaz, Broker ve Canlı Harita

MQTT ile bir GPS takip cihazı üç küçük parçadan oluşur: GPS konumunu okuyup JSON olarak yayınlayan bir cihaz, onu yönlendiren bir broker ve abone olup bir işaretçiyi hareket ettiren bir harita. MQTT tek bir bağlantıyı açık tuttuğu ve mesaj başına yalnızca birkaç bayt eklediği için Wi-Fi veya hücresel ağ üzerinden sık konum bildiren cihazlara çok uygundur.

Bu eğitimde üçünü de kuruyoruz: seri GPS modüllü ESP32 firmware'i, donanım olmadan test edebilmeniz için bir Python simülatörü ve Leaflet ile MQTT.js kullanan bir tarayıcı haritası. Payload OwnTracks location formatını izler; böylece OwnTracks çalıştıran telefonlar da aynı haritayı paylaşabilir.

Mimari

Veri akışı
GPS module ──NMEA──▶ ESP32 ──MQTT/TLS 8883──▶ broker ──WSS 8084──▶ browser map
                                               │
                                               └──▶ recorder / database (optional)
ParçaGöreviNe seçmeli
GPS modülüUART üzerinden NMEA cümleleri gönderiru-blox NEO-6M/M8N veya benzeri, 9600 baud
CihazNMEA'yı ayrıştırır, ne zaman yayınlayacağına karar verirESP32 (Wi-Fi) veya ESP32 + LTE modem
BrokerYönlendirir, son konumu saklar, ACL'leri uygularTLS destekli herhangi bir MQTT 3.1.1/5 broker
HaritaWebSocket üzerinden abone olur, işaretçileri çizerStatik bir sayfada Leaflet + MQTT.js
Depolama (isteğe bağlı)Geçmişi tutarOwnTracks Recorder, InfluxDB, PostgreSQL

Topic'ler ve payload

Her takip cihazı için bir topic, erişilebilirlik için de ayrı bir durum topic'i kullanın:

Topic düzeni
testmqtt/gps/<user>/<device>          location JSON, retained
testmqtt/gps/<user>/<device>/status   "online" / "offline", retained (Last Will)

Kendi broker'ınızda genellikle OwnTracks uygulamalarının ve Recorder'ın beklediği owntracks/<user>/<device> yapısını kullanırsınız. Burada örneklerin paylaşılan bir test endpoint'inde güvenle çalışabilmesi için testmqtt/ prefix'ini kullanıyoruz. Hangi aboneliğin hangi cihazla eşleştiğini topic matcher ile kontrol edebilirsiniz.

Payload, OwnTracks location mesajının bir alt kümesidir:

Konum payload'u
{
  "_type": "location",
  "tid": "t1",
  "lat": 52.520008,
  "lon": 13.404954,
  "alt": 38,
  "vel": 12,
  "cog": 270,
  "acc": 6,
  "tst": 1792236000
}

lat/lon ondalık derecedir, tst konumun (yayının değil) UTC Unix zaman damgasıdır, vel km/sa, cog derece cinsinden yön, acc ise metre cinsinden tahmini doğruluktur. tid haritalarda gösterilen iki karakterlik bir etikettir. Konum zamanını her zaman gönderin: QoS 1 yeniden denemeleri, çevrimdışı tamponlar ve retained mesaj nedeniyle bir abone, ölçülmesinden çok sonra bir konum alabilir.

Konumu retained olarak yayınlayın; böylece sonradan açılan bir harita bilinen son konumu anında gösterir. Retained mesajların nasıl çalıştığını ve nasıl temizlendiğini retained mesajlar yazısında bulabilirsiniz.

ESP32 firmware'i

Modülün TX ucunu ESP32'nin GPIO16'sına, RX ucunu GPIO17'sine bağlayın, 3,3 V ile besleyin (kartınızı kontrol edin) ve Arduino Kütüphane Yöneticisi'nden TinyGPSPlus ile PubSubClient kütüphanelerini kurun. Daha önce ESP32 ile MQTT projesi kurmadıysanız Arduino ile ESP32 MQTT yazısıyla başlayın.

gps_tracker.ino
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
#include <TinyGPSPlus.h>
#include <time.h>

const char* WIFI_SSID  = "your-ssid";
const char* WIFI_PASS  = "your-wifi-password";
const char* MQTT_HOST  = "broker.connectmq.com";
const int   MQTT_PORT  = 8883;
const char* MQTT_USER  = "username";
const char* MQTT_PASS  = "password";
const char* LOC_TOPIC  = "testmqtt/gps/alice/tracker1";
const char* STAT_TOPIC = "testmqtt/gps/alice/tracker1/status";

TinyGPSPlus gps;
HardwareSerial gpsSerial(2);
WiFiClientSecure net;
PubSubClient mqtt(net);

double lastLat = 0, lastLon = 0;
unsigned long lastSend = 0;

time_t fixEpoch() {
  struct tm t = {};
  t.tm_year = gps.date.year() - 1900;
  t.tm_mon  = gps.date.month() - 1;
  t.tm_mday = gps.date.day();
  t.tm_hour = gps.time.hour();
  t.tm_min  = gps.time.minute();
  t.tm_sec  = gps.time.second();
  return mktime(&t);  // TZ is set to UTC in setup()
}

void connectMqtt() {
  while (!mqtt.connected()) {
    String id = "gps-" + String((uint32_t)ESP.getEfuseMac(), HEX);
    // Last Will: the broker publishes "offline" if the tracker vanishes
    if (mqtt.connect(id.c_str(), MQTT_USER, MQTT_PASS, STAT_TOPIC, 1, true, "offline")) {
      mqtt.publish(STAT_TOPIC, "online", true);
    } else {
      Serial.printf("MQTT connect failed, state=%d\n", mqtt.state());
      delay(5000);
    }
  }
}

void publishFix() {
  char payload[256];
  snprintf(payload, sizeof(payload),
    "{\"_type\":\"location\",\"tid\":\"t1\",\"lat\":%.6f,\"lon\":%.6f,"
    "\"alt\":%d,\"vel\":%d,\"cog\":%d,\"acc\":%d,\"tst\":%lu}",
    gps.location.lat(), gps.location.lng(),
    (int)gps.altitude.meters(), (int)gps.speed.kmph(), (int)gps.course.deg(),
    (int)(gps.hdop.hdop() * 5),           // rough heuristic, not a real accuracy
    (unsigned long)fixEpoch());
  mqtt.publish(LOC_TOPIC, payload, true);  // retained
}

void setup() {
  Serial.begin(115200);
  gpsSerial.begin(9600, SERIAL_8N1, 16, 17);
  setenv("TZ", "UTC0", 1);
  tzset();

  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) delay(500);

  net.setInsecure();  // first test only, use net.setCACert(rootCA) in production
  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setBufferSize(512);
  mqtt.setKeepAlive(30);
}

void loop() {
  while (gpsSerial.available()) gps.encode(gpsSerial.read());
  if (!mqtt.connected()) connectMqtt();
  mqtt.loop();

  if (!gps.location.isValid() || !gps.date.isValid() || gps.location.age() > 2000) return;

  double moved = TinyGPSPlus::distanceBetween(
      gps.location.lat(), gps.location.lng(), lastLat, lastLon);
  unsigned long now = millis();

  // publish on movement (> 25 m) or every 60 s as a heartbeat
  if (moved > 25 || now - lastSend > 60000) {
    publishFix();
    lastLat = gps.location.lat();
    lastLon = gps.location.lng();
    lastSend = now;
  }
}

Korunmaya değer birkaç tasarım kararı:

  • Mesafeye göre bildirim. Her saniye yayın yapmak park halindeyken veri israf eder; dakikada bir yayın yapmak ise hareket halinde izi kırık kırık gösterir. Mesafe artı heartbeat ikisini de sağlar.
  • Durum topic'inde Last Will. Güç veya kapsama kesilirse broker yaklaşık 1,5 × keep alive sonra offline yayınlar. Harita işaretçiyi griye çevirebilir. Ayrıntılar için Last Will and Testament.
  • Baştan itibaren TLS. setInsecure() masanızda test için yeterlidir; gerçek cihazlarda broker'ın kök CA'sını setCACert() ile yükleyin. Konum verisi kişisel veridir.
  • Bağlantı döngüsü çevrimdışıyken bloklar ve NMEA baytları UART tamponunda birikir. Üretimdeki bir cihazda bloklamadan yeniden bağlanın ve izde boşluk olmaması için konumları tamponlayın.

Donanımsız test: bir Python simülatörü

Hiçbir şey lehimlemeden önce bir takip cihazını simüle edin. Bu script (paho-mqtt 2.x) Berlin'de bir rota boyunca ilerler ve firmware ile aynı payload'u aynı topic'lere yayınlar:

gps_sim.py
import json, os, time
import paho.mqtt.client as mqtt

HOST, PORT = "broker.connectmq.com", 8883
TOPIC = "testmqtt/gps/alice/sim1"
STATUS = TOPIC + "/status"
ROUTE = [(52.5200, 13.4050), (52.5163, 13.3777), (52.5096, 13.3759), (52.5074, 13.3904)]

def steps(a, b, n=10):
    for i in range(n):
        f = i / n
        yield a[0] + (b[0] - a[0]) * f, a[1] + (b[1] - a[1]) * f

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="gps-sim-1")
if os.getenv("MQTT_USER"):
    client.username_pw_set(os.environ["MQTT_USER"], os.getenv("MQTT_PASS"))
client.tls_set()
client.will_set(STATUS, "offline", qos=1, retain=True)
client.connect(HOST, PORT, keepalive=30)
client.loop_start()
client.publish(STATUS, "online", qos=1, retain=True)

for a, b in zip(ROUTE, ROUTE[1:]):
    for lat, lon in steps(a, b):
        fix = {"_type": "location", "tid": "s1", "lat": round(lat, 6),
               "lon": round(lon, 6), "acc": 8, "vel": 30, "tst": int(time.time())}
        client.publish(TOPIC, json.dumps(fix), qos=1, retain=True)
        print("sent", fix["lat"], fix["lon"])
        time.sleep(2)

client.publish(STATUS, "offline", qos=1, retain=True).wait_for_publish()
client.disconnect()
client.loop_stop()

Mesajların gelişini bir terminalden izleyin:

Tüm takip cihazlarına abone olun
mosquitto_sub -h broker.connectmq.com -p 8883 --capath /etc/ssl/certs \
  -u "$MQTT_USER" -P "$MQTT_PASS" -t 'testmqtt/gps/#' -v

Ya da online MQTT istemcisini açıp testmqtt/gps/# topic'ine abone olun. Konum retained olduğu için simülatör durduktan sonra bağlanan bir abone de son konumu alır.

Tarayıcıda canlı harita

Tarayıcılar MQTT'yi WebSocket üzerinden konuşur. Bu statik sayfa OpenStreetMap karolarıyla Leaflet'i ve 8084 portunda (wss, yol /mqtt) MQTT.js 5'i kullanır:

map.html
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>MQTT GPS map</title>
  <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 map = L.map('map').setView([52.52, 13.40], 13);
  L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
    attribution: '&copy; OpenStreetMap contributors'
  }).addTo(map);

  const markers = {}, trails = {};
  const client = mqtt.connect('wss://broker.connectmq.com:8084/mqtt', {
    username: 'username',   // a read-only map user
    password: 'password',
  });

  client.on('connect', () => client.subscribe('testmqtt/gps/+/+', { qos: 1 }));
  client.on('message', (topic, payload) => {
    let p;
    try { p = JSON.parse(payload.toString()); } catch { return; }
    if (p._type !== 'location') return;
    const pos = [p.lat, p.lon];
    if (!markers[topic]) {
      markers[topic] = L.marker(pos).addTo(map).bindTooltip(p.tid || topic);
      trails[topic] = L.polyline([], { weight: 3 }).addTo(map);
    }
    markers[topic].setLatLng(pos);
    trails[topic].addLatLng(pos);
    map.panTo(pos);
  });
</script>
</body>
</html>

testmqtt/gps/+/+ prefix'in altında tam olarak iki seviyeyle eşleşir; yani konumları alır ama bir seviye daha derindeki /status topic'lerini almaz. Çevrimdışı cihazları griye çevirmek istiyorsanız onlara ayrıca abone olun. Geçmiş ve grafikler içeren daha kapsamlı bir panel için canlı MQTT dashboard'u oluşturma yazısına bakın.

Güvenlik ve gizlilik

Bir konum akışı, bir kişinin nerede uyuduğunu, çalıştığını ve seyahat ettiğini gösterir. Buna göre davranın:

  1. Cihaz başına bir kullanıcı ve yalnızca kendi .../<user>/<device> topic'lerine yayına izin veren bir ACL.
  2. Haritalar ve dashboard'lar için, gösterebileceği kullanıcılarla sınırlı salt okunur bir kullanıcı.
  3. Her yerde TLS: cihazlar için 8883 portu, tarayıcılar için 8084 (wss).
  4. Herkese wildcard yok. # ile abone olan biri broker'daki tüm takip cihazlarını görür.
  5. Saklanan geçmiş için bir saklama politikası. Bkz. MQTT ve KVKK/GDPR.

Test kontrol listesi

  • Simülatörü çalıştırın ve işaretçinin haritada hareket ettiğini doğrulayın.
  • Harita sayfasını yenileyin: son konum anında görünmelidir (retained mesaj).
  • Simülatörü Ctrl+C ile durdurun: yaklaşık 1,5 × keep alive sonra durum topic'i offline olmalıdır (Last Will).
  • Haritaya cihaz kullanıcısıyla girip başka bir topic'e yayın yapmayı deneyin: broker bunu reddetmelidir.
  • Gerçek cihazı dışarı çıkarın ve uçtan uca gecikmenizi görmek için tst değerini alınma zamanıyla karşılaştırın.

Takip mesajlarını tarayıcıda izleyin

Bu şekilde kurulan bir takip cihazı tamamen anlaşılabilecek kadar küçüktür: birkaç düzine satır firmware, cihaz başına bir retained topic, erişilebilirlik için bir Last Will ve statik bir harita sayfası. Simülatör, harita ve ACL'ler beklendiği gibi çalıştığında gerçek donanıma geçin ve her şeyi kendi broker'ınıza yönlendirin.

Sıkça sorulan sorular

GPS takip cihazında neden HTTP yerine MQTT kullanılmalı?

Bir takip cihazı güvenilmez bağlantılar üzerinden sık sık küçük mesajlar gönderir. MQTT tek bir bağlantıyı açık tutar, mesaj başına yalnızca birkaç bayt ekler, son konumu yeni abonelere retained olarak saklayabilir ve kapanan bir cihazı Last Will mesajıyla bildirir. HTTP ise her konum için tüm başlıklarıyla yeni bir istek gerektirir.

OwnTracks JSON formatını kullanmak zorunda mıyım?

Hayır, herhangi bir JSON işinizi görür. OwnTracks location formatı (_type, lat, lon, tst, acc…) pratiktir çünkü OwnTracks uygulamaları, OwnTracks Recorder ve birçok akıllı ev entegrasyonu bu formatı zaten anlar; böylece kendi yaptığınız cihaz ile telefonlar aynı haritayı paylaşabilir.

Bir GPS takip cihazı ne sıklıkla yayın yapmalı?

Sabit bir saat yerine harekete göre yayınlayın: örneğin cihaz 25–50 metreden fazla hareket ettiğinde, ayrıca dururken her bir iki dakikada bir heartbeat. Böylece hareket halinde harita akıcı olur, park halindeyken de veri ve pil tasarrufu sağlarsınız.