ESP32 und Arduino

ESP32-CAM MQTT: JPEG-Schnappschüsse aufnehmen und als binäre Payloads veröffentlichen

Machen Sie aus einem AI-Thinker ESP32-CAM eine MQTT-Kamera, die JPEG-Schnappschüsse auf Befehl oder per Timer veröffentlicht, innerhalb der Nachrichtengrenzen des Brokers bleibt und jedes Bild mit einem kleinen Python-Subscriber speichert.

AI-Thinker-ESP32-CAM-Board, das in einem ESP32-CAM-MQTT-Projekt einen JPEG-Schnappschuss an einen Cloud-Broker veröffentlicht
Probieren Sie es auf Ihrem eigenen MQTT-Broker ausHolen Sie sich im Free-Tarif einen privaten, verwalteten MQTT-Broker und starten Sie mit fertigen Codebeispielen.
Kostenlos testen Codebeispiele Keine Kreditkarte erforderlich

Dieses ESP32-CAM-MQTT-Tutorial zeigt, wie Sie auf einem AI-Thinker ESP32-CAM JPEG-Schnappschüsse aufnehmen und jedes Bild als rohe binäre MQTT-Payload veröffentlichen. Am Ende haben Sie eine Kamera, die per Timer oder auf Befehl ein Bild sendet, ein Python-Skript, das jedes Bild auf der Festplatte speichert, und ein klares Verständnis der Größengrenzen für Nachrichten, die darüber entscheiden, ob ein Bild überhaupt ankommt.

Warum Kamerabilder über MQTT senden?

MQTT wird meist mit winzigen Sensormesswerten verbunden, doch dem Protokoll selbst ist egal, was in einer Payload steckt. Ein JPEG ist nur ein Byte-Array, und die Spezifikation erlaubt Payloads, die weit größer sind als jeder Schnappschuss einer kleinen Kamera. Bilder über denselben Broker wie Ihre Telemetrie zu senden, hat echte Vorteile:

  • Eine Verbindung, ein Sicherheitsmodell. Die Kamera verwendet dieselben Zugangsdaten, TLS-Einstellungen und Topic-Konventionen wie jedes andere Gerät.
  • Ereignisgesteuerte Aufnahme. Ein Bewegungsmelder, ein Türkontakt oder eine Dashboard-Schaltfläche kann einen Befehl veröffentlichen, und die Kamera antwortet mit einem Bild. Kein Polling, kein HTTP-Server auf dem Gerät.
  • Fan-out inklusive. Mehrere Subscriber (ein Archivskript, ein Dashboard, ein Alarm-Flow) können dasselbe Bild empfangen, ohne dass die Kamera zusätzliche Arbeit leistet.

Dasselbe Muster findet sich in der Fertigung: Eine günstige Kamera an einer Montagelinie kann bei jedem Takt oder auf Anforderung der SPS einen Schnappschuss zur Sichtprüfung oder Dokumentation senden, etwa unter acme/werk-stuttgart/montage/linie-1/station-4/snapshot. Mehr zur KI-gestützten Auswertung zeigt der Beitrag zur KI-gestützten visuellen Inspektion.

MQTT ist allerdings kein Videoprotokoll. Es eignet sich gut für Schnappschüsse alle paar Sekunden oder bei Ereignissen und schlecht für kontinuierliches Video-Streaming. Wenn Sie Live-Video benötigen, verwenden Sie ein spezielles Streaming-Protokoll und behalten Sie MQTT für Steuerung und Metadaten.

Benötigte Hardware und Software

Komponente Hinweise
AI-Thinker ESP32-CAM mit OV2640 Das verbreitetste Modul; die folgende Pin-Belegung gilt für dieses Board
USB-Seriell-Adapter (FTDI oder ESP32-CAM-MB) GPIO0 muss beim Flashen auf GND gezogen werden
Stabile 5-V-Versorgung, mindestens 1 A empfohlen Spannungseinbrüche bei der Aufnahme sind die häufigste Ursache für Resets
Arduino IDE 2.x mit dem esp32-Board-Paket Board: „AI Thinker ESP32-CAM“
Bibliothek PubSubClient von Nick O'Leary Installation über den Bibliotheksverwalter
Python 3 mit paho-mqtt 2.x Für das Empfangsskript

