Bu ESP32-CAM MQTT eğitimi, bir AI-Thinker ESP32-CAM üzerinde JPEG görüntülerin nasıl yakalanacağını ve her karenin ham ikili (binary) bir MQTT payload'ı olarak nasıl yayınlanacağını gösterir. Sonunda bir zamanlayıcıyla veya komutla fotoğraf gönderen bir kameraya, her görüntüyü diske kaydeden bir Python betiğine ve bir karenin ulaşıp ulaşmayacağını belirleyen mesaj boyutu sınırlarına dair net bir anlayışa sahip olacaksınız.
Kamera Görüntüleri Neden MQTT Üzerinden Gönderilir?
MQTT genellikle küçük sensör ölçümleriyle ilişkilendirilir, ancak protokolün kendisi bir payload'ın içinde ne olduğunu umursamaz. Bir JPEG yalnızca bir bayt dizisidir ve spesifikasyon, küçük bir kameranın üreteceği herhangi bir görüntüden çok daha büyük payload'lara izin verir. Görüntüleri telemetrinizle aynı broker üzerinden göndermenin gerçek avantajları vardır:
- Tek bağlantı, tek güvenlik modeli. Kamera, diğer tüm cihazlarla aynı kimlik bilgilerini, TLS ayarlarını ve topic kurallarını kullanır.
- Olay güdümlü yakalama. Bir hareket sensörü, bir kapı kontağı veya bir dashboard düğmesi bir komut yayınlayabilir ve kamera bir kareyle yanıt verir. Yoklama yok, cihazda HTTP sunucusu yok.
- Ücretsiz dağıtım (fan-out). Birkaç subscriber (bir arşiv betiği, bir dashboard, bir uyarı akışı) kamera ekstra iş yapmadan aynı kareyi alabilir.
Yine de MQTT bir video protokolü değildir. Birkaç saniyede bir veya olaylarla alınan anlık görüntüler için uygundur, sürekli video akışı için ise uygun değildir. Canlı videoya ihtiyacınız varsa özel bir akış protokolü kullanın ve MQTT'yi kontrol ve meta veriler için saklayın.
Gereken Donanım ve Yazılım
| Öğe | Notlar |
|---|---|
| OV2640'lı AI-Thinker ESP32-CAM | En yaygın modül; aşağıdaki pin haritası bu kart içindir |
| USB-seri adaptör (FTDI veya ESP32-CAM-MB) | Yükleme sırasında GPIO0 GND'ye çekilmelidir |
| Kararlı 5 V besleme, en az 1 A önerilir | Yakalama sırasındaki voltaj düşmeleri sıfırlamaların bir numaralı nedenidir |
| esp32 kart paketiyle Arduino IDE 2.x | Kart: "AI Thinker ESP32-CAM" |
| Nick O'Leary'nin PubSubClient kütüphanesi | Library Manager'dan kurulur |
| paho-mqtt 2.x ile Python 3 | Alıcı betik için |
Daha önce hiç bir ESP32'yi bir broker'a bağlamadıysanız ESP32 IoT eğitimi Wi-Fi, PubSubClient temelleri ve yeniden bağlanma mantığını daha ayrıntılı ele alır.
Asıl Kısıt Mesaj Boyutudur
Herhangi bir kod yazmadan önce bir çözünürlüğe karar verin; çünkü JPEG'in boyutu onu hangi broker'a gönderebileceğinizi ve ne kadar RAM'e ihtiyacınız olduğunu belirler.
| Yayın yaptığınız yer | Maksimum mesaj boyutu |
|---|---|
| broker.connectmq.com herkese açık test alanı | 64 KB |
| Serverless (Free) plan | 256 KB |
| Serverless (Paid) plan | 1 MB |
JPEG boyutu çözünürlüğe, jpeg_quality ayarına (düşük sayı daha yüksek kalite ve daha büyük dosya demektir) ve sahnenin kendisine bağlıdır. Hareketli, ayrıntılı bir sahne beyaz bir duvardan daha kötü sıkıştırılır. Pratik başlangıç noktaları:
FRAMESIZE_QVGA(320x240), 64 KB'ın altına kolayca sığan küçük kareler üretir; herkese açık MQTT test broker'ı üzerinde test için idealdir.FRAMESIZE_VGA(640x480) izleme için iyi bir uzlaşmadır ve genellikle ücretsiz plana sığar.FRAMESIZE_SVGAve üzeri 64 KB'ı aşabilir; bunlara güvenmeden önce seri monitördefb->lendeğerini kontrol edin.
Aşağıdaki sketch kare uzunluğunu bir sınırla karşılaştırır ve broker'ın bağlantıyı düşürmesine izin vermek yerine fazla büyük kareleri atlar.
PubSubClient ile Büyük Payload'ları Akış Halinde Göndermek
PubSubClient normalde topic'i ve payload'ı varsayılan olarak 256 bayt olan dahili bir tampona kopyalar. Bunu setBufferSize() ile artırabilirsiniz, ancak kamera kare tamponunun üzerine 50 KB'lık bir tampon ayırmak RAM israfıdır. Daha iyi yaklaşım akış API'sidir:
beginPublish(topic, length, retained), MQTT başlığını nihai uzunlukla yazar.write(buffer, length), JPEG baytlarını doğrudan kamera kare tamponundan gönderir.endPublish()paketi tamamlar.
Payload hiçbir zaman kopyalanmaz; bu yüzden büyük kareler bile yalnızca küçük bir MQTT tamponuna ihtiyaç duyar. PubSubClient'ın QoS 0 ile yayın yaptığını unutmayın; bu anlık görüntüler için sorun değildir: biri kaybolursa bir sonraki birkaç saniye uzaklıktadır. Genel olarak tampon ayarı hakkında daha fazla bilgi için PubSubClient tampon boyutu yazısına bakın.
ESP32-CAM MQTT Sketch'i
Topic düzeni, karelerinizin herkese açık broker'daki başka kimseninkilerle çakışmaması için benzersiz bir önek kullanır:
| Topic | Yön | Payload |
|---|---|---|
connectmq/demo/cam-7f3a/snapshot |
kamera yayınlar | ham JPEG baytları |
connectmq/demo/cam-7f3a/status |
kamera yayınlar, retained | online / offline (Last Will) |
connectmq/demo/cam-7f3a/cmd |
kamera subscribe olur | capture, flash_on, flash_off |
cam-7f3a ifadesini kendi rastgele string'inizle değiştirin.
#include <WiFi.h>
#include <PubSubClient.h>
#include "esp_camera.h"
// ---------- Wi-Fi ve MQTT ayarları ----------
const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 1883;
const char* CLIENT_ID = "cam-7f3a-esp32"; // boş olmamalı ve benzersiz olmalı
const char* TOPIC_SNAP = "connectmq/demo/cam-7f3a/snapshot";
const char* TOPIC_STATUS = "connectmq/demo/cam-7f3a/status";
const char* TOPIC_CMD = "connectmq/demo/cam-7f3a/cmd";
const size_t MAX_PAYLOAD = 60 * 1024; // herkese açık 64 KB sınırının altında kal
const uint32_t CAPTURE_INTERVAL = 30000; // her 30 sn'de zamanlanmış görüntü
const int FLASH_LED_PIN = 4;
// ---------- AI-Thinker ESP32-CAM pin haritası ----------
#define PWDN_GPIO_NUM 32
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 0
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27
#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 21
#define Y4_GPIO_NUM 19
#define Y3_GPIO_NUM 18
#define Y2_GPIO_NUM 5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22
WiFiClient net;
PubSubClient mqtt(net);
uint32_t lastCapture = 0;
volatile bool captureRequested = false;
bool initCamera() {
camera_config_t config = {};
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = Y2_GPIO_NUM; config.pin_d1 = Y3_GPIO_NUM;
config.pin_d2 = Y4_GPIO_NUM; config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM; config.pin_d5 = Y7_GPIO_NUM;
config.pin_d6 = Y8_GPIO_NUM; config.pin_d7 = Y9_GPIO_NUM;
config.pin_xclk = XCLK_GPIO_NUM;
config.pin_pclk = PCLK_GPIO_NUM;
config.pin_vsync = VSYNC_GPIO_NUM;
config.pin_href = HREF_GPIO_NUM;
config.pin_sccb_sda = SIOD_GPIO_NUM;
config.pin_sccb_scl = SIOC_GPIO_NUM;
config.pin_pwdn = PWDN_GPIO_NUM;
config.pin_reset = RESET_GPIO_NUM;
config.xclk_freq_hz = 20000000;
config.pixel_format = PIXFORMAT_JPEG;
config.frame_size = FRAMESIZE_VGA; // kareler çok büyükse QVGA deneyin
config.jpeg_quality = 12; // 10-63, düşük = daha iyi kalite, daha büyük dosya
config.fb_count = 1;
config.fb_location = CAMERA_FB_IN_PSRAM;
config.grab_mode = CAMERA_GRAB_LATEST;
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK) {
Serial.printf("Camera init failed: 0x%x\n", err);
return false;
}
return true;
}
void onMessage(char* topic, byte* payload, unsigned int length) {
String cmd;
for (unsigned int i = 0; i < length; i++) cmd += (char)payload[i];
cmd.trim();
if (cmd == "capture") captureRequested = true;
else if (cmd == "flash_on") digitalWrite(FLASH_LED_PIN, HIGH);
else if (cmd == "flash_off") digitalWrite(FLASH_LED_PIN, LOW);
}
void connectWifi() {
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.printf("\nWi-Fi OK, IP %s\n", WiFi.localIP().toString().c_str());
}
void connectMqtt() {
while (!mqtt.connected()) {
Serial.print("Connecting to MQTT... ");
// Last Will: kamera kaybolursa broker "offline" (retained) yayınlar
if (mqtt.connect(CLIENT_ID, TOPIC_STATUS, 1, true, "offline")) {
Serial.println("connected");
mqtt.publish(TOPIC_STATUS, "online", true);
mqtt.subscribe(TOPIC_CMD);
} else {
Serial.printf("failed, state=%d, retrying in 5 s\n", mqtt.state());
delay(5000);
}
}
}
void publishSnapshot() {
camera_fb_t* fb = esp_camera_fb_get();
if (!fb) {
Serial.println("Capture failed");
return;
}
if (fb->len > MAX_PAYLOAD) {
Serial.printf("Frame %u bytes exceeds limit, skipped\n", (unsigned)fb->len);
esp_camera_fb_return(fb);
return;
}
bool ok = mqtt.beginPublish(TOPIC_SNAP, fb->len, false);
if (ok) {
size_t sent = mqtt.write(fb->buf, fb->len);
ok = mqtt.endPublish() && sent == fb->len;
}
Serial.printf("Snapshot %u bytes -> %s\n", (unsigned)fb->len, ok ? "sent" : "FAILED");
esp_camera_fb_return(fb);
}
void setup() {
Serial.begin(115200);
pinMode(FLASH_LED_PIN, OUTPUT);
digitalWrite(FLASH_LED_PIN, LOW);
if (!initCamera()) {
delay(5000);
ESP.restart();
}
connectWifi();
mqtt.setServer(MQTT_HOST, MQTT_PORT);
mqtt.setCallback(onMessage);
mqtt.setKeepAlive(30);
mqtt.setSocketTimeout(10); // büyük yazmalar varsayılandan fazlasını gerektirir
connectMqtt();
}
void loop() {
if (WiFi.status() != WL_CONNECTED) connectWifi();
if (!mqtt.connected()) connectMqtt();
mqtt.loop();
uint32_t now = millis();
if (captureRequested || now - lastCapture >= CAPTURE_INTERVAL) {
captureRequested = false;
lastCapture = now;
publishSnapshot();
}
}
Kartı GPIO0 GND'ye bağlıyken yükleyin, ardından jumper'ı çıkarın ve reset'e basın. Seri monitör IP adresini, başarılı bir MQTT bağlantısını ve her görüntü için bayt cinsinden boyutunu içeren bir satır göstermelidir.
Görüntüleri Python ile Almak ve Kaydetmek
Bilgisayarınızda bu betik snapshot topic'ine subscribe olur ve her kareyi zaman damgalı bir dosyaya yazar. Ayrıca istek üzerine nasıl görüntü isteneceğini de gösterir.
import time
from datetime import datetime
from pathlib import Path
import paho.mqtt.client as mqtt
PREFIX = "connectmq/demo/cam-7f3a"
OUT = Path("snapshots")
OUT.mkdir(exist_ok=True)
def on_connect(client, userdata, flags, reason_code, properties):
print("Connected:", reason_code)
client.subscribe(f"{PREFIX}/snapshot", qos=0)
client.subscribe(f"{PREFIX}/status", qos=1)
def on_message(client, userdata, msg):
if msg.topic.endswith("/status"):
print("Camera status:", msg.payload.decode())
return
if not msg.payload.startswith(b"\xff\xd8"):
print("Payload is not a JPEG, ignored")
return
name = OUT / f"{datetime.now():%Y%m%d-%H%M%S-%f}.jpg"
name.write_bytes(msg.payload)
print(f"Saved {name} ({len(msg.payload)} bytes)")
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="cam-7f3a-viewer")
client.on_connect = on_connect
client.on_message = on_message
client.connect("broker.connectmq.com", 1883, keepalive=60)
client.loop_start()
time.sleep(2)
client.publish(f"{PREFIX}/cmd", "capture", qos=1) # hemen bir kare iste
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
client.loop_stop()
client.disconnect()
FF D8 baytlarının (JPEG görüntü başlangıç işareti) kontrolü, başka biri topic'inize metin yayınlarsa betiğin anlamsız veri kaydetmesini önler. Komut satırından da görüntü tetikleyebilirsiniz:
mosquitto_pub -h broker.connectmq.com -p 1883 -i cam-7f3a-cli -t connectmq/demo/cam-7f3a/cmd -m capture
ESP32-CAM MQTT Kurulumunu Production'a Taşımak
Herkese açık alan herkes tarafından okunabilir; bu yüzden onu kullanırken kamerayı asla özel bir şeye doğrultmayın. Gerçek bir kurulum için bir yönetilen MQTT broker planında broker oluşturun ve sketch'i TLS'e geçirin:
- ConnectMQ'ya kaydolun, bir broker oluşturun ve otomatik oluşturulan kullanıcı adını, parolayı ve client ID'yi kopyalayın.
WiFiClientyerineWiFiClientSecurekullanın, ISRG Root X1 sertifikasınısetCACert()ile yükleyin ve portu 8883 yapın.mqtt.connect("YOUR_CLIENT_ID", "YOUR_USERNAME", "YOUR_PASSWORD", TOPIC_STATUS, 1, true, "offline")çağırın. ConnectMQ client ID'leri verilen kimlik bilgilerine bağlar; bu yüzden portaldakini kullanın.MAX_PAYLOADdeğerini planınıza göre artırın; örneğin ücretsiz Serverless planında 256 KB'ın hemen altına.
TLS, kamera tamponlarının üzerine ek RAM kullanımı getirir. AI-Thinker modülünde kare tamponlarını dahili belleğin dışında tutan PSRAM bulunur, ancak bellek ayırma hataları görürseniz tek bir kare tamponuna ve daha küçük bir kare boyutuna inin. ESP32 MQTT SSL rehberi sertifika yüklemeyi adım adım anlatır.
Sorun Giderme Kontrol Listesi
- Açılışta veya yakalamada "Brownout detector was triggered". Sağlam bir 5 V besleme ve kısa kablolar kullanın; kamera ve Wi-Fi radyosu akım tepelerini birlikte çeker.
Camera init failed: 0x105veya benzeri. Şerit kablo gevşek veya pin haritası kart varyantınızla eşleşmiyor.- Görüntü "FAILED" ama küçük mesajlar çalışıyor. Kare broker sınırını aşıyor veya soket yazma sırasında zaman aşımına uğradı.
frame_sizedeğerini düşürün,jpeg_qualitysayısını artırın veyasetSocketTimeout()değerini yükseltin. - Kamera büyük bir yayından hemen sonra bağlantıyı kesiyor. Herkese açık broker'da 64 KB'tan büyük bir mesaj reddedilir. Seri logda yazdırılan boyutu kontrol edin.
- Görüntüler başta yeşil veya karanlık görünüyor. OV2640'ın otomatik pozlamayı oturtması için birkaç kareye ihtiyacı vardır; açılıştan sonra bir kare yakalayıp atın.
- Wi-Fi koptuktan sonra yeniden bağlanma döngüleri. Backoff ekleyin ve dashboard'ların kamerayı çevrimdışı göstermesi için Last Will'i koruyun; ESP32 MQTT yeniden bağlanma yazısına bakın.
Kamera Topic'leri İçin Tasarım İpuçları
İkili kareleri kendi topic'lerinde tutun ve meta verileri ayrı gönderin. Bir snapshot/meta topic'inde boyut, çözünürlük ve tetikleme nedenini içeren küçük bir JSON mesajı, dashboard'ların görüntüyü indirmeden tepki vermesini sağlar. "Son fotoğraf" davranışına ihtiyacınız yoksa paylaşımlı broker'larda görüntü mesajlarını retained yapmaktan kaçının; çünkü her yeni subscriber tam JPEG'i anında alır. Kareleri birden fazla servis tüketiyorsa bunu MQTT fan-out'u halleder; bir bulut MQTT broker da bu altyapıyı kendiniz çalıştırma ihtiyacını ortadan kaldırır.
ConnectMQ ile Başlayın
Kameranızı önce ücretsiz herkese açık alanda test edin, görüntüler önem kazandığında özel bir broker'a geçin. ConnectMQ size bir TLS uç noktası, otomatik oluşturulan kimlik bilgileri ve görüntü trafiğinin gelişini izlemek için canlı bir topic gezgini sunar. app.connectmq.com adresinde ücretsiz Serverless planında kredi kartı gerektirmeden bir broker oluşturun ve sketch'teki host ile kimlik bilgilerini değiştirin.
Sık Sorulan Sorular
Bir ESP32-CAM, MQTT üzerinden video akışı yapabilir mi?
Kareleri hızlı art arda gönderebilir, ancak MQTT video için tasarlanmamıştır ve paylaşımlı broker'larda hız sınırları geçerlidir. Anlık görüntüler ve olayla tetiklenen yakalamalar için MQTT'yi, canlı video için bir akış protokolünü kullanın.
Yayınlayabileceğim en büyük görüntü nedir?
Broker'a bağlıdır: herkese açık test alanında 64 KB, Serverless (Free) planında 256 KB ve Serverless (Paid) planında 1 MB. fb->len değerini kontrol edin ve sınırınızın altında kalan bir kare boyutu seçin.
Görüntüler için PubSubClient tamponunu artırmam gerekir mi?
beginPublish(), write() ve endPublish() kullanıyorsanız hayır. Bu çağrılar kareyi doğrudan akış halinde gönderir; bu yüzden başlık için varsayılan tampon yeterlidir.
Görüntü mesajları retained olmalı mı?
Genellikle hayır. Büyük bir ikili mesajı retained yapmak, her yeni subscriber'ın onu anında alması anlamına gelir; özellikle "en son görüntü" davranışı istemiyorsanız yalnızca küçük status topic'ini retained yapın.



