Heute zeige ich euch ein extrem kompaktes und spannendes Hardware-Projekt: Wir bauen einen eigenen, mobilen Fotoapparat.
Der Clou ist das nach einer Aufnahme über das WLAN die Fotos direkt in euer Google Drive Cloud verschickt werden und somit nichts verloren geht. Eine LED zeigt an das die Kamera aktiv ist und schläft wieder ein, wenn keine Taste gedrückt wird. Das spart Akku Kapazität.

Das Ziel: Ein kleiner Tastendruck genügt, eine gelbe LED (Light-Emitting Diode – Leuchtdiode) leuchtet zur Bestätigung auf, und das aufgenommene Foto landet völlig kabellos über das heimische WLAN (Wireless Local Area Network – kabelloses lokales Netzwerk) direkt im eigenen Google Drive.
Hier sind die Fakten und der komplette Bauplan zum Nachbauen.
Die Hardware: Klein, aber oho
Als Herzstück nutzen wir das Seeed Studio XIAO ESP32S3 Sense. Diese winzige Platine mit zusätzlichen Kamera Modul bringt alles mit, was wir brauchen:
- Einen leistungsstarken ESP32-S3 Mikrocontroller
- Ein aufsteckbares Kameramodul (OV2640) mit 2 Megapixeln.
- Eine externe selbstklebende Antenne für guten Empfang.
- Optional auf der Rückseite zwei Lötstellen zum anbringen eines LiPo-Akku mindestens 1000mA (Lithium-Polymer-Akkumulator), der unser Projekt zusätzlich mobil machen wird, an BAT+ und BAT-.
- Erwähnenswert aber für diese Projekt nicht berücksichtigt, das Board hat auch noch ein Mikrofon.

Zusätzlich benötigen wir nur:
1x Standard-Taster (als Auslöser)
1x farbige LED incl Vorwiderstand mit Fassung

Der Verdrahtungsplan
Der Aufbau ist sehr übersichtlich, da die Hauptplatine bereits vieles mitbringt.
- Kamera und Antenne: Diese werden einfach auf die mitgelieferte Erweiterungsplatine (Sense Expansion Board) gesteckt.
- LiPo-Akku (Lithium-Polymer-Akkumulator): Auf der Unterseite der Erweiterungsplatine wird ein 3,7-Volt-Akku angelötet. Das Board kümmert sich automatisch um die Stromversorgung und lädt den Akku sogar auf, wenn du ein USB-Kabel (Universal Serial Bus) anschließt.
- Der Taster (Auslöser):
- Verbinde einen Kontakt des Tasters mit dem Anschlussstift D3 (GPIO 4) auf dem Board.
- Verbinde den anderen Kontakt des Tasters mit dem Anschlussstift GND (Ground / Masse – der Minuspol).
- Technischer Hintergrund: Wir nutzen einen internen Widerstand im Mikrocontroller, weshalb kein zusätzliches Bauteil für den Taster nötig ist.
- Die gelbe LED (Light-Emitting Diode – Leuchtdiode):
- Verbinde das lange Bein (Pluspol) der LED mit einem Vorwiderstand (etwa 220 bis 330 Ohm). Das andere Ende des Widerstands verbindest du mit dem Anschlussstift D2 (GPIO 3). Der Widerstand schützt die LED davor, durchzubrennen.
- Verbinde das kurze Bein (Minuspol) der LED ebenfalls mit GND (Masse).

Pinbelegung: Belegungsplan: Seeed Studio XIAO ESP32S3 /Sense /Plus
Quelle und Hersteller Details: https://www.seeedstudio.com/XIAO-ESP32S3-Sense-p-5639.html
Vorbereitung Skript Erstellung bei Google
Das Software-Konzept: Warum Google Drive?
Ein direkter Upload zu Google Fotos ist aufgrund der strengen Sicherheitsanmeldung (OAuth 2.0) für kleine Mikrocontroller viel zu komplex und fehleranfällig.
Die praxisnahe Lösung: Wir nutzen ein kostenloses Google Apps Script. Das ist ein kleines Programm auf den Google-Servern, das als Vermittler dient. Unser ESP32 wandelt das Bild in einen langen Text um (Base64-Verfahren) und sendet diesen über das Internet per HTTP (Hypertext Transfer Protocol) an unser Skript. Das Skript wandelt den Text wieder in ein Bild um und legt es sicher im Google Drive ab.
Schritt 1: Das Google Apps Script einrichten
- Gehe auf script.google.com und erstelle ein „Neues Projekt“.
- Füge den folgenden Code in den Editor ein:

Skript zum kopieren:
function doPost(e) {
try {
// Nimmt den reinen Base64-Text vom ESP32 entgegen
var bildText = e.postData.contents;
// Wandelt den Text zurück in echte Bilddaten
var decodiertesBild = Utilities.base64Decode(bildText);
// Erzeugt einen einmaligen Dateinamen mit der aktuellen Zeit
var dateiName = "Kamera_" + new Date().getTime() + ".jpg";
// Erstellt die Datei für Google Drive
var dateiFormat = Utilities.newBlob(decodiertesBild, 'image/jpeg', dateiName);
var ordner = DriveApp.getRootFolder();
var neueDatei = ordner.createFile(dateiFormat);
return ContentService.createTextOutput("Erfolg! Bild gespeichert als: " + dateiName);
} catch (fehler) {
return ContentService.createTextOutput("Server-Fehler: " + fehler.toString());
}
}- Klicke oben rechts auf Bereitstellen -> Neue Bereitstellung.
- Wähle als Typ das Zahnrad und dann Web-App.
- Stelle „Wer hat Zugriff“ erstmal auf Jeder. (Nur temporär öffnen und auf eigene Gefahr) Besser Zugriff beschränken auf „Jeder mit Google-Konto“ oder „Nur ich“ oder WebDAV Lösung siehe Teil 2
- Kopiere dir am Ende die angezeigte Web-App URL (Uniform Resource Locator – die genaue Internetadresse, die mit
/execendet). Diese brauchen wir gleich für den Code.
Den Mikrocontroller programmieren
Wichtiger Fakt vorab: Damit die Kamera genug Speicherplatz hat, um das Bild aufzubauen, musst du vor dem Hochladen in deiner Programmierumgebung zwingend den PSRAM (Pseudo-Static Random Access Memory – ein zusätzlicher Arbeitsspeicher auf der Platine) aktivieren!

