Created
January 8, 2026 20:04
-
-
Save HDRobotica/41efd1407ffe4dae4ba39b06a53fd15c to your computer and use it in GitHub Desktop.
AI Thinker ESP32-CAM Online Ofline code
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #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