Wenn Sie noch nie einen ESP32 mit einem Broker verbunden haben, behandelt das ESP32-IoT-Tutorial WLAN, PubSubClient-Grundlagen und Reconnect-Logik ausführlicher.

Die Nachrichtengröße ist die eigentliche Einschränkung

Bevor Sie Code schreiben, legen Sie eine Auflösung fest, denn die Größe des JPEG entscheidet, an welchen Broker Sie es senden können und wie viel RAM Sie brauchen.

Ziel der Veröffentlichung Maximale Nachrichtengröße
Öffentlicher MQTT-Broker broker.connectmq.com 64 KB
Serverless (Free) Plan 256 KB
Serverless (Paid) Plan 1 MB

Die JPEG-Größe hängt von der Auflösung, der Einstellung jpeg_quality (eine niedrigere Zahl bedeutet höhere Qualität und größere Dateien) und der Szene selbst ab. Eine unruhige, detailreiche Szene komprimiert schlechter als eine weiße Wand. Praktische Ausgangspunkte:

  • FRAMESIZE_QVGA (320x240) erzeugt kleine Bilder, die problemlos unter 64 KB bleiben – ideal zum Testen mit dem öffentlichen MQTT-Broker.
  • FRAMESIZE_VGA (640x480) ist ein guter Kompromiss für die Überwachung und passt meist in den kostenlosen Plan.
  • FRAMESIZE_SVGA und höher können 64 KB überschreiten; prüfen Sie fb->len im seriellen Monitor, bevor Sie sich darauf verlassen.

Der folgende Sketch prüft die Bildlänge gegen ein Limit und überspringt zu große Bilder, statt den Broker die Verbindung trennen zu lassen.

Große Payloads mit PubSubClient streamen

PubSubClient kopiert Topic und Payload normalerweise in einen internen Puffer, der standardmäßig 256 Bytes groß ist. Sie könnten ihn mit setBufferSize() vergrößern, doch einen 50-KB-Puffer zusätzlich zum Bildpuffer der Kamera zu reservieren, verschwendet RAM. Besser ist die Streaming-API:

  1. beginPublish(topic, length, retained) schreibt den MQTT-Header mit der endgültigen Länge.
  2. write(buffer, length) sendet die JPEG-Bytes direkt aus dem Bildpuffer der Kamera.
  3. endPublish() schließt das Paket ab.

Die Payload wird nie kopiert, sodass selbst große Bilder nur einen kleinen MQTT-Puffer benötigen. Beachten Sie, dass PubSubClient mit QoS 0 veröffentlicht, was für Schnappschüsse in Ordnung ist: Geht einer verloren, ist der nächste nur Sekunden entfernt. Mehr zur Puffereinstellung im Allgemeinen finden Sie unter PubSubClient-Puffergröße.

Der ESP32-CAM-MQTT-Sketch

Der Topic-Aufbau verwendet ein eindeutiges Präfix, damit Ihre Bilder nicht mit denen anderer Nutzer des öffentlichen MQTT-Brokers kollidieren:

Topic Richtung Payload
connectmq/demo/cam-7f3a/snapshot Kamera veröffentlicht rohe JPEG-Bytes
connectmq/demo/cam-7f3a/status Kamera veröffentlicht, retained online / offline (Last Will)
connectmq/demo/cam-7f3a/cmd Kamera abonniert capture, flash_on, flash_off

Ersetzen Sie cam-7f3a durch einen eigenen zufälligen String.

#include <WiFi.h>
#include <PubSubClient.h>
#include "esp_camera.h"

// ---------- Wi-Fi and MQTT settings ----------
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";           // muss nicht leer und eindeutig sein
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;   // unter dem öffentlichen Limit von 64 KB bleiben
const uint32_t CAPTURE_INTERVAL = 30000;       // zeitgesteuerter Schnappschuss alle 30 s
const int      FLASH_LED_PIN    = 4;

