diff --git a/Cargo.lock b/Cargo.lock index 5550d9d..8108f6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,6 +126,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -248,6 +254,26 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "irsdk" +version = "0.1.0" +dependencies = [ + "csv", + "ibt", + "itertools", + "thiserror", + "windows", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.17" @@ -538,6 +564,27 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -551,6 +598,17 @@ dependencies = [ "windows-strings", ] +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + [[package]] name = "windows-implement" version = "0.60.2" @@ -579,6 +637,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + [[package]] name = "windows-result" version = "0.4.1" @@ -597,6 +665,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + [[package]] name = "winnow" version = "0.7.14" diff --git a/Cargo.toml b/Cargo.toml index 8deb446..f795b2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "3" -members = ["crates/ibt"] +members = ["crates/ibt", "crates/irsdk"] [workspace.package] edition = "2024" @@ -8,20 +8,22 @@ rust-version = "1.85.0" repository = "https://github.com/igneousflock/irsdk-rs" license = "MIT" keywords = ["iracing", "ibt", "irsdk"] -categories = ["encodings"] +categories = ["encoding"] [workspace.dependencies] aligned-vec = "0.6" bit-iter = "1.3" bytemuck = "1.24" chrono = "0.4" +claims = "0.8" +csv = "1.4" indexmap = "2.12" +itertools = "0.14.0" num_enum = "0.7" saphyr = "0.0.6" serde = "1.0" thiserror = "2.0" -claims = "0.8" -csv = "1.4" +windows = "0.62" [workspace.lints.clippy] cloned_instead_of_copied = "warn" diff --git a/crates/ibt/src/lib.rs b/crates/ibt/src/lib.rs index 680711d..29f9ce9 100644 --- a/crates/ibt/src/lib.rs +++ b/crates/ibt/src/lib.rs @@ -4,7 +4,7 @@ mod aligned; mod file; -mod raw; +pub mod raw; pub mod telemetry; #[cfg(test)] diff --git a/crates/ibt/src/raw.rs b/crates/ibt/src/raw.rs index 1f85f92..4ee8689 100644 --- a/crates/ibt/src/raw.rs +++ b/crates/ibt/src/raw.rs @@ -103,6 +103,14 @@ impl Header { Ok(header) } + + pub unsafe fn from_raw_ptr(ptr: *const Self) -> Result { + let header = unsafe { *ptr }; + if header.ver != 2 { + return Err(RawTelemError::InvalidApiVersion(header.ver)); + } + Ok(header) + } } impl DiskSubHeader { diff --git a/crates/ibt/src/telemetry/headers.rs b/crates/ibt/src/telemetry/headers.rs index 9fdd4be..5b38ced 100644 --- a/crates/ibt/src/telemetry/headers.rs +++ b/crates/ibt/src/telemetry/headers.rs @@ -44,7 +44,7 @@ pub struct DiskSubHeader { /// Number of laps run in the session pub lap_count: u32, - // Number of records in this file + /// Number of records in this file pub record_count: usize, } diff --git a/crates/ibt/src/telemetry/sample.rs b/crates/ibt/src/telemetry/sample.rs index 1435427..1ff7c12 100644 --- a/crates/ibt/src/telemetry/sample.rs +++ b/crates/ibt/src/telemetry/sample.rs @@ -11,7 +11,7 @@ use crate::{ pub struct Sample<'data>(&'data [u8]); impl<'data> Sample<'data> { - pub(crate) fn new(data: &'data [u8]) -> Self { + pub fn new(data: &'data [u8]) -> Self { Self(data) } diff --git a/crates/ibt/src/telemetry/var.rs b/crates/ibt/src/telemetry/var.rs index 16b0305..51c1043 100644 --- a/crates/ibt/src/telemetry/var.rs +++ b/crates/ibt/src/telemetry/var.rs @@ -84,7 +84,7 @@ pub struct VarHeader { } impl VarHeader { - pub(crate) fn from_raw(raw: &raw::VarHeader) -> Self { + pub fn from_raw(raw: &raw::VarHeader) -> Self { let ty = raw .ty .try_into() diff --git a/crates/irsdk/Cargo.toml b/crates/irsdk/Cargo.toml new file mode 100644 index 0000000..b23bd49 --- /dev/null +++ b/crates/irsdk/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "irsdk" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +repository.workspace = true +license.workspace = true +keywords.workspace = true +categories.workspace = true + +[lints] +workspace = true + +[dependencies] +ibt = { version = "0.1.0", path = "../ibt" } + +itertools.workspace = true +thiserror.workspace = true +windows = { workspace = true, features = ["Win32_System_Memory", "Win32_System_Threading"] } + +[dev-dependencies] +csv = "1.4.0" diff --git a/crates/irsdk/examples/list_available_live_vars.rs b/crates/irsdk/examples/list_available_live_vars.rs new file mode 100644 index 0000000..44206f0 --- /dev/null +++ b/crates/irsdk/examples/list_available_live_vars.rs @@ -0,0 +1,16 @@ +use csv::Writer; +use irsdk::IRacingClient; +use std::error::Error; + +fn main() -> Result<(), Box> { + let client = IRacingClient::connect()?; + + let mut writer = Writer::from_writer(std::io::stdout()); + for var in client.vars().all_vars() { + writer + .serialize(var) + .expect("could not serialize var header as csv"); + } + + Ok(()) +} diff --git a/crates/irsdk/examples/stream_var.rs b/crates/irsdk/examples/stream_var.rs new file mode 100644 index 0000000..a7309d2 --- /dev/null +++ b/crates/irsdk/examples/stream_var.rs @@ -0,0 +1,19 @@ +use irsdk::IRacingClient; +use std::error::Error; +use std::time::Duration; + +const USAGE: &str = "Usage: stream_var "; + +fn main() -> Result<(), Box> { + let var_name = std::env::args().nth(1).expect(USAGE); + let client = IRacingClient::connect()?; + + let var = client.vars().var(&var_name).expect("unknown var"); + + while let Ok(sample) = client.next_sample() { + println!("{:?}", sample.read_var(var)); + std::thread::sleep(Duration::from_millis(50)); + } + + Ok(()) +} diff --git a/crates/irsdk/src/lib.rs b/crates/irsdk/src/lib.rs new file mode 100644 index 0000000..2882c4c --- /dev/null +++ b/crates/irsdk/src/lib.rs @@ -0,0 +1,123 @@ +use ibt::raw; +use ibt::telemetry::{Header, Sample, VarBufInfo, VarHeader, VarSet}; +use itertools::Itertools; +use windows::Win32::Foundation::{CloseHandle, HANDLE}; +use windows::Win32::System::Memory::{ + FILE_MAP_READ, MEMORY_MAPPED_VIEW_ADDRESS, MapViewOfFile, OpenFileMappingW, UnmapViewOfFile, +}; +use windows::Win32::System::Threading::{ + OpenEventW, SYNCHRONIZATION_SYNCHRONIZE, WaitForSingleObject, +}; +use windows::core::{PCWSTR, w}; + +const MEM_MAP_FILE_NAME: PCWSTR = w!(r"Local\IRSDKMemMapFileName"); +const DATA_VALID_EVENT_NAME: PCWSTR = w!(r"Local\IRSDKDataValidEvent"); +const TIMEOUT_MS: u32 = 1000; + +#[derive(Clone, Debug, thiserror::Error)] +pub enum IRacingClientError { + #[error(transparent)] + Windows(#[from] windows::core::Error), + + #[error(transparent)] + RawTelemError(#[from] ibt::RawTelemError), + + #[error(transparent)] + RawConversionError(#[from] ibt::telemetry::RawConversionError), +} + +#[derive(Debug)] +pub struct IRacingClient { + file_mapping_handle: HANDLE, + mem_map_address: MEMORY_MAPPED_VIEW_ADDRESS, + event_handle: HANDLE, + vars: VarSet, +} + +impl IRacingClient { + pub fn connect() -> Result { + let file_mapping_handle = + unsafe { OpenFileMappingW(FILE_MAP_READ.0, false, MEM_MAP_FILE_NAME)? }; + let mem_map_address = unsafe { MapViewOfFile(file_mapping_handle, FILE_MAP_READ, 0, 0, 0) }; + let event_handle = + unsafe { OpenEventW(SYNCHRONIZATION_SYNCHRONIZE, false, DATA_VALID_EVENT_NAME)? }; + + let mut client = Self { + file_mapping_handle, + mem_map_address, + event_handle, + vars: VarSet::new(vec![]), + }; + + let raw_header = client.next_raw_header()?; + + // Read the var headers once + let vh_offset = raw_header.var_header_offset as usize; + let vh_len = raw::VAR_HEADER_SIZE * raw_header.num_vars as usize; + let vh_slice = unsafe { client.slice(vh_offset, vh_len) }; + + let var_headers = raw::VarHeader::slice_from_fraw_bytes(vh_slice) + .iter() + .map(VarHeader::from_raw) + .collect(); + client.vars = VarSet::new(var_headers); + + Ok(client) + } + + unsafe fn slice(&self, offset: usize, len: usize) -> &[u8] { + unsafe { + let ptr = (self.mem_map_address.Value as *const u8).add(offset); + std::slice::from_raw_parts(ptr, len) + } + } + + fn next_raw_header(&self) -> Result { + // wait for event signal + unsafe { WaitForSingleObject(self.event_handle, TIMEOUT_MS) }; + + // read the header + let ptr = self.mem_map_address.Value as *const raw::Header; + let raw_header = unsafe { raw::Header::from_raw_ptr(ptr)? }; + + Ok(raw_header) + } + + pub fn next_header(&self) -> Result { + let raw_header = self.next_raw_header()?; + let header = Header::from_raw(&raw_header)?; + + Ok(header) + } + + pub fn next_sample(&self) -> Result, IRacingClientError> { + let raw_header = self.next_raw_header()?; + let header = Header::from_raw(&raw_header)?; + + let newest_var_buf = raw_header + .var_bufs + .iter() + .map(VarBufInfo::from_raw) + .process_results(|a| a.max_by_key(|vb| vb.tick_count))? + .expect("there are always four var bufs"); + + // TODO: figure out lifetimes here + let sample_slice = unsafe { self.slice(newest_var_buf.buf_offset, header.buf_len) }; + Ok(Sample::new(sample_slice)) + } + + pub fn vars(&self) -> &VarSet { + &self.vars + } +} + +impl Drop for IRacingClient { + fn drop(&mut self) { + unsafe { + // TODO: verify safety. if one of these fails, can we still close the others? + let _ = CloseHandle(self.event_handle); + let _ = UnmapViewOfFile(self.mem_map_address); + let _ = CloseHandle(self.file_mapping_handle); + } + } +}