Assetto Corsa — UDP Telemetry Protocol Specification

# Assetto Corsa — UDP Telemetry Protocol Specification

> **Game**: Assetto Corsa (2014) by Kunos Simulazioni
> **Engine**: Kunos in-house engine
> **Last verified**: 2026-06-03 (Abarth 500, Vallelunga, Soft tyres)
> **Parser status**: ✅ KSUDP (`parser_ksudp.rs`), ✅ Telemetry Tool (`parser_telemetry_tool.rs`), ❌ Lua Plugin (not yet working)

---

## Overview — Three Independent Feeds

AC exposes telemetry through **three completely independent UDP feeds**, each with different protocols, data depths, and reliability:

| Feed | Port | Protocol | Source | Rate | Data Depth | Status |
|---|---|---|---|---|---|---|
| **KSUDP** | 9996 | Binary (handshaked) | AC built-in engine | ~20 Hz (broken) | Low (~40%) | ⚠️ Sparse |
| **Telemetry Tool** | 10101 | Binary (no handshake) | 3rd-party Python plugin | ~60 Hz | Very High (~95%) | ✅ Working |
| **Lua Plugin** | 5005 | JSON | Our CSP Lua app | Configurable | High (~90%) + CSP unique | ❌ LAZY=FULL bug |

**Primary feed: Telemetry Tool (10101)** — reliable, high-frequency, data-rich.
**Secondary: Lua Plugin (5005)** — complementary CSP data (damage, fuel, tyre wear, weather).
**Deprioritized: KSUDP (9996)** — sparse data, unreliable handshake.

---

## Cross-References

