Uygulamalı Eğitimler

Arduino ile ESP32 MQTT Eğitimi (PubSubClient)

Arduino ve PubSubClient ile ESP32 MQTT: WiFi’ye ve broker’a bağlanın, sensör JSON’u publish edin, komutlara abone olun, güvenle yeniden bağlanın, TLS açın.

Güncellendi · 8 dk okuma

ESP32, MQTT projeleri için ilk tercih edilen mikrodenetleyicidir: dahili WiFi, TLS için bol RAM ve birinci sınıf Arduino desteği. Bu eğitimde PubSubClient kütüphanesiyle; sensör okumalarını JSON olarak publish eden, bir komut topic’ini dinleyen, kendi kendine yeniden bağlanan ve — son adımda — TLS’e geçen eksiksiz bir sketch oluşturuyoruz.

Neye ihtiyacınız var

  • Bir ESP32 geliştirme kartı ve bir USB kablosu.
  • Boards Manager üzerinden esp32 by Espressif Systems kart paketi kurulmuş Arduino IDE 2.x (veya PlatformIO).
  • Library Manager üzerinden kurulmuş, Nick O’Leary’nin geliştirdiği PubSubClient kütüphanesi.
  • Bir broker. Test için 1883 portundaki herkese açık HiveMQ broker’ını kullanıyoruz — demo için yeterli, ancak topic’leri herkes okuyabilir.

Topic düzeni

Düzenli bir topic yapısı cihazların yönetimini kolaylaştırır. Her kart, benzersiz bir cihaz kimliğine dayanan kendi önekini alır:

TopicYönAmaç
testmqtt/esp32/<id>/telemetryESP32’den broker’aHer 10 saniyede bir sensör JSON’u
testmqtt/esp32/<id>/statusESP32’den broker’aonline / offline (retained, Last Will)
testmqtt/esp32/<id>/cmdBroker’dan ESP32’yeled:on gibi komutlar

İsimlendirme kuralları için MQTT topic’leri ve wildcard’lar yazısına bakın.

Eksiksiz sketch

esp32_mqtt.ino
#include <WiFi.h>
#include <PubSubClient.h>

const char* WIFI_SSID     = "your-ssid";
const char* WIFI_PASSWORD = "your-wifi-password";

const char* MQTT_HOST = "broker.hivemq.com";
const uint16_t MQTT_PORT = 1883;
const char* MQTT_USER = nullptr;   // set for brokers that require auth
const char* MQTT_PASS = nullptr;

const int LED_PIN = 2;             // on-board LED on many dev boards

WiFiClient net;
PubSubClient mqtt(net);

char deviceId[24];
char topicTelemetry[64];
char topicStatus[64];
char topicCmd[64];

unsigned long lastReconnectAttempt = 0;
unsigned long lastPublish = 0;

void onMessage(char* topic, byte* payload, unsigned int length) {
  String msg;
  for (unsigned int i = 0; i < length; i++) msg += (char)payload[i];
  Serial.printf("Message on %s: %s\n", topic, msg.c_str());

  if (msg == "led:on")  digitalWrite(LED_PIN, HIGH);
  if (msg == "led:off") digitalWrite(LED_PIN, LOW);
}

void connectWiFi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  Serial.print("Connecting to WiFi");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.printf("\nWiFi connected, IP %s\n", WiFi.localIP().toString().c_str());
}

bool connectMqtt() {
  // Last Will: broker publishes "offline" (retained) if we drop unexpectedly
  bool ok = mqtt.connect(deviceId, MQTT_USER, MQTT_PASS,
                         topicStatus, 1, true, "offline");
  if (!ok) {
    Serial.printf("MQTT connect failed, state=%d\n", mqtt.state());
    return false;
  }
  Serial.println("MQTT connected");
  mqtt.publish(topicStatus, "online", true);
  mqtt.subscribe(topicCmd, 1);
  return true;
}

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);

  // Unique client ID from the chip's MAC address
  uint64_t mac = ESP.getEfuseMac();
  snprintf(deviceId, sizeof(deviceId), "esp32-%04X%08X",
           (uint16_t)(mac >> 32), (uint32_t)mac);
  snprintf(topicTelemetry, sizeof(topicTelemetry), "testmqtt/esp32/%s/telemetry", deviceId);
  snprintf(topicStatus, sizeof(topicStatus), "testmqtt/esp32/%s/status", deviceId);
  snprintf(topicCmd, sizeof(topicCmd), "testmqtt/esp32/%s/cmd", deviceId);
  Serial.printf("Device ID: %s\n", deviceId);

  connectWiFi();

  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setCallback(onMessage);
  mqtt.setBufferSize(512);   // default is 256 bytes for the whole packet
  mqtt.setKeepAlive(30);
}

