#!/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/ - Simuliert Fahrbefehle (loggt sie) - Liefert Fake-Sensordaten Usage: 1. Leg JPG-Bilder in ./test_images/ (z.B. Fotos aus deiner Wohnung) 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 from pathlib import Path from datetime import datetime from flask import Flask, jsonify, send_file, request, Response # Logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) app = Flask(__name__) # Konfiguration IMAGES_DIR = Path(__file__).parent / "test_images" API_KEY = "claudes_eyes_secret_2025" # State current_image_index = 0 position = {"x": 0, "y": 0, "rotation": 0} camera_angle = {"pan": 90, "tilt": 90} 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! """ global current_image_index if not check_api_key(): return jsonify({"error": "Invalid API key"}), 401 # Finde Testbilder if not IMAGES_DIR.exists(): IMAGES_DIR.mkdir(parents=True) return jsonify({ "error": f"Keine Bilder gefunden! Leg JPGs in {IMAGES_DIR} ab." }), 404 images = sorted(IMAGES_DIR.glob("*.jpg")) if not images: images = sorted(IMAGES_DIR.glob("*.png")) if not images: return jsonify({ "error": f"Keine Bilder gefunden! Leg JPGs in {IMAGES_DIR} ab." }), 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. Nach /api/capture liegt das neue Bild hier. """ foto_path = IMAGES_DIR.parent / "foto.jpg" if not foto_path.exists(): 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""" print(""" โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— โ•‘ โ•‘ โ•‘ ๐Ÿค– MOCK ESP32 SERVER - Claude's Eyes โ•‘ โ•‘ โ•‘ โ•‘ Simuliert den Roboter fรผr Tests ohne Hardware. โ•‘ โ•‘ โ•‘ โ• โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ โ•‘ โ•‘ โ•‘ 1. Leg Testbilder in ./test_images/ ab (JPG oder PNG) โ•‘ โ•‘ Tipp: Mach 10-20 Fotos aus deiner Wohnung! โ•‘ โ•‘ โ•‘ โ•‘ 2. Passe config.yaml an: โ•‘ โ•‘ esp32: โ•‘ โ•‘ host: "localhost" โ•‘ โ•‘ port: 5000 โ•‘ โ•‘ โ•‘ โ•‘ 3. Starte die Bridge in einem anderen Terminal โ•‘ โ•‘ โ•‘ โ• โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ โ•‘ โ•‘ โ•‘ Server: http://localhost:5000 โ•‘ โ•‘ API-Key: {api_key} โ•‘ โ•‘ โ•‘ โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• """.format(api_key=API_KEY)) # 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: print(f"โš ๏ธ Keine Bilder in {IMAGES_DIR} gefunden!") print(" Leg dort JPG/PNG-Dateien ab fรผr den Test.\n") print("\n๐Ÿš€ Starte Server...\n") app.run(host="0.0.0.0", port=5000, debug=True) if __name__ == "__main__": main()