// ---------- AI-Thinker ESP32-CAM pin map ----------
#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;   // QVGA versuchen, wenn Bilder zu groß sind
  config.jpeg_quality = 12;              // 10-63, niedriger = bessere Qualität, größere Datei
  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: Broker veröffentlicht "offline" (retained), wenn die Kamera verschwindet
    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);   // große Schreibvorgänge brauchen mehr als den Standardwert
  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();
  }
}

Flashen Sie das Board mit GPIO0 auf GND, entfernen Sie dann die Brücke und drücken Sie Reset. Der serielle Monitor sollte die IP-Adresse, eine erfolgreiche MQTT-Verbindung und pro Schnappschuss eine Zeile mit seiner Größe in Bytes anzeigen.

Die Bilder in Python empfangen und speichern

Auf Ihrem Rechner abonniert dieses Skript das Snapshot-Topic und schreibt jedes Bild in eine Datei mit Zeitstempel. Es zeigt außerdem, wie Sie eine Aufnahme auf Anforderung auslösen.

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)   # sofort ein Bild anfordern

try:
    while True:
        time.sleep(1)
except KeyboardInterrupt:
    client.loop_stop()
    client.disconnect()

Die Prüfung auf die Bytes FF D8 (die JPEG-Startmarkierung) schützt das Skript davor, Unsinn zu speichern, wenn jemand anderes Text auf Ihrem Topic veröffentlicht. Sie können eine Aufnahme auch über die Kommandozeile auslösen:

mosquitto_pub -h broker.connectmq.com -p 1883 -i cam-7f3a-cli -t connectmq/demo/cam-7f3a/cmd -m capture

Das ESP32-CAM-MQTT-Setup in die Produktion überführen

Der öffentliche MQTT-Broker ist für alle lesbar, richten Sie die Kamera dabei also nie auf etwas Privates. Für einen echten Einsatz wechseln Sie zu einem verwalteten MQTT-Broker und stellen den Sketch auf TLS um:

  1. Registrieren Sie sich bei ConnectMQ, erstellen Sie einen Broker und kopieren Sie den generierten Benutzernamen, das Passwort und die Client ID.
  2. Ersetzen Sie WiFiClient durch WiFiClientSecure, laden Sie das Zertifikat ISRG Root X1 mit setCACert() und ändern Sie den Port auf 8883.
  3. Rufen Sie mqtt.connect("YOUR_CLIENT_ID", "YOUR_USERNAME", "YOUR_PASSWORD", TOPIC_STATUS, 1, true, "offline") auf. ConnectMQ bindet Client IDs an die ausgegebenen Zugangsdaten, verwenden Sie also die aus dem Portal.
  4. Erhöhen Sie MAX_PAYLOAD passend zu Ihrem Plan, zum Beispiel auf knapp unter 256 KB im kostenlosen Serverless-Plan.

TLS benötigt zusätzlich zu den Kamerapuffern weiteren RAM. Das AI-Thinker-Modul hat PSRAM, das die Bildpuffer aus dem internen Speicher heraushält, doch wenn Sie Allokationsfehler sehen, reduzieren Sie auf einen Bildpuffer und eine kleinere Bildgröße. Die Anleitung zu ESP32 MQTT SSL führt Schritt für Schritt durch das Laden von Zertifikaten.

Checkliste zur Fehlerbehebung

  1. „Brownout detector was triggered“ beim Booten oder bei der Aufnahme. Verwenden Sie eine solide 5-V-Versorgung und kurze Kabel; Kamera und WLAN-Funk ziehen gleichzeitig Stromspitzen.
  2. Camera init failed: 0x105 oder Ähnliches. Das Flachbandkabel sitzt locker, oder die Pin-Belegung passt nicht zu Ihrer Board-Variante.
  3. Snapshot „FAILED“, aber kleine Nachrichten funktionieren. Das Bild überschreitet das Broker-Limit, oder der Socket ist mitten im Schreiben in einen Timeout gelaufen. Verringern Sie frame_size, erhöhen Sie die Zahl bei jpeg_quality oder vergrößern Sie setSocketTimeout().
  4. Die Kamera trennt sich direkt nach einer großen Veröffentlichung. Auf dem öffentlichen MQTT-Broker wird eine Nachricht über 64 KB abgelehnt. Prüfen Sie die im seriellen Log ausgegebene Größe.
  5. Bilder wirken anfangs grün oder dunkel. Die OV2640 braucht einige Bilder, bis sich die Belichtungsautomatik eingependelt hat; nehmen Sie nach dem Booten ein Bild auf und verwerfen Sie es.
  6. Reconnect-Schleifen nach WLAN-Abbrüchen. Ergänzen Sie ein Backoff und behalten Sie den Last Will bei, damit Dashboards die Kamera als offline anzeigen; siehe ESP32 MQTT Reconnect.

