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
GPS module ──NMEA──▶ ESP32 ──MQTT/TLS 8883──▶ broker ──WSS 8084──▶ browser map
│
└──▶ recorder / database (optional)| Parça | Görevi | Ne seçmeli |
|---|---|---|
| GPS modülü | UART üzerinden NMEA cümleleri gönderir | u-blox NEO-6M/M8N veya benzeri, 9600 baud |
| Cihaz | NMEA'yı ayrıştırır, ne zaman yayınlayacağına karar verir | ESP32 (Wi-Fi) veya ESP32 + LTE modem |
| Broker | Yönlendirir, son konumu saklar, ACL'leri uygular | TLS destekli herhangi bir MQTT 3.1.1/5 broker |
| Harita | WebSocket üzerinden abone olur, işaretçileri çizer | Statik bir sayfada Leaflet + MQTT.js |
| Depolama (isteğe bağlı) | Geçmişi tutar | OwnTracks 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:
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:
{
"_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.
#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
offlineyayı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:
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:
mosquitto_sub -h broker.connectmq.com -p 8883 --capath /etc/ssl/certs \
-u "$MQTT_USER" -P "$MQTT_PASS" -t 'testmqtt/gps/#' -vYa 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:
<!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: '© 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:
- Cihaz başına bir kullanıcı ve yalnızca kendi
.../<user>/<device>topic'lerine yayına izin veren bir ACL. - Haritalar ve dashboard'lar için, gösterebileceği kullanıcılarla sınırlı salt okunur bir kullanıcı.
- Her yerde TLS: cihazlar için 8883 portu, tarayıcılar için 8084 (
wss). - Herkese wildcard yok.
#ile abone olan biri broker'daki tüm takip cihazlarını görür. - 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
offlineolmalı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
tstdeğ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.

