## File: README.md # F1 Race Replay 🏎️ 🏁 A Python application for visualizing Formula 1 race telemetry and replaying race events with interactive controls and a graphical interface. > **HUGE NEWS:** The telemetry stream feature is now in a usable state. See the [telemetry demo documentation](./telemetry.md) for access instructions, data format details, and usage ideas. ## Features - **Race Replay Visualization:** Watch the race unfold with real-time driver positions on a rendered track. - **Safety Car Visualization:** See the Safety Car deploy from pit lane, lead the field, and return to pits — with animated transitions and pulsing glow effects. - **Insights Menu:** Floating menu for quick access to telemetry analysis tools (launches automatically with replay). - **Leaderboard:** See live driver positions and current tyre compounds. - **Lap & Time Display:** Track the current lap and total race time. - **Driver Status:** Drivers who retire or go out are marked as "OUT" on the leaderboard. - **Interactive Controls:** Pause, rewind, fast forward, and adjust playback speed using on-screen buttons or keyboard shortcuts. - **Legend:** On-screen legend explains all controls. - **Driver Telemetry Insights:** View speed, gear, DRS status, and current lap for selected drivers when selected on the leaderboard. ## Controls - **Pause/Resume:** SPACE or Pause button - **Rewind/Fast Forward:** ← / → or Rewind/Fast Forward buttons - **Playback Speed:** ↑ / ↓ or Speed button (cycles through 0.5x, 1x, 2x, 4x) - **Set Speed Directly:** Keys 1–4 - **Restart**: **R** to restart replay - **Toggle DRS Zone**: **D** to hide/show DRS Zone - **Toggle Progress Bar**: **B** to hide/show progress bar - **Toggle Driver Names**: **L** to hide/show driver names on track - **Select driver/drivers**: Click to select driver or shift click to select multiple drivers ## Safety Car The replay includes a **simulated Safety Car** that appears on track whenever the F1 data indicates a Safety Car deployment (track status code `4`). Since the F1 API does not provide GPS telemetry for the actual Safety Car, its position is simulated based on the race leader's position. ### How it works - **Data source:** The Safety Car deployment timing comes from the real F1 track status data via FastF1 (`session.track_status`). - **Position simulation:** The SC is placed ~500 meters ahead of the race leader on the track reference polyline. This approximates where the real SC would be relative to the field. - **Three animation phases:** - **Deploying** — The SC animates from the pit lane onto the track over ~3 seconds, with a pulsing glow and "SC DEPLOYING" label. - **On Track** — The SC drives ahead of the leader with a steady amber glow and "SC" label. - **Returning** — The SC animates back to the pit lane over ~3 seconds, with a fading pulsing glow and "SC IN" label. - **Visual appearance:** The SC is drawn as a larger orange/amber circle (8px radius vs 6px for regular cars) with an orange outline ring and always-visible "SC" label. ### Technical details The SC position computation happens in `_compute_safety_car_positions()` in `src/f1_data.py`. Each frame gets a `safety_car` field: ```json { "safety_car": { "x": 1234.56, "y": 7890.12, "phase": "on_track", "alpha": 1.0 } } ``` | Field | Description | |-------|-------------| | `x`, `y` | World coordinates of the SC | | `phase` | `"deploying"`, `"on_track"`, or `"returning"` | | `alpha` | Opacity value from `0.0` (invisible) to `1.0` (fully visible), used for fade in/out animation | > **Note:** If you have existing cached `.pkl` files from previous runs, you must re-run with `--refresh-data` to generate SC position data. Older cached files will simply show no Safety Car. ## Qualifying Session Support (in development) Recently added support for Qualifying session replays with telemetry visualization including speed, gear, throttle, and brake over the lap distance. This feature is still being refined. ## Requirements - Python 3.11+ - [FastF1](https://github.com/theOehrly/Fast-F1) - [Arcade](https://api.arcade.academy/en/latest/) - numpy Install dependencies: ```bash pip install -r requirements.txt ``` FastF1 cache folder will be created automatically on first run. If it is not created, you can manually create a folder named `.fastf1-cache` in the project root > **First Run Notice:** Loading a session for the first time may take noticeably longer because telemetry data must be downloaded, processed, and cached locally. Subsequent launches of the same session are significantly faster.. ## Environment Setup To get started with this project locally, you can follow these steps: 1. **Clone the Repository:** ```bash git clone https://github.com/IAmTomShaw/f1-race-replay cd f1-race-replay ``` 2. **Create a Virtual Environment:** This process differs based on your operating system. - On macOS/Linux: ```bash python3 -m venv venv source venv/bin/activate ``` - On Windows: ```bash python -m venv venv .\venv\Scripts\activate ``` 3. **Install Dependencies:** ```bash pip install -r requirements.txt ``` 4. **Run the Application:** You can now run the application using the instructions in the Usage section below. ## Troubleshooting If the pull data proccess fails, run: ```bash pip install --upgrade fastf1 ``` ## Usage **DEFAULT GUI MENU:** To use the new GUI menu system, you can simply run: ```bash python main.py ``` This will open a graphical interface where you can select the year and round of the race weekend you want to replay. This is still a new feature, so please report any issues you encounter. **OPTIONAL CLI MENU:** To use the CLI menu system, you can simply run: ```bash python main.py --cli ``` This will prompt you with series of questions and a list of options to make your choice from using the arrow keys and enter key. If you would already know the year and round number of the session you would like to watch, you run the commands directly as follows: Run the main script and specify the year and round: ```bash python main.py --viewer --year 2025 --round 12 ``` To run without HUD: ```bash python main.py --viewer --year 2025 --round 12 --no-hud ``` To run a Sprint session (if the event has one), add `--sprint`: ```bash python main.py --viewer --year 2025 --round 12 --sprint ``` The application will load a pre-computed telemetry dataset if you have run it before for the same event. To force re-computation of telemetry data, use the `--refresh-data` flag: ```bash python main.py --viewer --year 2025 --round 12 --refresh-data ``` ### Qualifying Session Replay To run a Qualifying session replay, use the `--qualifying` flag: ```bash python main.py --viewer --year 2025 --round 12 --qualifying ``` To run a Sprint Qualifying session (if the event has one), add `--sprint`: ```bash python main.py --viewer --year 2025 --round 12 --qualifying --sprint ``` ## File Structure ``` f1-race-replay/ ├── main.py # Entry point, handles session loading and starts the replay ├── requirements.txt # Python dependencies ├── README.md # Project documentation ├── roadmap.md # Planned features and project vision ├── resources/ │ └── preview.png # Race replay preview image ├── src/ │ ├── f1_data.py # Telemetry loading, processing, frame generation & SC position simulation │ ├── arcade_replay.py # Visualization and UI logic │ └── ui_components.py # UI components like buttons and leaderboard │ ├── interfaces/ │ │ └── qualifying.py # Qualifying session interface and telemetry visualization │ │ └── race_replay.py # Race replay interface, SC rendering & telemetry visualization │ └── lib/ │ └── tyres.py # Type definitions for telemetry data structures │ └── time.py # Time formatting utilities └── .fastf1-cache/ # FastF1 cache folder (created automatically upon first run) └── computed_data/ # Computed telemetry data (created automatically upon first run) ``` ## Building Custom Telemetry Windows When you start a race replay, an **Insights Menu** automatically appears, providing quick access to various telemetry analysis tools. You can easily create custom insight windows that receive live telemetry data using the `PitWallWindow` base class: ```python from src.gui.pit_wall_window import PitWallWindow class MyInsightWindow(PitWallWindow): def setup_ui(self): # Create your custom UI pass def on_telemetry_data(self, data): # Process telemetry data pass ``` The `PitWallWindow` base class handles all telemetry stream connection logic automatically, allowing you to focus solely on your window's functionality. **Key Features:** - Automatic connection to telemetry stream - Built-in status bar with connection state - Proper cleanup on window close - Simple API - just implement `setup_ui()` and `on_telemetry_data()` **Documentation & Examples:** - See [docs/PitWallWindow.md](./docs/PitWallWindow.md) for complete guide - See [docs/InsightsMenu.md](./docs/InsightsMenu.md) for adding insights to the menu - Run the example: `python -m src.gui.example_pit_wall_window` - Test the menu: `python -m src.gui.insights_menu` ## Customization - Change track width, colors, and UI layout in `src/arcade_replay.py`. - Adjust telemetry processing in `src/f1_data.py`. - Create custom telemetry windows using `PitWallWindow` base class (see above). ## Contributing There have been several contributions from the community that have helped enhance this project. I have added a [contributors.md](./contributors.md) file to acknowledge those who have contributed features and improvements. If you would like to contribute, feel free to: - Open pull requests for UI improvements or new features. - Report issues on GitHub. Please see [roadmap.md](./roadmap.md) for planned features and project vision. # Known Issues - If you are using a `conda` environment, you might need to install a few extra packages if you get this error: ``` arcade.application.NoOpenGLException: Unable to create an OpenGL 3.3+ context. Check to make sure your system supports OpenGL 3.3 or higher ``` You can easily fix this by running this command: ```bash $ conda install -c conda-forge libstdcxx-ng ``` Thanks to @el-mandaloriano for showing how to resolve this issue: #12 - The leaderboard appears to be inaccurate for the first few corners of the race. The leaderboard is also temporarily affected by a driver going in the pits. At the end of the race, the leaderboard is sometimes affected by the drivers' final x,y positions being further ahead than other drivers. These are known issues caused by inaccuracies in the telemetry and are being worked on for future releases. It's likely that these issues will be fixed in stages as improving the leaderboard accuracy is a complex task. ## 📝 License This project is licensed under the MIT License. ## ⚠️ Disclaimer No copyright infringement intended. Formula 1 and related trademarks are the property of their respective owners. All data used is sourced from publicly available APIs and is used for educational and non-commercial purposes only. --- Built with ❤️ by [Tom Shaw](https://tomshaw.dev) --- ## File: docs/InsightsMenu.md # Insights Menu ## Overview The Insights Menu is a PySide6 window that launches automatically when the race replay starts. It provides quick access to telemetry analysis tools and insight windows. The menu stays open alongside the replay and allows you to launch multiple insight windows. ### Active Insights - **Example Insight Window** - A working example demonstrating the PitWallWindow pattern - **Telemetry Stream Viewer** - View raw telemetry data in real-time ## Usage The menu launches automatically when you start a race replay and open a "Race" session. ## Adding New Buttons to the Menu To add a new insight button to the menu, follow these steps: ### Step 1: Create Your Insight Window First, create your insight window using the `PitWallWindow` base class: ```python # src/gui/my_custom_insight.py from src.gui.pit_wall_window import PitWallWindow from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel class MyCustomInsight(PitWallWindow): """My custom telemetry insight.""" def setup_ui(self): """Create the custom UI.""" central_widget = QWidget() self.setCentralWidget(central_widget) layout = QVBoxLayout(central_widget) self.data_label = QLabel("Waiting for data...") layout.addWidget(self.data_label) def on_telemetry_data(self, data): """Process incoming telemetry data.""" # Your data processing logic self.data_label.setText(f"Latest data: {data}") def on_stream_error(self, error_msg): """Handle errors.""" self.data_label.setText(f"Error: {error_msg}") ``` ### Step 2: Add a Launch Method In `src/gui/insights_menu.py`, add a launch method for your insight: ```python def launch_my_custom_insight(self): """Launch my custom insight window.""" print("🚀 Launching: My Custom Insight") from src.gui.my_custom_insight import MyCustomInsight window = MyCustomInsight() window.show() self.opened_windows.append(window) ``` ### Step 3: Add the Button to a Category In the `setup_ui()` method of `InsightsMenu`, add your button to an existing category or create a new one: **Adding to an existing category:** ```python content_layout.addWidget(self.create_category_section( "Live Telemetry", [ ("Telemetry Stream Viewer", "View raw telemetry data", self.launch_telemetry_viewer), ("My Custom Insight", "Description of what it does", self.launch_my_custom_insight), # Add here ] )) ``` **Creating a new category:** ```python content_layout.addWidget(self.create_category_section( "Custom Analysis", [ ("My Custom Insight", "Description of what it does", self.launch_my_custom_insight), ] )) ``` ### Step 4: Test Your Button Run the menu standalone to test your new button: ```bash python -m src.gui.insights_menu ``` This will open the Insights Menu without starting a replay. The window wouldn't be connected to telemetry unless you start a replay, but you can verify that the button launches your insight window correctly.s ## Architecture ### Menu Structure ``` InsightsMenu (QMainWindow) ├── Header │ ├── Title: "🏎️ F1 Insights" │ └── Subtitle: "Launch telemetry insights and analysis tools" ├── Scrollable Content (QScrollArea) │ ├── Category Section 1 │ │ ├── Category Label (e.g., "EXAMPLE INSIGHTS") │ │ ├── Separator Line │ │ ├── Insight Button 1 (name + description) │ │ ├── Insight Button 2 │ │ └── ... │ ├── Category Section 2 │ │ └── ... │ └── Stretch (pushes footer to bottom) └── Footer ├── Info Label: "Requires telemetry stream enabled" └── Close Menu Button ``` ### Key Components **`create_category_section(category_name, insights)`** - Creates a category section with multiple insight buttons - `insights`: List of tuples `(name, description, callback)` - Each tuple becomes a clickable button **`create_insight_button(name, description, callback)`** - Creates a styled button with bold name and smaller description - Minimum height: 50px - Connects the button to the launch callback **`opened_windows` list** - Keeps references to all launched insight windows - Prevents Python from garbage collecting active windows - Windows remain open even if menu is closed ### Launch Process 1. User starts replay with `python main.py --viewer` 2. `main.py` calls `launch_insights_menu()` from `src.run_session` 3. Menu window is created and shown 4. User clicks insight buttons to open analysis windows 5. Each insight launches in its own window with independent telemetry connection ## Customization ### Styling The menu uses inline stylesheets (no external CSS). The default theme is dark with minimal styling: - **Background**: Dark (inherits from system theme) - **Font**: Arial at various sizes (24pt title, 12pt buttons, 10pt descriptions) - **Buttons**: Minimum 50px height with name and description - **Cursor**: Pointing hand cursor on buttons To customize the appearance, edit the `setup_ui()` method and add a stylesheet: ```python self.setStyleSheet(""" QMainWindow { background-color: #1e1e1e; } QPushButton { background-color: #2d2d2d; border: 1px solid #3d3d3d; border-radius: 4px; padding: 8px; } QPushButton:hover { border: 1px solid #e10600; /* Ferrari red */ background-color: #3d3d3d; } """) ``` ### Window Size and Position Adjust in `__init__()`: ```python self.setGeometry(50, 50, 300, 600) # x, y, width, height ``` ### Button Layout and Appearance Modify `create_insight_button()` to customize buttons: ```python def create_insight_button(self, name, description, callback): button = QPushButton() # Custom layout btn_layout = QVBoxLayout() name_label = QLabel(name) name_label.setFont(QFont("Arial", 14, QFont.Bold)) # Larger font desc_label = QLabel(description) desc_label.setFont(QFont("Arial", 9, QFont.Italic)) # Italic description btn_layout.addWidget(name_label) btn_layout.addWidget(desc_label) button.setLayout(btn_layout) button.setMinimumHeight(60) # Taller buttons button.clicked.connect(callback) return button ``` ## See Also - [PitWallWindow.md](./PitWallWindow.md) - Base class for creating insights - [../src/gui/insights_menu.py](../src/gui/insights_menu.py) - Menu implementation - [../src/gui/example_pit_wall_window.py](../src/gui/example_pit_wall_window.py) - Example insight --- ## File: docs/PitWallWindow.md # PitWallWindow Developer Guide ## Overview `PitWallWindow` is a base class that simplifies creating custom telemetry-enabled windows in the F1 Race Replay project. It handles all the complexity of connecting to the telemetry stream, allowing developers to focus solely on building their window's functionality. ## Why Use PitWallWindow? Without `PitWallWindow`, you would need to: - Create and configure a `TelemetryStreamClient` - Connect Qt signals for data reception, connection status, and errors - Handle proper cleanup when the window closes - Manage connection state and message counting With `PitWallWindow`, you simply: 1. Extend the class 2. Implement `setup_ui()` to create your interface 3. Implement `on_telemetry_data()` to process data ## Using the Template To quickly get started, use the [`pit_wall_window_template.py`](../src/gui/pit_wall_window_template.py) template file. It provides a pre-configured structure with all necessary imports and method stubs, so you can focus on implementing your insight's core logic. ## Quick Start ```python from src.gui.pit_wall_window import PitWallWindow from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel class MyInsightWindow(PitWallWindow): def __init__(self): super().__init__() self.setWindowTitle("My Custom Insight") def setup_ui(self): central_widget = QWidget() self.setCentralWidget(central_widget) layout = QVBoxLayout(central_widget) self.info_label = QLabel("Waiting for data...") layout.addWidget(self.info_label) def on_telemetry_data(self, data): if 'frame_index' in data: self.info_label.setText(f"Frame: {data['frame_index']}") ``` Your window will automatically connect to the telemetry stream and start receiving data. **Example:** ```python def on_telemetry_data(self, data): # Update frame counter if 'frame_index' in data: self.frame_label.setText(f"Frame: {data['frame_index']}") # Process driver data if 'frame' in data and 'drivers' in data['frame']: drivers = data['frame']['drivers'] for code, driver in drivers.items(): speed = driver.get('speed', 0) if speed > 300: # Highlight high-speed moments self.highlight_driver(code, speed) ``` ### `on_connection_status_changed(status)` (Optional) Called when the connection state changes. **Parameters:** - `status` (str): One of "Connected", "Connecting...", or "Disconnected" **Example:** ```python def on_connection_status_changed(self, status): if status == "Connected": self.enable_controls() else: self.disable_controls() ``` ### `on_stream_error(error_msg)` (Optional) Called when a stream error occurs. The error is already displayed in the status bar, but you can add custom handling. **Parameters:** - `error_msg` (str): Description of the error **Example:** ```python def on_stream_error(self, error_msg): self.error_log.append(f"[{datetime.now()}] {error_msg}") ``` ## Built-in Features ### Status Bar Every `PitWallWindow` includes a status bar with: - **Connection Status**: Shows current connection state with color coding (green=connected, orange=connecting, red=disconnected) - **Message Counter**: Displays total messages received You can add additional widgets to the status bar: ```python def setup_ui(self): # ... your UI setup ... # Add custom status widget my_status = QLabel("Custom Info") self.status_bar.addPermanentWidget(my_status) ``` ### Automatic Cleanup The base class handles proper cleanup when the window closes, ensuring the telemetry client is stopped and resources are released. ## Running Your Window ### Standalone ```python if __name__ == "__main__": app = QApplication(sys.argv) window = MyInsightWindow() window.show() sys.exit(app.exec()) ``` ### From Main Application Add a menu item or button in the main application: ```python def launch_my_insight(self): self.insight_window = MyInsightWindow() self.insight_window.show() ```