refactor: TUI multi-feed monitor with binary parsers
- Add ratatui/crossterm TUI replacing stdout data dump - Add binary parsers for KSUDP RTCarInfo (port 9996) and Telemetry Tool (port 10101) - Add shared binary_utils module for safe packet reading - Add ring-buffer frequency calculation and feed health state machine - Add parse_first_driver() to avoid unnecessary multi-driver allocation - Add 27 unit tests (14 feed_state + 6 ksudp + 7 telemetry_tool) - Move parsing outside mutex lock to minimize TUI contention - Replace expect() on env var with graceful error handling - Remove unused serde dependency - Delete obsolete stats.rs and telemetry.rs
This commit is contained in:
parent
1731495197
commit
7a1c37fc86
11 changed files with 2308 additions and 307 deletions
218
src/telemetry_frame.rs
Normal file
218
src/telemetry_frame.rs
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
//! 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 std::fmt;
|
||||
|
||||
/// Identifies which feed source produced this frame.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum FeedSource {
|
||||
Ksudp,
|
||||
TelemetryTool,
|
||||
}
|
||||
|
||||
/// 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)]
|
||||
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>,
|
||||
|
||||
// ── 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,
|
||||
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 {
|
||||
match self.gear {
|
||||
0 => "R".to_string(),
|
||||
1 => "N".to_string(),
|
||||
n => format!("{}", n - 1),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for FeedSource {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
FeedSource::Ksudp => write!(f, "KSUDP"),
|
||||
FeedSource::TelemetryTool => write!(f, "Telemetry Tool"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue