Backend tasks A→B→C→F→E→D from the Track Map Implementation plan:
A. PCARS V1 position data: read world_position (3×i16) and current_lap_distance
from viewed participant entry, compute derived spline_position, add unit tests.
B. Dual position mode: auto-detect spline vs coordinate mode in track map analysis,
compute cumulative 2D distance as pseudo-spline for PCARS/future games.
C. Persistent TrackModel: Corner, Sector, TrackModel data structures with serde,
TrackModelStore with async CRUD at data/tracks/<game>/<track_name>.json.
F. Track model building algorithm: resample boundaries, compute center line,
track width, heading, curvature, auto-detect corners (κ > 0.005), 3 sectors,
coordinate normalization to [0,1]².
E. TrackMapping use case: new boundary recording workflow with step-by-step
guidance, session integration.
D. Track API endpoints: GET/DELETE /api/tracks/{game}/{track_name},
GET /api/tracks, GET .../corners, POST /api/tracks/build.
251 lines
9.1 KiB
Rust
251 lines
9.1 KiB
Rust
//! 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<String>,
|
|
pub car_name: Option<String>,
|
|
pub nationality: Option<String>,
|
|
pub tyre_compound: Option<String>,
|
|
pub track_position: Option<i16>,
|
|
pub leaderboard_position: Option<i16>,
|
|
pub is_connected: Option<bool>,
|
|
pub is_car_in_pitlane: Option<bool>,
|
|
pub lap_invalidated: Option<bool>,
|
|
pub is_drs_available: Option<bool>,
|
|
pub is_drs_enabled: Option<bool>,
|
|
pub is_race_finished: Option<bool>,
|
|
pub turbo_boost: Option<f32>,
|
|
pub ers_max_kj: Option<f32>,
|
|
pub ers_current_kj: Option<f32>,
|
|
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<u32>,
|
|
|
|
// ── PCARS-specific fields ──────────────────────────────────────
|
|
pub oil_temp_celsius: Option<i16>,
|
|
pub water_temp_celsius: Option<i16>,
|
|
pub fuel_capacity: Option<u8>,
|
|
pub boost_amount: Option<u8>,
|
|
pub crash_state: Option<u8>,
|
|
pub odometer_km: Option<f32>,
|
|
pub aero_damage: Option<u8>,
|
|
pub engine_damage: Option<u8>,
|
|
pub ambient_temperature: Option<i8>,
|
|
pub track_temperature: Option<i8>,
|
|
pub rain_density: Option<u8>,
|
|
pub track_length: Option<f32>,
|
|
pub full_position: Option<[f32; 3]>,
|
|
pub current_lap_distance: Option<u16>,
|
|
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),
|
|
}
|
|
}
|