// rusty-telemetry — Sim racing telemetry analysis platform
// Copyright (C) 2026 Jan-Peter v. Hunnius
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
//! Generic telemetry frame model — unified representation of parsed sim racing data.
//!
//! Both the KSUDP (port 9996) and Telemetry Tool (port 10101) parsers produce
//! `TelemetryFrame` instances from their raw binary data.
use serde::{Deserialize, Serialize};
/// Identifies which feed source produced this frame.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FeedSource {
Ksudp,
TelemetryTool,
Pcars,
}
/// Per-wheel index convention: FL=0, FR=1, RL=2, RR=3.
pub const NUM_WHEELS: usize = 4;
/// A single parsed telemetry snapshot from any supported feed.
///
/// Fields present in both feeds are stored directly; fields unique to one feed
/// are wrapped in `Option`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TelemetryFrame {
// ── Speed ──────────────────────────────────────────────────────
pub speed_kmh: f32,
pub speed_mph: f32,
pub speed_ms: f32,
// ── Engine & Drivetrain ────────────────────────────────────────
pub engine_rpm: f32,
pub gear: i32,
// ── Driver Inputs ──────────────────────────────────────────────
pub throttle: f32,
pub brake: f32,
pub clutch: f32,
pub steer: f32,
// ── Lap Timing ─────────────────────────────────────────────────
pub lap_time_ms: i32,
pub last_lap_ms: i32,
pub best_lap_ms: i32,
pub lap_count: i32,
// ── World Position ─────────────────────────────────────────────
pub world_x: f32,
pub world_y: f32,
pub world_z: f32,
pub spline_position: f32,
// ── Acceleration (G-forces) ────────────────────────────────────
pub acc_g_vertical: f32,
pub acc_g_horizontal: f32,
pub acc_g_frontal: f32,
// ── Boolean Flags ──────────────────────────────────────────────
pub is_abs_enabled: bool,
pub is_abs_in_action: bool,
pub is_tc_enabled: bool,
pub is_tc_in_action: bool,
pub is_in_pit: bool,
pub is_engine_limiter_on: bool,
// ── Per-Wheel Data (FL, FR, RL, RR) ────────────────────────────
pub wheel_angular_speed: [f32; NUM_WHEELS],
pub slip_angle: [f32; NUM_WHEELS],
pub slip_ratio: [f32; NUM_WHEELS],
pub tyre_slip: [f32; NUM_WHEELS],
pub nd_slip: [f32; NUM_WHEELS],
pub suspension_height: [f32; NUM_WHEELS],
pub tyre_dirty_level: [f32; NUM_WHEELS],
// ── KSUDP-specific fields ──────────────────────────────────────
pub slip_angle_contact_patch: [f32; NUM_WHEELS],
pub wheel_load: [f32; NUM_WHEELS],
pub wheel_dy: [f32; NUM_WHEELS],
pub wheel_mz: [f32; NUM_WHEELS],
pub camber_rad: [f32; NUM_WHEELS],
pub tyre_radius: [f32; NUM_WHEELS],
pub tyre_loaded_radius: [f32; NUM_WHEELS],
pub cg_height: f32,
pub car_slope: f32,
pub car_position_normalized: f32,
// ── Telemetry Tool-specific fields ─────────────────────────────
pub driver_name: Option,
pub car_name: Option,
pub nationality: Option,
pub tyre_compound: Option,
pub track_position: Option,
pub leaderboard_position: Option,
pub is_connected: Option,
pub is_car_in_pitlane: Option,
pub lap_invalidated: Option,
pub is_drs_available: Option,
pub is_drs_enabled: Option,
pub is_race_finished: Option,
pub turbo_boost: Option,
pub ers_max_kj: Option,
pub ers_current_kj: Option,
pub ride_height_front_rear: Option<[f32; 2]>,
pub tyre_core_temp: Option<[f32; NUM_WHEELS]>,
pub world_velocity: Option<[f32; 3]>,
pub local_velocity: Option<[f32; 3]>,
pub sector_times: Option<[i32; 3]>,
pub last_sector_times: Option<[i32; 3]>,
pub driver_id: Option,
// ── PCARS-specific fields ──────────────────────────────────────
pub oil_temp_celsius: Option,
pub water_temp_celsius: Option,
pub fuel_capacity: Option,
pub boost_amount: Option,
pub crash_state: Option,
pub odometer_km: Option,
pub aero_damage: Option,
pub engine_damage: Option,
pub ambient_temperature: Option,
pub track_temperature: Option,
pub rain_density: Option,
pub track_length: Option,
pub full_position: Option<[f32; 3]>,
pub current_lap_distance: Option,
pub tyre_wear: Option<[u8; 4]>,
pub brake_temp_celsius: Option<[i16; 4]>,
pub suspension_damage: Option<[u8; 4]>,
// ── Meta ───────────────────────────────────────────────────────
pub source: FeedSource,
pub raw_size: usize,
}
impl Default for TelemetryFrame {
fn default() -> Self {
Self {
speed_kmh: 0.0,
speed_mph: 0.0,
speed_ms: 0.0,
engine_rpm: 0.0,
gear: 0,
throttle: 0.0,
brake: 0.0,
clutch: 0.0,
steer: 0.0,
lap_time_ms: 0,
last_lap_ms: 0,
best_lap_ms: 0,
lap_count: 0,
world_x: 0.0,
world_y: 0.0,
world_z: 0.0,
spline_position: 0.0,
acc_g_vertical: 0.0,
acc_g_horizontal: 0.0,
acc_g_frontal: 0.0,
is_abs_enabled: false,
is_abs_in_action: false,
is_tc_enabled: false,
is_tc_in_action: false,
is_in_pit: false,
is_engine_limiter_on: false,
wheel_angular_speed: [0.0; NUM_WHEELS],
slip_angle: [0.0; NUM_WHEELS],
slip_ratio: [0.0; NUM_WHEELS],
tyre_slip: [0.0; NUM_WHEELS],
nd_slip: [0.0; NUM_WHEELS],
suspension_height: [0.0; NUM_WHEELS],
tyre_dirty_level: [0.0; NUM_WHEELS],
slip_angle_contact_patch: [0.0; NUM_WHEELS],
wheel_load: [0.0; NUM_WHEELS],
wheel_dy: [0.0; NUM_WHEELS],
wheel_mz: [0.0; NUM_WHEELS],
camber_rad: [0.0; NUM_WHEELS],
tyre_radius: [0.0; NUM_WHEELS],
tyre_loaded_radius: [0.0; NUM_WHEELS],
cg_height: 0.0,
car_slope: 0.0,
car_position_normalized: 0.0,
driver_name: None,
car_name: None,
nationality: None,
tyre_compound: None,
track_position: None,
leaderboard_position: None,
is_connected: None,
is_car_in_pitlane: None,
lap_invalidated: None,
is_drs_available: None,
is_drs_enabled: None,
is_race_finished: None,
turbo_boost: None,
ers_max_kj: None,
ers_current_kj: None,
ride_height_front_rear: None,
tyre_core_temp: None,
world_velocity: None,
local_velocity: None,
sector_times: None,
last_sector_times: None,
driver_id: None,
oil_temp_celsius: None,
water_temp_celsius: None,
fuel_capacity: None,
boost_amount: None,
crash_state: None,
odometer_km: None,
aero_damage: None,
engine_damage: None,
ambient_temperature: None,
track_temperature: None,
rain_density: None,
track_length: None,
full_position: None,
current_lap_distance: None,
tyre_wear: None,
brake_temp_celsius: None,
suspension_damage: None,
source: FeedSource::Ksudp,
raw_size: 0,
}
}
}
impl TelemetryFrame {
/// Format lap time from milliseconds as M:SS.mmm
pub fn format_lap_time(ms: i32) -> String {
if ms <= 0 {
return "-:--.---".to_string();
}
let total_ms = ms as u32;
let minutes = total_ms / 60000;
let seconds = (total_ms % 60000) / 1000;
let millis = total_ms % 1000;
format!("{}:{:02}.{:03}", minutes, seconds, millis)
}
/// Gear display: 0=Reverse, 1=Neutral, 2+=Gear N-1
pub fn gear_display(&self) -> String {
gear_display(self.gear)
}
}
/// AC gear encoding: 0=Reverse, 1=Neutral, 2+=Gear N-1.
pub fn gear_display(gear: i32) -> String {
match gear {
0 => "R".to_string(),
1 => "N".to_string(),
n => format!("{}", n - 1),
}
}