""" dashboard/app.py ---------------- Flask backend for the BATL CARLA Orchestrator Dashboard. Serves the HTML UI, exposes REST endpoints, and streams real-time simulation output via Server-Sent Events (SSE). NOTE: This file lives inside the /dashboard sub-folder. PROJECT_ROOT is therefore TWO levels up: dashboard/app.py → dashboard/ → Fox/ """ import os import sys from flask import Flask, render_template, request, jsonify, Response import subprocess # ------------------------------------------------------------------ # Path bootstrapping — make the project root importable regardless # of which directory the user launches from. # ------------------------------------------------------------------ PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) if PROJECT_ROOT not in sys.path: sys.path.insert(0, PROJECT_ROOT) import config from src.scenario_loader import list_scenarios # ------------------------------------------------------------------ # Flask app — templates and static assets are siblings of this file. # ------------------------------------------------------------------ DASHBOARD_DIR = os.path.dirname(os.path.abspath(__file__)) app = Flask( __name__, template_folder=os.path.join(DASHBOARD_DIR, "templates"), static_folder=os.path.join(DASHBOARD_DIR, "static"), ) # ── Routes ──────────────────────────────────────────────────────── @app.route("/") def index(): return render_template("index.html") @app.route("/api/config", methods=["GET"]) def get_config(): scenarios = list_scenarios() return jsonify({ "scenarios": scenarios, "default_scenario": getattr(config, "DEFAULT_SCENARIO", "braking"), "default_frames": getattr(config, "MAX_FRAMES", 200), "default_weather": getattr(config, "DEFAULT_WEATHER", "ClearNoon"), "weather_options": [ "ClearNoon", "CloudyNoon", "WetNoon", "SoftRainNoon", "HardRainNoon", "ClearSunset", "Night" ], }) @app.route("/api/scenario_params/", methods=["GET"]) def get_scenario_params(scenario_name): from src.scenario_loader import load_scenario try: scenario = load_scenario(scenario_name) tunable_params = {} # Convention: tunable constants are UPPERCASE class-level attributes for attr in dir(scenario): if attr.isupper() and not attr.startswith("_"): val = getattr(scenario, attr) if isinstance(val, (int, float, str, bool)): tunable_params[attr] = val return jsonify({"success": True, "params": tunable_params}) except Exception as e: return jsonify({"success": False, "error": str(e)}) @app.route("/api/run", methods=["POST"]) def run_simulation(): data = request.json or {} scenario = data.get("scenario") or "braking" frames = data.get("frames") or "" weather = data.get("weather") or "" no_record = bool(data.get("no_record")) params = data.get("params") or "" # Build the command — run.bat lives in PROJECT_ROOT cmd = ["cmd.exe", "/c", "run.bat", scenario] if frames: cmd.extend(["--frames", str(frames)]) if weather: cmd.extend(["--weather", weather]) if no_record: cmd.append("--no-record") if params: cmd.extend(["--params", f'"{params}"']) def generate(): try: # cwd = project root so that run.bat is reachable env = os.environ.copy() env["PYTHONUNBUFFERED"] = "1" process = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, cwd=PROJECT_ROOT, env=env, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP, ) yield f"data: [INFO] Starting command: {' '.join(cmd)}\n\n" for line in iter(process.stdout.readline, ""): if line: yield f"data: {line.rstrip()}\n\n" process.stdout.close() return_code = process.wait() yield f"data: [PROCESS_COMPLETED] Exit Code: {return_code}\n\n" except Exception as e: yield f"data: [ERROR] Failed to start process: {str(e)}\n\n" return Response(generate(), mimetype="text/event-stream") # ── Entry point ─────────────────────────────────────────────────── if __name__ == "__main__": print("Starting BATL CARLA Dashboard orchestrator...") print(f" Project root : {PROJECT_ROOT}") print(f" Dashboard dir: {DASHBOARD_DIR}") app.run(host="0.0.0.0", port=5000, debug=True)