No description
Find a file
Jan-Peter v. Hunnius ba84fc0a6b docs: update README and AGENTS.md for session persistence, characteristics, track endpoints
- 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
2026-06-24 09:40:13 +02:00
src feat: persistent sessions, remove track_map use case, car characteristics DB 2026-06-24 09:32:16 +02:00
.gitignore feat: multi-port UDP listener with data dump 2026-06-03 11:50:06 +02:00
AGENTS.md docs: update README and AGENTS.md for session persistence, characteristics, track endpoints 2026-06-24 09:40:13 +02:00
Cargo.lock feat: add run deletion, session reopen, and feed merging (v0.6.0) 2026-06-06 09:38:42 +02:00
Cargo.toml feat: add run deletion, session reopen, and feed merging (v0.6.0) 2026-06-06 09:38:42 +02:00
README.md docs: update README and AGENTS.md for session persistence, characteristics, track endpoints 2026-06-24 09:40:13 +02:00

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.100 in config.ini
  • PCARS: Set target IP to 192.168.1.100 in 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:

  1. Sends HANDSHAKE to AC's port 9996
  2. Receives 408-byte response (car name, driver name, track name in UTF-16LE)
  3. Sends SUBSCRIBE_UPDATE to 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.

  1. Download from RaceDepartment
  2. Extract to <AC>/apps/python/Telemetry_Tool_plugin/
  3. Edit config.ini:
    [telemetry_tool_plugin]
    to_ip = 127.0.0.1
    to_port = 10101
    
  4. Enable in python.ini: [TELEMETRY_TOOL_PLUGIN] ACTIVE=1
  5. 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:

  1. Create a session with a use case (shift_points or track_mapping)
  2. Start runs — each run captures frames from the ring buffer between start and stop
  3. Complete the session — auto-analyzes captured data and stores results
  4. Retrieve analysis — shift point RPM ranges or track boundary coordinates
  5. Re-open if needed — re-open a completed session to add more runs, then complete again
  6. 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:

  1. Extracts frames independently from each feed using ring buffer markers
  2. Sorts all frames by (lap_count, lap_time_ms)
  3. Deduplicates near-simultaneous frames (within 15ms) by keeping the higher-priority feed
  4. 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 name
  • guidance — 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.