Skip to content

Latest commit

 

History

History
306 lines (213 loc) · 13.3 KB

File metadata and controls

306 lines (213 loc) · 13.3 KB

SimStrategist — Developer Usage Guide

What is this?

SimStrategist is a Flask web app that displays and analyses live in-game telemetry from racing simulators. It supports F1 2018–2024 (Gen 2–5, via binary UDP packets), Le Mans Ultimate (LMU, via JSON over TCP/UDP), and Forza Horizon 4/5 (binary UDP, game version auto-detected from packet size). F1 2017 (Gen 1 legacy format) and Forza Horizon 3 are not supported.

A Claude-powered AI strategy analyser is built in, using real-time weather and tyre data to recommend pit strategies. The live telemetry dashboard also includes a Driver Inputs panel showing throttle, brake, clutch, and steering — switchable between a bar view and a scrolling graph.


Architecture Overview

Two independent processes must run simultaneously:

F1 game ──UDP:20777──▶ f1/server.py ──▶ f1/telemetry_state.py ──▶ app.py API routes ──▶ browser
                        (binary parse)     (thread-safe singleton)   (SSE / JSON)
LMU game ──TCP:5100──▶ lmu/server.py ──▶ lmu/telemetry_state.py ──▶ app.py API routes ──▶ browser
                        (JSON parse)       (thread-safe singleton)
Forza Horizon ──UDP:20055──▶ forza_hrzn/server.py ──▶ forza_hrzn/telemetry_state.py ──▶ app.py API routes ──▶ browser
  (FH4: 20044)               (binary parse,             (thread-safe singleton)
                              size-based version detect)

All listeners run as daemon threads inside app.py — you only need to start one process. However, f1/server.py can also be run standalone (useful when recording).

Key files

File Role
app.py Flask app, starts background threads, defines all HTTP routes
f1/server.py Parses binary F1 UDP packets; optionally records them
f1/telemetry_state.py Thread-safe singleton with numpy circular buffers
f1/config.py UDP settings, buffer sizes, UI colours
f1/recorder.py Standalone UDP sniffer — saves packets to a .f1rec file
f1/replayer.py Reads a .f1rec file and replays packets over UDP
strategy/ai_strategy.py Calls Claude API to generate 3 race strategies
strategy/weather_history.py Records weather samples every 10 s during a session
lmu/server.py JSON-over-TCP/UDP listener for Le Mans Ultimate
forza_hrzn/server.py Binary UDP listener for Forza Horizon 4/5; detects game from packet size
forza_hrzn/telemetry_state.py Thread-safe singleton with numpy circular buffers (mirrors F1 interface)
forza_hrzn/config.py UDP ports (FH4: 20044, FH5: 20055), buffer sizes, version map
static/scripts/input-trace.js Driver Inputs panel — bar and graph rendering for throttle, brake, clutch, steering

Setup

1. Install dependencies

pip install -r requirements.txt

2. Initialise the database

Only needed once (or to reset):

sqlite3 strategist.db < queries/create_users.sqlite3-query
sqlite3 strategist.db < queries/create_games.sqlite3-query
sqlite3 strategist.db < queries/create_sessions.sqlite3-query

3. Set the Anthropic API key (for AI strategy)

export ANTHROPIC_API_KEY=your-key-here

Without this the /api/strategy/ai endpoint will return an error, but everything else works fine.

4. Run the app

python3 app.py

Open http://localhost:5051. Register an account, then go to /setup to configure your game.

Port: defaults to 5051. Override with PORT=8080 python3 app.py. F1 game setting: in-game go to Settings → Telemetry Settings and set the UDP IP to your machine's IP and port to 20777.


API Routes

Method Path Description
GET /api/telemetry Single JSON snapshot of current state
GET /api/telemetry/stream?game=f1|lmu|forza_hrzn Server-Sent Events stream (~60 Hz)
GET /api/weather/history?game=f1|lmu Weather history list for this session
POST /api/strategy/ai Trigger AI strategy analysis (body: {"game":"f1"})

Telemetry snapshot shape

