#!/usr/bin/env python3 """ Claude's Eyes - Mock ESP32 Server Simuliert den ESP32-Roboter für Tests ohne echte Hardware. Features: - Liefert Testbilder aus ./test_images/ - ODER nutzt eine echte USB-Webcam (use_real_webcam: true in config) - Simuliert Fahrbefehle (loggt sie) - Liefert Fake-Sensordaten Usage: 1. Leg JPG-Bilder in ./test_images/ (z.B. Fotos aus deiner Wohnung) ODER aktiviere use_real_webcam in config.yaml 2. python mock_esp32.py 3. In config.yaml: host: "localhost", port: 5000 4. Starte die Bridge - Claude "fährt" durch deine Testbilder! """ import os import random import logging import base64 import yaml from pathlib import Path from datetime import datetime from flask import Flask, jsonify, send_file, request, Response # OpenCV für Webcam (optional) try: import cv2 OPENCV_AVAILABLE = True except ImportError: OPENCV_AVAILABLE = False # Logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) app = Flask(__name__) # Konfiguration SCRIPT_DIR = Path(__file__).parent IMAGES_DIR = SCRIPT_DIR / "test_images" FOTO_PATH = SCRIPT_DIR / "foto.jpg" # Hier wird das aktuelle Foto gespeichert API_KEY = "claudes_eyes_secret_2025" # Mock-Konfiguration (wird aus config.yaml geladen) mock_config = { "use_real_webcam": False, "webcam_device": 0, "webcam_width": 640, "webcam_height": 480, } # Webcam-Objekt (wird bei Bedarf initialisiert) webcam = None # State current_image_index = 0 position = {"x": 0, "y": 0, "rotation": 0} camera_angle = {"pan": 90, "tilt": 90} def load_mock_config(): """Lädt die Mock-Konfiguration aus config.yaml""" global mock_config # Versuche config.local.yaml zuerst, dann config.yaml for config_name in ["config.local.yaml", "config.yaml"]: config_path = SCRIPT_DIR / config_name if config_path.exists(): try: with open(config_path, 'r', encoding='utf-8') as f: config = yaml.safe_load(f) if config and "mock" in config: mock_config.update(config["mock"]) logger.info(f"Mock-Config geladen aus {config_name}") return except Exception as e: logger.warning(f"Fehler beim Laden von {config_name}: {e}") logger.info("Keine Mock-Config gefunden, nutze Defaults") def init_webcam(): """Initialisiert die Webcam""" global webcam if not OPENCV_AVAILABLE: logger.error("OpenCV nicht installiert! Installiere mit: pip install opencv-python") return False if webcam is not None: return True device = mock_config.get("webcam_device", 0) width = mock_config.get("webcam_width", 640) height = mock_config.get("webcam_height", 480) try: webcam = cv2.VideoCapture(device) if not webcam.isOpened(): logger.error(f"Konnte Webcam {device} nicht öffnen!") webcam = None return False # Auflösung setzen webcam.set(cv2.CAP_PROP_FRAME_WIDTH, width) webcam.set(cv2.CAP_PROP_FRAME_HEIGHT, height) actual_w = int(webcam.get(cv2.CAP_PROP_FRAME_WIDTH)) actual_h = int(webcam.get(cv2.CAP_PROP_FRAME_HEIGHT)) logger.info(f"Webcam {device} initialisiert: {actual_w}x{actual_h}") return True except Exception as e: logger.error(f"Webcam-Fehler: {e}") webcam = None return False def capture_from_webcam() -> bool: """ Nimmt ein Bild von der Webcam auf und speichert es als foto.jpg Returns: True wenn erfolgreich, False bei Fehler """ global webcam if not init_webcam(): return False try: # Bild aufnehmen ret, frame = webcam.read() if not ret or frame is None: logger.error("Konnte kein Bild von Webcam lesen!") return False # Als JPEG speichern cv2.imwrite(str(FOTO_PATH), frame) logger.info(f"📷 Webcam-Bild aufgenommen: {FOTO_PATH.name}") return True except Exception as e: logger.error(f"Webcam-Capture-Fehler: {e}") return False def release_webcam(): """Gibt die Webcam frei""" global webcam if webcam is not None: webcam.release() webcam = None logger.info("Webcam freigegeben") def check_api_key(): """Prüft den API-Key""" key = request.args.get("key", "") if key != API_KEY: return False return True @app.route("/") def index(): """Startseite""" return """ Mock ESP32 - Claude's Eyes