- **Session analysis**: [Telemetry Feed Analysis — Session 2026-06-03](joplin://395bc805650d44b6bfec423ca095ed64)
- **Main project plan**: [Sim Racing Telemetry Analysis Platform — Project Plan](joplin://6c0dcb2a567348fd9796f50c790082e4)
- **ACC protocol**: [ACC — UDP Telemetry Protocol Specification](joplin://6ae7005d9810437093d63470cff98b59)
- **AC EVO status**: [AC EVO — Telemetry Status & Research](joplin://2171d34ab9c1431ea3a979d30d206e23)
- **AC Rally status**: [AC Rally — Telemetry Status & Research](joplin://b7b331aa87544b6ebe5db5b8d7bcd2a0)
- **PCARS protocol**: [Project CARS 1 & 2 — UDP Telemetry Protocol Specification](joplin://c6bd2c45938246fa9d61776deae9874b)
- **DiRT Rally protocol**: [DiRT Rally 1 & 2.0 — UDP Telemetry Protocol Specification](joplin://877a753ad06a40e08059834d8c8fb438)

---

## Feed A: KSUDP (Port 9996) — Built-in Engine Feed

### Connection Protocol (3-Phase Handshake)

1. AC listens on port 9996 for registration packets
2. Client sends a registration packet: `1u32.to_le_bytes()` (4 bytes: `[1, 0, 0, 0]`) to AC's port 9996
3. AC responds by streaming RTCarInfo packets to the client's return address
4. Client must send periodic keep-alive registration packets (every ~10 seconds)
5. AC sends RTCarInfo structs at ~20 Hz

**Important**: Only one client can bind to :9996 at a time. If rusty-telemetry binds first, AC cannot bind its listener → zero packets. The handshake model requires AC to bind first, then the client registers.

### RTCarInfo Binary Struct (Naturally Aligned)

The C struct uses **natural alignment** (NOT packed). This means padding bytes exist:
- 3 bytes padding after `char identifier` (offset 0) before `int size` (offset 4)
- 2 bytes padding after the 6 bools (offsets 20–25) before the next float (offset 28)

**Total struct size: 328 bytes** (not 323).

### Field-by-Field Byte Map

| Offset | Size | Type | Field | Notes |
|---|---|---|---|---|
| 0 | 1 | char | identifier | Must be `0x61` = `'a'` |
| 1 | 3 | — | padding | (natural alignment) |
| 4 | 4 | int32_le | size | Self-reported struct size (328) |
| 8 | 4 | float32_le | speedKmh | |
| 12 | 4 | float32_le | speedMph | |
| 16 | 4 | float32_le | speedMs | |
| 20 | 1 | bool | isAbsEnabled | |
| 21 | 1 | bool | isAbsInAction | |
| 22 | 1 | bool | isTcInAction | |
| 23 | 1 | bool | isTcEnabled | |
| 24 | 1 | bool | isInPit | |
| 25 | 1 | bool | isEngineLimiterOn | |
| 26 | 2 | — | padding | (natural alignment) |
| 28 | 4 | float32_le | accG_vertical | G-force vertical |
| 32 | 4 | float32_le | accG_horizontal | G-force horizontal |
| 36 | 4 | float32_le | accG_frontal | G-force frontal |
| 40 | 4 | int32_le | lapTime | Current lap time (ms) |
| 44 | 4 | int32_le | lastLap | Last lap time (ms) |
| 48 | 4 | int32_le | bestLap | Best lap time (ms) |
| 52 | 4 | int32_le | lapCount | Current lap number |
| 56 | 4 | float32_le | gas | Throttle 0.0–1.0 |
| 60 | 4 | float32_le | brake | Brake 0.0–1.0 |
| 64 | 4 | float32_le | fuel | Fuel level 0.0–1.0 (NOT clutch) |
| 68 | 4 | float32_le | engineRPM | |
| 72 | 4 | float32_le | steer | Steering -1.0 to 1.0 |
| 76 | 4 | int32_le | gear | Current gear |
| 80 | 4 | float32_le | cgHeight | Centre of gravity height |
| 84 | 16 | float32_le × 4 | wheelAngularSpeed | Per-wheel |
| 100 | 16 | float32_le × 4 | slipAngle | Per-wheel |
| 116 | 16 | float32_le × 4 | slipAngleContactPatch | Per-wheel |
| 132 | 16 | float32_le × 4 | slipRatio | Per-wheel |
| 148 | 16 | float32_le × 4 | tyreSlip | Per-wheel |
| 164 | 16 | float32_le × 4 | ndSlip | Per-wheel normalized slip |
| 180 | 16 | float32_le × 4 | wheelLoad | Per-wheel (N) |
| 196 | 16 | float32_le × 4 | wheelDy | Per-wheel lateral displacement |
| 212 | 16 | float32_le × 4 | wheelMz | Per-wheel self-aligning torque |
| 228 | 16 | float32_le × 4 | tyreDirtyLevel | Per-wheel marble buildup |
| 244 | 16 | float32_le × 4 | camberRAD | Per-wheel camber in radians |
| 260 | 16 | float32_le × 4 | tyreRadius | Per-wheel |
| 276 | 16 | float32_le × 4 | tyreLoadedRadius | Per-wheel |
| 292 | 16 | float32_le × 4 | suspensionHeight | Per-wheel |
| 308 | 4 | float32_le | carPositionNormalized | 0.0–1.0 = spline position |
| 312 | 4 | float32_le | carSlope | |
| 316 | 4 | float32_le | worldX | World position X |
| 320 | 4 | float32_le | worldY | World position Y |
| 324 | 4 | float32_le | worldZ | World position Z |

### KSUDP — Known Issues

1. **Extremely sparse**: Only 13 packets in ~5 min (expected ~6,000 at 20 Hz)
2. **Handshake unreliability**: Registration packet format may be incomplete
3. **No clutch input**: Offset 64 is fuel level, not clutch
4. **No tyre temperatures**: No tyre_core_temps or tread temps
5. **No multi-car data**: Only player car
6. **No velocity vectors**: No world_speed or local_speed
7. **KSUDP is considered a legacy/abandoned feature** by Kunos

### KSUDP — Rust Parser

- File: `src/parser_ksudp.rs` (267 lines)
- Function: `parse_rt_car_info(data: &[u8]) -> Result<TelemetryFrame, KsudpParseError>`
- Identifier check: byte 0 must be `b'a'`
- Validation: reported size at offset 4 vs actual packet length
- All per-wheel fields read as `read_f32_4(data, offset)` → `[f32; 4]` (FL, FR, RL, RR)

---

## Feed B: Telemetry Tool (Port 10101) — PRIMARY FEED

### Source

Third-party Python plugin: `<AC>/apps/python/Telemetry_Tool_plugin/Telemetry_Tool_plugin.py` by IkoRein v1.3.
Sends binary telemetry for **all cars** in the session. No handshake needed — just bind to port 10101 and receive.

### Connection Model

- rusty-telemetry binds UDP socket to `0.0.0.0:10101`
- AC Python plugin sends packets to `127.0.0.1:10101`
- No registration, no handshake, no keep-alive needed

### Performance (Verified 2026-06-03)

| Metric | Value |
|---|---|
| Packets in 4.9 min session | 17,496 data + 17 heartbeats |
| Rate | ~60 Hz (one per rendered frame) |
| Bytes per data packet | 357 per driver |
| Total session data | ~10.6 MB across 2 sessions |
| Packet loss | Zero observed |
| Validation | All markers intact (`test_float=666.666`, `test_int=666`) |

### Packet Structure

**Packet byte 0 = packet_type:**
- `22` = Telemetry data (contains N × 357-byte driver structs)
- `23` = Version info (sent every ~100 frames, typically ignored)
- Anything else = unknown

**Heartbeat packets**: 5 bytes, sent periodically (type byte + 4 bytes).

### Driver Struct (357 bytes per driver)

| Offset | Size | Type | Field | Notes |
|---|---|---|---|---|
| 0 | 4 | uint32_le | driver_id | Unique per car |
| 4 | 4 | uint32_le | lap_time | Current lap time (ms) |
| 8 | 4 | uint32_le | last_lap | Last lap time (ms) |
| 12 | 4 | uint32_le | best_lap | Best lap time (ms) |
| 16 | 12 | int32_le × 3 | sector_times | Current sector times |
| 28 | 12 | int32_le × 3 | last_sector_times | Previous lap sector times |
| 40 | 4 | float32_le | steer | -1.0 to 1.0 |
| 45 | 4 | float32_le | throttle | 0.0–1.0 |
| 49 | 4 | float32_le | brake | 0.0–1.0 |
| 53 | 4 | float32_le | clutch | 0.0–1.0 |
| 57 | 2 | int16_le | gear | Current gear |
| 59 | 4 | float32_le | engine_rpm | |
| 63 | 4 | float32_le | speed_kmh | |
| 67 | 2 | int16_le | track_position | Position in track order |
| 69 | 2 | int16_le | leaderboard_position | Race position |
| 71 | 2 | int16_le | lap_count | Current lap |
| 73 | 4 | float32_le | world_x | World position X |
| 77 | 4 | float32_le | world_y | World position Y |
| 81 | 4 | float32_le | world_z | World position Z |
| 85 | 8 | float64_le | spline_position | 0.0–1.0 track progress |
| 93 | 4 | float32_le | test_float | Validation: 666.666 |
| 97 | 33 | char[33] | driver_name | Null-terminated string |
| 130 | 4 | char[4] | nationality | e.g. "SM", "IT" |
| 134 | 33 | char[33] | car_name | e.g. "abarth500" |
| 167 | 10 | char[10] | tyre_compound | e.g. "SM" |
| 177 | 4 | uint32_le | test_int | Validation: 666 |
| 181 | 1 | bool | is_connected | |
| 182 | 1 | bool | is_in_pit | |
| 183 | 1 | bool | is_car_in_pitlane | |
| 184 | 1 | bool | lap_invalidated | |
| 185 | 4 | float32_le | acc_g_vertical | |
| 189 | 4 | float32_le | acc_g_horizontal | |
| 193 | 4 | float32_le | acc_g_frontal | |
| 197 | 1 | bool | is_drs_available | |
| 198 | 1 | bool | is_drs_enabled | |
| 199 | 1 | bool | is_race_finished | |
| 205 | 4 | float32_le | ers_max_kj | ERS capacity |
| 209 | 4 | float32_le | ers_current_kj | ERS current charge |
| 213 | 16 | float32_le × 4 | tyre_dirty_level | Per-wheel marbles |
| 229 | 4 | float32_le | turbo_boost | Turbo boost pressure |
| 233 | 8 | float32_le × 2 | ride_height_front_rear | Front, rear ride height |
| 241 | 16 | float32_le × 4 | tyre_core_temp | Per-wheel °C |
| 257 | 16 | float32_le × 4 | tyre_slip_ratio | Per-wheel slip ratio |
| 273 | 16 | float32_le × 4 | tyre_slip_angle | Per-wheel slip angle |
| 289 | 16 | float32_le × 4 | nd_slip | Per-wheel normalized slip |
| 305 | 16 | float32_le × 4 | wheel_slip | Per-wheel slip indicator |
| 321 | 12 | float32_le × 3 | world_velocity | 3D world velocity vector |
| 333 | 12 | float32_le × 3 | local_velocity | 3D local velocity vector |

### Telemetry Tool — Coaching Coverage

| Use Case | Supported? | Data Source |
|---|---|---|
| Braking point analysis | ✅ | speed, spline_position, brake input |
| Racing line tracking | ✅ | world_pos (x,y,z), spline_pos |
| Throttle/traction control | ✅ | tyre_slip_ratio, nd_slip, wheel_slip |
| Understeer/oversteer detection | ✅ (~95%) | slip_angle + nd_slip per wheel |
| Tyre management | ✅ | tyre_core_temp, tyre_dirty_level |
| Lap time comparison | ✅ | lap_time, best_lap, sector_times |
| G-force analysis | ✅ | 3-axis accelerations |
| Ride height / setup validation | ✅ | ride_height F/R |
| Multi-car tracking | ✅ | All drivers in packet |
| ERS/DRS/P2P | ✅ | ers_*, drs_* fields |
| Turbo monitoring | ✅ | turbo_boost |

### Telemetry Tool — Rust Parser

- File: `src/parser_telemetry_tool.rs` (407 lines)
- Fast path: `parse_first_driver_no_strings(data)` — skips strings, returns `(TelemetryFrame, driver_id)`
- Full path: `parse_first_driver(data)` — includes driver_name, car_name, nationality, tyre_compound
- Multi-driver: `parse_packet(data)` — returns `Vec<TelemetryFrame>` for all drivers
- String offsets are exported as constants for cache-based string loading:
  - `STRING_OFFSET_DRIVER_NAME = 98` (1 + 97)
  - `STRING_OFFSET_CAR_NAME = 135` (1 + 134)
  - `STRING_OFFSET_TYRE_COMPOUND = 168` (1 + 167)

---

## Feed C: Lua Plugin (Port 5005) — CSP Extension Feed

### Source

Our custom CSP (Custom Shaders Patch) Lua application, deployed to `<AC>/apps/lua/ac-telemetry-plugin/`.
Sends JSON datagrams via LuaSocket. We control the code — can add any CSP API field.

### Status: NOT WORKING (Root Cause Identified)

**Root cause**: `LAZY = FULL` in `manifest.ini` line 13.
CSP's lazy loading means the Lua script never executes until the user clicks the app icon in the in-game taskbar. The user never clicked it during sessions → 0 bytes received.

**Fix**: Change `LAZY = FULL` to `LAZY = NONE` in `manifest.ini`, or remove the `[CORE]` section's `LAZY` line entirely (NONE is default).

### Expected Data (After Fix)

JSON datagrams at configurable Hz (target 60 Hz). Based on CSP Lua API:

```json
{
  "speed_ms": 42.5,
  "rpm": 6500,
  "gear": 4,
  "throttle": 0.85,
  "brake": 0.0,
  "clutch": 0.0,
  "steer": 0.1,
  "fuel": 0.72,
  "lap_time_ms": 78123,
  "position": {"x": 100.5, "y": 2.3, "z": -50.7},
  "suspension_travel": [0.05, 0.04, 0.06, 0.05],
  "tyre_wear": [0.98, 0.97, 0.96, 0.95],
  "damage": {
    "body": 0.0,
    "suspension": 0.0,
    "engine": 0.0,
    "transmission": 0.0,
    "aero": 0.0
  },
  "weather": {
    "track_temp": 28,
    "ambient_temp": 22,
    "rain": 0.0
  },
  "tyre_temps": [85.2, 84.8, 88.1, 87.5]
}
```

### Lua Plugin — Unique Fields (Not in 10101)

| Field | CSP API | Value |
|---|---|---|
| Per-wheel suspension travel | `car.wheels[i].suspensionTravel` | Detailed damper analysis |
| 5-zone damage model | `car.damage.*` | Body/aero/suspension/engine/transmission |
| Fuel level | `car.fuel` | Strategy coaching |
| Tyre wear | CSP tyre API | Long-run degradation |
| Weather/track conditions | `ac.*` weather calls | Ambient temp, track temp, rain |

### Lua Plugin — Secondary Concern

After fixing LAZY, `require("socket")` may fail if LuaSocket is unavailable in CSP's sandboxed Lua environment. Diagnostic `ac.log()` checkpoints are in place to confirm this on the next AC run.

---

## Data Comparison Matrix

| Capability | 9996 KSUDP | 10101 Telemetry Tool | 5005 Lua Plugin |
|---|---|---|---|
| **Status** | ⚠️ Sparse (13 pkts) | ✅ Working (17K pkts) | ❌ LAZY=FULL bug |
| **Rate** | ~0.04 Hz (broken) | ~60 Hz ✅ | Configurable |
| **Format** | Binary | Binary | JSON |
| **Basic inputs** | ✅ | ✅ | ✅ |
| **G-forces** | ⚠️ Basic 3-axis | ✅ 3-axis | ✅ 3-axis |
| **Wheel slip** | ✅ Ratios + angles | ✅ Ratios + angles + nd_slip | ✅ Per-wheel |
| **Tyre temps** | ❌ | ✅ Core temps | ⚠️ Can add via CSP |
| **Tyre wear** | ❌ | ❌ | ✅ CSP API |
| **Suspension** | ✅ Height per wheel | ✅ Ride height F/R | ✅ Per-wheel travel |
| **Damage** | ❌ | ❌ | ✅ 5-zone CSP |
| **Fuel** | ✅ (offset 64) | ❌ | ✅ CSP API |
| **World position** | ✅ | ✅ | ⚠️ Trivial to add |
| **Spline position** | ✅ | ✅ (float64!) | ⚠️ Trivial to add |
| **Multi-car** | ❌ | ✅ All drivers | ❌ Player only |
| **Velocity vectors** | ❌ | ✅ 3D world + local | ❌ |
| **Weather** | ❌ | ❌ | ✅ CSP API |
| **ERS/DRS/Turbo** | ❌ | ✅ | ❌ |
| **Coaching quality** | Low (~40%) | Very High (~95%) | High (~90%) + CSP |

---

## Configuration Summary

| Setting | Value |
|---|---|
| AC setting | `SETUDP_REMOTE_TELEMETRY=1` in `race.ini` or Content Manager |
| KSUDP port | 9996 (hardcoded by AC) |
| Telemetry Tool port | 10101 (configured in Python plugin) |
| Lua Plugin port | 5005 (configured in our `config.lua`) |
| Bind address | `0.0.0.0` (listen on all interfaces) |
| Target address | `127.0.0.1` (localhost only) |

---

*Document extracted from: Telemetry Feed Analysis Session 2026-06-03 (note 395bc805650d44b6bfec423ca095ed64) and rusty-telemetry source code*
*Last updated: 2026-06-06*

id: aed9f3be040943048273a16e05a8100f
parent_id: 94aa3283ead4477d8449e324a27eb3d0
created_time: 2026-06-06T07:53:28.032Z
updated_time: 2026-06-06T08:27:31.460Z
is_conflict: 0
latitude: 0.00000000
longitude: 0.00000000
altitude: 0.0000
author: 
source_url: 
is_todo: 0
todo_due: 0
todo_completed: 0
source: joplin-desktop
source_application: net.cozic.joplin-desktop
application_data: 
order: 1780732408032
user_created_time: 2026-06-06T07:53:28.032Z
user_updated_time: 2026-06-06T08:27:31.460Z
encryption_cipher_text: 
encryption_applied: 0
markup_language: 1
is_shared: 0
share_id: 
conflict_original_id: 
master_key_id: 
user_data: 
deleted_time: 0
type_: 1