2025-10-18 20:51:16 -05:00
|
|
|
"""
|
2025-10-19 03:57:03 -05:00
|
|
|
Raspberry Pi Telemetry Stream Simulator - Lap-Level Data
|
2025-10-18 20:51:16 -05:00
|
|
|
|
2025-10-19 03:57:03 -05:00
|
|
|
Reads the ALONSO_2023_MONZA_LAPS.csv file lap by lap and simulates
|
2025-10-19 00:23:17 -05:00
|
|
|
live telemetry streaming from a Raspberry Pi sensor.
|
2025-10-19 03:57:03 -05:00
|
|
|
Sends data to the HPC simulation layer via HTTP POST at fixed
|
|
|
|
|
1-minute intervals between laps.
|
2025-10-18 20:51:16 -05:00
|
|
|
|
|
|
|
|
Usage:
|
2025-10-19 03:57:03 -05:00
|
|
|
python simulate_pi_stream.py
|
|
|
|
|
python simulate_pi_stream.py --interval 30 # 30 seconds between laps
|
2025-10-18 20:51:16 -05:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
|
import time
|
|
|
|
|
import sys
|
|
|
|
|
from pathlib import Path
|
2025-10-19 00:23:17 -05:00
|
|
|
from typing import Dict, Any
|
|
|
|
|
import pandas as pd
|
2025-10-18 20:51:16 -05:00
|
|
|
import requests
|
|
|
|
|
|
|
|
|
|
|
2025-10-19 03:57:03 -05:00
|
|
|
def load_lap_csv(filepath: Path) -> pd.DataFrame:
|
|
|
|
|
"""Load lap-level telemetry data from CSV file."""
|
|
|
|
|
df = pd.read_csv(filepath)
|
2025-10-19 00:23:17 -05:00
|
|
|
|
2025-10-19 03:57:03 -05:00
|
|
|
# Convert lap_time to timedelta if it's not already
|
|
|
|
|
if 'lap_time' in df.columns and df['lap_time'].dtype == 'object':
|
|
|
|
|
df['lap_time'] = pd.to_timedelta(df['lap_time'])
|
2025-10-19 00:23:17 -05:00
|
|
|
|
2025-10-19 03:57:03 -05:00
|
|
|
print(f"✓ Loaded {len(df)} laps from {filepath}")
|
2025-10-19 00:23:17 -05:00
|
|
|
print(f" Laps: {df['lap_number'].min():.0f} → {df['lap_number'].max():.0f}")
|
|
|
|
|
|
|
|
|
|
return df
|
|
|
|
|
|
|
|
|
|
|
2025-10-19 03:57:03 -05:00
|
|
|
def lap_to_json(row: pd.Series) -> Dict[str, Any]:
|
|
|
|
|
"""Convert a lap DataFrame row to a JSON-compatible dictionary."""
|
2025-10-19 00:23:17 -05:00
|
|
|
data = {
|
|
|
|
|
'lap_number': int(row['lap_number']) if pd.notna(row['lap_number']) else None,
|
|
|
|
|
'total_laps': int(row['total_laps']) if pd.notna(row['total_laps']) else None,
|
2025-10-19 03:57:03 -05:00
|
|
|
'lap_time': str(row['lap_time']) if pd.notna(row['lap_time']) else None,
|
|
|
|
|
'average_speed': float(row['average_speed']) if pd.notna(row['average_speed']) else 0.0,
|
|
|
|
|
'max_speed': float(row['max_speed']) if pd.notna(row['max_speed']) else 0.0,
|
2025-10-19 00:23:17 -05:00
|
|
|
'tire_compound': str(row['tire_compound']) if pd.notna(row['tire_compound']) else 'UNKNOWN',
|
2025-10-19 03:57:03 -05:00
|
|
|
'tire_life_laps': int(row['tire_life_laps']) if pd.notna(row['tire_life_laps']) else 0,
|
2025-10-19 00:23:17 -05:00
|
|
|
'track_temperature': float(row['track_temperature']) if pd.notna(row['track_temperature']) else 0.0,
|
2025-10-19 03:57:03 -05:00
|
|
|
'rainfall': bool(row['rainfall'])
|
2025-10-19 00:23:17 -05:00
|
|
|
}
|
2025-10-18 20:51:16 -05:00
|
|
|
return data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def simulate_stream(
|
2025-10-19 00:23:17 -05:00
|
|
|
df: pd.DataFrame,
|
2025-10-18 20:51:16 -05:00
|
|
|
endpoint: str,
|
2025-10-19 03:57:03 -05:00
|
|
|
interval: int = 60,
|
2025-10-18 20:51:16 -05:00
|
|
|
start_lap: int = 1,
|
|
|
|
|
end_lap: int = None
|
|
|
|
|
):
|
|
|
|
|
"""
|
2025-10-19 03:57:03 -05:00
|
|
|
Simulate live telemetry streaming with fixed interval between laps.
|
2025-10-18 20:51:16 -05:00
|
|
|
|
|
|
|
|
Args:
|
2025-10-19 03:57:03 -05:00
|
|
|
df: DataFrame with lap-level telemetry data
|
2025-10-18 20:51:16 -05:00
|
|
|
endpoint: HPC API endpoint URL
|
2025-10-19 03:57:03 -05:00
|
|
|
interval: Fixed interval in seconds between laps (default: 60 seconds)
|
2025-10-18 20:51:16 -05:00
|
|
|
start_lap: Starting lap number
|
|
|
|
|
end_lap: Ending lap number (None = all laps)
|
|
|
|
|
"""
|
|
|
|
|
# Filter by lap range
|
2025-10-19 00:23:17 -05:00
|
|
|
filtered_df = df[df['lap_number'] >= start_lap].copy()
|
2025-10-18 20:51:16 -05:00
|
|
|
if end_lap:
|
2025-10-19 00:23:17 -05:00
|
|
|
filtered_df = filtered_df[filtered_df['lap_number'] <= end_lap].copy()
|
2025-10-18 20:51:16 -05:00
|
|
|
|
2025-10-19 00:23:17 -05:00
|
|
|
if len(filtered_df) == 0:
|
2025-10-19 03:57:03 -05:00
|
|
|
print("❌ No laps in specified lap range")
|
2025-10-18 20:51:16 -05:00
|
|
|
return
|
|
|
|
|
|
2025-10-19 00:23:17 -05:00
|
|
|
# Reset index for easier iteration
|
|
|
|
|
filtered_df = filtered_df.reset_index(drop=True)
|
|
|
|
|
|
2025-10-19 03:57:03 -05:00
|
|
|
print(f"\n🏁 Starting lap-level telemetry stream simulation")
|
2025-10-18 20:51:16 -05:00
|
|
|
print(f" Endpoint: {endpoint}")
|
|
|
|
|
print(f" Laps: {start_lap} → {end_lap or 'end'}")
|
2025-10-19 03:57:03 -05:00
|
|
|
print(f" Interval: {interval} seconds between laps")
|
|
|
|
|
print(f" Total laps: {len(filtered_df)}")
|
|
|
|
|
print(f" Est. duration: {len(filtered_df) * interval / 60:.1f} minutes\n")
|
2025-10-18 20:51:16 -05:00
|
|
|
|
|
|
|
|
sent_count = 0
|
|
|
|
|
error_count = 0
|
|
|
|
|
|
|
|
|
|
try:
|
2025-10-19 00:23:17 -05:00
|
|
|
for i in range(len(filtered_df)):
|
|
|
|
|
row = filtered_df.iloc[i]
|
2025-10-19 03:57:03 -05:00
|
|
|
lap_num = int(row['lap_number'])
|
2025-10-18 20:51:16 -05:00
|
|
|
|
2025-10-19 03:57:03 -05:00
|
|
|
# Convert lap to JSON
|
|
|
|
|
lap_data = lap_to_json(row)
|
2025-10-19 00:23:17 -05:00
|
|
|
|
2025-10-19 03:57:03 -05:00
|
|
|
# Send lap data
|
2025-10-18 20:51:16 -05:00
|
|
|
try:
|
|
|
|
|
response = requests.post(
|
|
|
|
|
endpoint,
|
2025-10-19 03:57:03 -05:00
|
|
|
json=lap_data,
|
|
|
|
|
timeout=5.0
|
2025-10-18 20:51:16 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if response.status_code == 200:
|
|
|
|
|
sent_count += 1
|
2025-10-19 03:57:03 -05:00
|
|
|
progress = (i + 1) / len(filtered_df) * 100
|
|
|
|
|
|
|
|
|
|
# Print lap info
|
|
|
|
|
print(f" 📡 Lap {lap_num}/{int(row['total_laps'])}: "
|
|
|
|
|
f"Avg Speed: {row['average_speed']:.1f} km/h, "
|
|
|
|
|
f"Tire: {row['tire_compound']} (age: {int(row['tire_life_laps'])} laps) "
|
|
|
|
|
f"[{progress:.0f}%]")
|
2025-10-19 00:23:17 -05:00
|
|
|
|
2025-10-19 03:57:03 -05:00
|
|
|
# Show response if it contains strategies
|
|
|
|
|
try:
|
|
|
|
|
response_data = response.json()
|
|
|
|
|
if 'strategies_generated' in response_data:
|
|
|
|
|
print(f" ✓ Generated {response_data['strategies_generated']} strategies")
|
|
|
|
|
except:
|
|
|
|
|
pass
|
2025-10-18 20:51:16 -05:00
|
|
|
else:
|
|
|
|
|
error_count += 1
|
2025-10-19 03:57:03 -05:00
|
|
|
print(f" ⚠ Lap {lap_num}: HTTP {response.status_code}: {response.text[:100]}")
|
2025-10-18 20:51:16 -05:00
|
|
|
|
|
|
|
|
except requests.RequestException as e:
|
|
|
|
|
error_count += 1
|
2025-10-19 03:57:03 -05:00
|
|
|
print(f" ⚠ Lap {lap_num}: Connection error: {str(e)[:100]}")
|
2025-10-19 00:23:17 -05:00
|
|
|
|
2025-10-19 03:57:03 -05:00
|
|
|
# Sleep for fixed interval before next lap (except for last lap)
|
|
|
|
|
if i < len(filtered_df) - 1:
|
|
|
|
|
time.sleep(interval)
|
2025-10-18 20:51:16 -05:00
|
|
|
|
|
|
|
|
print(f"\n✅ Stream complete!")
|
2025-10-19 03:57:03 -05:00
|
|
|
print(f" Sent: {sent_count} laps")
|
2025-10-18 20:51:16 -05:00
|
|
|
print(f" Errors: {error_count}")
|
|
|
|
|
|
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
|
print(f"\n⏸ Stream interrupted by user")
|
2025-10-19 03:57:03 -05:00
|
|
|
print(f" Sent: {sent_count}/{len(filtered_df)} laps")
|
2025-10-18 20:51:16 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
parser = argparse.ArgumentParser(
|
2025-10-19 03:57:03 -05:00
|
|
|
description="Simulate Raspberry Pi lap-level telemetry streaming"
|
2025-10-18 20:51:16 -05:00
|
|
|
)
|
2025-10-19 03:57:03 -05:00
|
|
|
parser.add_argument("--endpoint", type=str, default="http://localhost:8000/ingest/telemetry",
|
|
|
|
|
help="HPC API endpoint (default: http://localhost:8000/ingest/telemetry)")
|
|
|
|
|
parser.add_argument("--interval", type=int, default=60,
|
|
|
|
|
help="Fixed interval in seconds between laps (default: 60)")
|
2025-10-18 20:51:16 -05:00
|
|
|
parser.add_argument("--start-lap", type=int, default=1, help="Starting lap number")
|
|
|
|
|
parser.add_argument("--end-lap", type=int, default=None, help="Ending lap number")
|
|
|
|
|
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
|
|
try:
|
2025-10-19 00:57:50 -05:00
|
|
|
# Hardcoded CSV file location in the same folder as this script
|
|
|
|
|
script_dir = Path(__file__).parent
|
2025-10-19 03:57:03 -05:00
|
|
|
data_path = script_dir / "ALONSO_2023_MONZA_LAPS.csv"
|
2025-10-19 00:23:17 -05:00
|
|
|
|
2025-10-19 03:57:03 -05:00
|
|
|
df = load_lap_csv(data_path)
|
2025-10-18 20:51:16 -05:00
|
|
|
simulate_stream(
|
2025-10-19 00:23:17 -05:00
|
|
|
df,
|
2025-10-18 20:51:16 -05:00
|
|
|
args.endpoint,
|
2025-10-19 03:57:03 -05:00
|
|
|
args.interval,
|
2025-10-18 20:51:16 -05:00
|
|
|
args.start_lap,
|
|
|
|
|
args.end_lap
|
|
|
|
|
)
|
|
|
|
|
except FileNotFoundError:
|
2025-10-19 00:57:50 -05:00
|
|
|
print(f"❌ File not found: {data_path}")
|
2025-10-19 03:57:03 -05:00
|
|
|
print(f" Make sure ALONSO_2023_MONZA_LAPS.csv is in the scripts/ folder")
|
2025-10-18 20:51:16 -05:00
|
|
|
sys.exit(1)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
print(f"❌ Error: {e}")
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|