🤖 Mock ESP32 Server

Simuliert den Claude's Eyes Roboter für Tests.

API Endpoints:

Für die Python Bridge:

Die Bridge holt das Bild von /api/capture und lädt es per Selenium in Claude.ai hoch!

So kann Claude im Chat die Bilder direkt sehen.

Status:

API-Key: {key}

""".format( key=API_KEY, images_dir=IMAGES_DIR, image_count=len(list(IMAGES_DIR.glob("*.jpg"))) if IMAGES_DIR.exists() else 0, current_index=current_image_index ) @app.route("/api/capture", methods=["GET"]) def capture(): """ Macht ein "Foto" und liefert es DIREKT als JPEG zurück. Das ist wie beim echten ESP32 - Bild wird direkt gestreamt. Kein JSON, sondern das Bild selbst! Je nach Konfiguration: - use_real_webcam: true → Bild von USB-Webcam - use_real_webcam: false → Bild aus test_images/ Ordner """ global current_image_index if not check_api_key(): return jsonify({"error": "Invalid API key"}), 401 # ════════════════════════════════════════════════════════════════ # WEBCAM-MODUS: Echtes Bild von USB-Webcam # ════════════════════════════════════════════════════════════════ if mock_config.get("use_real_webcam", False): if capture_from_webcam(): # Foto wurde in foto.jpg gespeichert, das zurückgeben if FOTO_PATH.exists(): return send_file(FOTO_PATH, mimetype="image/jpeg") else: return jsonify({"error": "Webcam-Capture fehlgeschlagen"}), 500 else: # Fallback: Bestehendes foto.jpg nutzen falls vorhanden if FOTO_PATH.exists(): logger.warning("Webcam-Fehler, nutze bestehendes foto.jpg") return send_file(FOTO_PATH, mimetype="image/jpeg") return jsonify({"error": "Webcam nicht verfügbar und kein foto.jpg vorhanden"}), 500 # ════════════════════════════════════════════════════════════════ # TESTBILD-MODUS: Bild aus test_images/ Ordner # ════════════════════════════════════════════════════════════════ if not IMAGES_DIR.exists(): IMAGES_DIR.mkdir(parents=True) # Fallback: foto.jpg nutzen falls vorhanden if FOTO_PATH.exists(): logger.info("📷 Kein test_images/, nutze foto.jpg") return send_file(FOTO_PATH, mimetype="image/jpeg") return jsonify({ "error": f"Keine Bilder gefunden! Leg JPGs in {IMAGES_DIR} ab oder aktiviere use_real_webcam." }), 404 images = sorted(IMAGES_DIR.glob("*.jpg")) if not images: images = sorted(IMAGES_DIR.glob("*.png")) if not images: # Fallback: foto.jpg nutzen falls vorhanden if FOTO_PATH.exists(): logger.info("📷 Keine Testbilder, nutze foto.jpg") return send_file(FOTO_PATH, mimetype="image/jpeg") return jsonify({ "error": f"Keine Bilder gefunden! Leg JPGs in {IMAGES_DIR} ab oder aktiviere use_real_webcam." }), 404 # Aktuelles Testbild holen image = images[current_image_index % len(images)] logger.info(f"📷 Capture: {image.name} (#{current_image_index + 1}/{len(images)})") # Bild direkt zurückgeben (wie echter ESP32) return send_file(image, mimetype="image/jpeg") @app.route("/foto.jpg", methods=["GET"]) def get_foto(): """ Liefert das aktuelle Foto - immer dieselbe URL! Das ist der Hauptendpoint für Claude.ai Chat. Bei Webcam-Modus wird hier immer das letzte Webcam-Bild geliefert. """ if not FOTO_PATH.exists(): # Bei Webcam-Modus: Mach ein Foto falls noch keins da ist if mock_config.get("use_real_webcam", False): if capture_from_webcam(): return send_file(FOTO_PATH, mimetype="image/jpeg") return jsonify({"error": "Noch kein Foto aufgenommen! Erst /api/capture aufrufen."}), 404 logger.info(f"📷 Foto abgerufen: foto.jpg") return send_file(FOTO_PATH, mimetype="image/jpeg") @app.route("/api/status", methods=["GET"]) def status(): """Liefert Fake-Sensordaten""" if not check_api_key(): return jsonify({"error": "Invalid API key"}), 401 # Zähle verfügbare Bilder image_count = 0 if IMAGES_DIR.exists(): image_count = len(list(IMAGES_DIR.glob("*.jpg"))) + len(list(IMAGES_DIR.glob("*.png"))) data = { "mock": True, "timestamp": datetime.now().isoformat(), "distance_cm": random.randint(20, 200), "battery_voltage": round(random.uniform(7.0, 8.4), 2), "uptime_ms": random.randint(10000, 1000000), "position": position, "camera_angle": camera_angle, "imu": { "accel_x": round(random.uniform(-0.1, 0.1), 3), "accel_y": round(random.uniform(-0.1, 0.1), 3), "accel_z": round(random.uniform(0.95, 1.05), 3), "gyro_x": round(random.uniform(-1, 1), 2), "gyro_y": round(random.uniform(-1, 1), 2), "gyro_z": round(random.uniform(-1, 1), 2), }, "wifi_rssi": random.randint(-70, -30), "test_images": { "total": image_count, "current_index": current_image_index } } logger.info(f"📊 Status: distance={data['distance_cm']}cm, battery={data['battery_voltage']}V") return jsonify(data) @app.route("/api/command", methods=["POST"]) def command(): """Nimmt Fahrbefehle an""" global current_image_index, position, camera_angle if not check_api_key(): return jsonify({"error": "Invalid API key"}), 401 data = request.get_json() or {} action = data.get("action", "").lower() speed = data.get("speed", 50) duration = data.get("duration_ms", 500) logger.info(f"🎮 Command: {action} (speed={speed}, duration={duration}ms)") # Simuliere Bewegung if action == "forward": position["y"] += 1 current_image_index += 1 # Nächstes Bild logger.info(f" → Vorwärts, jetzt bei Bild #{current_image_index + 1}") elif action == "backward": position["y"] -= 1 current_image_index = max(0, current_image_index - 1) logger.info(f" → Rückwärts, jetzt bei Bild #{current_image_index + 1}") elif action == "left": position["rotation"] = (position["rotation"] - 45) % 360 logger.info(f" → Links drehen, Rotation: {position['rotation']}°") elif action == "right": position["rotation"] = (position["rotation"] + 45) % 360 logger.info(f" → Rechts drehen, Rotation: {position['rotation']}°") elif action == "stop": logger.info(" → Stop") elif action == "look_left": camera_angle["pan"] = max(0, camera_angle["pan"] - 30) logger.info(f" → Kamera links, Pan: {camera_angle['pan']}°") elif action == "look_right": camera_angle["pan"] = min(180, camera_angle["pan"] + 30) logger.info(f" → Kamera rechts, Pan: {camera_angle['pan']}°") elif action == "look_up": camera_angle["tilt"] = max(0, camera_angle["tilt"] - 20) logger.info(f" → Kamera hoch, Tilt: {camera_angle['tilt']}°") elif action == "look_down": camera_angle["tilt"] = min(180, camera_angle["tilt"] + 20) logger.info(f" → Kamera runter, Tilt: {camera_angle['tilt']}°") elif action == "look_center": camera_angle = {"pan": 90, "tilt": 90} logger.info(" → Kamera zentriert") else: return jsonify({"error": f"Unknown action: {action}"}), 400 return jsonify({ "status": "ok", "mock": True, "action": action, "position": position, "camera_angle": camera_angle, "current_image_index": current_image_index }) @app.route("/api/display", methods=["POST"]) def display(): """Simuliert Display-Steuerung""" if not check_api_key(): return jsonify({"error": "Invalid API key"}), 401 data = request.get_json() or {} logger.info(f"🖥️ Display: {data}") return jsonify({"status": "ok", "mock": True}) def main(): """Startet den Mock-Server""" # Lade Konfiguration load_mock_config() use_webcam = mock_config.get("use_real_webcam", False) webcam_device = mock_config.get("webcam_device", 0) print(""" ╔══════════════════════════════════════════════════════════════╗ ║ ║ ║ 🤖 MOCK ESP32 SERVER - Claude's Eyes ║ ║ ║ ║ Simuliert den Roboter für Tests ohne Hardware. ║ ║ ║ ╠══════════════════════════════════════════════════════════════╣""") if use_webcam: print("""║ ║ ║ 📷 WEBCAM-MODUS AKTIV ║ ║ Bilder kommen von deiner USB-Webcam (Device {device}) ║ ║ ║""".format(device=webcam_device)) else: print("""║ ║ ║ 📁 TESTBILD-MODUS ║ ║ Leg Testbilder in ./test_images/ ab (JPG oder PNG) ║ ║ Tipp: Mach 10-20 Fotos aus deiner Wohnung! ║ ║ ║ ║ ODER aktiviere Webcam in config.yaml: ║ ║ mock: ║ ║ use_real_webcam: true ║ ║ ║""") print("""╠══════════════════════════════════════════════════════════════╣ ║ ║ ║ Für die Bridge - config.yaml: ║ ║ esp32: ║ ║ host: "localhost" ║ ║ port: 5000 ║ ║ ║ ╠══════════════════════════════════════════════════════════════╣ ║ ║ ║ Server: http://localhost:5000 ║ ║ API-Key: {api_key} ║ ║ ║ ╚══════════════════════════════════════════════════════════════╝ """.format(api_key=API_KEY)) # Webcam testen falls aktiviert if use_webcam: if not OPENCV_AVAILABLE: print("❌ OpenCV nicht installiert!") print(" Installiere mit: pip install opencv-python") print(" Oder deaktiviere Webcam in config.yaml\n") else: print(f"📷 Teste Webcam {webcam_device}...") if init_webcam(): print(f"✅ Webcam bereit!") # Test-Capture if capture_from_webcam(): print(f"✅ Test-Bild aufgenommen: {FOTO_PATH}") else: print(f"❌ Webcam {webcam_device} konnte nicht geöffnet werden!") print(" Prüfe ob eine Webcam angeschlossen ist.\n") else: # Erstelle Bilder-Ordner falls nicht existiert if not IMAGES_DIR.exists(): IMAGES_DIR.mkdir(parents=True) print(f"\n⚠️ Ordner {IMAGES_DIR} erstellt - leg dort Testbilder ab!\n") # Zähle Bilder images = list(IMAGES_DIR.glob("*.jpg")) + list(IMAGES_DIR.glob("*.png")) if images: print(f"📁 Gefunden: {len(images)} Testbilder") for img in images[:5]: print(f" - {img.name}") if len(images) > 5: print(f" ... und {len(images) - 5} weitere") else: # Prüfe ob foto.jpg existiert if FOTO_PATH.exists(): print(f"📷 Nutze bestehendes {FOTO_PATH.name}") else: print(f"⚠️ Keine Bilder in {IMAGES_DIR} gefunden!") print(" Leg dort JPG/PNG-Dateien ab für den Test.") print(" Oder aktiviere use_real_webcam in config.yaml\n") print("\n🚀 Starte Server...\n") try: app.run(host="0.0.0.0", port=5000, debug=True) finally: # Webcam freigeben beim Beenden release_webcam() if __name__ == "__main__": main()