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 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 (Lithium-Polymer-Akkumulator), der unser Projekt zusätzlich mobil machen würde.
- 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).

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“ zwingend auf Jeder. (Nur temporär öffnen und auf eigene Gefahr)
- 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.iniunterbuild_flags = -D BOARD_HAS_PSRAMeintragen.
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>
// --- 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; // Ist D3
const int ledPin = 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...");
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;
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;
}
Serial.println("Kamera ist bereit. Warte auf Knopfdruck.");
}
void loop() {
if (digitalRead(tasterPin) == LOW) {
digitalWrite(ledPin, HIGH);
Serial.println("Klick! Mache ein Foto...");
camera_fb_t * bildspeicher = esp_camera_fb_get();
if (!bildspeicher) {
Serial.println("Fehler bei der Aufnahme.");
digitalWrite(ledPin, LOW);
return;
}
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);
// Wir werten Code 200 (OK) oder Code 302 (Umleitung nach Skript-Ausführung) als Erfolg
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.");
}
esp_camera_fb_return(bildspeicher);
digitalWrite(ledPin, LOW);
// Eine kurze Pause, damit nicht aus Versehen mehrere Bilder gemacht werden
delay(2000);
}
}Ergebnis:
Drücke den Knopf und wenige Sekunden später findest du dein Foto in deinem Google Drive:


3D Druck:
hier der Openscad Skript. 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();
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);
}
}


Download der fertigen Druckdateien https://makerworld.com/de/models/2892682-diy-esp32s3-xiao-sense-variable-camera-case
Viel Spaß beim Tüfteln und Nachbauen!
Youtube Video:
