diff --git a/src/binary_utils.rs b/src/binary_utils.rs index 9afa718..ef4cd32 100644 --- a/src/binary_utils.rs +++ b/src/binary_utils.rs @@ -51,12 +51,6 @@ pub fn read_u16(data: &[u8], offset: usize) -> u16 { u16::from_le_bytes(buf) } -/// Read a signed `i8` from the data at the given offset. -/// Returns 0 if the offset is out of bounds. -pub fn read_i8(data: &[u8], offset: usize) -> i8 { - data.get(offset).map(|&b| b as i8).unwrap_or(0) -} - /// Read a little-endian `f32` from the data at the given offset. /// Returns 0.0 if the offset is out of bounds (safe fallback for malformed packets). pub fn read_f32(data: &[u8], offset: usize) -> f32 { diff --git a/src/feed_state.rs b/src/feed_state.rs index 52f6532..db10dda 100644 --- a/src/feed_state.rs +++ b/src/feed_state.rs @@ -43,7 +43,7 @@ pub struct FeedState { pub last_packet_ago_secs: Option, pub start_time: Option, pub last_packet_time: Option, - pub packet_timestamps: Vec, + pub packet_timestamps: [Instant; RING_CAPACITY], pub ring_index: usize, pub ring_count: usize, pub last_frame: Option, @@ -62,7 +62,7 @@ impl FeedState { last_packet_ago_secs: None, start_time: None, last_packet_time: None, - packet_timestamps: vec![Instant::now(); RING_CAPACITY], + packet_timestamps: [Instant::now(); RING_CAPACITY], ring_index: 0, ring_count: 0, last_frame: None, @@ -85,9 +85,7 @@ impl FeedState { self.last_packet_ago_secs = Some(0.0); if self.ring_count < RING_CAPACITY { - if self.ring_count < self.packet_timestamps.len() { - self.packet_timestamps[self.ring_count] = now; - } + self.packet_timestamps[self.ring_count] = now; self.ring_count += 1; self.ring_index = self.ring_count % RING_CAPACITY; } else { diff --git a/src/main.rs b/src/main.rs index 409144a..03a7ef2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -120,12 +120,32 @@ fn install_panic_hook() { })); } +struct CachedStrings { + driver_id: u32, + driver_name: Option, + nationality: Option, + car_name: Option, + tyre_compound: Option, +} + +impl CachedStrings { + fn new() -> Self { + Self { + driver_id: u32::MAX, + driver_name: None, + nationality: None, + car_name: None, + tyre_compound: None, + } + } +} + async fn run_feed( feed: FeedConfig, socket: tokio::net::UdpSocket, session_dir: String, state: Arc>, - shutdown: Arc, + mut shutdown_rx: tokio::sync::watch::Receiver, ) { let handshake_target: Option = if feed.needs_handshake { let target: std::net::SocketAddr = match format!("{}:{}", game_host(), feed.port).parse() { @@ -268,6 +288,7 @@ async fn run_feed( }; let mut file = BufWriter::new(file); let mut buf = vec![0u8; BUFFER_SIZE]; + let mut string_cache = CachedStrings::new(); let mut hs_interval: Option = handshake_target.map(|_| { tokio::time::interval(Duration::from_secs(HANDSHAKE_INTERVAL_SECS)) @@ -295,8 +316,25 @@ async fn run_feed( _ => None, } } else { - // Telemetry Tool: parse only the first driver - parser_telemetry_tool::parse_first_driver(&buf[..len]).ok() + // Telemetry Tool: parse first driver, cache rarely-changing strings + match parser_telemetry_tool::parse_first_driver_no_strings(&buf[..len]) { + Ok((mut frame, driver_id)) => { + if driver_id != string_cache.driver_id { + string_cache.driver_id = driver_id; + let raw = &buf[..len]; + string_cache.driver_name = Some(binary_utils::read_string(raw, parser_telemetry_tool::STRING_OFFSET_DRIVER_NAME, parser_telemetry_tool::STRING_LEN_DRIVER_NAME)); + string_cache.nationality = Some(binary_utils::read_string(raw, parser_telemetry_tool::STRING_OFFSET_NATIONALITY, parser_telemetry_tool::STRING_LEN_NATIONALITY)); + string_cache.car_name = Some(binary_utils::read_string(raw, parser_telemetry_tool::STRING_OFFSET_CAR_NAME, parser_telemetry_tool::STRING_LEN_CAR_NAME)); + string_cache.tyre_compound = Some(binary_utils::read_string(raw, parser_telemetry_tool::STRING_OFFSET_TYRE_COMPOUND, parser_telemetry_tool::STRING_LEN_TYRE_COMPOUND)); + } + frame.driver_name = string_cache.driver_name.clone(); + frame.car_name = string_cache.car_name.clone(); + frame.nationality = string_cache.nationality.clone(); + frame.tyre_compound = string_cache.tyre_compound.clone(); + Some(frame) + } + Err(_) => None, + } }; // Update state in a single brief lock acquisition. @@ -344,7 +382,7 @@ async fn run_feed( } } } - _ = shutdown.notified() => { + _ = shutdown_rx.changed() => { break; } } @@ -391,7 +429,7 @@ async fn main() { let mut feed_displays: Vec<(FeedDisplayInfo, Arc>)> = Vec::new(); let mut feed_tasks: Vec> = Vec::new(); - let shutdown = Arc::new(tokio::sync::Notify::new()); + let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false); for feed in FEEDS { let state = Arc::new(std::sync::Mutex::new(FeedState::new())); @@ -446,7 +484,7 @@ async fn main() { if let Some(socket) = socket_opt { let feed_clone = feed.clone(); let dir = session_dir.clone(); - let sh = shutdown.clone(); + let sh = shutdown_rx.clone(); feed_tasks.push(tokio::spawn(async move { run_feed(feed_clone, socket, dir, state, sh).await; })); @@ -507,7 +545,7 @@ async fn main() { } event_running.store(false, Ordering::Relaxed); - shutdown.notify_waiters(); + let _ = shutdown_tx.send(true); match tokio::time::timeout( Duration::from_secs(3), diff --git a/src/parser_ksudp.rs b/src/parser_ksudp.rs index 4146b2c..b7f4188 100644 --- a/src/parser_ksudp.rs +++ b/src/parser_ksudp.rs @@ -93,7 +93,7 @@ pub fn parse_rt_car_info(data: &[u8]) -> Result // Driver inputs throttle: read_f32(data, 51), brake: read_f32(data, 55), - clutch: read_f32(data, 59), + clutch: 0.0, // offset 59 is fuel (0.0–1.0), not clutch; KSUDP has no clutch input engine_rpm: read_f32(data, 63), steer: read_f32(data, 67), diff --git a/src/parser_pcars.rs b/src/parser_pcars.rs index 200f5d7..f0bacc8 100644 --- a/src/parser_pcars.rs +++ b/src/parser_pcars.rs @@ -366,15 +366,6 @@ fn parse_v2_car_physics(data: &[u8]) -> Result Ok(frame) } -/// Read a u16 at the given offset (little-endian). -fn read_u16(data: &[u8], offset: usize) -> u16 { - let buf: [u8; 2] = data - .get(offset..offset + 2) - .and_then(|s| s.try_into().ok()) - .unwrap_or([0u8; 2]); - u16::from_le_bytes(buf) -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/parser_telemetry_tool.rs b/src/parser_telemetry_tool.rs index 272222b..8a22c2b 100644 --- a/src/parser_telemetry_tool.rs +++ b/src/parser_telemetry_tool.rs @@ -104,6 +104,7 @@ pub fn parse_packet(data: &[u8]) -> Result Result { if data.is_empty() { return Err(TelemetryToolParseError::TooShort { actual: 0, needed: 1 }); @@ -125,8 +126,56 @@ pub fn parse_first_driver(data: &[u8]) -> Result 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 { @@ -195,11 +244,6 @@ fn parse_driver(data: &[u8]) -> Result tyre_slip: read_f32_4(data, 305), // Telemetry Tool-specific fields - driver_id: Some(read_u32(data, 0)), - driver_name: Some(read_string(data, 97, 33)), - nationality: Some(read_string(data, 130, 4)), - car_name: Some(read_string(data, 134, 33)), - tyre_compound: Some(read_string(data, 167, 10)), track_position: Some(read_i16(data, 67)), leaderboard_position: Some(read_i16(data, 69)), is_connected: Some(read_bool(data, 181)), diff --git a/src/telemetry_frame.rs b/src/telemetry_frame.rs index 5fcd063..9de4c08 100644 --- a/src/telemetry_frame.rs +++ b/src/telemetry_frame.rs @@ -3,8 +3,6 @@ //! 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 { @@ -242,13 +240,3 @@ impl TelemetryFrame { } } } - -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"), - FeedSource::Pcars => write!(f, "PCARS"), - } - } -}