- In der Arduino-IDE: Board als XIAO_ESP32S3 und unter Werkzeuge -> PSRAM -> „OPI PSRAM„.
- In PlatformIO (VS Code): In der platformio.ini unter build_flags = -D BOARD_HAS_PSRAM eintragen.
Sonst kommt Fehlermeldung:
E (11149) cam_hal: cam_dma_config(509): frame buffer malloc failed
E (11149) cam_hal: cam_config(599): cam_dma_config failed
E (11149) camera: Camera config failed with error 0xffffffff
Hier ist der fertige Sketch. Trage oben deine WLAN-Daten und deine frisch kopierte Google Web-App URL ein
Quellcode:
#include "esp_camera.h"
#include <WiFi.h>
#include <HTTPClient.h>
#include <base64.h> // NEU: Für die Text-Übersetzung des Bildes
// --- Hier deine Daten eintragen ---
const char* netzwerkName = "ID-Labor";
const char* netzwerkPasswort = "geheimesPasswort";
String zielAdresse = "https://script.google.com/macros/skript_Web-App_url_einfügen/exec";
// --- Anschlussstifte (Pins) ---
const int tasterPin = 4; // GPIO 4 ist D3
const int ledPin = 3; // GPIO 3 ist D2
// --- Kamera-Anschlüsse ---
#define PWDN_GPIO_NUM -1
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 10
#define SIOD_GPIO_NUM 40
#define SIOC_GPIO_NUM 39
#define Y9_GPIO_NUM 48
#define Y8_GPIO_NUM 11
#define Y7_GPIO_NUM 12
#define Y6_GPIO_NUM 14
#define Y5_GPIO_NUM 16
#define Y4_GPIO_NUM 18
#define Y3_GPIO_NUM 17
#define Y2_GPIO_NUM 15
#define VSYNC_GPIO_NUM 38
#define HREF_GPIO_NUM 47
#define PCLK_GPIO_NUM 13
void setup() {
Serial.begin(115200);
pinMode(tasterPin, INPUT_PULLUP);
pinMode(ledPin, OUTPUT);
digitalWrite(ledPin, LOW);
Serial.print("Verbinde mit WLAN (Wireless Local Area Network – kabelloses lokales Netzwerk)...");
WiFi.begin(netzwerkName, netzwerkPasswort);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println(" Erfolgreich verbunden!");
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.frame_size = FRAMESIZE_SVGA;
config.pixel_format = PIXFORMAT_JPEG; // JPEG (Joint Photographic Experts Group – digitales Bildformat)
config.jpeg_quality = 10;
config.fb_count = 1;
esp_err_t fehler = esp_camera_init(&config);
if (fehler != ESP_OK) {
Serial.printf("Kamera-Start fehlgeschlagen: 0x%x", fehler);
return;
}
// Bild um 180 Grad drehen, wenn wie bei mir verdreht im Gehaeuse eingebaut
sensor_t * s = esp_camera_sensor_get();
if (s != NULL) {
s->set_vflip(s, 1); // Schritt 1: Bild vertikal spiegeln (oben und unten tauschen)
s->set_hmirror(s, 1); // Schritt 2: Bild horizontal spiegeln (links und rechts tauschen)
Serial.println("Kamerabild wurde per Software um 180 Grad gedreht.");
}
Serial.println("Kamera ist bereit. Warte auf Knopfdruck.");
}
void loop() {
if (digitalRead(tasterPin) == LOW) {
digitalWrite(ledPin, HIGH);
Serial.println("Klick! Auslöser gedrückt...");
// Den alten Zwischenspeicher leeren (Scheinschuss / Dummy-Shot)
camera_fb_t * alterPuffer = esp_camera_fb_get();
if (alterPuffer) {
esp_camera_fb_return(alterPuffer); // Das alte Bild wird sofort ungelesen gelöscht
Serial.println("Alten Bildpuffer geleert.");
}
// Jetzt das brandaktuelle Foto aufnehmen
Serial.println("Neues Foto aufnehmen...");
camera_fb_t * bildspeicher = esp_camera_fb_get();
if (!bildspeicher) {
Serial.println("Fehler bei der Aufnahme.");
digitalWrite(ledPin, LOW);
return;
}
// Das aktuelle Foto an Google Drive senden
if (WiFi.status() == WL_CONNECTED) {
HTTPClient http;
http.begin(zielAdresse);
http.addHeader("Content-Type", "text/plain");
Serial.println("Übersetze Bild in Text (Base64)...");
String base64Bild = base64::encode(bildspeicher->buf, bildspeicher->len);
Serial.println("Sende Bild an Google...");
int antwortCode = http.POST(base64Bild);
if (antwortCode == 200 || antwortCode == 302) {
Serial.printf("Erfolg! Bild ist auf dem Weg (Antwortcode: %d)\n", antwortCode);
} else {
Serial.printf("Fehler beim Senden: Code %d\n", antwortCode);
}
http.end();
} else {
Serial.println("Keine WLAN-Verbindung.");
}
// Speicher freigeben und LED ausschalten
esp_camera_fb_return(bildspeicher);
digitalWrite(ledPin, LOW);
// Pause zur Entprellung des Tasters
delay(2000);
}
}Erster Test: Ergebnis im Seriellen Monitor

Wird nichts im Seriellen Monitor angezeigt, dann CDC aktivieren:

Drücke den Knopf und wenige Sekunden später findest du dein Foto in deinem Google Drive:


