fix: resolve 7 code review issues from v0.4.x
- Fix KSUDP clutch field reading fuel at offset 59 (KSUDP has no clutch input) - Replace Notify with watch::channel for reliable shutdown (no missed wakeups) - Deduplicate read_u16 from parser_pcars into shared binary_utils - Remove dead Display impl for FeedSource - Remove unused read_i8 helper from binary_utils - Change Vec<Instant> to [Instant; 50] in FeedState (eliminate heap alloc) - Cache rarely-changing strings by driver_id in Telemetry Tool parser
This commit is contained in:
parent
8f50302a78
commit
56d3169033
7 changed files with 98 additions and 45 deletions
|
|
@ -51,12 +51,6 @@ pub fn read_u16(data: &[u8], offset: usize) -> u16 {
|
||||||
u16::from_le_bytes(buf)
|
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.
|
/// 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).
|
/// Returns 0.0 if the offset is out of bounds (safe fallback for malformed packets).
|
||||||
pub fn read_f32(data: &[u8], offset: usize) -> f32 {
|
pub fn read_f32(data: &[u8], offset: usize) -> f32 {
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ pub struct FeedState {
|
||||||
pub last_packet_ago_secs: Option<f64>,
|
pub last_packet_ago_secs: Option<f64>,
|
||||||
pub start_time: Option<Instant>,
|
pub start_time: Option<Instant>,
|
||||||
pub last_packet_time: Option<Instant>,
|
pub last_packet_time: Option<Instant>,
|
||||||
pub packet_timestamps: Vec<Instant>,
|
pub packet_timestamps: [Instant; RING_CAPACITY],
|
||||||
pub ring_index: usize,
|
pub ring_index: usize,
|
||||||
pub ring_count: usize,
|
pub ring_count: usize,
|
||||||
pub last_frame: Option<TelemetryFrame>,
|
pub last_frame: Option<TelemetryFrame>,
|
||||||
|
|
@ -62,7 +62,7 @@ impl FeedState {
|
||||||
last_packet_ago_secs: None,
|
last_packet_ago_secs: None,
|
||||||
start_time: None,
|
start_time: None,
|
||||||
last_packet_time: None,
|
last_packet_time: None,
|
||||||
packet_timestamps: vec![Instant::now(); RING_CAPACITY],
|
packet_timestamps: [Instant::now(); RING_CAPACITY],
|
||||||
ring_index: 0,
|
ring_index: 0,
|
||||||
ring_count: 0,
|
ring_count: 0,
|
||||||
last_frame: None,
|
last_frame: None,
|
||||||
|
|
@ -85,9 +85,7 @@ impl FeedState {
|
||||||
self.last_packet_ago_secs = Some(0.0);
|
self.last_packet_ago_secs = Some(0.0);
|
||||||
|
|
||||||
if self.ring_count < RING_CAPACITY {
|
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_count += 1;
|
||||||
self.ring_index = self.ring_count % RING_CAPACITY;
|
self.ring_index = self.ring_count % RING_CAPACITY;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
52
src/main.rs
52
src/main.rs
|
|
@ -120,12 +120,32 @@ fn install_panic_hook() {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct CachedStrings {
|
||||||
|
driver_id: u32,
|
||||||
|
driver_name: Option<String>,
|
||||||
|
nationality: Option<String>,
|
||||||
|
car_name: Option<String>,
|
||||||
|
tyre_compound: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
async fn run_feed(
|
||||||
feed: FeedConfig,
|
feed: FeedConfig,
|
||||||
socket: tokio::net::UdpSocket,
|
socket: tokio::net::UdpSocket,
|
||||||
session_dir: String,
|
session_dir: String,
|
||||||
state: Arc<std::sync::Mutex<FeedState>>,
|
state: Arc<std::sync::Mutex<FeedState>>,
|
||||||
shutdown: Arc<tokio::sync::Notify>,
|
mut shutdown_rx: tokio::sync::watch::Receiver<bool>,
|
||||||
) {
|
) {
|
||||||
let handshake_target: Option<std::net::SocketAddr> = if feed.needs_handshake {
|
let handshake_target: Option<std::net::SocketAddr> = if feed.needs_handshake {
|
||||||
let target: std::net::SocketAddr = match format!("{}:{}", game_host(), feed.port).parse() {
|
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 file = BufWriter::new(file);
|
||||||
let mut buf = vec![0u8; BUFFER_SIZE];
|
let mut buf = vec![0u8; BUFFER_SIZE];
|
||||||
|
let mut string_cache = CachedStrings::new();
|
||||||
|
|
||||||
let mut hs_interval: Option<tokio::time::Interval> = handshake_target.map(|_| {
|
let mut hs_interval: Option<tokio::time::Interval> = handshake_target.map(|_| {
|
||||||
tokio::time::interval(Duration::from_secs(HANDSHAKE_INTERVAL_SECS))
|
tokio::time::interval(Duration::from_secs(HANDSHAKE_INTERVAL_SECS))
|
||||||
|
|
@ -295,8 +316,25 @@ async fn run_feed(
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Telemetry Tool: parse only the first driver
|
// Telemetry Tool: parse first driver, cache rarely-changing strings
|
||||||
parser_telemetry_tool::parse_first_driver(&buf[..len]).ok()
|
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.
|
// Update state in a single brief lock acquisition.
|
||||||
|
|
@ -344,7 +382,7 @@ async fn run_feed(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ = shutdown.notified() => {
|
_ = shutdown_rx.changed() => {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -391,7 +429,7 @@ async fn main() {
|
||||||
|
|
||||||
let mut feed_displays: Vec<(FeedDisplayInfo, Arc<std::sync::Mutex<FeedState>>)> = Vec::new();
|
let mut feed_displays: Vec<(FeedDisplayInfo, Arc<std::sync::Mutex<FeedState>>)> = Vec::new();
|
||||||
let mut feed_tasks: Vec<tokio::task::JoinHandle<()>> = Vec::new();
|
let mut feed_tasks: Vec<tokio::task::JoinHandle<()>> = Vec::new();
|
||||||
let shutdown = Arc::new(tokio::sync::Notify::new());
|
let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
|
||||||
|
|
||||||
for feed in FEEDS {
|
for feed in FEEDS {
|
||||||
let state = Arc::new(std::sync::Mutex::new(FeedState::new()));
|
let state = Arc::new(std::sync::Mutex::new(FeedState::new()));
|
||||||
|
|
@ -446,7 +484,7 @@ async fn main() {
|
||||||
if let Some(socket) = socket_opt {
|
if let Some(socket) = socket_opt {
|
||||||
let feed_clone = feed.clone();
|
let feed_clone = feed.clone();
|
||||||
let dir = session_dir.clone();
|
let dir = session_dir.clone();
|
||||||
let sh = shutdown.clone();
|
let sh = shutdown_rx.clone();
|
||||||
feed_tasks.push(tokio::spawn(async move {
|
feed_tasks.push(tokio::spawn(async move {
|
||||||
run_feed(feed_clone, socket, dir, state, sh).await;
|
run_feed(feed_clone, socket, dir, state, sh).await;
|
||||||
}));
|
}));
|
||||||
|
|
@ -507,7 +545,7 @@ async fn main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
event_running.store(false, Ordering::Relaxed);
|
event_running.store(false, Ordering::Relaxed);
|
||||||
shutdown.notify_waiters();
|
let _ = shutdown_tx.send(true);
|
||||||
|
|
||||||
match tokio::time::timeout(
|
match tokio::time::timeout(
|
||||||
Duration::from_secs(3),
|
Duration::from_secs(3),
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ pub fn parse_rt_car_info(data: &[u8]) -> Result<TelemetryFrame, KsudpParseError>
|
||||||
// Driver inputs
|
// Driver inputs
|
||||||
throttle: read_f32(data, 51),
|
throttle: read_f32(data, 51),
|
||||||
brake: read_f32(data, 55),
|
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),
|
engine_rpm: read_f32(data, 63),
|
||||||
steer: read_f32(data, 67),
|
steer: read_f32(data, 67),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -366,15 +366,6 @@ fn parse_v2_car_physics(data: &[u8]) -> Result<TelemetryFrame, PcarsParseError>
|
||||||
Ok(frame)
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,7 @@ pub fn parse_packet(data: &[u8]) -> Result<TelemetryToolPacket, TelemetryToolPar
|
||||||
///
|
///
|
||||||
/// Avoids allocating a `Vec` for all drivers when only the local player's
|
/// Avoids allocating a `Vec` for all drivers when only the local player's
|
||||||
/// telemetry is needed.
|
/// telemetry is needed.
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn parse_first_driver(data: &[u8]) -> Result<TelemetryFrame, TelemetryToolParseError> {
|
pub fn parse_first_driver(data: &[u8]) -> Result<TelemetryFrame, TelemetryToolParseError> {
|
||||||
if data.is_empty() {
|
if data.is_empty() {
|
||||||
return Err(TelemetryToolParseError::TooShort { actual: 0, needed: 1 });
|
return Err(TelemetryToolParseError::TooShort { actual: 0, needed: 1 });
|
||||||
|
|
@ -125,8 +126,56 @@ pub fn parse_first_driver(data: &[u8]) -> Result<TelemetryFrame, TelemetryToolPa
|
||||||
parse_driver(&payload[..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.
|
/// Parse a single 357-byte driver structure.
|
||||||
fn parse_driver(data: &[u8]) -> Result<TelemetryFrame, TelemetryToolParseError> {
|
fn parse_driver(data: &[u8]) -> Result<TelemetryFrame, TelemetryToolParseError> {
|
||||||
|
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<TelemetryFrame, TelemetryToolParseError> {
|
||||||
// Validation: check test_float at offset 93
|
// Validation: check test_float at offset 93
|
||||||
let test_float_val = read_f32(data, 93);
|
let test_float_val = read_f32(data, 93);
|
||||||
if (test_float_val - TEST_FLOAT).abs() > 1.0 {
|
if (test_float_val - TEST_FLOAT).abs() > 1.0 {
|
||||||
|
|
@ -195,11 +244,6 @@ fn parse_driver(data: &[u8]) -> Result<TelemetryFrame, TelemetryToolParseError>
|
||||||
tyre_slip: read_f32_4(data, 305),
|
tyre_slip: read_f32_4(data, 305),
|
||||||
|
|
||||||
// Telemetry Tool-specific fields
|
// 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)),
|
track_position: Some(read_i16(data, 67)),
|
||||||
leaderboard_position: Some(read_i16(data, 69)),
|
leaderboard_position: Some(read_i16(data, 69)),
|
||||||
is_connected: Some(read_bool(data, 181)),
|
is_connected: Some(read_bool(data, 181)),
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,6 @@
|
||||||
//! Both the KSUDP (port 9996) and Telemetry Tool (port 10101) parsers produce
|
//! Both the KSUDP (port 9996) and Telemetry Tool (port 10101) parsers produce
|
||||||
//! `TelemetryFrame` instances from their raw binary data.
|
//! `TelemetryFrame` instances from their raw binary data.
|
||||||
|
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
/// Identifies which feed source produced this frame.
|
/// Identifies which feed source produced this frame.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum FeedSource {
|
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"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue