- Document session persistence model (split metadata/frames files, lazy load) - Add characteristics API endpoints and response examples - Add track API endpoints to endpoint table - Fix use case name track_map → track_mapping - Update architecture with characteristics.rs and track_model.rs - Update test count to 33 - Rewrite AGENTS.md: accurate ports, persistence paths, tech stack
17 KiB
rusty-telemetry
Multi-port UDP telemetry listener and analysis platform for sim racing games.
Receives real-time telemetry from Assetto Corsa and Project CARS 1 & 2 via UDP, parses binary packet formats, and provides live data via a terminal dashboard and REST API. Supports use-case driven analysis sessions for shift point optimization and track mapping, with persistent session storage and cross-session car characteristics aggregation.
Supported Games
| Game | Feed | Port | Protocol | Data |
|---|---|---|---|---|
| Assetto Corsa | KSUDP (built-in) | 9996 | 3-phase handshake, binary RTCarInfo | Speed, RPM, gear, inputs, G-forces, lap times, per-wheel slip, suspension |
| Assetto Corsa | Telemetry Tool Plugin | 10101 | Binary per-driver structs | All of KSUDP + tyre core temps, driver names, ERS, DRS, all cars |
| Project CARS 1 | UDP Telemetry | 5606 | Binary V1 format (1367 bytes) | Speed, RPM, gear, inputs, engine temps, damage, weather, tyre wear |
| Project CARS 2 | UDP Telemetry | 5606 | Binary V2 format (559 bytes) | All of PCARS1 + full 3D position, turbo boost, tyre compounds |
All feeds are optional. If a feed fails to bind (e.g. the game isn't running), it shows as "not available" in the TUI and the other feeds continue normally.
Build and Run
cargo run --release
Press q or Ctrl+C to quit.
Configuration
Environment Variables
| Variable | Purpose | Default |
|---|---|---|
RUSTY_TELEMETRY_GAME_HOST |
IP of the machine running the sim game (used for KSUDP handshake target) | 127.0.0.1 |
RUSTY_TELEMETRY_LISTEN |
Local address to bind for incoming UDP feeds (Telemetry Tool, PCARS) | 0.0.0.0 |
RUSTY_TELEMETRY_BIND |
Fallback for both of the above (backward compatibility) | — |
RUSTY_TELEMETRY_API_PORT |
REST API server port | 8080 |
RUSTY_TELEMETRY_API_HOST |
REST API server bind address | 127.0.0.1 |
Same Machine
No configuration needed — defaults work out of the box:
cargo run --release
Remote Setup (game on a different machine)
Run rusty-telemetry on a separate machine (e.g. 192.168.1.100) while the game runs on another (e.g. 192.168.1.2):
# On the analysis machine:
RUSTY_TELEMETRY_GAME_HOST=192.168.1.2 cargo run --release
Then point the games at the analysis machine:
- KSUDP: Works automatically — AC sends telemetry back to the handshake's source address
- Telemetry Tool: Set
to_ip = 192.168.1.100inconfig.ini - PCARS: Set target IP to
192.168.1.100in the in-game UDP settings
Feed Setup
KSUDP (AC built-in, port 9996)
No additional setup. AC's built-in Remote Telemetry UDP server listens on port 9996 at startup. rusty-telemetry performs a 3-phase handshake automatically:
- Sends
HANDSHAKEto AC's port 9996 - Receives 408-byte response (car name, driver name, track name in UTF-16LE)
- Sends
SUBSCRIBE_UPDATEto start continuous streaming at ~20 Hz
Important: Start AC before rusty-telemetry so AC can bind port 9996 first.
Telemetry Tool Plugin (AC, port 10101)
Optional third-party Python plugin by IkoRein. Provides richer data than KSUDP: tyre core temperatures, driver names, ERS, DRS, and data for all cars in the session.
- Download from RaceDepartment
- Extract to
<AC>/apps/python/Telemetry_Tool_plugin/ - Edit
config.ini:[telemetry_tool_plugin] to_ip = 127.0.0.1 to_port = 10101 - Enable in
python.ini:[TELEMETRY_TOOL_PLUGIN] ACTIVE=1 - Restart AC
Project CARS 1 & 2 (port 5606)
Configure in-game to send telemetry to your machine's IP on port 5606.
PCARS 2: Options → System → UDP Protocol → set target IP/port, format 2 recommended.
PCARS 1: Options → Visual → UDP Protocol → set target IP/port.
The parser auto-detects the format: PCARS 1 (1367-byte packets, 3-byte header) or PCARS 2 (559-byte car physics packets, 12-byte header).
TUI
The terminal UI shows one panel per active feed:
┌──────────────────────────────────────────────────────────┐
│ rusty-telemetry v0.6.0 -- Telemetry Listener │
├──────────────────────────────────────────────────────────┤
│ ╔ * Receiving ╗ │
│ ║ 20.0 Hz 1200 packets 15.2 KB/s last: 0.05s ║
│ ║ 245 km/h Gear:5 8750 RPM Lap:1:23.456 ║
│ ║ THR: 85% BRK: 0% Pos:0.453 ║
│ ╚→ :9996 recv :42132 [subscribed]╝ │
│ │
│ ╔ * Receiving ╗ │
│ ║ 60.0 Hz 3600 packets 45.8 KB/s last: 0.01s ║
│ ║ 180 km/h Gear:4 7200 RPM Lap:1:18.123 ║
│ ║ THR: 72% BRK: 0% Pos:0.500 ║
│ ╚ Telemetry Tool Port :10101 ╝ │
│ │
│ ╔ . Bound, waiting for data ╗ │
│ ║ 0.0 Hz 0 packets 0.0 KB/s last: never ║
│ ╚ PCARS Port :5606 ╝ │
├──────────────────────────────────────────────────────────┤
│ Recording: 550e8400-… Ctrl+C Quit │
│ ⚠ Handshake response timed out (5s) — 12s ago │
└──────────────────────────────────────────────────────────┘
Each panel shows:
- Health indicator and status (
*green/alive,~yellow/stale,.gray/waiting,!red/error) - Packet frequency, total count, data rate, time since last packet
- Live telemetry: speed, gear, RPM, lap time, throttle/brake percentage, track position
- Connection mode (handshake with target/recv ports, or bind-listen)
Data Capture
All feeds are continuously recorded into in-memory ring buffers (5 minutes, 18,000 frames per feed). A recording is auto-started on launch; on shutdown, ring buffer contents are flushed to per-feed JSONL files:
./data/<uuid>/
├── manifest.json
├── feed_9996_ksudp.jsonl
├── feed_10101_telemetry_tool.jsonl
└── feed_5606_pcars.jsonl
Each recording has a manifest.json with metadata (game, track, car, use case, status, timestamps) and JSONL files for each feed. Each line is a serialized TelemetryFrame object.
Session-Based Analysis
Use-case sessions allow targeted data capture and analysis:
- Create a session with a use case (
shift_pointsortrack_mapping) - Start runs — each run captures frames from the ring buffer between start and stop
- Complete the session — auto-analyzes captured data and stores results
- Retrieve analysis — shift point RPM ranges or track boundary coordinates
- Re-open if needed — re-open a completed session to add more runs, then complete again
- Delete runs — remove unwanted runs from a session (must be stopped first)
Sessions are persistent: session metadata, runs, and analysis results survive a restart. On startup, all sessions are loaded from disk and incomplete runs are normalized to stopped state. Session data is stored separately from recordings:
./data/sessions/
├── <session_id>.json # Metadata (runs, analysis results, car_name)
└── <session_id>.frames.json # Telemetry frames (lazy-loaded on demand)
Session metadata is loaded at startup for fast listing; frame data is lazy-loaded only when analysis or frame extraction is requested. Writes are atomic (temp file + rename) and frame serialization is offloaded to a background thread to avoid blocking the API.
Feed Merging
When multiple feeds are available during a recording (e.g., both KSUDP on port 9996 and Telemetry Tool on port 10101), runs can merge frames from multiple sources for more accurate analysis data.
To start a merged run, specify merge_feeds in the start run request:
{
"feed_name": "ksudp",
"merge_feeds": ["telemetry_tool"]
}
The merge algorithm:
- Extracts frames independently from each feed using ring buffer markers
- Sorts all frames by
(lap_count, lap_time_ms) - Deduplicates near-simultaneous frames (within 15ms) by keeping the higher-priority feed
- Feed priority: Telemetry Tool > KSUDP > PCARS
This results in higher effective data density (combining 60 Hz TT + 20 Hz KSUDP) while preferring the richer Telemetry Tool data where available.
REST API
A built-in HTTP API server runs on port 8080 for recording management and live telemetry access.
Endpoints
| Method | Path | Description |
|---|---|---|
| GET | /api/live |
Live telemetry snapshot (all feeds) |
| GET | /api/recordings |
List all recordings |
| POST | /api/recordings |
Start new recording |
| GET | /api/recordings/{id} |
Get recording details |
| POST | /api/recordings/stop |
Stop active recording |
| POST | /api/recordings/{id}/stop |
Stop specific recording |
| DELETE | /api/recordings/{id} |
Delete recording |
| GET | /api/recordings/{id}/data/{feed_name} |
Download raw feed JSONL |
| GET | /api/use-cases |
List valid use cases |
| GET | /api/games |
List supported games |
| GET | /api/sessions |
List all sessions |
| POST | /api/sessions |
Create analysis session |
| GET | /api/sessions/{id} |
Get session details |
| DELETE | /api/sessions/{id} |
Delete session |
| POST | /api/sessions/{id}/runs |
Start a run |
| POST | /api/sessions/{id}/runs/{run_id}/stop |
Stop a run |
| DELETE | /api/sessions/{id}/runs/{run_id} |
Delete a stopped run from session |
| POST | /api/sessions/{id}/reopen |
Re-open a completed session |
| POST | /api/sessions/{id}/complete |
Complete session + auto-analyze |
| GET | /api/sessions/{id}/analysis |
Get analysis results |
| POST | /api/sessions/{id}/analyze |
Re-run analysis on completed session |
| GET | /api/characteristics/cars |
List all cars with aggregated shift-point data |
| GET | /api/characteristics/cars/{car} |
Get detailed shift-point profile for a car |
| GET | /api/tracks |
List all track models |
| GET | /api/tracks/{track} |
Get a track model by key |
| POST | /api/sessions/{id}/build-track-model |
Build a track model from a track mapping session |
Start Recording Body
{
"name": "My Session",
"game": "assetto_corsa",
"track": "nurburgring",
"car": "ferrari_488",
"use_case": "general",
"notes": "Testing session"
}
All fields are optional.
Live Telemetry Response
{
"feeds": [
{
"label": "KSUDP",
"port": 9996,
"health": "alive",
"frequency_hz": 20.0,
"total_packets": 1200,
"data_rate_kb_s": 15.2,
"speed_kmh": 245.0,
"gear": "5",
"engine_rpm": 8750.0,
"throttle": 0.85,
"brake": 0.0,
"lap_time": "1:23.456",
"spline_position": 0.453
}
],
"active_recording": "550e8400-e29b-41d4-a716-446655440000"
}
Create Session Body
{
"use_case": "shift_points",
"label": "Shift Point Analysis Run 1"
}
Start Run Body
{
"feed_name": "ksudp",
"label": "Acceleration runs",
"merge_feeds": ["telemetry_tool"]
}
merge_feeds is optional. When specified, frames from the listed feeds are merged with the primary feed's frames into a single deduplicated dataset. This is useful for combining the higher-frequency Telemetry Tool data (~60 Hz) with KSUDP data (~20 Hz) for more accurate analysis.
Delete Run
Deletes a stopped run from a session. If the session was completed and had analysis results, the analysis is cleared (since the underlying data changed). Re-run analysis after deletion if needed.
Returns 204 No Content on success. Cannot delete a run that is currently recording — stop it first.
Reopen Session
Re-opens a completed session, changing its status back to active. This clears the completed_at timestamp and any analysis results, allowing new runs to be added. Complete the session again when done to re-run analysis.
{}
Returns the updated session on success.
Use Cases Response
[
{ "id": "shift_points", "name": "ShiftPoints", "guidance": "1. Drive to a long straight..." },
{ "id": "track_mapping", "name": "TrackMapping", "guidance": "1. Drive a complete lap..." }
]
Each object contains:
id— use case identifier (use in session creation)name— human-readable nameguidance— step-by-step instructions for the use case
Car Characteristics
The characteristics endpoints aggregate shift-point analysis data across all completed shift_points sessions for each car. This provides a growing database of optimal shift RPMs per gear, useful for comparing setups and tracking improvements over time.
GET /api/characteristics/cars — returns a summary list:
[
{
"car_name": "Ferrari 488 GT3",
"session_count": 3,
"recommended_shift_rpm": 7500.0
}
]
GET /api/characteristics/cars/{car} — returns detailed per-gear data:
{
"car_name": "Ferrari 488 GT3",
"gears": [
{
"gear": 2,
"min_rpm": 3200,
"max_rpm": 8500,
"min_speed_kmh": 45.0,
"max_speed_kmh": 120.0
}
],
"recommended_shift_rpm": 7500.0,
"session_count": 3
}
Shift Point Analysis Response
{
"gear_curves": [
{
"gear": 2,
"gear_display": "1",
"run_label": "Run 1, Run 2 (2 runs)",
"data_points": [
{ "speed_kmh": 45.2, "rpm": 3200, "acceleration_g": 0.85 },
{ "speed_kmh": 55.8, "rpm": 4100, "acceleration_g": 0.82 }
],
"min_rpm": 3200,
"max_rpm": 8500,
"min_speed_kmh": 45.0,
"max_speed_kmh": 120.0,
"sample_count": 1200
}
],
"crossover_points": [
{
"from_gear": 2,
"to_gear": 3,
"from_gear_display": "1",
"to_gear_display": "2",
"crossover_speed_kmh": 95.0,
"crossover_rpm": 7500
}
],
"recommended_shift_rpm": 7500.0,
"runs_analyzed": 3,
"total_frames_analyzed": 2100
}
Web Client
A companion web application (Racecraft) provides a browser-based interface for live telemetry, session management, and data analysis visualization. It connects to the REST API and renders shift point charts, track maps, and real-time gauges.
To use with the web client, start the API on all interfaces:
RUSTY_TELEMETRY_API_HOST=0.0.0.0 cargo run --release
Then start the web client on the same machine — it proxies /api/* to localhost:8080 automatically. Access from any machine on the LAN via the Vite dev server's network URL.
Testing
cargo test
33 unit tests covering:
- Binary parsers for all three feed formats (KSUDP, Telemetry Tool, PCARS V1/V2)
- Ring buffer frequency calculation and edge cases
- Feed health state machine transitions (Waiting → Alive → Stale → Alive)
- Packet validation, error handling, boundary conditions
Architecture
src/
├── main.rs Multi-port UDP listener, TUI event loop, KSUDP handshake
├── api.rs REST API server (axum) — recordings, sessions, tracks, characteristics, live telemetry
├── recording.rs Recording manager — ring buffers, manifest CRUD, flush to JSONL
├── session.rs Session manager — persistent sessions, runs, frame extraction, lazy-loading
├── analysis.rs Analysis algorithms — shift points, track mapping
├── characteristics.rs Car characteristics aggregation — cross-session shift-point profiles
├── track_model.rs Track model geometry — center line computation, boundary data
├── binary_utils.rs Shared safe binary read helpers (bounds-checked, zero fallback)
├── telemetry_frame.rs Unified telemetry data model (TelemetryFrame)
├── feed_state.rs Per-feed health/frequency state machine with ring buffer
├── parser_ksudp.rs AC KSUDP binary parser (RTCarInfo, 6 tests)
├── parser_telemetry_tool.rs AC Telemetry Tool binary parser (357-byte driver struct, 7 tests)
├── parser_pcars.rs PCARS 1 & 2 binary parser with auto-detection (5 tests)
├── ui.rs Ratatui TUI rendering
└── warnings.rs Thread-safe warning buffer with TUI bar + stderr fallback
License
Private project — not yet licensed for distribution.