diff --git a/ai_intelligence_layer/__pycache__/main.cpython-313.pyc b/ai_intelligence_layer/__pycache__/main.cpython-313.pyc index d97d7fa..2f3e0ff 100644 Binary files a/ai_intelligence_layer/__pycache__/main.cpython-313.pyc and b/ai_intelligence_layer/__pycache__/main.cpython-313.pyc differ diff --git a/ai_intelligence_layer/main.py b/ai_intelligence_layer/main.py index d955e1a..f2fe67a 100644 --- a/ai_intelligence_layer/main.py +++ b/ai_intelligence_layer/main.py @@ -6,11 +6,15 @@ Supports WebSocket connections from Pi for bidirectional control. """ from fastapi import FastAPI, HTTPException, status, WebSocket, WebSocketDisconnect from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse from contextlib import asynccontextmanager import logging import asyncio import random from typing import Dict, Any, List +from datetime import datetime +import json from config import get_settings from models.input_models import ( @@ -52,11 +56,14 @@ class ConnectionManager: def __init__(self): self.active_connections: List[WebSocket] = [] + self.vehicle_counter = 0 async def connect(self, websocket: WebSocket): await websocket.accept() self.active_connections.append(websocket) + self.vehicle_counter += 1 logger.info(f"Pi client connected. Total connections: {len(self.active_connections)}") + return self.vehicle_counter def disconnect(self, websocket: WebSocket): self.active_connections.remove(websocket) @@ -74,7 +81,38 @@ class ConnectionManager: except Exception as e: logger.error(f"Error broadcasting to client: {e}") +class DashboardManager: + """Manages WebSocket connections for dashboard clients.""" + + def __init__(self): + self.active_dashboards: List[WebSocket] = [] + + async def connect(self, websocket: WebSocket): + await websocket.accept() + self.active_dashboards.append(websocket) + logger.info(f"Dashboard connected. Total dashboards: {len(self.active_dashboards)}") + + def disconnect(self, websocket: WebSocket): + if websocket in self.active_dashboards: + self.active_dashboards.remove(websocket) + logger.info(f"Dashboard disconnected. Total dashboards: {len(self.active_dashboards)}") + + async def broadcast(self, message: Dict[str, Any]): + """Broadcast message to all connected dashboards.""" + disconnected = [] + for dashboard in self.active_dashboards: + try: + await dashboard.send_json(message) + except Exception as e: + logger.error(f"Error broadcasting to dashboard: {e}") + disconnected.append(dashboard) + + # Clean up disconnected dashboards + for dashboard in disconnected: + self.disconnect(dashboard) + websocket_manager = ConnectionManager() +dashboard_manager = DashboardManager() @asynccontextmanager @@ -118,6 +156,15 @@ app.add_middleware( allow_headers=["*"], ) +# Mount static files +app.mount("/static", StaticFiles(directory="static"), name="static") + + +@app.get("/") +async def dashboard(): + """Serve the dashboard HTML page.""" + return FileResponse("static/dashboard.html") + @app.get("/api/health", response_model=HealthResponse) async def health_check(): @@ -297,6 +344,30 @@ async def analyze_strategies(request: AnalyzeRequest): """ +@app.websocket("/ws/dashboard") +async def websocket_dashboard_endpoint(websocket: WebSocket): + """ + WebSocket endpoint for dashboard clients. + Broadcasts vehicle connection status and lap data updates. + """ + await dashboard_manager.connect(websocket) + + try: + # Keep connection alive + while True: + # Receive any messages (mostly just keepalive pings) + data = await websocket.receive_text() + # Echo back if needed + if data == "ping": + await websocket.send_text("pong") + except WebSocketDisconnect: + logger.info("[Dashboard] Client disconnected") + except Exception as e: + logger.error(f"[Dashboard] Error: {e}") + finally: + dashboard_manager.disconnect(websocket) + + @app.websocket("/ws/pi") async def websocket_pi_endpoint(websocket: WebSocket): """ @@ -309,7 +380,7 @@ async def websocket_pi_endpoint(websocket: WebSocket): """ global current_race_context, last_control_command - await websocket_manager.connect(websocket) + vehicle_id = await websocket_manager.connect(websocket) # Clear telemetry buffer for fresh connection # This ensures lap counting starts from scratch for each Pi session @@ -320,6 +391,13 @@ async def websocket_pi_endpoint(websocket: WebSocket): logger.info("[WebSocket] Telemetry buffer cleared for new connection") + # Notify dashboards of new vehicle connection + await dashboard_manager.broadcast({ + "type": "vehicle_connected", + "vehicle_id": vehicle_id, + "timestamp": datetime.now().isoformat() + }) + try: # Send initial welcome message await websocket.send_json({ @@ -407,6 +485,23 @@ async def websocket_pi_endpoint(websocket: WebSocket): "reasoning": control_command.get("reasoning", "") }) + # Broadcast to dashboards with strategy + await dashboard_manager.broadcast({ + "type": "lap_data", + "vehicle_id": vehicle_id, + "lap_data": enriched, + "control_output": { + "brake_bias": control_command["brake_bias"], + "differential_slip": control_command["differential_slip"] + }, + "strategy": { + "strategy_name": top_strategy.strategy_name, + "risk_level": top_strategy.risk_level, + "brief_description": top_strategy.brief_description + } if top_strategy else None, + "timestamp": datetime.now().isoformat() + }) + logger.info(f"{'='*60}\n") except Exception as e: @@ -426,6 +521,19 @@ async def websocket_pi_endpoint(websocket: WebSocket): "differential_slip": 5, # Neutral "message": f"Collecting data ({len(buffer_data)}/3 laps)" }) + + # Broadcast to dashboards (no strategy yet) + await dashboard_manager.broadcast({ + "type": "lap_data", + "vehicle_id": vehicle_id, + "lap_data": enriched, + "control_output": { + "brake_bias": 5, + "differential_slip": 5 + }, + "strategy": None, + "timestamp": datetime.now().isoformat() + }) except Exception as e: logger.error(f"[WebSocket] Error processing telemetry: {e}") @@ -454,6 +562,13 @@ async def websocket_pi_endpoint(websocket: WebSocket): # Clear buffer when connection closes to ensure fresh start for next connection telemetry_buffer.clear() logger.info("[WebSocket] Telemetry buffer cleared on disconnect") + + # Notify dashboards of vehicle disconnect + await dashboard_manager.broadcast({ + "type": "vehicle_disconnected", + "vehicle_id": vehicle_id, + "timestamp": datetime.now().isoformat() + }) def generate_control_command( diff --git a/ai_intelligence_layer/static/dashboard.html b/ai_intelligence_layer/static/dashboard.html new file mode 100644 index 0000000..1072211 --- /dev/null +++ b/ai_intelligence_layer/static/dashboard.html @@ -0,0 +1,474 @@ + + + + + + F1 AI Intelligence Layer - Vehicle Dashboard + + + +
+

🏎️ F1 AI Intelligence Layer Dashboard

+

Real-time vehicle telemetry, strategy generation, and control outputs

+
+ ● Connecting... +
+
+ +
+
+ No vehicle connections yet. Waiting for Pi clients to connect... +
+
+ + + + diff --git a/hpcsim/__pycache__/api.cpython-313.pyc b/hpcsim/__pycache__/api.cpython-313.pyc index 9e596c0..69959e8 100644 Binary files a/hpcsim/__pycache__/api.cpython-313.pyc and b/hpcsim/__pycache__/api.cpython-313.pyc differ