Skip to content

Instantly share code, notes, and snippets.

@HDRobotica
Created January 8, 2026 20:04
Show Gist options
  • Select an option

  • Save HDRobotica/41efd1407ffe4dae4ba39b06a53fd15c to your computer and use it in GitHub Desktop.

Select an option

Save HDRobotica/41efd1407ffe4dae4ba39b06a53fd15c to your computer and use it in GitHub Desktop.
AI Thinker ESP32-CAM Online Ofline code
#include "esp_camera.h"
#include <WiFi.h>
#include <WebServer.h>
#include "FS.h"
#include "SD_MMC.h"
#include "esp_wifi.h"
#include <vector>
// WiFi-Konfiguration
const char* ssid = "your WIFI SSID";
const char* password = "your WIFI Password";
// SD-Karten Pins (aus deiner Tabelle)
#define SD_MMC_CMD 15
#define SD_MMC_CLK 14
#define SD_MMC_D0 2
#define SD_MMC_D1 4
#define SD_MMC_D2 12
#define SD_MMC_D3 13
// Kamerapins für AI Thinker ESP32-CAM
#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
#define LED_GPIO_NUM 4
// VR Modus
bool vrMode = false;
bool vrSideBySide = false;
WebServer server(80);
// Globale Variablen
bool sdCardAvailable = false;
bool ledState = false;
bool camStreaming = false;
bool isRecording = false;
int frameCounter = 0;
unsigned long lastFrameTime = 0;
unsigned long lastStatsUpdate = 0;
unsigned long recordingStartTime = 0;
unsigned long lastCaptureTime = 0;
const int TARGET_FPS = 25;
const int FRAME_INTERVAL = 1000 / TARGET_FPS;
const int RECORDING_FPS = 5;
String currentVideoFolder = "";
camera_fb_t* lastPhoto = NULL;
// Hilfsfunktionen deklarieren
bool initSDCard();
bool savePhotoToSD(camera_fb_t *fb, const char* filename);
int countPhotosOnSD();
int countVideoFolders();
String createVideoFolder();
void setupCamera();
void setupWiFi();
// VR HTML - Ohne Buttons, nur reiner Stream
const char VR_HTML[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>ESP32Cam VR Modus</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
-webkit-user-select: none;
user-select: none;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
background: #000;
position: fixed;
touch-action: none;
}
.vr-container {
display: flex;
width: 100%;
height: 100%;
position: relative;
}
.vr-eye {
flex: 1;
overflow: hidden;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.vr-stream {
width: 100%;
height: 100%;
object-fit: cover;
transform: rotate(90deg);
image-rendering: pixelated;
}
/* Landscape Mode für Handy in VR-Brille */
@media screen and (orientation: landscape) {
.vr-container {
flex-direction: row;
}
.vr-stream {
transform: rotate(0deg);
}
}
/* Portrait Mode - normale Handy-Nutzung */
@media screen and (orientation: portrait) {
.vr-container {
flex-direction: column;
}
.vr-stream {
transform: rotate(90deg);
}
}
</style>
</head>
<body>
<div class="vr-container">
<div class="vr-eye">
<img id="streamLeft" class="vr-stream" crossorigin="anonymous">
</div>
<div class="vr-eye">
<img id="streamRight" class="vr-stream" crossorigin="anonymous">
</div>
</div>
<script>
let streamLeft = document.getElementById('streamLeft');
let streamRight = document.getElementById('streamRight');
let isStreaming = true;
function updateVRStream() {
if (!isStreaming) return;
const timestamp = Date.now();
const url = '/stream?t=' + timestamp;
streamLeft.src = url;
streamRight.src = url;
// Auto-refresh für kontinuierlichen Stream
setTimeout(updateVRStream, 100);
}
// Tastatur-Shortcuts (versteckte Funktionen)
document.addEventListener('keydown', (e) => {
switch(e.key.toLowerCase()) {
case ' ': // Leertaste - Stream umschalten
isStreaming = !isStreaming;
if (isStreaming) {
updateVRStream();
} else {
streamLeft.src = '';
streamRight.src = '';
}
break;
case 'p': // P - Foto aufnehmen
fetch('/capture');
break;
case 'h': // H - Zurück zur Hauptseite
window.location.href = '/';
break;
case 'escape': // ESC - Zurück zur Hauptseite
window.location.href = '/';
break;
}
});
// Touch für Zurück
let touchStartTime = 0;
document.addEventListener('touchstart', (e) => {
touchStartTime = Date.now();
});
document.addEventListener('touchend', (e) => {
const touchDuration = Date.now() - touchStartTime;
if (touchDuration > 3000) { // 3 Sekunden halten
window.location.href = '/';
}
});
// Initialisierung
document.addEventListener('DOMContentLoaded', () => {
// Starte Stream
updateVRStream();
// Vollbild für bessere VR Erfahrung
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(err => {
console.log(`Fehler beim Vollbild: ${err.message}`);
});
}
});
// Handle orientation changes
window.addEventListener('orientationchange', function() {
setTimeout(() => {
// Force reflow and restart stream
streamLeft.src = '';
streamRight.src = '';
setTimeout(() => {
if (isStreaming) {
updateVRStream();
}
}, 100);
}, 300);
});
</script>
</body>
</html>
)rawliteral";
// HTML Oberfläche mit verbesserter Navigation
const char INDEX_HTML[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESP32Cam HD Robotics</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: Arial, sans-serif;
background: linear-gradient(135deg, #0f0f0f 0%, #1a1a1a 100%);
padding: 10px;
padding-bottom: 120px;
min-height: 100vh;
color: #e0e0e0;
}
.header {
text-align: center;
background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
color: white;
padding: 15px;
border-radius: 15px;
margin-bottom: 15px;
box-shadow: 0 6px 12px rgba(0,0,0,0.3);
border: 1px solid #3a506b;
}
h1 {
font-size: 1.8rem;
margin-bottom: 8px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
}
.ip-address {
font-size: 0.95rem;
opacity: 0.9;
color: #bdc3c7;
font-family: 'Courier New', monospace;
}
.video-container {
background: linear-gradient(135deg, #1e1e1e 0%, #2d2d2d 100%);
border-radius: 15px;
overflow: hidden;
margin-bottom: 20px;
text-align: center;
padding: 15px;
box-shadow: 0 6px 12px rgba(0,0,0,0.3);
border: 1px solid #3a3a3a;
}
#videoStream {
width: 100%;
max-width: 640px;
border-radius: 10px;
background: #000;
box-shadow: 0 4px 8px rgba(0,0,0,0.5);
border: 2px solid #3a506b;
}
.stats-container {
background: linear-gradient(135deg, #1e1e1e 0%, #2d2d2d 100%);
padding: 15px;
border-radius: 15px;
margin-bottom: 20px;
box-shadow: 0 6px 12px rgba(0,0,0,0.3);
border: 1px solid #3a3a3a;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.stat-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
border-radius: 10px;
transition: transform 0.2s;
}
.stat-item:hover {
transform: translateY(-3px);
background: linear-gradient(135deg, #34495e 0%, #2c3e50 100%);
}
.stat-label {
font-weight: bold;
color: #bdc3c7;
font-size: 14px;
}
.stat-value {
color: #1abc9c;
font-weight: bold;
font-size: 14px;
font-family: 'Courier New', monospace;
}
/* Untere Menüleiste */
.bottom-menu {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
padding: 15px 10px;
display: flex;
justify-content: space-around;
align-items: center;
box-shadow: 0 -4px 12px rgba(0,0,0,0.3);
border-top: 2px solid #1abc9c;
z-index: 1000;
}
.menu-btn {
background: none;
border: none;
color: white;
padding: 12px 8px;
border-radius: 10px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: all 0.3s;
min-width: 70px;
}
.menu-btn:hover {
background: rgba(255, 255, 255, 0.1);
transform: translateY(-3px);
}
.menu-btn:active {
transform: translateY(0);
}
.menu-btn.active {
background: #1abc9c;
color: white;
box-shadow: 0 4px 8px rgba(26, 188, 156, 0.3);
}
.btn-icon {
font-size: 22px;
margin-bottom: 5px;
}
.btn-text {
font-size: 11px;
text-align: center;
}
.loading {
text-align: center;
padding: 30px;
color: #bdc3c7;
font-size: 16px;
}
.error {
color: #e74c3c;
font-weight: bold;
background: rgba(231, 76, 60, 0.1);
padding: 10px;
border-radius: 5px;
margin: 10px 0;
}
.success {
color: #2ecc71;
font-weight: bold;
background: rgba(46, 204, 113, 0.1);
padding: 10px;
border-radius: 5px;
margin: 10px 0;
}
.message-container {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 1001;
max-width: 90%;
}
.gallery-modal {
display: none;
position: fixed;
z-index: 1002;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.95);
padding: 20px;
overflow-y: auto;
}
.gallery-content {
background: linear-gradient(135deg, #1e1e1e 0%, #2d2d2d 100%);
margin: 10% auto;
padding: 25px;
border-radius: 15px;
width: 95%;
max-width: 900px;
max-height: 80vh;
overflow-y: auto;
border: 1px solid #3a506b;
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 15px;
margin-top: 20px;
}
.gallery-item {
position: relative;
border-radius: 10px;
overflow: hidden;
cursor: pointer;
background: #2c3e50;
height: 150px;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.3s;
}
.gallery-item:hover {
transform: scale(1.05);
box-shadow: 0 6px 12px rgba(0,0,0,0.3);
}
.gallery-item img {
width: 100%;
height: 100%;
object-fit: cover;
}
.delete-btn {
position: absolute;
top: 8px;
right: 8px;
background: rgba(231, 76, 60, 0.9);
color: white;
border: none;
border-radius: 50%;
width: 30px;
height: 30px;
cursor: pointer;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
}
.website-footer {
text-align: center;
color: #7f8c8d;
font-size: 14px;
margin-top: 20px;
padding: 10px;
border-top: 1px solid #3a506b;
}
.copyright {
text-align: center;
color: #1abc9c;
font-size: 12px;
margin-top: 10px;
padding: 5px;
font-family: 'Courier New', monospace;
}
@media (max-width: 600px) {
.bottom-menu {
padding: 12px 5px;
}
.menu-btn {
padding: 10px 5px;
min-width: 60px;
font-size: 12px;
}
.btn-icon {
font-size: 20px;
}
.btn-text {
font-size: 10px;
}
.gallery-grid {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
}
.gallery-item {
height: 120px;
}
}
</style>
</head>
<body>
<div class="header">
<h1>📹 ESP32Cam HD Robotics</h1>
<div class="ip-address">IP: %IP% | SD: %SD_STATUS% | FPS: <span id="currentFPS">0</span></div>
</div>
<div class="video-container">
<div id="loading" class="loading">
<p>🔄 Kamera wird initialisiert... Bitte warten</p>
</div>
<img id="videoStream" crossorigin="anonymous">
<div id="message" style="display:none; margin-top:15px;"></div>
</div>
<div class="stats-container">
<div class="stats-grid">
<div class="stat-item">
<span class="stat-label">WiFi Signal:</span>
<span class="stat-value" id="rssi">- dBm</span>
</div>
<div class="stat-item">
<span class="stat-label">RAM frei:</span>
<span class="stat-value" id="freeRam">- KB</span>
</div>
<div class="stat-item">
<span class="stat-label">SD Speicher:</span>
<span class="stat-value" id="sdSpace">-</span>
</div>
<div class="stat-item">
<span class="stat-label">Fotos:</span>
<span class="stat-value" id="photoCount">0</span>
</div>
<div class="stat-item">
<span class="stat-label">Videos:</span>
<span class="stat-value" id="videoCount">0</span>
</div>
<div class="stat-item">
<span class="stat-label">Aufnahme:</span>
<span class="stat-value" id="recStatus">AUS</span>
</div>
</div>
</div>
<!-- Untere Menüleiste -->
<div class="bottom-menu">
<button class="menu-btn" onclick="toggleStream()" id="streamBtn">
<span class="btn-icon">▶️</span>
<span class="btn-text">Stream</span>
</button>
<button class="menu-btn" onclick="toggleLED()" id="ledBtn">
<span class="btn-icon">💡</span>
<span class="btn-text" id="ledText">LED AUS</span>
</button>
<button class="menu-btn" onclick="capturePhoto()" id="photoBtn">
<span class="btn-icon">📸</span>
<span class="btn-text">Foto</span>
</button>
<button class="menu-btn" onclick="showGallery()" id="galleryBtn">
<span class="btn-icon">🖼️</span>
<span class="btn-text">Galerie</span>
</button>
<button class="menu-btn" onclick="window.location.href='/vr'" id="vrBtn">
<span class="btn-icon">🥽</span>
<span class="btn-text">VR Modus</span>
</button>
<button class="menu-btn" onclick="toggleRecording()" id="recordBtn">
<span class="btn-icon">⏺️</span>
<span class="btn-text" id="recordText">Video</span>
</button>
<button class="menu-btn" onclick="refreshStats()" id="statsBtn">
<span class="btn-icon">📊</span>
<span class="btn-text">Info</span>
</button>
<button class="menu-btn" onclick="formatSDCard()" id="formatBtn">
<span class="btn-icon">💾</span>
<span class="btn-text">Format</span>
</button>
</div>
<div class="website-footer">
www.hdrobotics.de
</div>
<!-- Copyright -->
<div class="copyright">
© HD Robotics Electronics 2025
</div>
<!-- Gallery Modal -->
<div id="galleryModal" class="gallery-modal">
<div class="gallery-content">
<h2 style="color: white; margin-bottom: 20px; text-align: center;">📷 Galerie</h2>
<div style="margin-bottom: 20px; display: flex; gap: 10px; justify-content: center;">
<button onclick="refreshGallery()" style="padding: 10px 20px; background: #1abc9c; color: white; border: none; border-radius: 8px; cursor: pointer;">
🔄 Aktualisieren
</button>
<button onclick="closeGallery()" style="padding: 10px 20px; background: #e74c3c; color: white; border: none; border-radius: 8px; cursor: pointer;">
❌ Schließen
</button>
</div>
<div id="galleryLoading" class="loading">Lade Bilder...</div>
<div id="galleryGrid" class="gallery-grid" style="display: none;"></div>
<div id="galleryError" style="display: none; text-align: center; color: #e74c3c; margin-top: 20px;"></div>
<div class="copyright" style="margin-top: 20px;">
© HD Robotics Electronics 2025
</div>
</div>
</div>
<script>
let isStreaming = false;
let isRecording = false;
let ledState = false;
let frameTimes = [];
let lastFrameTime = 0;
let photoCounter = 0;
let videoCounter = 0;
let recTimer = null;
const videoStream = document.getElementById('videoStream');
const loadingElement = document.getElementById('loading');
const streamBtn = document.getElementById('streamBtn');
const recordBtn = document.getElementById('recordBtn');
const ledBtn = document.getElementById('ledBtn');
const messageElement = document.getElementById('message');
const galleryModal = document.getElementById('galleryModal');
const galleryGrid = document.getElementById('galleryGrid');
const galleryLoading = document.getElementById('galleryLoading');
const galleryError = document.getElementById('galleryError');
// MJPEG Stream Funktion
function startMJPEGStream() {
if (isStreaming) return;
videoStream.src = '/stream';
videoStream.style.display = 'block';
loadingElement.style.display = 'none';
isStreaming = true;
streamBtn.innerHTML = '<span class="btn-icon">⏸️</span><span class="btn-text">Stop</span>';
// FPS Berechnung
const checkFPS = () => {
const now = Date.now();
frameTimes = frameTimes.filter(time => now - time < 1000);
const fps = frameTimes.length;
document.getElementById('currentFPS').textContent = fps;
if (isStreaming) {
requestAnimationFrame(checkFPS);
}
};
requestAnimationFrame(checkFPS);
}
function stopStream() {
videoStream.src = '';
videoStream.style.display = 'none';
loadingElement.style.display = 'block';
isStreaming = false;
streamBtn.innerHTML = '<span class="btn-icon">▶️</span><span class="btn-text">Start</span>';
}
function toggleStream() {
if (isStreaming) {
stopStream();
} else {
startMJPEGStream();
}
}
// FPS Tracking für Video
videoStream.onload = function() {
const now = Date.now();
if (lastFrameTime > 0) {
frameTimes.push(now);
}
lastFrameTime = now;
// Automatisch nächsten Frame laden für kontinuierlichen Stream
if (isStreaming) {
setTimeout(() => {
videoStream.src = '/stream?t=' + Date.now();
}, 100);
}
};
videoStream.onerror = function() {
if (isStreaming) {
setTimeout(() => {
videoStream.src = '/stream?t=' + Date.now();
}, 500);
}
};
// Video Recording - OHNE Fehlermeldung bei Erfolg
async function toggleRecording() {
try {
const endpoint = isRecording ? '/stopRecording' : '/startRecording';
const response = await fetch(endpoint);
if (response.ok) {
isRecording = !isRecording;
if (isRecording) {
recordBtn.innerHTML = '<span class="btn-icon">⏹️</span><span class="btn-text">Stop</span>';
document.getElementById('recordText').textContent = 'Stop';
showMessage('🎬 Videoaufnahme gestartet', 'success');
} else {
recordBtn.innerHTML = '<span class="btn-icon">⏺️</span><span class="btn-text">Video</span>';
document.getElementById('recordText').textContent = 'Video';
showMessage('🛑 Videoaufnahme gestoppt', 'success');
refreshStats();
}
} else {
const data = await response.text();
showMessage('❌ Fehler: ' + data, 'error');
}
} catch (error) {
// Keine Fehlermeldung mehr - nur für Debugging
console.log('Recording toggle:', isRecording ? 'gestoppt' : 'gestartet');
}
}
// LED umschalten - OHNE Fehlermeldung bei Erfolg
async function toggleLED() {
try {
const response = await fetch('/led');
const text = await response.text();
if (response.ok) {
ledState = text.includes('EIN');
ledBtn.innerHTML = ledState ?
'<span class="btn-icon">💡</span><span class="btn-text">EIN</span>' :
'<span class="btn-icon">💡</span><span class="btn-text">AUS</span>';
document.getElementById('ledText').textContent = ledState ? 'EIN' : 'AUS';
// Keine Popup-Meldung mehr, nur leise Statusänderung
} else {
showMessage('❌ LED-Fehler', 'error');
}
} catch (error) {
// Keine Fehlermeldung mehr
console.log('LED toggled');
}
}
// Foto machen
async function capturePhoto() {
try {
const response = await fetch('/capture');
if (response.ok) {
photoCounter++;
document.getElementById('photoCount').textContent = photoCounter;
// Miniatur anzeigen
const img = document.createElement('img');
img.src = '/lastPhoto?t=' + Date.now();
img.style.width = '100px';
img.style.height = '75px';
img.style.margin = '10px';
img.style.borderRadius = '8px';
img.style.border = '3px solid #1abc9c';
img.style.cursor = 'pointer';
img.style.boxShadow = '0 4px 8px rgba(0,0,0,0.3)';
img.onclick = function() {
window.open(this.src, '_blank');
};
messageElement.innerHTML = '';
messageElement.appendChild(img);
messageElement.style.display = 'block';
setTimeout(() => {
messageElement.style.display = 'none';
}, 3000);
// Gallery aktualisieren
if (galleryModal.style.display === 'block') {
refreshGallery();
}
}
} catch (error) {
console.log('Foto aufgenommen');
}
}
// Gallery Funktionen - VERBESSERT
function showGallery() {
galleryModal.style.display = 'block';
refreshGallery();
}
function closeGallery() {
galleryModal.style.display = 'none';
}
async function refreshGallery() {
try {
galleryGrid.style.display = 'none';
galleryLoading.style.display = 'block';
galleryError.style.display = 'none';
const response = await fetch('/listFiles');
galleryGrid.innerHTML = '';
if (response.ok) {
const data = await response.json();
if (data.files && data.files.length > 0) {
// Sortiere nach Name (neueste zuerst)
data.files.sort((a, b) => b.name.localeCompare(a.name));
data.files.forEach(file => {
const item = document.createElement('div');
item.className = 'gallery-item';
const img = document.createElement('img');
// Verwende den direkten Pfad ohne URL-Parameter für bessere Performance
img.src = '/getFile?path=' + encodeURIComponent(file.path);
img.alt = file.name;
img.onclick = () => window.open(img.src, '_blank');
img.onerror = function() {
this.src = 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><rect width="100" height="100" fill="%232c3e50"/><text x="50" y="50" font-family="Arial" font-size="10" fill="white" text-anchor="middle" dy=".3em">Bild laden fehlgeschlagen</text></svg>';
};
const deleteBtn = document.createElement('button');
deleteBtn.className = 'delete-btn';
deleteBtn.innerHTML = '×';
deleteBtn.onclick = (e) => {
e.stopPropagation();
deleteFile(file.path);
};
item.appendChild(img);
item.appendChild(deleteBtn);
galleryGrid.appendChild(item);
});
} else {
galleryGrid.innerHTML = '<p style="color: #bdc3c7; text-align: center; grid-column: 1/-1; padding: 20px;">Keine Bilder gefunden</p>';
}
} else {
throw new Error('Server antwortete nicht OK');
}
galleryLoading.style.display = 'none';
galleryGrid.style.display = 'grid';
} catch (error) {
console.error('Gallery Fehler:', error);
galleryLoading.style.display = 'none';
galleryError.style.display = 'block';
galleryError.innerHTML = '<div class="error">❌ Fehler beim Laden der Galerie. Bitte aktualisieren.</div>';
}
}
async function deleteFile(path) {
if (!confirm('Datei wirklich löschen?')) return;
try {
const response = await fetch('/deleteFile?path=' + encodeURIComponent(path));
if (response.ok) {
refreshGallery();
refreshStats();
}
} catch (error) {
console.log('Datei gelöscht');
}
}
// SD-Karte formatieren
async function formatSDCard() {
if (!confirm('⚠️ WARNUNG: SD-Karte wird formatiert!\n\nAlle Daten gehen verloren!\nWirklich fortfahren?')) {
return;
}
try {
const response = await fetch('/formatSD');
const data = await response.text();
if (response.ok) {
showMessage('✅ SD-Karte erfolgreich formatiert!', 'success');
refreshStats();
} else {
showMessage('❌ Fehler: ' + data, 'error');
}
} catch (error) {
showMessage('❌ Fehler beim Formatieren', 'error');
}
}
// Stats aktualisieren
async function refreshStats() {
try {
const response = await fetch('/stats');
const data = await response.json();
document.getElementById('rssi').textContent = data.rssi + ' dBm';
document.getElementById('freeRam').textContent = Math.round(data.freeRam / 1024) + ' KB';
document.getElementById('sdSpace').textContent = data.freeSpace + '/' + data.totalSpace + ' MB';
document.getElementById('photoCount').textContent = data.photoCount;
document.getElementById('videoCount').textContent = data.videoCount || 0;
document.getElementById('recStatus').textContent = data.isRecording ? 'AN' : 'AUS';
photoCounter = data.photoCount || 0;
videoCounter = data.videoCount || 0;
if (data.isRecording) {
isRecording = true;
recordBtn.innerHTML = '<span class="btn-icon">⏹️</span><span class="btn-text">Stop</span>';
document.getElementById('recordText').textContent = 'Stop';
}
// SD Status im Header aktualisieren
document.getElementById('sdStatus').textContent = data.sdCard ? '✔' : '✘';
} catch (error) {
console.log('Stats aktualisiert');
}
}
// Nachricht anzeigen (nur bei echten Fehlern)
function showMessage(text, type) {
if (type === 'success' && (text.includes('LED') || text.includes('Videoaufnahme'))) {
return; // Keine Popups für normale Funktionen
}
const messageDiv = document.createElement('div');
messageDiv.className = type;
messageDiv.textContent = text;
messageDiv.style.position = 'fixed';
messageDiv.style.top = '20px';
messageDiv.style.left = '50%';
messageDiv.style.transform = 'translateX(-50%)';
messageDiv.style.padding = '15px 20px';
messageDiv.style.borderRadius = '10px';
messageDiv.style.zIndex = '1003';
messageDiv.style.boxShadow = '0 6px 12px rgba(0,0,0,0.3)';
messageDiv.style.minWidth = '300px';
messageDiv.style.textAlign = 'center';
document.body.appendChild(messageDiv);
setTimeout(() => {
messageDiv.remove();
}, 3000);
}
// Initialisierung
document.addEventListener('DOMContentLoaded', () => {
// Auto-Start Stream nach 2 Sekunden
setTimeout(startMJPEGStream, 2000);
// Stats alle 5 Sekunden aktualisieren
setInterval(refreshStats, 5000);
refreshStats();
});
</script>
</body>
</html>
)rawliteral";
// SD-Karte initialisieren
bool initSDCard() {
Serial.println("📁 Initialisiere SD-Karte...");
if(SD_MMC.cardType() != CARD_NONE) {
SD_MMC.end();
delay(100);
}
SD_MMC.setPins(SD_MMC_CLK, SD_MMC_CMD, SD_MMC_D0, SD_MMC_D1, SD_MMC_D2, SD_MMC_D3);
if(!SD_MMC.begin("/sdcard", true, false, 4000000)) {
Serial.println("❌ SD-Karte Montage fehlgeschlagen!");
return false;
}
delay(100);
uint8_t cardType = SD_MMC.cardType();
if(cardType == CARD_NONE) {
Serial.println("❌ Keine SD-Karte gefunden!");
return false;
}
Serial.print("✅ SD-Karten Typ: ");
if(cardType == CARD_MMC) Serial.println("MMC");
else if(cardType == CARD_SD) Serial.println("SDSC");
else if(cardType == CARD_SDHC) Serial.println("SDHC");
else Serial.println("Unbekannt");
uint64_t cardSize = SD_MMC.cardSize() / (1024 * 1024);
Serial.printf("📊 SD-Kartengröße: %lluMB\n", cardSize);
File testFile = SD_MMC.open("/test.txt", FILE_WRITE);
if(testFile) {
testFile.println("Test erfolgreich - HD Robotics 2025");
testFile.close();
Serial.println("✅ SD-Karte schreibbar");
SD_MMC.remove("/test.txt");
return true;
} else {
Serial.println("⚠️ SD-Karte könnte schreibgeschützt sein");
return false;
}
}
// Hilfsfunktion um Dateien rekursiv zu finden
void findJPGFiles(File dir, std::vector<String> &files) {
while(File entry = dir.openNextFile()) {
if (!entry) break;
String fileName = entry.name();
if (fileName.startsWith(".") || fileName.equals("System Volume Information")) {
entry.close();
continue;
}
if (entry.isDirectory()) {
findJPGFiles(entry, files);
} else if (fileName.endsWith(".jpg") || fileName.endsWith(".JPG") ||
fileName.endsWith(".jpeg") || fileName.endsWith(".JPEG")) {
if (!fileName.startsWith("frame_") && !fileName.startsWith("/video_")) {
files.push_back(fileName);
}
}
entry.close();
}
}
// Zähle Fotos auf SD-Karte
int countPhotosOnSD() {
if (!sdCardAvailable) return 0;
File root = SD_MMC.open("/");
if (!root) return 0;
std::vector<String> files;
findJPGFiles(root, files);
root.close();
return files.size();
}
// Zähle Video-Ordner
int countVideoFolders() {
if (!sdCardAvailable) return 0;
File root = SD_MMC.open("/");
if (!root) return 0;
int count = 0;
File file = root.openNextFile();
while (file) {
if (file.isDirectory()) {
String folderName = String(file.name());
if (folderName.startsWith("video_")) {
count++;
}
}
file = root.openNextFile();
}
root.close();
return count;
}
// Foto auf SD-Karte speichern
bool savePhotoToSD(camera_fb_t *fb, const char* filename) {
if (!sdCardAvailable || !fb) return false;
File file = SD_MMC.open(filename, FILE_WRITE);
if (!file) {
return false;
}
size_t written = file.write(fb->buf, fb->len);
file.flush();
file.close();
return (written == fb->len);
}
// Erstelle Video-Ordner
String createVideoFolder() {
if (!sdCardAvailable) return "";
char folderName[32];
sprintf(folderName, "/video_%lu", millis());
if (SD_MMC.mkdir(folderName)) {
return String(folderName);
} else {
return "";
}
}
// Kamera initialisieren
void setupCamera() {
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 = 10000000;
config.pixel_format = PIXFORMAT_JPEG;
config.frame_size = FRAMESIZE_QVGA;
config.jpeg_quality = 12;
config.fb_count = 2;
config.grab_mode = CAMERA_GRAB_LATEST;
config.fb_location = CAMERA_FB_IN_PSRAM;
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK) {
config.frame_size = FRAMESIZE_QQVGA;
config.jpeg_quality = 15;
config.xclk_freq_hz = 5000000;
config.fb_count = 1;
err = esp_camera_init(&config);
if (err != ESP_OK) {
return;
}
}
sensor_t *s = esp_camera_sensor_get();
if (s->id.PID == OV2640_PID) {
s->set_vflip(s, 1);
s->set_hmirror(s, 1);
s->set_brightness(s, 0);
s->set_contrast(s, 0);
s->set_saturation(s, 0);
s->set_special_effect(s, 0);
s->set_whitebal(s, 1);
s->set_gainceiling(s, GAINCEILING_8X);
s->set_colorbar(s, 0);
s->set_framesize(s, FRAMESIZE_QVGA);
}
}
// WiFi initialisieren
void setupWiFi() {
Serial.print("📶 Verbinde mit WiFi: ");
Serial.println(ssid);
WiFi.disconnect(true);
delay(1000);
WiFi.mode(WIFI_STA);
esp_wifi_set_ps(WIFI_PS_NONE);
WiFi.setSleep(false);
WiFi.begin(ssid, password);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
Serial.print(".");
attempts++;
digitalWrite(LED_GPIO_NUM, attempts % 2);
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\n✅ WiFi verbunden!");
Serial.print("📡 IP: ");
Serial.println(WiFi.localIP());
} else {
Serial.println("\n⚠️ WiFi Verbindung fehlgeschlagen! Starte AP...");
WiFi.disconnect(true);
delay(100);
WiFi.mode(WIFI_AP);
if (WiFi.softAP("ESP32-CAM HD Robotics", "12345678", 1, 0, 4)) {
Serial.print("📡 AP IP: ");
Serial.println(WiFi.softAPIP());
}
}
}
// Stream Handler
void handleStream() {
camera_fb_t *fb = esp_camera_fb_get();
if (!fb) {
server.send(500, "text/plain", "Kamera-Fehler");
return;
}
server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
server.sendHeader("Pragma", "no-cache");
server.sendHeader("Expires", "-1");
server.sendHeader("Access-Control-Allow-Origin", "*");
WiFiClient client = server.client();
server.setContentLength(fb->len);
server.send(200, "image/jpeg", "");
client.write(fb->buf, fb->len);
esp_camera_fb_return(fb);
frameCounter++;
if (isRecording && sdCardAvailable && (millis() - lastCaptureTime >= (1000 / RECORDING_FPS))) {
char filename[64];
sprintf(filename, "%s/frame_%06d.jpg", currentVideoFolder.c_str(), (int)((millis() - recordingStartTime) / (1000 / RECORDING_FPS)));
camera_fb_t *recording_fb = esp_camera_fb_get();
if (recording_fb) {
savePhotoToSD(recording_fb, filename);
esp_camera_fb_return(recording_fb);
}
lastCaptureTime = millis();
}
}
// VR HTML Seite
void handleVR() {
vrMode = true;
String html = FPSTR(VR_HTML);
server.send(200, "text/html", html);
}
// Foto aufnehmen
void handleCapture() {
camera_fb_t *fb = esp_camera_fb_get();
if (!fb) {
server.send(500, "text/plain", "Kamera-Fehler");
return;
}
if (lastPhoto != NULL) {
esp_camera_fb_return(lastPhoto);
}
lastPhoto = fb;
char filename[32];
sprintf(filename, "/photo_%lu.jpg", millis());
bool saved = savePhotoToSD(fb, filename);
// Sofortige Antwort senden
server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
server.sendHeader("Pragma", "no-cache");
server.sendHeader("Expires", "-1");
server.sendHeader("Access-Control-Allow-Origin", "*");
WiFiClient client = server.client();
server.setContentLength(fb->len);
server.send(200, "image/jpeg", "");
client.write(fb->buf, fb->len);
}
// Letztes Foto zurückgeben
void handleLastPhoto() {
if (lastPhoto == NULL) {
server.send(404, "text/plain", "Kein Foto vorhanden");
return;
}
server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
server.sendHeader("Pragma", "no-cache");
server.sendHeader("Expires", "-1");
server.sendHeader("Access-Control-Allow-Origin", "*");
WiFiClient client = server.client();
server.setContentLength(lastPhoto->len);
server.send(200, "image/jpeg", "");
client.write(lastPhoto->buf, lastPhoto->len);
}
// Video-Aufnahme starten
void handleStartRecording() {
if (isRecording) {
server.send(200, "text/plain", "Aufnahme läuft bereits");
return;
}
if (!sdCardAvailable) {
server.send(500, "text/plain", "SD-Karte nicht verfügbar");
return;
}
currentVideoFolder = createVideoFolder();
if (currentVideoFolder == "") {
server.send(500, "text/plain", "Fehler beim Erstellen des Video-Ordners");
return;
}
isRecording = true;
recordingStartTime = millis();
lastCaptureTime = 0;
server.send(200, "text/plain", "Videoaufnahme gestartet");
}
// Video-Aufnahme stoppen
void handleStopRecording() {
if (!isRecording) {
server.send(200, "text/plain", "Keine Aufnahme aktiv");
return;
}
isRecording = false;
currentVideoFolder = "";
server.send(200, "text/plain", "Videoaufnahme gestoppt");
}
// Dateien auflisten - KORRIGIERT UND VERBESSERT
void handleListFiles() {
if (!sdCardAvailable) {
server.send(500, "application/json", "{\"error\":\"SD-Karte nicht verfügbar\"}");
return;
}
String json = "{\"files\":[";
bool firstFile = true;
int fileCount = 0;
// Rekursiv nach JPG-Dateien suchen
std::vector<String> allFiles;
File root = SD_MMC.open("/");
if (root) {
// Rekursiv alle JPGs finden
findJPGFiles(root, allFiles);
root.close();
// Sortieren (neueste zuerst)
std::sort(allFiles.begin(), allFiles.end(), [](const String &a, const String &b) {
return a > b; // Absteigend sortieren
});
// Auf 50 Dateien begrenzen
int limit = min(50, (int)allFiles.size());
for (int i = 0; i < limit; i++) {
String fileName = allFiles[i];
// Überprüfen ob Datei existiert und JPG ist
if (fileName.length() > 0) {
if (!firstFile) json += ",";
json += "{\"name\":\"" + fileName.substring(fileName.lastIndexOf('/') + 1) + "\",";
json += "\"path\":\"" + fileName + "\",";
// Dateigröße ermitteln
File file = SD_MMC.open(fileName);
size_t fileSize = 0;
if (file) {
fileSize = file.size();
file.close();
}
json += "\"size\":" + String(fileSize) + "}";
firstFile = false;
fileCount++;
}
}
}
json += "]}";
server.sendHeader("Access-Control-Allow-Origin", "*");
server.send(200, "application/json", json);
}
// Datei abrufen
void handleGetFile() {
if (!sdCardAvailable) {
server.send(500, "text/plain", "SD-Karte nicht verfügbar");
return;
}
String path = server.arg("path");
if (path == "") {
server.send(400, "text/plain", "Pfadparameter fehlt");
return;
}
File file = SD_MMC.open(path);
if (!file || file.isDirectory()) {
server.send(404, "text/plain", "Datei nicht gefunden");
if (file) file.close();
return;
}
size_t fileSize = file.size();
if (fileSize == 0) {
file.close();
server.send(404, "text/plain", "Datei ist leer");
return;
}
server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
server.sendHeader("Pragma", "no-cache");
server.sendHeader("Expires", "-1");
server.sendHeader("Access-Control-Allow-Origin", "*");
server.sendHeader("Content-Length", String(fileSize));
server.sendHeader("Content-Type", "image/jpeg");
WiFiClient client = server.client();
const size_t bufferSize = 4096;
uint8_t buffer[bufferSize];
size_t bytesRead;
while ((bytesRead = file.read(buffer, bufferSize)) > 0) {
client.write(buffer, bytesRead);
}
file.close();
}
// Datei löschen
void handleDeleteFile() {
if (!sdCardAvailable) {
server.send(500, "text/plain", "SD-Karte nicht verfügbar");
return;
}
String path = server.arg("path");
if (path == "") {
server.send(400, "text/plain", "Pfadparameter fehlt");
return;
}
if (SD_MMC.remove(path)) {
server.send(200, "text/plain", "Datei gelöscht");
} else {
server.send(500, "text/plain", "Fehler beim Löschen");
}
}
// SD-Karte formatieren
void handleFormatSD() {
if (!sdCardAvailable) {
server.send(500, "text/plain", "SD-Karte nicht verfügbar");
return;
}
// Alle Dateien löschen
File root = SD_MMC.open("/");
if (root) {
File file = root.openNextFile();
while (file) {
String path = String(file.name());
if (file.isDirectory()) {
if (!path.equals("/System Volume Information") && !path.startsWith("/.")) {
SD_MMC.rmdir(path);
}
} else {
SD_MMC.remove(path);
}
file = root.openNextFile();
}
root.close();
}
// Test schreiben
File testFile = SD_MMC.open("/test_format.txt", FILE_WRITE);
if (testFile) {
testFile.println("Formatierung erfolgreich - HD Robotics 2025");
testFile.close();
SD_MMC.remove("/test_format.txt");
server.send(200, "text/plain", "SD-Karte erfolgreich formatiert!");
} else {
server.send(500, "text/plain", "Fehler beim Formatieren!");
}
}
// LED Handler - OHNE Fehlermeldungen
void handleLED() {
ledState = !ledState;
pinMode(LED_GPIO_NUM, OUTPUT);
digitalWrite(LED_GPIO_NUM, ledState ? HIGH : LOW);
String message = ledState ? "LED EIN" : "LED AUS";
server.send(200, "text/plain", message);
}
// System Stats Handler
void handleStats() {
uint64_t totalBytes = 0;
uint64_t usedBytes = 0;
uint64_t freeBytes = 0;
if (sdCardAvailable) {
totalBytes = SD_MMC.totalBytes() / (1024 * 1024);
usedBytes = SD_MMC.usedBytes() / (1024 * 1024);
freeBytes = totalBytes - usedBytes;
}
String json = "{";
json += "\"rssi\":" + String(WiFi.RSSI()) + ",";
json += "\"freeRam\":" + String(ESP.getFreeHeap()) + ",";
json += "\"sdCard\":" + String(sdCardAvailable ? "true" : "false") + ",";
json += "\"photoCount\":" + String(countPhotosOnSD()) + ",";
json += "\"videoCount\":" + String(countVideoFolders()) + ",";
json += "\"isRecording\":" + String(isRecording ? "true" : "false") + ",";
json += "\"totalSpace\":" + String(totalBytes) + ",";
json += "\"usedSpace\":" + String(usedBytes) + ",";
json += "\"freeSpace\":" + String(freeBytes) + ",";
json += "\"uptime\":" + String(millis() / 1000);
json += "}";
server.sendHeader("Access-Control-Allow-Origin", "*");
server.send(200, "application/json", json);
}
// Root Handler
void handleRoot() {
String html = FPSTR(INDEX_HTML);
html.replace("%IP%", WiFi.localIP().toString());
html.replace("%SD_STATUS%", sdCardAvailable ? "✔" : "✘");
server.send(200, "text/html", html);
}
void setup() {
Serial.begin(115200);
delay(100);
Serial.println("\n\n");
Serial.println("╔══════════════════════════════════════════╗");
Serial.println("║ ESP32-CAM HD Robotics - v3.1 ║");
Serial.println("║ Korrigierte Version ║");
Serial.println("║ © HD Robotics Electronics 2025 ║");
Serial.println("╚══════════════════════════════════════════╝");
Serial.println();
pinMode(LED_GPIO_NUM, OUTPUT);
digitalWrite(LED_GPIO_NUM, LOW);
Serial.println("🔧 Initialisiere System...");
delay(500);
sdCardAvailable = initSDCard();
setupCamera();
setupWiFi();
server.on("/", handleRoot);
server.on("/stream", handleStream);
server.on("/vr", handleVR);
server.on("/capture", handleCapture);
server.on("/lastPhoto", handleLastPhoto);
server.on("/led", handleLED);
server.on("/stats", handleStats);
server.on("/startRecording", handleStartRecording);
server.on("/stopRecording", handleStopRecording);
server.on("/listFiles", handleListFiles);
server.on("/getFile", handleGetFile);
server.on("/deleteFile", handleDeleteFile);
server.on("/formatSD", handleFormatSD);
server.onNotFound([]() {
server.send(404, "text/plain", "404: Nicht gefunden - HD Robotics 2025");
});
server.begin();
Serial.println("✅ WebServer gestartet");
Serial.print("🌐 Öffne: http://");
Serial.println(WiFi.localIP());
Serial.println("🥽 VR Modus: http://" + WiFi.localIP().toString() + "/vr");
for (int i = 0; i < 3; i++) {
digitalWrite(LED_GPIO_NUM, HIGH);
delay(200);
digitalWrite(LED_GPIO_NUM, LOW);
delay(200);
}
if (sdCardAvailable) {
Serial.println("✅ SD-Karte bereit für Fotos und Videos");
}
Serial.println("\n✅ System bereit");
Serial.println("==========================================");
}
void loop() {
server.handleClient();
unsigned long now = millis();
if (now - lastStatsUpdate > 30000) {
if (WiFi.getMode() == WIFI_STA && WiFi.status() != WL_CONNECTED) {
setupWiFi();
}
if (sdCardAvailable && now % 30000 == 0) {
uint8_t cardType = SD_MMC.cardType();
if (cardType == CARD_NONE) {
sdCardAvailable = false;
}
}
lastStatsUpdate = now;
}
delay(1);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment