2025-10-18 19:15:41 -05:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
|
|
|
|
|
|
from fastapi import FastAPI, Body, HTTPException
|
|
|
|
|
from fastapi.responses import JSONResponse
|
|
|
|
|
from pydantic import BaseModel
|
|
|
|
|
import httpx
|
|
|
|
|
|
|
|
|
|
from .enrichment import Enricher
|
|
|
|
|
from .adapter import normalize_telemetry
|
|
|
|
|
|
|
|
|
|
app = FastAPI(title="HPCSim Enrichment API", version="0.1.0")
|
|
|
|
|
|
|
|
|
|
# Single Enricher instance keeps state across laps
|
|
|
|
|
_enricher = Enricher()
|
|
|
|
|
|
|
|
|
|
# Simple in-memory store of recent enriched records
|
|
|
|
|
_recent: List[Dict[str, Any]] = []
|
|
|
|
|
_MAX_RECENT = 200
|
|
|
|
|
|
|
|
|
|
# Optional callback URL to forward enriched data to next stage
|
|
|
|
|
_CALLBACK_URL = os.getenv("NEXT_STAGE_CALLBACK_URL")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class EnrichedRecord(BaseModel):
|
|
|
|
|
lap: int
|
|
|
|
|
aero_efficiency: float
|
|
|
|
|
tire_degradation_index: float
|
|
|
|
|
ers_charge: float
|
|
|
|
|
fuel_optimization_score: float
|
|
|
|
|
driver_consistency: float
|
|
|
|
|
weather_impact: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.post("/ingest/telemetry")
|
|
|
|
|
async def ingest_telemetry(payload: Dict[str, Any] = Body(...)):
|
2025-10-19 02:00:56 -05:00
|
|
|
"""Receive raw telemetry (from Pi), normalize, enrich, return enriched with race context.
|
2025-10-18 19:15:41 -05:00
|
|
|
|
|
|
|
|
Optionally forwards to NEXT_STAGE_CALLBACK_URL if set.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
normalized = normalize_telemetry(payload)
|
2025-10-19 02:00:56 -05:00
|
|
|
result = _enricher.enrich_with_context(normalized)
|
|
|
|
|
enriched = result["enriched_telemetry"]
|
|
|
|
|
race_context = result["race_context"]
|
2025-10-18 19:15:41 -05:00
|
|
|
except Exception as e:
|
|
|
|
|
raise HTTPException(status_code=400, detail=f"Failed to enrich: {e}")
|
|
|
|
|
|
2025-10-19 02:00:56 -05:00
|
|
|
# Store enriched telemetry in recent buffer
|
2025-10-18 19:15:41 -05:00
|
|
|
_recent.append(enriched)
|
|
|
|
|
if len(_recent) > _MAX_RECENT:
|
|
|
|
|
del _recent[: len(_recent) - _MAX_RECENT]
|
|
|
|
|
|
|
|
|
|
# Async forward to next stage if configured
|
2025-10-19 02:00:56 -05:00
|
|
|
# Send both enriched telemetry and race context
|
2025-10-18 19:15:41 -05:00
|
|
|
if _CALLBACK_URL:
|
|
|
|
|
try:
|
|
|
|
|
async with httpx.AsyncClient(timeout=5.0) as client:
|
2025-10-19 02:00:56 -05:00
|
|
|
await client.post(_CALLBACK_URL, json=result)
|
2025-10-18 19:15:41 -05:00
|
|
|
except Exception:
|
|
|
|
|
# Don't fail ingestion if forwarding fails; log could be added here
|
|
|
|
|
pass
|
|
|
|
|
|
2025-10-19 02:00:56 -05:00
|
|
|
return JSONResponse(result)
|
2025-10-18 19:15:41 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.post("/enriched")
|
|
|
|
|
async def post_enriched(enriched: EnrichedRecord):
|
|
|
|
|
"""Allow posting externally enriched records (bypass local computation)."""
|
|
|
|
|
rec = enriched.model_dump()
|
|
|
|
|
_recent.append(rec)
|
|
|
|
|
if len(_recent) > _MAX_RECENT:
|
|
|
|
|
del _recent[: len(_recent) - _MAX_RECENT]
|
|
|
|
|
return JSONResponse(rec)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/enriched")
|
|
|
|
|
async def list_enriched(limit: int = 50):
|
|
|
|
|
limit = max(1, min(200, limit))
|
|
|
|
|
return JSONResponse(_recent[-limit:])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/healthz")
|
|
|
|
|
async def healthz():
|
|
|
|
|
return {"status": "ok", "stored": len(_recent)}
|