You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
138 lines
4.9 KiB
138 lines
4.9 KiB
"""
|
|
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/<scenario_name>", 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)
|