//! Binary parser for Telemetry Tool plugin packets (port 10101). //! //! The Telemetry Tool is a third-party AC Python plugin by IkoRein v1.3. //! It sends binary telemetry for all cars in the session. //! //! Reference: `/apps/python/Telemetry_Tool_plugin/Telemetry_Tool_plugin.py` use crate::binary_utils::{ read_bool, read_f32, read_f32_2, read_f32_3, read_f32_4, read_f64, read_i16, read_i32_3, read_string, read_u32, }; use crate::telemetry_frame::{FeedSource, TelemetryFrame}; /// Packet type byte indicating telemetry data. const PACKET_TYPE_TELEMETRY: u8 = 22; /// Packet type byte indicating version info (sent every ~100 frames). const PACKET_TYPE_VERSION: u8 = 23; /// Size of a single per-driver telemetry structure. const DRIVER_STRUCT_SIZE: usize = 357; /// Validation constants embedded by the plugin. const TEST_FLOAT: f32 = 666.666; const TEST_INT: u32 = 666; /// Parse error type for Telemetry Tool packets. #[derive(Debug)] pub enum TelemetryToolParseError { /// Packet too short for the declared content. TooShort { actual: usize, needed: usize }, /// Unknown packet type. UnknownPacketType { got: u8 }, } impl std::fmt::Display for TelemetryToolParseError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { TelemetryToolParseError::TooShort { actual, needed } => { write!(f, "packet too short: {} bytes (needed {})", actual, needed) } TelemetryToolParseError::UnknownPacketType { got } => { write!(f, "unknown packet type: {} (expected 22 or 23)", got) } } } } impl std::error::Error for TelemetryToolParseError {} /// Parsed result of a Telemetry Tool packet. /// /// Reserved for future multi-driver UI support. #[allow(dead_code)] pub enum TelemetryToolPacket { /// Telemetry data for one or more drivers. Telemetry(Vec), /// Version info packet (typically ignored). Version, } /// Parse a complete Telemetry Tool UDP packet. /// /// Returns either a list of parsed driver frames or a version marker. /// /// Reserved for future multi-driver UI support. #[allow(dead_code)] pub fn parse_packet(data: &[u8]) -> Result { if data.is_empty() { return Err(TelemetryToolParseError::TooShort { actual: 0, needed: 1 }); } let packet_type = data[0]; match packet_type { PACKET_TYPE_TELEMETRY => { let payload = &data[1..]; if payload.len() < DRIVER_STRUCT_SIZE { return Err(TelemetryToolParseError::TooShort { actual: data.len(), needed: 1 + DRIVER_STRUCT_SIZE, }); } let num_drivers = payload.len() / DRIVER_STRUCT_SIZE; let mut drivers = Vec::with_capacity(num_drivers); for i in 0..num_drivers { let start = i * DRIVER_STRUCT_SIZE; let end = start + DRIVER_STRUCT_SIZE; if end > payload.len() { break; } let driver_data = &payload[start..end]; drivers.push(parse_driver(driver_data)?); } Ok(TelemetryToolPacket::Telemetry(drivers)) } PACKET_TYPE_VERSION => Ok(TelemetryToolPacket::Version), _ => Err(TelemetryToolParseError::UnknownPacketType { got: packet_type }), } } /// Parse only the first driver from a Telemetry Tool UDP packet. /// /// Avoids allocating a `Vec` for all drivers when only the local player's /// telemetry is needed. #[allow(dead_code)] pub fn parse_first_driver(data: &[u8]) -> Result { if data.is_empty() { return Err(TelemetryToolParseError::TooShort { actual: 0, needed: 1 }); } let packet_type = data[0]; if packet_type != PACKET_TYPE_TELEMETRY { return Err(TelemetryToolParseError::UnknownPacketType { got: packet_type }); } let payload = &data[1..]; if payload.len() < DRIVER_STRUCT_SIZE { return Err(TelemetryToolParseError::TooShort { actual: data.len(), needed: 1 + DRIVER_STRUCT_SIZE, }); } parse_driver(&payload[..DRIVER_STRUCT_SIZE]) } /// Parse only the first driver, skipping rarely-changing string fields. /// /// Returns a tuple of (frame, raw_driver_id). Strings can be populated /// from a cache keyed by `raw_driver_id`. pub fn parse_first_driver_no_strings(data: &[u8]) -> Result<(TelemetryFrame, u32), TelemetryToolParseError> { if data.is_empty() { return Err(TelemetryToolParseError::TooShort { actual: 0, needed: 1 }); } let packet_type = data[0]; if packet_type != PACKET_TYPE_TELEMETRY { return Err(TelemetryToolParseError::UnknownPacketType { got: packet_type }); } let payload = &data[1..]; if payload.len() < DRIVER_STRUCT_SIZE { return Err(TelemetryToolParseError::TooShort { actual: data.len(), needed: 1 + DRIVER_STRUCT_SIZE, }); } let driver_data = &payload[..DRIVER_STRUCT_SIZE]; let driver_id = read_u32(driver_data, 0); let mut frame = parse_driver_core(driver_data)?; frame.driver_id = Some(driver_id); Ok((frame, driver_id)) } pub const STRING_OFFSET_DRIVER_NAME: usize = 1 + 97; pub const STRING_LEN_DRIVER_NAME: usize = 33; pub const STRING_OFFSET_NATIONALITY: usize = 1 + 130; pub const STRING_LEN_NATIONALITY: usize = 4; pub const STRING_OFFSET_CAR_NAME: usize = 1 + 134; pub const STRING_LEN_CAR_NAME: usize = 33; pub const STRING_OFFSET_TYRE_COMPOUND: usize = 1 + 167; pub const STRING_LEN_TYRE_COMPOUND: usize = 10; /// Parse a single 357-byte driver structure. fn parse_driver(data: &[u8]) -> Result { let mut frame = parse_driver_core(data)?; frame.driver_id = Some(read_u32(data, 0)); frame.driver_name = Some(read_string(data, 97, 33)); frame.nationality = Some(read_string(data, 130, 4)); frame.car_name = Some(read_string(data, 134, 33)); frame.tyre_compound = Some(read_string(data, 167, 10)); Ok(frame) } fn parse_driver_core(data: &[u8]) -> Result { // Validation: check test_float at offset 93 let test_float_val = read_f32(data, 93); if (test_float_val - TEST_FLOAT).abs() > 1.0 { eprintln!( "[WARN] Telemetry Tool: test_float validation: expected {}, got {}", TEST_FLOAT, test_float_val ); } // Validation: check test_int at offset 177 let test_int_val = read_u32(data, 177); if test_int_val != TEST_INT { eprintln!( "[WARN] Telemetry Tool: test_int validation: expected {}, got {}", TEST_INT, test_int_val ); } let speed_kmh = read_f32(data, 63); let frame = TelemetryFrame { source: FeedSource::TelemetryTool, raw_size: data.len(), // Speed (convert km/h back) speed_kmh, speed_mph: speed_kmh * 0.621371, speed_ms: speed_kmh / 3.6, // Engine & drivetrain engine_rpm: read_f32(data, 59), gear: read_i16(data, 57) as i32, // Driver inputs steer: read_f32(data, 41), throttle: read_f32(data, 45), brake: read_f32(data, 49), clutch: read_f32(data, 53), // Lap timing lap_time_ms: read_u32(data, 4) as i32, last_lap_ms: read_u32(data, 8) as i32, best_lap_ms: read_u32(data, 12) as i32, lap_count: read_i16(data, 71) as i32, // World position world_x: read_f32(data, 73), world_y: read_f32(data, 77), world_z: read_f32(data, 81), spline_position: read_f64(data, 85) as f32, // G-forces acc_g_vertical: read_f32(data, 185), acc_g_horizontal: read_f32(data, 189), acc_g_frontal: read_f32(data, 193), // Boolean flags is_in_pit: read_bool(data, 182), is_engine_limiter_on: read_bool(data, 196), // Per-wheel data tyre_dirty_level: read_f32_4(data, 213), slip_ratio: read_f32_4(data, 257), slip_angle: read_f32_4(data, 273), nd_slip: read_f32_4(data, 289), tyre_slip: read_f32_4(data, 305), // Telemetry Tool-specific fields track_position: Some(read_i16(data, 67)), leaderboard_position: Some(read_i16(data, 69)), is_connected: Some(read_bool(data, 181)), is_car_in_pitlane: Some(read_bool(data, 183)), lap_invalidated: Some(read_bool(data, 184)), is_drs_available: Some(read_bool(data, 197)), is_drs_enabled: Some(read_bool(data, 198)), is_race_finished: Some(read_bool(data, 199)), turbo_boost: Some(read_f32(data, 229)), ers_max_kj: Some(read_f32(data, 205)), ers_current_kj: Some(read_f32(data, 209)), ride_height_front_rear: Some(read_f32_2(data, 233)), tyre_core_temp: Some(read_f32_4(data, 241)), world_velocity: Some(read_f32_3(data, 321)), local_velocity: Some(read_f32_3(data, 333)), sector_times: Some(read_i32_3(data, 17)), last_sector_times: Some(read_i32_3(data, 29)), // Remaining KSUDP-only fields stay at default (0.0 / [0.0; 4]) ..Default::default() }; Ok(frame) } #[cfg(test)] mod tests { use super::*; fn make_driver_struct() -> Vec { let mut data = vec![0u8; DRIVER_STRUCT_SIZE]; // driver_id = 42 data[0..4].copy_from_slice(&42u32.to_le_bytes()); // lap_time = 65000 data[4..8].copy_from_slice(&65000u32.to_le_bytes()); // test_float at offset 93 data[93..97].copy_from_slice(&TEST_FLOAT.to_le_bytes()); // driver_name at offset 97 let name = b"TestDriver"; data[97..97 + name.len()].copy_from_slice(name); // test_int at offset 177 data[177..181].copy_from_slice(&TEST_INT.to_le_bytes()); // speed at offset 63 = 180.5 data[63..67].copy_from_slice(&180.5f32.to_le_bytes()); // engineRPM at offset 59 = 7200.0 data[59..63].copy_from_slice(&7200.0f32.to_le_bytes()); // gear at offset 57 = 5 data[57..59].copy_from_slice(&5i16.to_le_bytes()); // throttle at offset 45 = 0.85 data[45..49].copy_from_slice(&0.85f32.to_le_bytes()); // spline_position at offset 85 = 0.5 data[85..93].copy_from_slice(&0.5f64.to_le_bytes()); // x at offset 73 data[73..77].copy_from_slice(&100.0f32.to_le_bytes()); // is_in_pit at offset 182 = false data[182] = 0; data } #[test] fn test_parse_telemetry_packet_single_driver() { let driver = make_driver_struct(); let mut packet = vec![PACKET_TYPE_TELEMETRY]; packet.extend_from_slice(&driver); let result = parse_packet(&packet).unwrap(); match result { TelemetryToolPacket::Telemetry(drivers) => { assert_eq!(drivers.len(), 1); let f = &drivers[0]; assert_eq!(f.source, FeedSource::TelemetryTool); assert_eq!(f.driver_id, Some(42)); assert_eq!(f.lap_time_ms, 65000); assert!((f.speed_kmh - 180.5).abs() < 0.01); assert!((f.engine_rpm - 7200.0).abs() < 0.01); assert_eq!(f.gear, 5); assert!((f.throttle - 0.85).abs() < 0.01); assert!((f.world_x - 100.0).abs() < 0.01); assert_eq!(f.driver_name.as_deref(), Some("TestDriver")); } TelemetryToolPacket::Version => panic!("Expected Telemetry, got Version"), } } #[test] fn test_parse_version_packet() { let packet = vec![PACKET_TYPE_VERSION, 1, 2, 3]; let result = parse_packet(&packet).unwrap(); match result { TelemetryToolPacket::Version => {} _ => panic!("Expected Version"), } } #[test] fn test_unknown_packet_type() { let packet = vec![99u8]; let result = parse_packet(&packet); assert!(result.is_err()); if let Err(TelemetryToolParseError::UnknownPacketType { got }) = result { assert_eq!(got, 99); } else { panic!("Expected UnknownPacketType"); } } #[test] fn test_empty_packet() { let result = parse_packet(&[]); assert!(result.is_err()); } #[test] fn test_multiple_drivers() { let driver1 = make_driver_struct(); let mut driver2 = make_driver_struct(); // Change driver_id for second driver driver2[0..4].copy_from_slice(&99u32.to_le_bytes()); let mut packet = vec![PACKET_TYPE_TELEMETRY]; packet.extend_from_slice(&driver1); packet.extend_from_slice(&driver2); let result = parse_packet(&packet).unwrap(); match result { TelemetryToolPacket::Telemetry(drivers) => { assert_eq!(drivers.len(), 2); assert_eq!(drivers[0].driver_id, Some(42)); assert_eq!(drivers[1].driver_id, Some(99)); } TelemetryToolPacket::Version => panic!("Expected Telemetry"), } } #[test] fn test_per_wheel_slip_ratio() { let mut driver = make_driver_struct(); // slip_ratio at offset 257: [0.1, 0.2, 0.3, 0.4] for i in 0..4 { let val = (i as f32 + 1.0) * 0.1; driver[257 + i * 4..257 + i * 4 + 4].copy_from_slice(&val.to_le_bytes()); } let mut packet = vec![PACKET_TYPE_TELEMETRY]; packet.extend_from_slice(&driver); let result = parse_packet(&packet).unwrap(); match result { TelemetryToolPacket::Telemetry(drivers) => { let f = &drivers[0]; assert!((f.slip_ratio[0] - 0.1).abs() < 0.001); assert!((f.slip_ratio[1] - 0.2).abs() < 0.001); assert!((f.slip_ratio[2] - 0.3).abs() < 0.001); assert!((f.slip_ratio[3] - 0.4).abs() < 0.001); } TelemetryToolPacket::Version => panic!("Expected Telemetry"), } } }