{
  "connected": true,
  "telemetry": {
    "speed": 287, "throttle": 0.94, "brake": 0.0,
    "clutch": 0, "steer": -0.12,
    "gear": 7, "rpm": 11450, "drs": 1,
    "tyre_visual_compound": 16,
    "tyre_age_laps": 12, "fuel_in_tank": 28.4,
    "fuel_remaining_laps": 14.2,
    "engine_temp": 102, "tyres_surface_temp": [88, 89, 90, 91]
  },
  "lap_data": {
    "current_lap": 5, "current_lap_time": 82450,
    "last_lap_time": 83120, "best_lap_time": 82800,
    "car_position": 3, "pit_status": 0,
    "lap_distance": 1247.3
  },
  "session": {
    "track_id": 10, "session_type": 10,
    "total_laps": 57, "weather": 0,
    "track_temperature": 38, "air_temperature": 24
  }
}

Recording & Replaying Telemetry

The recording system lets you capture a real game session and replay it later for development without needing the game running.

File format: .f1rec

A compact binary format:

Bytes 0–15  : Header — magic b'F1REC\x00', version uint8, 9 reserved bytes
Per packet  : float64 timestamp (seconds since start)
              uint16  packet length
              N bytes raw UDP data

Packets are stored as raw, unmodified UDP datagrams — the full parsing pipeline runs normally during replay.


Option A — Record while the server is processing live data

Run f1/server.py directly with --record:

cd f1
python3 server.py --record ../example-data/myrace.f1rec

Press Ctrl+C to stop. The file is flushed and closed cleanly.


Option B — Standalone recorder (pure sniffer, no state updates)

Use this if you want to capture packets without any processing, or if you want to record and forward to another machine.

python3 f1/recorder.py example-data/myrace.f1rec
python3 f1/recorder.py example-data/myrace.f1rec --port 20777

Replaying a recording

Start the Flask app first (python3 app.py), then in a second terminal:

# Real-time replay (1x speed)
python3 f1/replayer.py example-data/myrace.f1rec

# Fast replay — great for testing state accumulation quickly
python3 f1/replayer.py example-data/myrace.f1rec --speed 4.0

# Maximum speed (no timing — floods all packets instantly)
python3 f1/replayer.py example-data/myrace.f1rec --speed 0

# Loop continuously — useful when developing the UI
python3 f1/replayer.py example-data/myrace.f1rec --loop

# All options
python3 f1/replayer.py example-data/myrace.f1rec --speed 2.0 --loop --host 127.0.0.1 --port 20777

The replayer sends packets to the local UDP port, so the app processes them exactly as if the game were running live.


How telemetry flows (step by step)

  1. F1 game sends binary UDP datagrams to port 20777 at ~20 Hz.
  2. UdpListener (f1/server.py) receives each datagram and peeks at the first 2 bytes (packet_format) to determine the game generation, then selects the appropriate header layout before parsing packet_id and player_car_index.
  3. The parsed dict is passed to state.update_telemetry() / update_lap_data() / update_session() in TelemetryState (f1/telemetry_state.py).
  4. TelemetryState holds the latest values plus a numpy circular buffer (2400 points = 120 s at 20 Hz) for history charts.
  5. app.py exposes /api/telemetry/stream as a Server-Sent Events endpoint. It polls state.last_update_time every 16 ms and pushes a JSON snapshot whenever new data arrives.
  6. The browser receives SSE events and updates the live dashboard without polling.

Forza Horizon telemetry flow

  1. Forza Horizon broadcasts a binary UDP datagram at ~60 Hz to the configured IP and port.
  2. UdpListener (forza_hrzn/server.py) receives each datagram and checks its length against VERSION_BY_SIZE ({324: 'fh4', 323: 'fh5'}). Packets of any other size are silently skipped.
  3. The 311-byte Sled + Dash struct is unpacked with struct.unpack_from. Speed (m/s) is multiplied by 3.6 for km/h; Accel/Brake/Clutch (uint8 0–255) are divided by 255; Steer (int8 −127–127) is divided by 127.
  4. The parsed dict is passed to state.update() in TelemetryState (forza_hrzn/telemetry_state.py), which stores the latest values and writes to a numpy circular buffer (7200 points = 120 s at 60 Hz).
  5. app.py streams snapshots via SSE at /api/telemetry/stream?game=forza_hrzn.

F1 game version detection

The server auto-detects the F1 game generation from the packet_format field (first 2 bytes of every packet, equal to the game year). No manual configuration is needed.

