Rust crate for reading real-time telemetry from racing simulators via Windows Shared Memory.
Add a single dependency, enable feature flags for the simulators you want, and call SimConnection::connect().
| Simulator | Feature flag | Notes |
|---|---|---|
| iRacing | iracing |
Event-based sync, 0% CPU idle |
| Assetto Corsa Evo | ac-evo |
Windows Shared Memory |
| Le Mans Ultimate | lmu |
Requires rF2 plugin DLL |
All simulators are enabled by default. Add to Cargo.toml:
[dependencies]
kerb = "0.1"To include only specific simulators, disable defaults:
[dependencies]
kerb = { version = "0.1", default-features = false, features = ["iracing"] }Call SimConnection::connect() — it auto-detects whichever sim is running and returns it wrapped in a Connection enum. Match on the variant to access each sim's full API:
use kerb::{Connection, SimConnection, SimError};
fn main() -> Result<(), SimError> {
let conn = SimConnection::connect()?;
match conn {
Connection::IRacing(c) => {
c.wait_for_data(16);
let frame = c.frame();
println!("rpm={:.0} gear={}", frame.rpm, frame.gear);
}
Connection::AcEvo(c) => {
let frame = c.frame()?;
println!("rpm={} gear={}", frame.physics.rpms, frame.physics.gear);
}
Connection::Lmu(c) => {
let frame = c.frame()?;
if let Some(player) = frame.player_telemetry() {
let rpm = player.engine_rpm;
let gear = player.gear;
println!("rpm={:.0} gear={}", rpm, gear);
}
}
_ => {}
}
Ok(())
}Important
The variants present in Connection depend on which features are enabled in your Cargo.toml. With default-features = false, features = ["iracing"] only Connection::IRacing exists — add _ => {} to handle any variants you don't care about.
Important
AC Evo and LMU frames use #[repr(C, packed)] structs mapped directly from shared memory. Rust forbids taking a reference to unaligned packed fields, so always copy fields to local variables before using them (e.g. in println!, arithmetic, or function calls). Accessing them directly will be a compile error.
For overlays that need to reconnect automatically:
use kerb::{Connection, SimConnection};
use std::io::{self, Write};
fn main() {
loop {
match SimConnection::connect() {
Ok(Connection::IRacing(conn)) => {
println!("Connected to iRacing");
if let Some(session) = conn.session_info() {
let track = session.get_value("WeekendInfo.TrackDisplayName")
.unwrap_or_default();
println!("Track: {}", track);
}
while conn.is_connected() {
conn.wait_for_data(100);
let f = conn.frame();
print!("\r[{}] {:.0} rpm {:.1} km/h",
f.gear, f.rpm, f.speed * 3.6);
let _ = io::stdout().flush();
}
println!("\nDisconnected.");
}
Ok(_) => {
// different sim connected
}
Err(e) => {
eprint!("\r{e}");
std::thread::sleep(std::time::Duration::from_secs(2));
}
}
}
}If multiple sims are running simultaneously and connect() picks the wrong one, use connect_to():
use kerb::{SimConnection, SimType};
let conn = SimConnection::connect_to(SimType::Lmu)?;All features are enabled by default. Use default-features = false to opt in selectively.
| Feature | Module | Connection variant |
Default |
|---|---|---|---|
iracing |
kerb::iracing |
Connection::IRacing |
yes |
ac-evo |
kerb::ac_evo |
Connection::AcEvo |
yes |
lmu |
kerb::lmu |
Connection::Lmu |
yes |
Connection: IRsdkConnection (via Connection::IRacing)
| Method | Returns | Scope | Notes |
|---|---|---|---|
frame() |
IracingFrame |
player's car | ~90 typed fields; IDE autocomplete works |
session_info() |
Option<IracingSession> |
whole session | Parsed YAML; cached until iRacing reports a change |
session_yaml() |
Option<String> |
whole session | Raw YAML string for manual parsing |
telemetry_snapshot() |
HashMap<String, TelemetryValue> |
player's car | Dynamic access by iRacing variable name |
var_list_snapshot() |
Vec<VarMeta> |
— | All variable names, types, units, and descriptions |
wait_for_data(ms) |
bool |
— | Blocks until new data or timeout; uses Win32 event (0% CPU) |
is_connected() |
bool |
— | true when iRacing is broadcasting telemetry |
IracingFrame is a typed struct with one pub field per variable — your IDE autocomplete shows all ~90 available fields directly. Fields use snake_case (SteeringWheelAngle → steering_wheel_angle).
Connection::IRacing(conn) => {
conn.wait_for_data(16);
let f = conn.frame();
println!("rpm={:.0} speed={:.1} km/h gear={} throttle={:.0}%",
f.rpm, f.speed * 3.6, f.gear, f.throttle * 100.0);
if let Some(session) = conn.session_info() {
let driver = session.get_value("DriverInfo.Drivers.0.UserName");
println!("driver: {:?}", driver);
}
}To save the raw session YAML to disk:
kerb::save_session(&conn, "session.yaml")?;Connection: AcEvoConnection (via Connection::AcEvo)
| Method | Returns | Scope | Notes |
|---|---|---|---|
frame() |
Result<AcEvoFrame> |
player's car | Plain struct with physics, graphics, static_data |
telemetry_snapshot() |
HashMap<String, TelemetryValue> |
player's car | Keys are field names from the physics/graphics/static structs |
var_list_snapshot() |
Vec<VarMeta> |
— | All available field names |
is_connected() |
bool |
— | true when status == AC_STATUS_LIVE (not paused, not replay) |
wait_for_data(ms) |
— | — | Sleep up to 16 ms; AC Evo has no data-ready event |
AcEvoFrame contents by page:
| Field | Struct | Update rate | What it contains |
|---|---|---|---|
physics |
AcPhysicsData |
Every sim tick | Inputs, RPM, speed, tyres, suspension, aero, damage, G-forces, brake bias |
graphics |
AcGraphicsData |
Every render frame | Lap times, position, flags, fuel, pit state, electronics, session/timing state |
static_data |
AcStaticData |
Once at session load | Car model, track name, player name, session type |
Connection::AcEvo(conn) => {
let frame = conn.frame()?;
let rpms = frame.physics.rpms;
let gear = frame.physics.gear;
let speed = frame.physics.speed_kmh;
println!("{rpms:.0} rpm gear {gear} {speed:.1} km/h");
// Electronics settings
let abs = frame.graphics.electronics.abs_level;
let tc = frame.graphics.electronics.tc_level;
let bb = frame.physics.brake_bias;
println!("ABS={abs} TC={tc} BB={bb:.3}");
// Session info
let track = &frame.static_data.track;
let lap = frame.graphics.session_state.current_lap;
println!("track={track} lap={lap}");
// Brake pad life
let pad_fl = frame.physics.pad_life[0];
println!("pad FL: {pad_fl:.0}%");
}To discover all available fields, save a snapshot:
kerb::save_telemetry_snapshot(&conn, "ac_snapshot.txt")?;Connection: LmuConnection (via Connection::Lmu)
| Method | Returns | Scope | Notes |
|---|---|---|---|
frame() |
Box<LmuFrame> |
all cars | ~500 KB struct; boxed to avoid stack overflow |
frame.player_telemetry() |
Option<&LmuVehicleTelemetry> |
player only | Cross-references scoring + telemetry by vehicle ID; returns None if not found |
frame.player_scoring_idx() |
Option<usize> |
player only | Index into frame.vehicles_scoring for the player's entry |
telemetry_snapshot() |
HashMap<String, TelemetryValue> |
player only | Field names from LmuVehicleTelemetry |
var_list_snapshot() |
Vec<VarMeta> |
— | All field names from LmuVehicleTelemetry |
is_connected() |
bool |
— | true when plugin is loaded and session has started |
wait_for_data(ms) |
— | — | Sleep up to 16 ms; LMU has no data-ready event |
Connection::Lmu(conn) => {
let frame = conn.frame()?;
if let Some(player) = frame.player_telemetry() {
let rpm = player.engine_rpm;
let gear = player.gear;
println!("{:.0} rpm gear {}", rpm, gear);
}
if let Some(idx) = frame.player_scoring_idx() {
let place = frame.vehicles_scoring[idx].place;
let last_lap = frame.vehicles_scoring[idx].last_lap_time;
println!("P{} last lap {:.3}s", place, last_lap);
}
for v in frame.vehicles_scoring() {
println!(" place {}", v.place);
}
let track = &frame.scoring_info.track_name;
let temp = frame.scoring_info.track_temp;
println!("track: {} temp: {:.1}°C", track, temp);
}Important
Plugin required. Install rFactor2SharedMemoryMapPlugin64.dll — see LMU Plugin Setup.
use kerb::{save_telemetry_snapshot, save_var_list_snapshot, save_session};
// All sims — accepts &Connection
save_telemetry_snapshot(&conn, "snapshot.txt")?;
save_var_list_snapshot(&conn, "vars.txt")?;
// iRacing only — accepts &IRsdkConnection
save_session(&iracing_conn, "session.yaml")?;# All simulators (default)
kerb = { git = "https://github.com/mvoof/kerb" }
# iRacing only
kerb = { git = "https://github.com/mvoof/kerb", default-features = false, features = ["iracing"] }iRacing uses Windows-1252 for all strings. The crate decodes them automatically. Use decode_cp1252(bytes) if you need to decode raw bytes yourself.
LMU does not expose telemetry by default. Install the rF2SharedMemoryMapPlugin:
- Download
rFactor2SharedMemoryMapPlugin64.dllfrom the releases page - Copy to
<Steam>\steamapps\common\Le Mans Ultimate\Plugins\ - In-game: Settings → Gameplay → Enable Plugins: ON
- Restart LMU
If the plugin is missing, SimConnection::connect() skips LMU and tries the next enabled sim.
Important
End users of the crate do not need this. src/iracing/types.rs is already committed to the repository with all current iRacing variables. Re-run codegen only if iRacing adds or changes variables after an SDK update.
IracingFrame is a struct with one pub field per iRacing variable. Field names are snake_case of the iRacing variable name (SteeringWheelAngle → steering_wheel_angle).
- Start iRacing and enter a session (practice, qualifying, or race)
- Run codegen — it connects to the live session, reads all variables, and writes
types.rs:
cargo run --manifest-path tools/iracing_type_gen/Cargo.toml -- src/iracing/types.rs- Commit the updated
src/iracing/types.rs
cargo bench --all-featuresCovers CP-1252 decoding, frame copies, snapshot HashMap allocation, and iRacing session cache behavior.
cargo run -p kerb-examples --example facade_iracing
cargo run -p kerb-examples --example facade_ac_evo
cargo run -p kerb-examples --example facade_lmu| Simulator | Documentation |
|---|---|
| iRacing | iRacing SDK (login required). Community reference: irsdkdocs |
| Assetto Corsa Evo | Shared Memory API Documentation — official Kunos thread; struct reference — Google Doc |
| Le Mans Ultimate | Uses rF2SharedMemoryMapPlugin — community plugin built on ISI/S397 internals sample |
MIT — see LICENSE