Eine Kamera am Vogelhaus, ein Rechner der erkennt wer da war — und eine Sammlung, die sich von allein fuellt. Zwei neuronale Netze: eines findet den Vogel im Bild, eines bestimmt die Art. 148 heimische Arten, auf Deutschland eingeschraenkt und neu normiert — ohne diese Einschraenkung wird aus einer Kohlmeise eine nordamerikanische Schwesterart. Daraus entsteht ein Sammelalbum: nummerierte Seiten, gefundene Arten in Farbe, fehlende als Schatten, dazu Erstfang, Punkte, Stufen und 15 Abzeichen. Laeuft vollstaendig auf eigener Hardware. Kein Cloud-Dienst, kein Konto, keine Telemetrie; die Bilder verlassen das eigene Netz nicht. Zwei getrennt deploybare Teile: die Sammlung als Django-Anwendung, die Erkennung als eigener Dienst. Der Erkennungs-Dienst fragt selbst an und braucht deshalb weder eine erreichbare Adresse noch einen offenen Port — er laeuft hinter einem gewoehnlichen Router. Dabei: Firmware fuer Raspberry Pi und ESP32-S3, Anbindung an Home Assistant ueber MQTT, Oberflaeche auf Deutsch und Englisch, Einstellungen je Kamera aus dem Browser, und ein Weg fuer Fotos von unterwegs. AGPL-3.0.
Vogelkamera auf ESP32-S3
Kameraseite für Vogelalbum auf einem ESP32-S3 mit Kameramodul — das Gegenstück zu
../pi/. Gleiche Pfade und gleiches JSON-Schema: der
Detector merkt nicht, ob ein Pi Zero oder ein ESP32 am anderen Ende hängt.
http://<kamera>:8080/stream MJPEG, ein Zuschauer
http://<kamera>:8080/ Vorschauseite
http://<kamera>:8081/status JSON: fps, RSSI, Temperatur, Laufzeit
http://<kamera>:8081/temp Chiptemperatur als Text
http://<kamera>:8081/gesund 200 oder 503
🔑 Die Diagnose liegt auf einem eigenen Port — das ist der eine Unterschied
zum Pi. Der Webserver im ESP32 arbeitet Anfragen mit einem Faden ab, und
der Stream gibt den nie zurück. Solange jemand zuschaut, käme auf 8080 niemand
mehr an /status — also genau dann nicht, wenn man es braucht. Am 23.09.2026
wirkte die Kamera deshalb tot, während sie brav streamte. Seitdem hat die
Diagnose ihren eigenen Port und ihren eigenen Faden.
⚠️ Bis zum 09.10.2026 stand hier für alles 8080. Wer die Zeile kopierte,
bekam eine Anfrage, die stumm ins Zeitlimit lief — dasselbe Bild wie eine tote
Kamera. Der Port ist VK_PORT + 1; wer im menuconfig den Hauptport ändert,
verschiebt beide.
Warum ESP32 statt Pi
Am Vogelhaus ist die Bildrate bandbreitenbegrenzt, nicht rechenbegrenzt: der Pi Zero W drückt über seine Chip-Antenne rund 2 Mbit/s durchs WLAN, und jeder gemessene Einbruch hing an der Funkstrecke. Ein ESP32-S3-Board mit externer Antenne greift genau da an — und kostet ein Vielfaches weniger, was für einen Bausatz den Unterschied zwischen verkaufbar und nicht verkaufbar macht.
Hardware
Getestet wird gegen ein ESP32-S3-CAM mit OV3660 (N16R8-Modul: 16 MB Flash,
8 MB PSRAM, externe Antenne). Die Firmware läuft aber auf jedem Board, dessen
Pinbelegung im menuconfig hinterlegt ist.
⚠️ Das Funkmodul muss ein ESP32-S3-WROOM-1U sein — das U steht für die Antennenbuchse. Bei manchen Klonen liegt eine Antenne bei, das Modul hat aber nur die Platinenantenne; dann ist das Kabel Deko.
⚠️ OV5640 nicht verwenden. Auf diesen Boards liefert er dokumentiert ein schwarzes Bild, und der Sensor überhitzt, weil die Versorgung für seinen internen 1,5-V-Regler zu hoch liegt. OV2640 und OV3660 laufen.
Bauen und flashen
. ~/esp/esp-idf/export.sh
cd esp32cam
idf.py set-target esp32s3
idf.py menuconfig # Menü "Vogelkamera": WLAN, Board, Bildgröße
idf.py -p /dev/ttyACM0 flash monitor
Die WLAN-Daten stehen bewusst nicht im Repo — sie landen über
menuconfig in sdkconfig, und das ist per .gitignore draußen.
Zwei USB-C-Buchsen am Board: eine hängt am USB-Seriell-Wandler (CH340), die
andere am nativen USB des Chips. Findet idf.py flash nichts, ist es die
andere Buchse. Je nach Board muss zum Flashen BOOT gehalten und RESET kurz
gedrückt werden.
Wenn die Kamera nicht anspringt
esp_camera_init scheitert mit ESP_ERR_NOT_FOUND (0x105), wenn die
Pinbelegung nicht zum Board passt — dann ist die Board-Auswahl im
menuconfig falsch, nicht die Kamera defekt. Die Firmware bricht deshalb
nicht ab: der HTTP-Server läuft weiter und /status nennt den Fehlercode.
Ein Gerät im Garten, das bei einem Fehler stumm bleibt, kostet einen Weg
nach draußen.
Zweiter Kandidat: CONFIG_SPIRAM_MODE_OCT in sdkconfig.defaults. Octal ist
für die R8-Module richtig; ein R2- oder Quad-Modul braucht QUAD, sonst
findet die Kamera keinen Framebuffer.
Ein Zuschauer, mit Absicht
Ein zweiter Client bekommt 503 statt eines halben Bildes. Der Grund steht
im Kopf von main/vogelkamera.c: ein zweiter Zuschauer
sättigt die Funkstrecke, der Detector fällt in Timeouts, und zwar ohne dass
irgendwo ein Fehler auftaucht — die Bildrate halbiert sich einfach. Genau das
ist bei der Entwicklung passiert: das Diagnosewerkzeug hat den Fehler erzeugt,
den es messen sollte.
Deshalb gibt es /status und /temp. Beide fassen den Stream nicht an und
sind der richtige Weg, die Kamera im Betrieb zu prüfen:
curl -s http://<kamera>:8081/status
{"kamera":"bereit","fehler":"0x0","laufzeit_s":615201,"bilder":3206804,
"fps":5.25,"letztes_bild_s":0.1,"zuschauer":true,"rssi":-71,
"temperatur_c":51.7,"speicher_frei":8104348,"psram_frei":7941536}
(Echte Ausgabe einer Kamera an einer Vogeltränke nach sieben Tagen Dauerbetrieb.) Port 8081, nicht 8080 — siehe oben.
ping taugt dafür nicht — ICMP fällt bei gesättigtem WLAN als Erstes aus,
während TCP weiterläuft. Eine Entwarnung per ping ist Glück, keine Messung.
Was die Werte sagen:
| Feld | Gesund | Woran es sonst liegt |
|---|---|---|
fps |
stabil über Stunden | fällt → Funkstrecke, zweiter Zuschauer, Hitze |
rssi |
über −70 dBm | darunter Antenne ausrichten oder näher ran |
temperatur_c |
unter 70 °C | geschlossenes Gehäuse in der Sonne drosselt |
letztes_bild_s |
unter 1 s bei Zuschauer | steht → Sensor hängt, siehe unten |
zuschauer |
true im Normalbetrieb |
false, obwohl der Detector läuft → er kommt nicht durch |
Bleibt der Sensor 30 Versuche am Stück stumm, startet das Gerät selbst neu. Zwei Sekunden Neustart sind billiger als ein Weg in den Garten.
Einstellungen, die wirklich etwas bringen
Bildgröße und Qualität sind Bandbreite, nicht Schönheit. Weniger Qualität heißt mehr Bilder pro Sekunde, und mehr Bilder finden mehr Vögel als schönere. Default ist XGA (1024x768) bei Qualität 12.
Der begrenzende Faktor ist der Bildwinkel, nicht die Auflösung. Am
Futterhaus scheitert die Erkennung daran, dass der Vogel unter den nötigen
5–8 % Bildfläche bleibt. Mehr Megapixel ändern daran nichts, nähere Montage
schon — 30–40 cm an die Landefläche, und die Landefläche in die untere
Bildmitte, nicht an den Bildrand (der unterste Bildrand ist eine tote Zone,
siehe python -m detector karte).
Kein WLAN-Stromsparen. WIFI_PS_NONE ist gesetzt: der Empfänger darf
zwischen zwei Bildern nicht schlafen. Die Bildrate ist der Zweck des Geräts,
nicht die Batterielaufzeit.
Feste Adresse
Der Detector braucht eine gleichbleibende Adresse. Der Dev-Server löst kein
mDNS auf (kein avahi), .local-Namen helfen also nicht — im Router eine
feste DHCP-Lease für die MAC eintragen.
Im Dauerbetrieb
Die Firmware läuft an einer Vogeltränke im Garten. Gemessen am laufenden Detector, drei aufeinanderfolgende Minuten:
| Bilder/s | |
|---|---|
| ESP32-S3-CAM mit OV3660 (dieses Projekt) | 5,2 |
| Raspberry Pi Zero W mit OV5647 (zum Vergleich) | 4,4 |
Insgesamt hat der Detector an dieser Kamera bisher über 2,8 Millionen Bilder verarbeitet. Die Bildrate ist bandbreitenlimitiert, nicht rechenlimitiert — deshalb der Rat zur Modulvariante mit Antennenbuchse (ESP32-S3-WROOM-1U) und externer Antenne.
Ungetestet sind die übrigen Pinbelegungen: Gebaut und betrieben wurde
bisher nur die S3-CAM-Variante. XIAO-S3 und AI-Thinker sind in menuconfig
vorgesehen, aber nie auf Hardware gelaufen.
Was fehlt
OTA-Updates sind nicht eingebaut. Die Partitionstabelle lässt hinter der 4-MB-App genug Platz, um sie ohne Umbau nachzurüsten — für ein Gerät, das später im Garten festsitzt, wäre das der nächste sinnvolle Schritt.