packet_format value Generation Header size Notes
2022–2024 Gen 5 28 bytes Includes overall_frame_id and secondary_player_car_index
2018–2021 Gen 2–4 23 bytes Original modern header; player_car_index at field position 8
≤ 2017 Gen 1 Not supported; packets are silently skipped

The correct header_size is computed once per packet and passed into each parser method (parse_car_telemetry, parse_lap_data, parse_session_data, parse_car_status) so that the offset into per-car data arrays is always correct.

Why use_reloader=False?

Flask's reloader forks the process, which would start duplicate UDP listener threads. use_reloader=False prevents this.


Telemetry state internals

TelemetryState is a singletonTelemetryState() always returns the same instance regardless of where it is called. It uses a threading.Lock around all reads and writes.

The circular buffer uses pre-allocated numpy arrays instead of deque for ~50% lower memory usage. When the buffer is full, the oldest entry is overwritten. get_history_df() handles the wrap-around when constructing the DataFrame.

history_index ──▶  [  old  |  new  |  newest  |  oldest  |  ... ]
                    ^write position; wraps to 0 when it reaches maxlen

AI Strategy

/api/strategy/ai calls strategy/ai_strategy.py, which:

  1. Takes a snapshot of the current telemetry + weather history.
  2. Builds a structured prompt and calls Claude via the Anthropic SDK.
  3. Returns three strategies: Standard, Push, and Fuel-save, each with stop laps, compounds, and estimated time delta.

Requires ANTHROPIC_API_KEY to be set.


Driver Inputs Panel

The telemetry page includes a Driver Inputs panel below the tyre temperatures. It shows throttle, brake, clutch, and steering in real time, with a toggle between two display modes:

Mode Description
Bars Horizontal progress bars; steering uses a center-origin bar that fills left or right
Graph Rolling canvas chart; maintains a 300-frame in-browser history, no extra API calls

Channel colours: throttle = green (#00ff87), brake = red (#ff4757), clutch = blue (#4d9fec), steering = grey (#999).

The panel is driven by static/scripts/input-trace.js, which exposes a single function — inputTrace.update(throttle, brake, clutch, steer) — called from telemetry.js on every SSE frame. Clutch is normalized to 0–1 before being passed in (F1 sends 0–100, LMU sends 0.0–1.0).


Ports reference

SimStrategist uses exactly three ports. Only two require any network/firewall configuration.

Port Protocol Direction Purpose Configurable?
20777 UDP Game → app F1 telemetry stream Fixed — set in the F1 game's UDP settings
5100 TCP LMU plugin → app Le Mans Ultimate telemetry stream Fixed — set in the LMU community plugin
20055 UDP Game → app Forza Horizon 5 telemetry stream Default; override with FORZA_HRZN_PORT=xxxx
20044 UDP Game → app Forza Horizon 4 telemetry stream Default for FH4; use FORZA_HRZN_PORT=20044
5051 HTTP Browser → app Flask web dashboard Yes — override with PORT=xxxx python3 app.py

What you need to configure:

  • F1: in-game go to Settings → Telemetry Settings, set IP to your machine's IP and port to 20777.
  • LMU: configure the telemetry plugin to connect to your machine's IP on port 5100.
  • Forza Horizon: in-game go to Settings → Gameplay & HUD → UDP Race Telemetry, set the IP to your machine's IP and port to 20055 (FH5) or 20044 (FH4).
  • Firewall: open UDP 20777, 20055/20044, and TCP 5100 inbound on the machine running the app.

VS Code users: the Ports panel in VS Code will show additional ports (e.g. 17415, 38000, and other high-numbered ports). These are VS Code's own internal processes (extension host, language servers, debugger) and are unrelated to SimStrategist. You can ignore them.


Common issues

Problem Likely cause
Dashboard shows "Disconnected" Game not sending to port 20777, or firewall blocking UDP
Address already in use on startup Another process is on 5051; set PORT=xxxx env var
AI strategy returns error ANTHROPIC_API_KEY not set or invalid
Replay has no effect on dashboard App not running, or replaying to wrong port
Recording stops mid-session Disk full, or Ctrl+C — recordings flush on clean exit
Forza shows "Disconnected" Wrong port in-game, or packet size doesn't match FH4/FH5 (check game title)