//! 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::Serialize; /// Identifies which feed source produced this frame. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] 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)] 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), } }