Zwei wichtige Stolpersteine aus der Praxis
1. Warum das Bild plötzlich auf dem Kopf stand
Beim Design des dreidimensional (3D) gedruckten Gehäuses gab es eine logische Vorgabe: Der USB-C-Anschluss zum Laden des Akkus muss unbedingt an der Unterseite sitzen. Da das mitgelieferte Flachbandkabel der Kamera extrem kurz ist, bedeutet das im Umkehrschluss, dass das gesamte Innenleben umgedreht eingebaut werden muss. Die Folge: Die Bilder landeten auf dem Kopf. Die Lösung: Ich musste das Gehäuse nicht neu designen. Der OV2640-Sensor erlaubt es, das Bild per Software im setup() horizontal und vertikal zu spiegeln. Das kostet beim Auslösen keine Rechenzeit!
2. Das Rätsel um das „vorletzte“ Bild
Bei den ersten Tests landete auf Google Drive seltsamerweise nie das aktuelle Foto, sondern immer das Bild vom vorherigen Knopfdruck. Grund ist der interne Bildzwischenspeicher (Frame-Buffer) der Kamera. Der Sensor läuft permanent im Hintergrund. In der zweisekündigen Pause nach dem Senden schießt die Kamera munter weiter und füllt den Speicher mit einem alten Bild. Beim nächsten Druck holt man genau diese „Konserve“ ab.
Die Lösung: Ein digitaler „Scheinschuss“ (Dummy-Shot). Sobald der Taster gedrückt wird, holen wir das erste Bild ab und werfen es sofort ungelesen weg. Erst das direkt danach geschossene zweite Foto wird an Google gesendet. So ist das Bild immer brandaktuell.
3D Druck:


Download der fertigen Druckdateien https://makerworld.com/de/models/2892682-diy-esp32s3-xiao-sense-variable-camera-case
Hier der Openscad Skript zum manuellen Anpassen.
Die Ablaufsteuerung ist Standardmäßig für die Vorderseite gezeichnet. Um die Rückwand zu sehen,
entferne die Schrägstriche vor „rueckwand();“ und setze welche vor „vorderseite();
Danach rendern und als STL-Datei exportieren

openSCAD Code
// =========================================================================
// OPENSCAD-SKRIPT: KAMERAGEHÄUSE FÜR SEEED STUDIO XIAO ESP32S3 SENSE
// =========================================================================
// --- 1. Globale Gehäusemaße (in mm) ---
gehaeuse_breite = 80; // Gesamtbreite von links nach rechts (Limit: 120 mm)
gehaeuse_hoehe = 50; // Gesamthöhe von unten nach oben
gehaeuse_tiefe = 25; // Tiefe des Hauptkörpers (ohne Objektiv)
wandstaerke = 2; // Dicke der Außenwände
ecken_radius = 4; // Radius für die Abrundung der Gehäuseecken
// --- 2. Parameter für den Schnappverschluss (Möglichkeit 1) ---
verschluss_nut_tiefe_z = 1.5; // Position der Rille im Gehäuse (ab Rückseite)
verschluss_wulst_aufmass = 0.1; // Wie weit die Rastnase übersteht (Toleranz)
// Verschiebung der Wulst auf Z (Möglichkeit 1 für tieferes Eintauchen):
verschluss_wulst_z_offset = 2.2;
// --- 3. Parameter für Bedienelemente & Durchbrüche ---
taster_durchmesser = 7;
taster_x_pos = 15;
led_durchmesser = 7;
led_x_pos = 65;
usb_breite = 12;
usb_hoehe = 7;
usb_x_pos = 33;
// --- 4. Parameter für die ESP-Halterung ---
halterung_esp_boden_breite = 18;
halterung_esp_boden_hoehe = 13;
halterung_esp_boden_tiefe = 3.5;
// Halterung Positionierung im Innenraum:
halterung_esp_seitenwand_hoehe = 12;
esp_pos_x = 40;
esp_pos_y = 42;
esp_pos_z = 22;
// Kreisauflösung für alle runden Formen
$fn = 64;
// --- Ablaufsteuerung ---
vorderseite();
//rueckwand();
// =========================================================================
// HAUPTMODULE
// =========================================================================
module vorderseite() {
difference() {
// 1. POSITIV-FORM: Alle sichtbaren Außenteile verschmelzen
union() {
grundkoerper();
objektiv_attrappe();
bedienelemente_oben();
esphalterung();
}
// 2. NEGATIV-FORM: Hier wird der Innenraum komplett ausgehöhlt
translate([wandstaerke, wandstaerke, -1])
abgerundeter_kasten(
gehaeuse_breite - (2 * wandstaerke),
gehaeuse_hoehe - (2 * wandstaerke),
gehaeuse_tiefe - wandstaerke + 1
);
// 3. NUT FÜR DEN SCHNAPPVERSCHLUSS
translate([wandstaerke - 0.5, wandstaerke - 0.5, verschluss_nut_tiefe_z])
abgerundeter_kasten(
gehaeuse_breite - (2 * wandstaerke) + 1,
gehaeuse_hoehe - (2 * wandstaerke) + 1,
1 // Höhe der Nut
);
// 4. DURCHBRUCH FÜR DIE LINSE
translate([gehaeuse_breite / 2, gehaeuse_hoehe / 2, gehaeuse_tiefe - wandstaerke - 1])
cylinder(h = 20, d = 8);
// 5. AUSSPARUNG FÜR DEN USB-C-ANSCHLUSS
translate([usb_x_pos, gehaeuse_hoehe - 51, (gehaeuse_tiefe / 2) - 2.5])
cube([wandstaerke + usb_breite, usb_breite, usb_hoehe]);
// 6. DURCHGEHENDES LOCH FÜR DEN INTERNEN TASTER
translate([taster_x_pos, gehaeuse_hoehe - 5, gehaeuse_tiefe / 2])
rotate([-90, 0, 0])
cylinder(h = 15, d = taster_durchmesser);
// 7. DURCHGEHENDES LOCH FÜR LED
translate([led_x_pos, gehaeuse_hoehe - 5, gehaeuse_tiefe / 2])
rotate([-90, 0, 0])
cylinder(h = 15, d = led_durchmesser);
}
esphalterung();
}
module rueckwand() {
translate([0, -gehaeuse_hoehe - 10, 0]) {
union() {
// 1. Die flache Außenplatte als Gehäusedeckel
difference() {
abgerundeter_kasten(gehaeuse_breite, gehaeuse_hoehe, wandstaerke);
}
// 2. Die innenliegende Basis-Lippe (Führungsschiene)
translate([wandstaerke + 0.2, wandstaerke + 0.2, wandstaerke])
difference() {
abgerundeter_kasten(
gehaeuse_breite - (2 * wandstaerke) - 0.4,
gehaeuse_hoehe - (2 * wandstaerke) - 0.4,
2
);
translate([2, 2, -1])
abgerundeter_kasten(
gehaeuse_breite - (2 * wandstaerke) - 4.4,
gehaeuse_hoehe - (2 * wandstaerke) - 4.4,
4
);
}
// 3. DIE RASTNASE (Wulst angepasst nach Möglichkeit 1)
translate([wandstaerke + 0.05, wandstaerke + 0.05, wandstaerke + verschluss_wulst_z_offset])
difference() {
abgerundeter_kasten(
gehaeuse_breite - (2 * wandstaerke) - (0.2 - verschluss_wulst_aufmass * 2),
gehaeuse_hoehe - (2 * wandstaerke) - (0.2 - verschluss_wulst_aufmass * 2),
1
);
translate([2, 2, -1])
abgerundeter_kasten(
gehaeuse_breite - (2 * wandstaerke) - 4.1,
gehaeuse_hoehe - (2 * wandstaerke) - 4.1,
3
);
}
}
}
}
// =========================================================================
// HILFS- UND DETAILMODULE
// =========================================================================
module grundkoerper() {
abgerundeter_kasten(gehaeuse_breite, gehaeuse_hoehe, gehaeuse_tiefe);
}
module esphalterung() {
// Erzeugt den Boden vom ESP unter Verwendung der Variablen
translate([esp_pos_x, esp_pos_y, esp_pos_z])
cube([halterung_esp_boden_breite, halterung_esp_boden_hoehe, halterung_esp_boden_tiefe + 1], center = true);
// Linke Seitenwand
translate([esp_pos_x - 10, esp_pos_y - 5, esp_pos_z - 4])
cube([2, 18, halterung_esp_seitenwand_hoehe], center = true);
// Rechte Seitenwand
translate([esp_pos_x + 10, esp_pos_y - 5, esp_pos_z - 4])
cube([2, 18, halterung_esp_seitenwand_hoehe], center = true);
}
module abgerundeter_kasten(b, h, t) {
hull() {
translate([ecken_radius, ecken_radius, 0]) cylinder(h=t, r=ecken_radius);
translate([b - ecken_radius, ecken_radius, 0]) cylinder(h=t, r=ecken_radius);
translate([ecken_radius, h - ecken_radius, 0]) cylinder(h=t, r=ecken_radius);
translate([b - ecken_radius, h - ecken_radius, 0]) cylinder(h=t, r=ecken_radius);
}
}
module objektiv_attrappe() {
translate([gehaeuse_breite / 2, gehaeuse_hoehe / 2, gehaeuse_tiefe]) {
difference() {
cylinder(h = 4, d = 36);
translate([0, 0, -1])
cylinder(h = 6, d = 28);
}
}
}
module bedienelemente_oben() {
// LINKER AUSLÖSER BUTTON
translate([15, gehaeuse_hoehe, gehaeuse_tiefe / 2])
rotate([-90, 0, 0]) {
cylinder(h = 2, d = 14);
translate([0, 0, 2])
cylinder(h = 3, d = 10);
}
// RECHTES DREHRAD
translate([gehaeuse_breite - 15, gehaeuse_hoehe, gehaeuse_tiefe / 2])
rotate([-90, 0, 0]) {
cylinder(h = 3, d = 12);
}
// DETAILLIERTER BLITZSCHUH NACH ISO-STANDARD
translate([gehaeuse_breite / 2, gehaeuse_hoehe, gehaeuse_tiefe / 2]) {
difference() {
translate([0, 2, 0])
cube([22, 4, 14], center = true);
translate([0, 2.8, 0])
cube([19, 2.5, 16], center = true);
}
translate([0, 1, 0])
rotate([-90, 0, 0])
cylinder(h = 1, d = 3);
}
}
Viel Spaß beim Tüfteln und Nachbauen!
Youtube Video:
Kamera-Upgrades: Welche Sensoren du alternativ einsetzen kannst
Da wir in diesem „Do-It-Yourself“ (DIY) Projekt den leistungsstarken Mikrocontroller der Serie „Espressif System 32“ in der Variante S3 (ESP32-S3) nutzen, sind wir nicht zwingend auf den klassischen Standard-Sensor OV2640 („OmniVision 2640“) festgelegt. Wegen des zusätzlichen PSRAM im S3-Chip, gibt es uns die Freiheit, Sensoren mit höheren Auflösungen oder speziellen Eigenschaften anzuschließen, um die Bildqualität in Google Drive noch weiter zu verbessern.
Auf den richtigen Anschluss achten: Die meisten ESP32-S3 Kamera-Entwicklungsboards (Development Boards) nutzen ein kleines flaches Bandkabel („Flexible Printed Circuit“ – FPC) mit 24 Pins. Das neue Kameramodul muss mechanisch genau in diese kleine Buchse passen.
Hier ist eine Übersicht der gängigsten Alternativen, die mechanisch an den gleichen 24-Pin-Anschluss deines Entwicklungsboards (Development Board) passen:
| Kameramodul | Megapixel (MP) | Maximale Auflösung | Besonderheit für dieses Projekt |
|---|---|---|---|
| OV2640 | 2 Megapixel | 1600 x 1200 Pixel | Der günstige und schnelle Standard. |
| OV3660 | 3 Megapixel | 2048 x 1536 Pixel | Etwas lichtstärker, oft mit einem breiteren und natürlicheren Sichtfeld. |
| OV5640 | 5 Megapixel | 2592 x 1944 Pixel | Deutlich schärfere Fotos. Ideal, wenn du in die bei Google Drive gespeicherten Bilder hineinzoomen möchtest. |
| NoIR-Versionen | 2 oder 5 Megapixel | Je nach Basissensor | „No Infrared“ (NoIR) bedeutet: Es fehlt der Infrarot-Filter auf der Linse. Das ist perfekt für Nachtaufnahmen in Kombination mit unsichtbaren Infrarot-Leuchtdioden (LEDs). Achtung: Am Tag wirken die Bilder dadurch stark pink oder violett. |
Was muss im Programmcode angepasst werden?
Die gute Nachricht: Die Standard-Programmbibliothek erkennt den angeschlossenen Sensor (egal ob OV2640, OV3660 oder OV5640) beim Start automatisch. Auch wenn du eine NoIR-Kamera verwendest, musst du dafür keinen speziellen Code schreiben, da die Technik im Inneren identisch bleibt und nur das winzige Filterglas fehlt.
Damit die Bilder aber in der vollen Auflösung deines neuen Sensors an die Schnittstelle gesendet werden, musst du im Quellcode lediglich die Bildgröße (frame_size) anpassen. Suche in deinem Code nach der Kamera-Konfiguration (meist direkt in der setup()-Funktion zu finden) und ändere die Einstellungen wie folgt:
// Kamera-Konfiguration
camera_config_t config;
// ... [Deine bestehende Pin-Belegung bleibt genau so stehen] ...
// ==========================================
// 1. BILDGRÖSSE (AUFLÖSUNG) EINSTELLEN
// ==========================================
// Entferne die zwei Schrägstriche (//) vor dem Sensor, den du nutzt:
// Option A: Standard für den OV2640 (und OV2640 NoIR)
config.frame_size = FRAMESIZE_UXGA; // 1600 x 1200 Pixel (2 Megapixel)
// Option B: Upgrade für den OV3660
// config.frame_size = FRAMESIZE_QXGA; // 2048 x 1536 Pixel (3 Megapixel)
// Option C: Upgrade für den OV5640 (und OV5640 NoIR)
// config.frame_size = FRAMESIZE_QSXGA; // 2592 x 1944 Pixel (5 Megapixel)
// ==========================================
// 2. BILDQUALITÄT EINSTELLEN
// ==========================================
// "jpeg_quality" bestimmt die Dateigröße der Bilder.
// Ein niedrigerer Wert bedeutet bessere Qualität, aber längere Upload-Zeiten in die Cloud.
// Wertebereich: 10 (sehr gut) bis 63 (stark komprimiert)
config.jpeg_quality = 12;
Wichtiger Hinweis zum Speicher:
Nicht vergessen, den wie oben beschrieben PSRAM zu aktivieren.
Weitere Verbesserungsvorschläge:
Nun könnt ihr um länger mit dem akku zu arbeiten, noch esp_sleep_enable_ext0_wakeup(GPIO_NUM_4, 0); (Taster an GPIO 4) ergänzen
FreeCAD Makro:
Auch mit FreeCAD kann per Makro (Klick hier) diese Kamera erstellt oder angepasst werden
import FreeCAD as App
import Part
# =========================================================================
# FREECAD-MAKRO: KAMERAGEHÄUSE FÜR SEEED STUDIO XIAO ESP32S3 SENSE
# =========================================================================
# --- 1. Globale Gehäusemaße (in mm) ---
gehaeuse_breite = 80
gehaeuse_hoehe = 50
gehaeuse_tiefe = 25
wandstaerke = 2
ecken_radius = 4
# --- 2. Parameter für den Schnappverschluss ---
verschluss_nut_tiefe_z = 1.5
verschluss_wulst_aufmass = 0.1
verschluss_wulst_z_offset = 2.2
# --- 3. Parameter für Bedienelemente & Durchbrüche ---
taster_durchmesser = 7
taster_x_pos = 15
led_durchmesser = 7
led_x_pos = 65
usb_breite = 12
usb_hoehe = 7
usb_x_pos = 33
# --- 4. Parameter für die ESP-Halterung ---
halterung_esp_boden_breite = 18
halterung_esp_boden_hoehe = 13
halterung_esp_boden_tiefe = 3.5
halterung_esp_seitenwand_hoehe = 12
esp_pos_x = 40
esp_pos_y = 42
esp_pos_z = 22
# =========================================================================
# HILFS- UND DETAILMODULE
# =========================================================================
def abgerundeter_kasten(b, h, t, r=ecken_radius):
"""Baut einen Kasten mit abgerundeten Ecken, ähnlich wie hull() in OpenSCAD"""
if r <= 0:
return Part.makeBox(b, h, t)
# Vier Zylinder in den Ecken
c1 = Part.makeCylinder(r, t, App.Vector(r, r, 0))
c2 = Part.makeCylinder(r, t, App.Vector(b - r, r, 0))
c3 = Part.makeCylinder(r, t, App.Vector(r, h - r, 0))
c4 = Part.makeCylinder(r, t, App.Vector(b - r, h - r, 0))
# Zwei sich kreuzende Rechtecke, um die Lücken zu füllen
box1 = Part.makeBox(b - 2*r, h, t, App.Vector(r, 0, 0))
box2 = Part.makeBox(b, h - 2*r, t, App.Vector(0, r, 0))
res = c1.fuse(c2).fuse(c3).fuse(c4).fuse(box1).fuse(box2)
return res.removeSplitter()
def grundkoerper():
return abgerundeter_kasten(gehaeuse_breite, gehaeuse_hoehe, gehaeuse_tiefe)
def objektiv_attrappe():
c1 = Part.makeCylinder(36/2, 4)
c2 = Part.makeCylinder(28/2, 6)
c2.translate(App.Vector(0, 0, -1))
res = c1.cut(c2)
res.translate(App.Vector(gehaeuse_breite / 2, gehaeuse_hoehe / 2, gehaeuse_tiefe))
return res
def bedienelemente_oben():
# Linker Auslöser Button
aus1 = Part.makeCylinder(14/2, 2)
aus2 = Part.makeCylinder(10/2, 3)
aus2.translate(App.Vector(0, 0, 2))
ausloeser = aus1.fuse(aus2)
ausloeser.rotate(App.Vector(0,0,0), App.Vector(1,0,0), -90)
ausloeser.translate(App.Vector(15, gehaeuse_hoehe, gehaeuse_tiefe / 2))
# Rechtes Drehrad
drehrad = Part.makeCylinder(12/2, 3)
drehrad.rotate(App.Vector(0,0,0), App.Vector(1,0,0), -90)
drehrad.translate(App.Vector(gehaeuse_breite - 15, gehaeuse_hoehe, gehaeuse_tiefe / 2))
# Blitzschuh
b1 = Part.makeBox(22, 4, 14)
b1.translate(App.Vector(-11, -2, -7))
b1.translate(App.Vector(0, 2, 0))
b2 = Part.makeBox(19, 2.5, 16)
b2.translate(App.Vector(-9.5, -1.25, -8))
b2.translate(App.Vector(0, 2.8, 0))
blitz = b1.cut(b2)
b3 = Part.makeCylinder(3/2, 1)
b3.rotate(App.Vector(0,0,0), App.Vector(1,0,0), -90)
b3.translate(App.Vector(0, 1, 0))
blitz = blitz.fuse(b3)
blitz.translate(App.Vector(gehaeuse_breite / 2, gehaeuse_hoehe, gehaeuse_tiefe / 2))
return ausloeser.fuse(drehrad).fuse(blitz)
def esphalterung():
# Boden
boden = Part.makeBox(halterung_esp_boden_breite, halterung_esp_boden_hoehe, halterung_esp_boden_tiefe + 1)
boden.translate(App.Vector(-halterung_esp_boden_breite/2, -halterung_esp_boden_hoehe/2, -(halterung_esp_boden_tiefe + 1)/2))
boden.translate(App.Vector(esp_pos_x, esp_pos_y, esp_pos_z))
# Linke Seitenwand
links = Part.makeBox(2, 18, halterung_esp_seitenwand_hoehe)
links.translate(App.Vector(-1, -9, -halterung_esp_seitenwand_hoehe/2))
links.translate(App.Vector(esp_pos_x - 10, esp_pos_y - 5, esp_pos_z - 4))
# Rechte Seitenwand
rechts = Part.makeBox(2, 18, halterung_esp_seitenwand_hoehe)
rechts.translate(App.Vector(-1, -9, -halterung_esp_seitenwand_hoehe/2))
rechts.translate(App.Vector(esp_pos_x + 10, esp_pos_y - 5, esp_pos_z - 4))
return boden.fuse(links).fuse(rechts)
# =========================================================================
# HAUPTMODULE
# =========================================================================
def vorderseite():
# 1. Positiv-Form
gk = grundkoerper()
oa = objektiv_attrappe()
bo = bedienelemente_oben()
eh1 = esphalterung()
positiv = gk.fuse(oa).fuse(bo).fuse(eh1)
# 2. Negativ-Form (Innenraum)
neg = abgerundeter_kasten(gehaeuse_breite - 2*wandstaerke,
gehaeuse_hoehe - 2*wandstaerke,
gehaeuse_tiefe - wandstaerke + 1)
neg.translate(App.Vector(wandstaerke, wandstaerke, -1))
# 3. Nut für Schnappverschluss
nut = abgerundeter_kasten(gehaeuse_breite - 2*wandstaerke + 1,
gehaeuse_hoehe - 2*wandstaerke + 1, 1)
nut.translate(App.Vector(wandstaerke - 0.5, wandstaerke - 0.5, verschluss_nut_tiefe_z))
# 4. Durchbruch für die Linse
linse = Part.makeCylinder(8/2, 20)
linse.translate(App.Vector(gehaeuse_breite / 2, gehaeuse_hoehe / 2, gehaeuse_tiefe - wandstaerke - 1))
# 5. Aussparung USB-C
usb = Part.makeBox(wandstaerke + usb_breite, usb_breite, usb_hoehe)
usb.translate(App.Vector(usb_x_pos, gehaeuse_hoehe - 51, (gehaeuse_tiefe / 2) - 2.5))
# 6. Loch für Taster
taster = Part.makeCylinder(taster_durchmesser/2, 15)
taster.rotate(App.Vector(0,0,0), App.Vector(1,0,0), -90)
taster.translate(App.Vector(taster_x_pos, gehaeuse_hoehe - 5, gehaeuse_tiefe / 2))
# 7. Loch für LED
led = Part.makeCylinder(led_durchmesser/2, 15)
led.rotate(App.Vector(0,0,0), App.Vector(1,0,0), -90)
led.translate(App.Vector(led_x_pos, gehaeuse_hoehe - 5, gehaeuse_tiefe / 2))
# Subtraktion aller Negativformen
result = positiv.cut(neg).cut(nut).cut(linse).cut(usb).cut(taster).cut(led)
# ESP-Halterung nochmal hinzufügen (wie im Original)
eh2 = esphalterung()
result = result.fuse(eh2)
return result.removeSplitter()
def rueckwand():
# 1. Flache Außenplatte
platte = abgerundeter_kasten(gehaeuse_breite, gehaeuse_hoehe, wandstaerke)
# 2. Basis-Lippe
lippe_aussen = abgerundeter_kasten(gehaeuse_breite - 2*wandstaerke - 0.4,
gehaeuse_hoehe - 2*wandstaerke - 0.4, 2)
lippe_innen = abgerundeter_kasten(gehaeuse_breite - 2*wandstaerke - 4.4,
gehaeuse_hoehe - 2*wandstaerke - 4.4, 4)
lippe_innen.translate(App.Vector(2, 2, -1))
lippe = lippe_aussen.cut(lippe_innen)
lippe.translate(App.Vector(wandstaerke + 0.2, wandstaerke + 0.2, wandstaerke))
# 3. Rastnase
rast_aussen = abgerundeter_kasten(gehaeuse_breite - 2*wandstaerke - (0.2 - verschluss_wulst_aufmass*2),
gehaeuse_hoehe - 2*wandstaerke - (0.2 - verschluss_wulst_aufmass*2), 1)
rast_innen = abgerundeter_kasten(gehaeuse_breite - 2*wandstaerke - 4.1,
gehaeuse_hoehe - 2*wandstaerke - 4.1, 3)
rast_innen.translate(App.Vector(2, 2, -1))
rast = rast_aussen.cut(rast_innen)
rast.translate(App.Vector(wandstaerke + 0.05, wandstaerke + 0.05, wandstaerke + verschluss_wulst_z_offset))
rw = platte.fuse(lippe).fuse(rast)
rw.translate(App.Vector(0, -gehaeuse_hoehe - 10, 0))
return rw.removeSplitter()
# =========================================================================
# AUSFÜHRUNG UND ERZEUGUNG IN FREECAD
# =========================================================================
# Ein neues Dokument erstellen, falls noch keines offen ist
if App.ActiveDocument is None:
App.newDocument("ESP32_Kamera")
# Vorderseite erstellen und in den Baum einfügen
obj_vorderseite = App.ActiveDocument.addObject("Part::Feature", "Vorderseite")
obj_vorderseite.Shape = vorderseite()
# Rückwand erstellen (auskommentiert wie in deinem Skript)
# obj_rueckwand = App.ActiveDocument.addObject("Part::Feature", "Rueckwand")
# obj_rueckwand.Shape = rueckwand()
# Ansicht aktualisieren
App.ActiveDocument.recompute()
Kopiere diesen Code, öffne FreeCAD, wechsle in das Menü Makro -> Makros…, klicke auf Erstellen,
vergib einen Namen (z. B. Kameragehaeuse.FCMacro) und füge den Text dort ein.
Danach kannst du es einfach mit dem grünen „Play“-Button ausführen.
Teil 2: Bilder sicher in die eigene Nextcloud (WebDAV) senden
In Teil 1 unseres DIY-Fotoapparats (Do It Yourself) haben wir den ESP32-S3 so programmiert, dass er seine Schnappschüsse direkt in Google Drive ablegt. Das funktioniert super, aber viele von uns haben ihre Daten lieber auf eigenen Servern oder bei europäischen Cloud-Anbietern.
Deshalb zeige ich euch in diesem nachträglichen Update, wie wir den Code so umbauen, dass der ESP32-S3 seine Bilder über eine verschlüsselte Verbindung in eine Nextcloud (oder jede andere Cloud mit WebDAV-Unterstützung) hochlädt.
Was ändert sich im Vergleich zu Teil 1?
Wenn wir von der Google-API (Application Programming Interface – eine Programmierschnittstelle) zu einer Standard-WebDAV-Lösung (Web-based Distributed Authoring and Versioning – ein Standard zur Bereitstellung von Dateien im Netz) wechseln, müssen wir drei Dinge im Code anpassen:
| Funktion | Google Drive (Teil 1) | Nextcloud / WebDAV (Teil 2) |
| Übertragungsweg | HTTP POST (sendet Daten an ein Skript) | HTTP PUT (legt eine Datei direkt ab) |
| Sicherheit | Der einfache HTTPClient reichte aus | Wir brauchen WiFiClientSecure für echtes HTTPS (Hypertext Transfer Protocol Secure) |
| Authentifizierung | Der geheime Schlüssel steckte in der Google-URL | Wir nutzen Benutzername und ein App-Passwort |
| Dateiname | Wurde automatisch vom Google-Skript erzeugt | Muss vom ESP32-S3 selbst generiert werden |
Vorbereitung: Das App-Passwort
Wichtig: Tragt niemals euer normales Nextcloud-Passwort in den Code des Mikrocontrollers ein! Wenn ihr 2FA (Zwei-Faktor-Authentifizierung) nutzt, funktioniert das ohnehin nicht.
Geht in eurer Nextcloud oben rechts auf Einstellungen -> Sicherheit und erstellt ganz unten ein neues App-Passwort (zum Beispiel mit dem Namen „ESP32-Kamera“). Dieses Passwort kopiert ihr euch für den Code.
Der neue Code für den Upload
Da wir beim WebDAV-Upload den Dateinamen selbst festlegen müssen, nutzen wir das NTP (Network Time Protocol), um uns die aktuelle Uhrzeit aus dem Internet zu holen. So bekommt jedes Bild einen einzigartigen Namen (z. B. 20260707_161230.jpg) und nichts wird überschrieben.
Ersetze die Upload-Funktion aus Teil 1 durch diesen neuen Block. Achte darauf, dass der Ordner fotos in deiner Nextcloud bereits existieren muss!
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <time.h> // Für den Zeitstempel im Dateinamen
// --- Deine Zugangsdaten ---
const char* webdavUser = "DEIN_BENUTZERNAME";
const char* webdavPass = "DEIN_APP_PASSWORT"; // Das neu erstellte App-Passwort!
const char* ntpServer = "pool.ntp.org";
// --- Setup-Ergänzung (nur einmal im setup() nach dem WLAN-Verbinden aufrufen) ---
void setupZeit() {
// Zeitzone konfigurieren: +3600 Sekunden (1 Stunde) für Deutschland,
// plus weitere 3600 Sekunden für die Sommerzeit.
configTime(3600, 3600, ntpServer);
Serial.println("Warte auf Internet-Zeit...");
}
// --- Hilfsfunktion für den Dateinamen ---
String generiereDateiname() {
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) {
Serial.println("Fehler beim Abrufen der Zeit. Nutze Notfall-Namen.");
return "bild_" + String(millis()) + ".jpg";
}
char zeitText[30];
// Format: JahrMonatTag_StundeMinuteSekunde.jpg
strftime(zeitText, sizeof(zeitText), "%Y%m%d_%H%M%S.jpg", &timeinfo);
return String(zeitText);
}
// --- Die eigentliche Upload-Funktion ---
void ladeBildHoch(uint8_t* bildDaten, size_t bildGroesse) {
// 1. Sichere Verbindung aufbauen
WiFiClientSecure client;
// Ignoriert die Prüfung des SSL-Zertifikats (Secure Sockets Layer).
// Für private Bastelprojekte ist das völlig in Ordnung.
client.setInsecure();
HTTPClient http;
// 2. URL mit dynamischem Dateinamen zusammenbauen
String dateiName = generiereDateiname();
String zielUrl = "https://webdav.netcloud.de/remote.php/dav/files/" + String(webdavUser) + "/fotos/" + dateiName;
Serial.println("Starte Upload zu: " + zielUrl);
// 3. Verbindung starten und Login-Daten übergeben
http.begin(client, zielUrl);
http.setAuthorization(webdavUser, webdavPass);
// 4. Das Bild mit HTTP PUT direkt auf den Server schieben
int httpAntwortCode = http.PUT(bildDaten, bildGroesse);
// 5. Auswertung, ob es geklappt hat
if (httpAntwortCode > 0) {
Serial.print("Upload erfolgreich! Server meldet Code: ");
Serial.println(httpAntwortCode);
// Code 201 heißt "Created" (Erfolgreich angelegt)
} else {
Serial.print("Fehler beim Upload: ");
Serial.println(http.errorToString(httpAntwortCode).c_str());
}
// 6. Verbindung sauber trennen, um Arbeitsspeicher freizugeben
http.end();
}Mit diesem Setup hast du die volle Kontrolle über deine Daten. Die Lösung läuft extrem stabil und lässt sich auch auf lokale Netzwerkspeicher (NAS – Network Attached Storage) anwenden, die WebDAV unterstützen.
Teil 3: Bilder sicher an die eigene FRITZ!Box senden
In der dritten Variante nun, wie ihr die Bilder an die eigene Fritzbox sendet.
1. Vorbereitung auf der Fritzbox:
FTP-Zugriff auf der FRITZ!Box aktivieren
In der FRITZ!Box-Oberfläche:
Heimnetz → USB / Speicher (NAS)
Dort:
- NAS-Speicher Fuktion aktivieren
- anschließend „Speicher für Benutzer aus dem Internet freigeben“ aktivieren.
Du kannst dabei festlegen, welche Verzeichnisse überhaupt aus dem Internet erreichbar sein sollen.

2. Benutzer
Bei dem klassischen FTP-Zugriff der FRITZ!Box ist der FTP-Benutzername:
ftpuser (Andere Namen haben bei mir nicht geklappt)
Dort Zugang zu NAS-Inhalten gewähren und Zugang aus dem Internet

3. Externe Adresse
Die FRITZ!Box zeigt dir nach der Aktivierung die benötigte FTP-Adresse an.
Typischerweise sieht das ungefähr so aus:

Mit einem FTP-Programm wie FileZilla kannst du die Verbindung testen:

⚠️ Aber wichtig: FTP ist unverschlüsselt
Bei normalem FTP werden Benutzername und Passwort nicht verschlüsselt übertragen. Für einen Zugriff über das öffentliche Internet würde ich deshalb nicht einfach Port 21 nach außen öffnen.
4. Benötigte Bibliothek in der Arduino-IDE / PlatformIO

5. Anpassung der Sketches bzw. Quellcode
Ein Access Point Name und Passwort aus dem Bereich wo sich die Kamera befindet und FRITZ!Box FTP-Zugangsdaten aus dem öffentlichen Netz, wie vorbereitet eintragen.
Quellcode:
#include "esp_camera.h"
#include <WiFi.h>
#include <ESP32_FTPClient.h> // Bibliothek für den Datei-Upload
// --- 1. Lokale WLAN-Zugangsdaten (wo die Kamera gerade eingewählt ist) ---
const char* netzwerkName = "Access-point Name";
const char* netzwerkPasswort = "AP Passwort";
// --- 2. FRITZ!Box FTP-Zugangsdaten aus dem öffentlichen Netz ---
char ftp_server[] = "11.123.111.250"; // Deine oeffendliche IP-Adresse oder DDNS-Adresse
char ftp_user[] = "ftpuser"; // Dein FRITZ!Box-Benutzer
char ftp_pass[] = "passwort"; // Passwort des Benutzers
char ftp_ordner[] = "/Bilder/"; // Zielordner auf der FRITZ!Box
int ftp_port = 12345; // Zielordner
// FTP-Client initialisieren
ESP32_FTPClient ftp(ftp_server, ftp_port, ftp_user, ftp_pass, 5000, 2);
// --- 3. Hardware-Anschlüsse ---
const int tasterPin = 4; // Taster an GPIO 4 (D3)
const int ledPin = 3; // LED an GPIO 3 (D2)
// --- 4. Kamera-Pins für Seeed Studio XIAO ESP32S3 Sense ---
#define PWDN_GPIO_NUM -1
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 10
#define SIOD_GPIO_NUM 40
#define SIOC_GPIO_NUM 39
#define Y9_GPIO_NUM 48
#define Y8_GPIO_NUM 11
#define Y7_GPIO_NUM 12
#define Y6_GPIO_NUM 14
#define Y5_GPIO_NUM 16
#define Y4_GPIO_NUM 18
#define Y3_GPIO_NUM 17
#define Y2_GPIO_NUM 15
#define VSYNC_GPIO_NUM 38
#define HREF_GPIO_NUM 47
#define PCLK_GPIO_NUM 13
void setup() {
Serial.begin(115200);
pinMode(tasterPin, INPUT_PULLUP);
pinMode(ledPin, OUTPUT);
digitalWrite(ledPin, LOW);
// Mit beliebigem WLAN verbinden
Serial.print("Verbinde mit WLAN (Wireless Local Area Network)...");
WiFi.begin(netzwerkName, netzwerkPasswort);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println(" Erfolgreich verbunden!");
// Kamera-Konfiguration
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.frame_size = FRAMESIZE_SVGA;
config.pixel_format = PIXFORMAT_JPEG; // JPEG - Joint Photographic Experts Group
config.jpeg_quality = 10;
config.fb_count = 1;
esp_err_t fehler = esp_camera_init(&config);
if (fehler != ESP_OK) {
Serial.printf("Kamera-Start fehlgeschlagen: 0x%x\n", fehler);
return;
}
// Bilddrehung um 180 Grad
sensor_t * s = esp_camera_sensor_get();
if (s != NULL) {
s->set_vflip(s, 1);
s->set_hmirror(s, 1);
}
Serial.println("Kamera bereit. Warte auf Knopfdruck...");
}
void loop() {
if (digitalRead(tasterPin) == LOW) {
digitalWrite(ledPin, HIGH);
Serial.println("Ausloeser gedrueckt...");
// 1. Scheinschuss (Dummy-Shot): Alten Bildpuffer leeren
camera_fb_t * alterPuffer = esp_camera_fb_get();
if (alterPuffer) {
esp_camera_fb_return(alterPuffer);
}
// 2. Echtes Foto aufnehmen
camera_fb_t * bildspeicher = esp_camera_fb_get();
if (!bildspeicher) {
Serial.println("Fehler bei der Aufnahme.");
digitalWrite(ledPin, LOW);
return;
}
// 3. Bild per FTP auf die FRITZ!Box hochladen
if (WiFi.status() == WL_CONNECTED) {
Serial.println("Verbinde mit FRITZ!Box FTP...");
ftp.OpenConnection();
// In das Zielverzeichnis wechseln
ftp.ChangeWorkDir(ftp_ordner);
// Eindeutigen Dateinamen mit Zeitstempel erzeugen
String dateiName = "Foto_" + String(millis()) + ".jpg";
Serial.println("Sende Bilddatei: " + dateiName);
ftp.InitFile("Type I"); // Binärmodus für Bilder
ftp.NewFile(dateiName.c_str());
ftp.WriteData(bildspeicher->buf, bildspeicher->len);
ftp.CloseFile();
ftp.CloseConnection();
Serial.println("Erfolgreich auf FRITZ!Box gespeichert!");
} else {
Serial.println("Keine WLAN-Verbindung vorhanden.");
}
// Speicher freigeben und LED ausschalten
esp_camera_fb_return(bildspeicher);
digitalWrite(ledPin, LOW);
// Kurze Pause zur Entprellung
delay(2000);
}
}Wenn alles geklappt, hochladen und ein Test Foto schießen.
Im Seriellen Monitor kann man sehen:

und natürlich dann auf der NAS das finale Bild:

