320 lines
11 KiB
Python
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()
|