diff --git a/Cargo.lock b/Cargo.lock index 4d47062..200b6a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -143,6 +143,12 @@ dependencies = [ "syn", ] +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "find-msvc-tools" version = "0.1.5" @@ -164,13 +170,19 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "hashlink" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown", + "hashbrown 0.15.5", ] [[package]] @@ -197,6 +209,16 @@ dependencies = [ "cc", ] +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", +] + [[package]] name = "irsdk" version = "0.1.0" @@ -205,6 +227,7 @@ dependencies = [ "bytemuck", "chrono", "csv", + "indexmap", "saphyr", "serde", ] diff --git a/Cargo.toml b/Cargo.toml index 660b684..c53df3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" aligned-vec = "0.6.4" bytemuck = { version = "1.24.0", features = ["derive"] } chrono = "0.4.42" +indexmap = "2.12.1" saphyr = "0.0.6" serde = { version = "1.0.228", features = ["derive"] } diff --git a/examples/dump_file.rs b/examples/dump_file.rs index 7eeb921..78fcd66 100644 --- a/examples/dump_file.rs +++ b/examples/dump_file.rs @@ -8,4 +8,5 @@ fn main() { println!("{:#?}", &file.header); println!("{:#?}", &file.disk_sub_header); + println!("{:#?}", &file.var_buf_info); } diff --git a/examples/dump_var.rs b/examples/dump_var.rs new file mode 100644 index 0000000..5424c78 --- /dev/null +++ b/examples/dump_var.rs @@ -0,0 +1,15 @@ +use irsdk::IbtFile; + +const USAGE: &str = "Usage: dump_var "; + +fn main() { + let file_name = std::env::args().nth(1).expect(USAGE); + let var_name = std::env::args().nth(2).expect(USAGE); + let file = IbtFile::from_file(&file_name).expect("could not open IBT file"); + + let var = file.vars.var(&var_name).expect("unknown var"); + + for sample in file.samples().take(500) { + println!("{:?}", sample.read_var(var)); + } +} diff --git a/examples/list_available_vars.rs b/examples/list_available_vars.rs index 77fbd90..8b32ae7 100644 --- a/examples/list_available_vars.rs +++ b/examples/list_available_vars.rs @@ -8,7 +8,7 @@ fn main() { let file = IbtFile::from_file(&file_name).expect("could not open IBT file"); let mut writer = Writer::from_writer(std::io::stdout()); - for var in &file.var_headers { + for var in file.vars.all_vars() { writer .serialize(var) .expect("could not serialize var header as csv"); diff --git a/src/aligned.rs b/src/aligned.rs new file mode 100644 index 0000000..2e9a600 --- /dev/null +++ b/src/aligned.rs @@ -0,0 +1,37 @@ +/// Wrapper for raw bytes that forces them to be aligned to `8`, the max alignment for SDK data +/// types +#[repr(align(8))] +pub struct Aligned([u8; SIZE]); + +impl Aligned { + /// Copy the data from the slice into a static array with alignment 8 + fn new(slice: &[u8]) -> Self { + Self(slice.try_into().expect("invalid slice length")) + } +} + +impl From> for u32 { + fn from(wrapper: Aligned<4>) -> Self { + *bytemuck::from_bytes(&wrapper.0) + } +} + +impl From> for f32 { + fn from(wrapper: Aligned<4>) -> Self { + *bytemuck::from_bytes(&wrapper.0) + } +} + +impl From> for f64 { + fn from(wrapper: Aligned<8>) -> Self { + *bytemuck::from_bytes(&wrapper.0) + } +} + +/// Copy the raw data into an aligned array and reinterpret it as the given type +pub fn align_cast(data: &[u8]) -> T +where + T: From> + bytemuck::Pod, +{ + Aligned::new(data).into() +} diff --git a/src/file.rs b/src/file.rs index 2f7737c..9868d50 100644 --- a/src/file.rs +++ b/src/file.rs @@ -5,7 +5,7 @@ use saphyr::LoadableYamlNode; use crate::{ raw, - telemetry::{DiskSubHeader, Header, VarHeader}, + telemetry::{DiskSubHeader, Header, Sample, VarBufInfo, VarHeader, VarSet}, }; #[derive(Clone, Debug)] @@ -15,7 +15,9 @@ pub struct IbtFile { pub header: Header, pub disk_sub_header: DiskSubHeader, - pub var_headers: Vec, + pub vars: VarSet, + + pub var_buf_info: VarBufInfo, } impl IbtFile { @@ -37,12 +39,16 @@ impl IbtFile { .iter() .map(VarHeader::from_raw) .collect(); + let vars = VarSet::new(var_headers); + + let var_buf_info = VarBufInfo::from_raw(&raw_header.var_bufs[0]); Ok(Self { data, header, disk_sub_header: sub_header, - var_headers, + vars, + var_buf_info, }) } @@ -57,4 +63,15 @@ impl IbtFile { let docs = saphyr::YamlOwned::load_from_str(&self.raw_session_data())?; Ok(docs[0].clone()) } + + pub fn sample(&self, idx: usize) -> Sample<'_> { + assert!(idx < self.disk_sub_header.record_count); + let len = self.header.buf_len; + let offset = self.var_buf_info.buf_offset + len * idx; + Sample::new(&self.data[offset..offset + len]) + } + + pub fn samples(&self) -> impl Iterator> { + (0..self.disk_sub_header.record_count).map(|idx| self.sample(idx)) + } } diff --git a/src/lib.rs b/src/lib.rs index e17a51c..39ea5cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +mod aligned; mod file; pub mod raw; pub mod telemetry; diff --git a/src/telemetry.rs b/src/telemetry/mod.rs similarity index 59% rename from src/telemetry.rs rename to src/telemetry/mod.rs index 55fcfbe..a95aa43 100644 --- a/src/telemetry.rs +++ b/src/telemetry/mod.rs @@ -1,13 +1,16 @@ -use std::{ - ffi::{CStr, c_char}, - time::Duration, -}; +mod record; +mod var; + +use std::time::Duration; use chrono::{DateTime, Utc}; use crate::raw; -#[derive(Clone, Copy, Debug)] +pub use record::{Record, Sample, Value}; +pub use var::{VarHeader, VarSet, VarType}; + +#[derive(Clone, Debug)] pub struct Header { pub tick_rate: u32, @@ -15,9 +18,12 @@ pub struct Header { pub session_info_update: u32, /// Length in bytes of the session info string - session_info_len: usize, + pub session_info_len: usize, /// Session info, encoded in YAML - session_info_offset: usize, + pub session_info_offset: usize, + + pub num_vars: usize, + pub buf_len: usize, } #[derive(Clone, Copy, Debug)] @@ -32,28 +38,15 @@ pub struct DiskSubHeader { /// Number of laps run in the session pub lap_count: u32, -} -#[derive(Clone, Copy, Debug, serde::Serialize)] -pub enum VarType { - Char, - Bool, - Int, - Bitfield, - Float, - Double, + // Number of records in this file + pub record_count: usize, } -#[derive(Clone, Debug, serde::Serialize)] -pub struct VarHeader { - pub ty: VarType, - pub offset: usize, - pub count: usize, - pub count_as_time: bool, - - pub name: String, - pub description: String, - pub unit: String, +#[derive(Clone, Copy, Debug)] +pub struct VarBufInfo { + pub tick_count: usize, + pub buf_offset: usize, } impl Header { @@ -75,6 +68,14 @@ impl Header { .session_info_offset .try_into() .expect("`session_info_offset` should be positive"), + num_vars: raw + .num_vars + .try_into() + .expect("`num_vars` should be positive"), + buf_len: raw + .buf_len + .try_into() + .expect("`buf_len` should be positive"), } } @@ -98,37 +99,25 @@ impl DiskSubHeader { .session_lap_count .try_into() .expect("`session_lap_count` should be positive"), + record_count: raw + .session_record_count + .try_into() + .expect("`session_record_count` should be positive"), } } } -impl VarHeader { - pub fn from_raw(raw: &raw::VarHeader) -> Self { - let ty = match raw.ty { - 0 => VarType::Char, - 1 => VarType::Bool, - 2 => VarType::Int, - 3 => VarType::Bitfield, - 4 => VarType::Float, - 5 => VarType::Double, - _ => panic!("invalid var type: `{}`", raw.ty), - }; - +impl VarBufInfo { + pub fn from_raw(raw: &raw::VarBuf) -> Self { Self { - ty, - offset: raw.offset.try_into().expect("`offset` to be positive"), - count: raw.count.try_into().expect("`count` to be positive"), - count_as_time: raw.count_as_time == 0, - name: string_from_c_chars(&raw.name), - description: string_from_c_chars(&raw.desc), - unit: string_from_c_chars(&raw.unit), + tick_count: raw + .tick_count + .try_into() + .expect("`tick_count` to be positive"), + buf_offset: raw + .buf_offset + .try_into() + .expect("`buf_offset` to be positive"), } } } - -// TODO: There may be some weird encoding on these strings -fn string_from_c_chars(buf: &[c_char]) -> String { - assert!(buf.contains(&0)); - let cstr = unsafe { CStr::from_ptr(buf.as_ptr()) }; - cstr.to_string_lossy().into_owned() -} diff --git a/src/telemetry/record.rs b/src/telemetry/record.rs new file mode 100644 index 0000000..973c364 --- /dev/null +++ b/src/telemetry/record.rs @@ -0,0 +1,47 @@ +use crate::{ + aligned::align_cast, + telemetry::{VarHeader, VarType}, +}; + +pub struct Sample<'data>(&'data [u8]); + +impl<'data> Sample<'data> { + pub fn new(data: &'data [u8]) -> Self { + Self(data) + } + + pub fn read_var(&self, var: &VarHeader) -> Record { + let size = var.ty.size() * var.count; + let slice = &self.0[var.offset..var.offset + size]; + + if var.count > 1 { + todo!("array values"); + } else { + let value = match var.ty { + VarType::Char => Value::Char(slice[0] as char), + VarType::Bool => Value::Bool(slice[0] != 0), + VarType::Int => Value::Int(align_cast(slice)), + VarType::Bitfield => Value::Bitfield(align_cast(slice)), + VarType::Float => Value::Float(align_cast(slice)), + VarType::Double => Value::Double(align_cast(slice)), + }; + Record::SingleValue(value) + } + } +} + +#[derive(Clone, Debug)] +pub enum Record { + SingleValue(Value), + Array(Vec), +} + +#[derive(Clone, Copy, Debug)] +pub enum Value { + Char(char), + Bool(bool), + Int(u32), + Bitfield(u32), + Float(f32), + Double(f64), +} diff --git a/src/telemetry/var.rs b/src/telemetry/var.rs new file mode 100644 index 0000000..af24613 --- /dev/null +++ b/src/telemetry/var.rs @@ -0,0 +1,87 @@ +use std::ffi::{CStr, c_char}; + +use indexmap::IndexMap; + +use crate::raw; + +#[derive(Clone, Debug)] +pub struct VarSet(IndexMap); + +impl VarSet { + pub fn new(mut vars: Vec) -> Self { + vars.sort_by_key(|v| v.offset); + let map = vars.into_iter().map(|v| (v.name.clone(), v)).collect(); + Self(map) + } + + pub fn var(&self, name: &str) -> Option<&VarHeader> { + self.0.get(name) + } + + pub fn all_vars(&self) -> impl Iterator { + self.0.values() + } +} + +#[derive(Clone, Copy, Debug, serde::Serialize)] +pub enum VarType { + Char, + Bool, + Int, + Bitfield, + Float, + Double, +} + +impl VarType { + pub fn size(&self) -> usize { + match self { + VarType::Char | VarType::Bool => 1, + VarType::Int | VarType::Bitfield | VarType::Float => 4, + VarType::Double => 8, + } + } +} + +#[derive(Clone, Debug, serde::Serialize)] +pub struct VarHeader { + pub ty: VarType, + pub offset: usize, + pub count: usize, + pub count_as_time: bool, + + pub name: String, + pub description: String, + pub unit: String, +} + +impl VarHeader { + pub fn from_raw(raw: &raw::VarHeader) -> Self { + let ty = match raw.ty { + 0 => VarType::Char, + 1 => VarType::Bool, + 2 => VarType::Int, + 3 => VarType::Bitfield, + 4 => VarType::Float, + 5 => VarType::Double, + _ => panic!("invalid var type: `{}`", raw.ty), + }; + + Self { + ty, + offset: raw.offset.try_into().expect("`offset` should be positive"), + count: raw.count.try_into().expect("`count` should be positive"), + count_as_time: raw.count_as_time == 0, + name: string_from_c_chars(&raw.name), + description: string_from_c_chars(&raw.desc), + unit: string_from_c_chars(&raw.unit), + } + } +} + +// TODO: There may be some weird encoding on these strings +fn string_from_c_chars(buf: &[c_char]) -> String { + assert!(buf.contains(&0)); + let cstr = unsafe { CStr::from_ptr(buf.as_ptr()) }; + cstr.to_string_lossy().into_owned() +}