void loop() {
  if (WiFi.status() != WL_CONNECTED) {
    connectWiFi();
  }

  if (!mqtt.connected()) {
    // Non-blocking reconnect: try every 5 seconds without stalling loop()
    unsigned long now = millis();
    if (now - lastReconnectAttempt > 5000) {
      lastReconnectAttempt = now;
      if (connectMqtt()) lastReconnectAttempt = 0;
    }
    return;
  }

  mqtt.loop();   // must run often: handles keep-alive and incoming messages

  unsigned long now = millis();
  if (now - lastPublish > 10000) {
    lastPublish = now;
    float temperature = temperatureRead();   // ESP32 internal sensor (rough)
    char payload[128];
    snprintf(payload, sizeof(payload),
             "{\"device\":\"%s\",\"temp\":%.1f,\"rssi\":%d,\"uptime\":%lu}",
             deviceId, temperature, WiFi.RSSI(), now / 1000);
    bool sent = mqtt.publish(topicTelemetry, payload);
    Serial.printf("Publish %s: %s\n", sent ? "ok" : "FAILED", payload);
  }
}

Cihaz kimliğini görmek için Serial Monitor’ü 115200 baud’da açın, ardından testmqtt/esp32/# topic’ine abone olarak telemetrinin tarayıcıya ulaşmasını izleyin. cmd topic’ine led:on publish ettiğinizde LED yanar.

ESP32 telemetrinizi canlı izleyin

Sketch nasıl çalışır

Benzersiz client ID’leri

MQTT broker’ları her client ID’si için yalnızca bir bağlantıya izin verir. İki kart ESP32Client gibi sabit kodlanmış aynı ID ile yüklenirse, sonsuz bir döngü içinde birbirlerinin bağlantısını koparırlar. ID’yi eFuse MAC adresinden türetmek, onu benzersiz ve yeniden başlatmalar arasında sabit tutar. Farklı bir şema istiyorsanız client ID üreteci kuralları açıklar.

Bloklamayan bir yeniden bağlanma döngüsü

Pek çok örnek delay(5000) içeren bir while (!client.connected()) döngüsü kullanır. Bu, karttaki diğer her şeyi bloklar. Yukarıdaki millis() deseni, loop()’un geri kalanı çalışmaya devam ederken her beş saniyede bir yeniden dener. mqtt.state() bir bağlantının neden başarısız olduğunu söyler: -2 ağ hatası, 4 hatalı kimlik bilgileri ve 5 yetkisiz anlamına gelir. Tam listeyle karşılaştırmak için MQTT bağlantı hataları rehberimize bakın.

Buffer boyutu ve MQTT_MAX_PACKET_SIZE

PubSubClient’ın buffer’ı paketin tamamını tutmalıdır: sabit başlık, topic ve payload. Varsayılan 256 bayttır ve uzun bir topic ile birlikte bir JSON payload’u bunu hızla aşar — bu durumda publish() sessizce false döndürür. Eski eğitimler kütüphane başlık dosyasındaki MQTT_MAX_PACKET_SIZE değerini düzenlemenizi söyler; 2.8 sürümünden bu yana bağlanmadan önce setBufferSize() çağırmanız yeterlidir. MQTT paket boyutu hesaplayıcı paketlerinizin gerçekte ne kadar büyük olduğunu gösterir.

PubSubClient sınırlamaları

  • Yalnızca MQTT 3.1.1 — MQTT 5 özellikleri yok.
  • Yalnızca QoS 0 ile publish eder; abonelikler QoS 0 veya 1 kullanabilir.
  • Yukarıda gösterildiği gibi retained publish’ler ve Last Will desteklenir.

Last Will ve retained mesajlarla çevrimiçi durumu

connect() çağrısı bir Last Will kaydeder: kartın gücü veya WiFi’si temiz bir bağlantı kesme olmadan giderse, broker keep-alive zaman aşımından sonra status topic’ine offline publish eder. Sketch, bağlandıktan hemen sonra aynı topic’e online publish eder. Her ikisi de retained olduğundan, daha sonra abone olan bir dashboard da güncel durumu anında görür. Bu desen Last Will and Testament ve retained mesajlar yazılarında açıklanmıştır.

Komut satırından test

Komut topic’ini test etmek için ikinci bir cihaza ihtiyacınız yok. Mosquitto client’ları kuruluysa bir komut gönderin ve telemetriyi izleyin (ID’yi Serial Monitor’de yazdırılanla değiştirin):

bash
# watch everything your board publishes
mosquitto_sub -h broker.hivemq.com -t 'testmqtt/esp32/#' -v

# switch the LED on
mosquitto_pub -h broker.hivemq.com -t 'testmqtt/esp32/esp32-ABCD12345678/cmd' -m 'led:on'

Daha fazla komut satırı tarifi için mosquitto_pub ve mosquitto_sub örnekleri yazısına bakın.

ArduinoJson ile JSON oluşturma

Birkaç alan için snprintf yeterlidir; ancak iç içe veriler veya JSON komutlarını ayrıştırmak için ArduinoJson kütüphanesi (sürüm 7) daha güvenlidir:

cpp
#include <ArduinoJson.h>

void publishTelemetry() {
  JsonDocument doc;
  doc["device"] = deviceId;
  doc["temp"] = temperatureRead();
  doc["rssi"] = WiFi.RSSI();

  char payload[256];
  size_t n = serializeJson(doc, payload, sizeof(payload));
  mqtt.publish(topicTelemetry, (const uint8_t*)payload, n, false);
}

Payload’ları küçük ve düz tutun: string’ler yerine kısa anahtarlar ve sayılar RAM’den, yayın süresinden ve broker bant genişliğinden tasarruf sağlar.

State kodlarıyla sorun giderme

mqtt.state()AnlamıTipik çözüm
-4Bağlantı zaman aşımıKeep-alive değerini ve loop()’un sık çalıştığını kontrol edin
-2Ağ bağlantısı başarısızYanlış host veya port, güvenlik duvarı ya da TLS handshake hatası
2Client ID reddedildiDaha kısa veya benzersiz bir client ID kullanın
4Hatalı kullanıcı adı veya parolaBüyük/küçük harf dahil kimlik bilgilerini kontrol edin
5YetkisizBroker’ın erişim kuralları bu client’ı reddediyor

WiFiClientSecure ile TLS ekleme

Düz 1883 portu kimlik bilgilerinizi ve verilerinizi açık metin olarak gönderir. 8883 portunda TLS’e geçmek üç değişiklik gerektirir: WiFiClientSecure.h’yi dahil edin, ağ client’ını değiştirin ve ona broker’ınızın CA sertifikasını verin:

TLS değişiklikleri
#include <WiFiClientSecure.h>

// PEM of the root CA that signed your broker's certificate
static const char ROOT_CA[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
...paste the root CA certificate here...
-----END CERTIFICATE-----
)EOF";

WiFiClientSecure net;
PubSubClient mqtt(net);

const uint16_t MQTT_PORT = 8883;

// in setup(), before mqtt.setServer():
net.setCACert(ROOT_CA);
// net.setInsecure();  // skips verification - local testing only!

Birçok herkese açık broker, kökü ISRG Root X1 olan Let’s Encrypt sertifikalarını kullanır; doğru kök sertifika için broker’ınızın dokümantasyonuna bakın. Bazı kurulumlarda TLS, sertifika geçerlilik kontrolleri için doğru saate de ihtiyaç duyar ve belirgin biçimde daha fazla RAM kullanır — ESP32 bunu rahatça kaldırır, ESP8266 zorlanır. Resmin tamamı için MQTT güvenliği en iyi uygulamaları yazımızı okuyun.

Bu sketch’i broker’ınızın host, port ve kimlik bilgileri önceden doldurulmuş hâlde mi istiyorsunuz? MQTT kod üreteci onu kendi broker ayarlarınız için oluşturur.

Sıkça sorulan sorular

PubSubClient neden 256 bayttan büyük mesajları publish etmiyor?

PubSubClient varsayılan olarak 256 baytlık bir buffer kullanır ve bu buffer, topic dahil MQTT paketinin tamamını tutmak zorundadır. Paket sığmadığında publish() false döndürür. Bağlanmadan önce client.setBufferSize(1024) veya daha büyük bir değerle çağırın.

ESP32’m neden MQTT broker’ından sürekli kopuyor?

En yaygın nedenler; başka bir cihazın da kullandığı bir client ID’si, client.loop()’un çalışmasını engelleyip keep-alive ping’lerinin kaçırılmasına yol açan uzun delay() çağrıları ve zayıf WiFi’dir. Her cihaz için benzersiz bir client ID kullanın ve loop()’u bloklamayan şekilde tutun.

PubSubClient QoS 1 ve MQTT 5’i destekliyor mu?

PubSubClient yalnızca MQTT 3.1.1 konuşur. QoS 0 veya 1 ile abone olabilir, ancak yalnızca QoS 0 ile publish eder. QoS 1 ile publish etmeniz veya MQTT 5 özelliklerine ihtiyacınız varsa AsyncMqttClient gibi kütüphanelere ya da ESP-IDF esp-mqtt bileşenine bakın.