Design-Tipps für Kamera-Topics

Halten Sie binäre Bilder auf einem eigenen Topic und senden Sie Metadaten separat. Eine kleine JSON-Nachricht auf einem Topic snapshot/meta mit Größe, Auflösung und Auslösegrund lässt Dashboards reagieren, ohne das Bild herunterzuladen. Vermeiden Sie es, Bildnachrichten auf geteilten Brokern als retained zu speichern, sofern Sie kein Verhalten „letztes Bild“ brauchen, denn jeder neue Subscriber erhält dann sofort das vollständige JPEG. Wenn mehrere Dienste die Bilder konsumieren, übernimmt das MQTT-Fan-out, und ein Cloud-MQTT-Broker erspart Ihnen den Betrieb dieser Infrastruktur.

Starten Sie mit ConnectMQ

Testen Sie Ihre Kamera zuerst auf dem kostenlosen öffentlichen MQTT-Broker und wechseln Sie zu einem privaten Broker, sobald die Bilder wichtig werden. ConnectMQ bietet Ihnen einen TLS-Endpunkt, generierte Zugangsdaten und einen Live-Topic-Explorer, in dem Sie den Snapshot-Verkehr eintreffen sehen. Erstellen Sie einen Broker im kostenlosen Serverless-Plan unter app.connectmq.com – keine Kreditkarte erforderlich – und tauschen Sie Host und Zugangsdaten im Sketch aus.

Häufig gestellte Fragen

Kann ein ESP32-CAM Video über MQTT streamen?

Er kann Bilder in schneller Folge senden, doch MQTT ist nicht für Video ausgelegt, und auf geteilten Brokern gelten Ratenlimits. Verwenden Sie MQTT für Schnappschüsse und ereignisgesteuerte Aufnahmen und ein Streaming-Protokoll für Live-Video.

Wie groß darf ein veröffentlichtes Bild höchstens sein?

Das hängt vom Broker ab: 64 KB auf dem öffentlichen MQTT-Broker, 256 KB im Serverless (Free) Plan und 1 MB im Serverless (Paid) Plan. Prüfen Sie fb->len und wählen Sie eine Bildgröße, die unter Ihrem Limit bleibt.

Muss ich den PubSubClient-Puffer für Bilder vergrößern?

Nicht, wenn Sie beginPublish(), write() und endPublish() verwenden. Diese Aufrufe streamen das Bild direkt, sodass der Standardpuffer für den Header ausreicht.

Sollten Snapshot-Nachrichten retained sein?

In der Regel nicht. Eine große binäre Nachricht als retained zu speichern, bedeutet, dass jeder neue Subscriber sie sofort erhält; speichern Sie nur das kleine Status-Topic als retained, sofern Sie nicht ausdrücklich ein Verhalten „neuestes Bild“ wünschen.

Verwandte Artikel

ESP32 NTP MQTT Zeitstempel: genaue Uhrzeit in jeder Nachricht

Synchronisieren Sie die ESP32-Uhr per NTP, formatieren Sie Zeitstempel in ISO 8601 und Epoch-Millisekunden und hängen Sie sie an MQTT-Payloads, damit Messwerte ihren echten Erfassungszeitpunkt behalten, auch wenn sie verspätet veröffentlicht werden.

8 Min. Lesezeit

Alle Artikel →

Ihr privater MQTT-Broker ist nur wenige Klicks entfernt

Konto anlegen, Broker-Zugangsdaten erhalten und das erste Gerät verbinden — ohne Server, die installiert, gepatcht oder skaliert werden müssen.