esp32-claude-robbie/python_bridge/mock_esp32.py

519 lines
19 KiB
Python

#!/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 """
<html>
<head><title>Mock ESP32 - Claude's Eyes</title></head>
<body style="font-family: monospace; padding: 20px;">
<h1>🤖 Mock ESP32 Server</h1>
<p>Simuliert den Claude's Eyes Roboter für Tests.</p>
<h2>API Endpoints:</h2>
<ul>
<li><a href="/api/capture?key={key}">/api/capture</a> - Foto aufnehmen (liefert JPEG direkt!)</li>
<li><a href="/api/status?key={key}">/api/status</a> - Sensor-Status</li>
<li>/api/command (POST) - Fahrbefehle</li>
</ul>
<h2>Für die Python Bridge:</h2>
<p>Die Bridge holt das Bild von <code>/api/capture</code> und lädt es per Selenium in Claude.ai hoch!</p>
<p>So kann Claude im Chat die Bilder direkt sehen.</p>
<h2>Status:</h2>
<ul>
<li>Bilder-Ordner: {images_dir}</li>
<li>Gefundene Bilder: {image_count}</li>
<li>Aktuelles Bild: #{current_index}</li>
</ul>
<p><small>API-Key: {key}</small></p>
</body>
</html>
""".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()