esp32-claude-robbie/python_bridge/mock_esp32.py

320 lines
11 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/
- 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 """
<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!
"""
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()