From b48c41f18860feb7ca473a2fb135ba2cd1c98549 Mon Sep 17 00:00:00 2001 From: Alex van de Sandt Date: Tue, 30 Dec 2025 19:21:17 -0600 Subject: [PATCH] Document a bunch of stuff --- crates/ibt/src/file.rs | 38 +++++++++++++++++++++++---- crates/ibt/src/lib.rs | 8 +++++- crates/ibt/src/raw.rs | 6 ++++- crates/ibt/src/telemetry/bitfields.rs | 8 ++++++ crates/ibt/src/telemetry/enums.rs | 5 ++++ crates/ibt/src/telemetry/headers.rs | 9 +++++++ crates/ibt/src/telemetry/mod.rs | 11 +++----- crates/ibt/src/telemetry/sample.rs | 9 +++++-- crates/ibt/src/telemetry/var.rs | 34 +++++++++++++++++++++--- 9 files changed, 108 insertions(+), 20 deletions(-) diff --git a/crates/ibt/src/file.rs b/crates/ibt/src/file.rs index 05eb75d..92508ab 100644 --- a/crates/ibt/src/file.rs +++ b/crates/ibt/src/file.rs @@ -11,28 +11,52 @@ use crate::{ #[derive(Debug, thiserror::Error)] pub enum IbtFileError { #[error(transparent)] - CastError(#[from] RawConversionError), + Io(#[from] std::io::Error), + /// An error occured converting values from raw telemetry #[error(transparent)] - Io(#[from] std::io::Error), + RawConversionError(#[from] RawConversionError), + /// An error occured decoding telemetry data #[error(transparent)] RawTelem(#[from] raw::RawTelemError), } +/// The contents of a `.ibt` file +/// +/// These files are broken up into a header, disk sub-header, +/// variable headers, variable data, and a session string. +/// +/// # Example +/// ```ignore +/// # use ibt::IbtFile; +/// +/// let ibt_file = IbtFile::from_file("example-telemetry-file.ibt").unwrap(); +/// let header = ibt_file.vars.var("RPM").unwrap(); +/// let rpm = ibt_file.sample(0).read_var(header); +/// ``` #[derive(Clone, Debug)] pub struct IbtFile { + /// Currently the entirety of the file. Must be aligned to 16-bytes to safely read multi-byte + /// data. data: AVec>, pub header: Header, pub disk_sub_header: DiskSubHeader, + /// Lists what variables are available pub vars: VarSet, + /// IBT files only have on variable buffer containing all samples pub var_buf_info: VarBufInfo, } impl IbtFile { + /// Open an IBT file at the given path + /// + /// # Errors + /// + /// Returns an error if the data is invalid or an IO error occurs. pub fn from_file>(path: P) -> Result { let data = AVec::from_slice(raw::ALIGNMENT, &std::fs::read(&path)?); @@ -64,6 +88,7 @@ impl IbtFile { }) } + /// Decode the session string as a plain String pub fn raw_session_data(&self) -> String { let offset = self.header.session_info_offset; let len = self.header.session_info_len; @@ -71,18 +96,21 @@ impl IbtFile { String::from_utf8_lossy(session_string).into_owned() } + /// Parse the session string as YAML pub fn session_data(&self) -> Result { let docs = saphyr::YamlOwned::load_from_str(&self.raw_session_data())?; Ok(docs[0].clone()) } + /// Retrive the nth sample 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]) + let sample_len = self.header.buf_len; + let offset = self.var_buf_info.buf_offset + sample_len * idx; + Sample::new(&self.data[offset..offset + sample_len]) } + /// Iterate over all telemetry samples in the file pub fn samples(&self) -> impl Iterator> { (0..self.disk_sub_header.record_count).map(|idx| self.sample(idx)) } diff --git a/crates/ibt/src/lib.rs b/crates/ibt/src/lib.rs index 4543379..680711d 100644 --- a/crates/ibt/src/lib.rs +++ b/crates/ibt/src/lib.rs @@ -1,3 +1,7 @@ +//! Utilities for decoding [iRacing][ir] telemetry data from `.ibt` files or the telemetry API +//! +//! [ir]: https://iracing.com + mod aligned; mod file; mod raw; @@ -6,4 +10,6 @@ pub mod telemetry; #[cfg(test)] mod test_utils; -pub use file::IbtFile; +pub use file::{IbtFile, IbtFileError}; +pub use raw::RawTelemError; +pub use saphyr; diff --git a/crates/ibt/src/raw.rs b/crates/ibt/src/raw.rs index a13780f..1f85f92 100644 --- a/crates/ibt/src/raw.rs +++ b/crates/ibt/src/raw.rs @@ -18,8 +18,12 @@ pub const VAR_HEADER_SIZE: usize = std::mem::size_of::(); #[derive(Clone, Copy, Debug, thiserror::Error)] pub enum RawTelemError { + /// API version (first four bytes) should always be `2` #[error("API version (first four bytes) should always be `2`, got `{0}`")] - InvalidApiVersion(c_int), + InvalidApiVersion( + /// the detected API version + c_int, + ), } #[derive(Clone, Copy, Debug, PartialEq, Eq, AnyBitPattern)] diff --git a/crates/ibt/src/telemetry/bitfields.rs b/crates/ibt/src/telemetry/bitfields.rs index 94d5694..6aa4aad 100644 --- a/crates/ibt/src/telemetry/bitfields.rs +++ b/crates/ibt/src/telemetry/bitfields.rs @@ -1,3 +1,9 @@ +//! Bitfield types + +/// Multiple related telemetry flags compressed into one value +/// +/// Internally, these values are 32 bit integers, where each binary bit may represent the state of +/// a certain flag. See each variant's internal type for the possible values. #[derive(Clone, Copy, Debug)] pub enum Bitfield { EngineWarnings(EngineWarnings), @@ -5,6 +11,8 @@ pub enum Bitfield { CameraState(CameraState), PitServiceFlags(PitServiceFlags), PaceFlags(PaceFlags), + /// The variable's type was `Bitfield` but this crate didn't know how to decode it. Please file + /// a bug report. Unknown(u32), } diff --git a/crates/ibt/src/telemetry/enums.rs b/crates/ibt/src/telemetry/enums.rs index 8902d0c..a8fb6dd 100644 --- a/crates/ibt/src/telemetry/enums.rs +++ b/crates/ibt/src/telemetry/enums.rs @@ -1,7 +1,12 @@ +//! Enum types + use num_enum::FromPrimitive; use crate::aligned::align_cast; +/// A value representing one of several states +/// +/// These are 32 bit integers under the hood, but translated to their actual meaning. #[derive(Clone, Copy, Debug)] pub enum Enum { TrackLocation(TrackLocation), diff --git a/crates/ibt/src/telemetry/headers.rs b/crates/ibt/src/telemetry/headers.rs index e46f1fc..9fdd4be 100644 --- a/crates/ibt/src/telemetry/headers.rs +++ b/crates/ibt/src/telemetry/headers.rs @@ -4,12 +4,15 @@ use chrono::{DateTime, Utc}; use crate::raw; +/// A value read from telemetry could not be cast or converted to its expected type #[derive(Clone, Copy, Debug, thiserror::Error)] #[error("field at struct offset `{offset}` could not be converted from the raw type")] pub struct RawConversionError { offset: usize, } +/// General session info as well as byte offsets for variable buffers and the session +/// string. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Header { pub tick_rate: u32, @@ -22,10 +25,12 @@ pub struct Header { /// Session info, encoded in YAML pub session_info_offset: usize, + /// Number of variables available in each sample pub num_vars: usize, pub buf_len: usize, } +/// Session information specific to IBT files #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct DiskSubHeader { /// Timestamp for the start of the session @@ -43,9 +48,13 @@ pub struct DiskSubHeader { pub record_count: usize, } +/// The status of a buffer of values #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct VarBufInfo { + /// In live telemetry, the tick this buffer's values represent. In a file, the number of ticks + /// present in the data. pub tick_count: usize, + /// Offset of the start of this buffer from the start of the file/memmap pub buf_offset: usize, } diff --git a/crates/ibt/src/telemetry/mod.rs b/crates/ibt/src/telemetry/mod.rs index ded2713..0e55838 100644 --- a/crates/ibt/src/telemetry/mod.rs +++ b/crates/ibt/src/telemetry/mod.rs @@ -1,14 +1,11 @@ -mod bitfields; -mod enums; +//! Structured telemetry data + +pub mod bitfields; +pub mod enums; mod headers; mod sample; mod var; -pub use bitfields::{Bitfield, EngineWarnings}; -pub use enums::{ - CarLeftRight, Enum, PaceMode, PitServiceStatus, SessionState, TrackLocation, TrackSurface, - TrackWetness, -}; pub use headers::{DiskSubHeader, Header, RawConversionError, VarBufInfo}; pub use sample::{Sample, Value}; pub use var::{VarHeader, VarSet, VarType}; diff --git a/crates/ibt/src/telemetry/sample.rs b/crates/ibt/src/telemetry/sample.rs index 2669c78..5cde781 100644 --- a/crates/ibt/src/telemetry/sample.rs +++ b/crates/ibt/src/telemetry/sample.rs @@ -2,16 +2,20 @@ use bytemuck::pod_collect_to_vec; use crate::{ aligned::align_cast, - telemetry::{Bitfield, Enum, VarHeader, VarType}, + telemetry::{VarHeader, VarType, bitfields::Bitfield, enums::Enum}, }; +/// A set of telemetry values at a specific point in time +/// +/// Obtained from an [`IbtFile`][crate::IbtFile] or live telemetry. pub struct Sample<'data>(&'data [u8]); impl<'data> Sample<'data> { - pub fn new(data: &'data [u8]) -> Self { + pub(crate) fn new(data: &'data [u8]) -> Self { Self(data) } + /// Extract a value from the sample pub fn read_var(&self, var: &VarHeader) -> Value { let size = var.ty.size() * var.count; let slice = &self.0[var.offset..var.offset + size]; @@ -42,6 +46,7 @@ impl<'data> Sample<'data> { } } +/// The value of a variable in a [`Sample`] #[derive(Clone, Debug)] pub enum Value { Char(char), diff --git a/crates/ibt/src/telemetry/var.rs b/crates/ibt/src/telemetry/var.rs index a2a5271..16b0305 100644 --- a/crates/ibt/src/telemetry/var.rs +++ b/crates/ibt/src/telemetry/var.rs @@ -5,20 +5,24 @@ use num_enum::TryFromPrimitive; use crate::raw; +/// Map of variable names to their headers #[derive(Clone, Debug)] pub struct VarSet(IndexMap); impl VarSet { pub fn new(mut vars: Vec) -> Self { + // use an `IndexMap` for in-order iteration over all values in a sample vars.sort_by_key(|v| v.offset); let map = vars.into_iter().map(|v| (v.name.clone(), v)).collect(); Self(map) } + /// Get a var's header by name pub fn var(&self, name: &str) -> Option<&VarHeader> { self.0.get(name) } + /// Get an iterator over all vars in the set pub fn all_vars(&self) -> impl Iterator { self.0.values() } @@ -27,15 +31,22 @@ impl VarSet { #[derive(Clone, Copy, Debug, PartialEq, Eq, TryFromPrimitive, serde::Serialize)] #[repr(i32)] pub enum VarType { + /// 1-byte character Char, + /// 1-byte boolean Bool, + /// 4-byte signed integer, see also [`Enum`][crate::telemetry::enums::Enum] Int, + /// Bitfield, see [`Bitfield`][crate::telemetry::bitfields::Bitfield] Bitfield, + /// 4-byte floating point Float, + /// 8-byte floating point Double, } impl VarType { + /// Size in bytes for any single value of this type pub fn size(&self) -> usize { match self { VarType::Char | VarType::Bool => 1, @@ -45,20 +56,35 @@ impl VarType { } } +/// Describes one of the variables available in a telemetry sample +/// +/// Obtained from a `VarSet` constructed from a telemetry file or live telemetry. #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] pub struct VarHeader { + /// The type of the variable pub ty: VarType, - pub offset: usize, - pub count: usize, - pub count_as_time: bool, + /// Offset of the variable in a sample + pub(crate) offset: usize, + /// Number of values of this variable in each sample + /// + /// Always 1 for non-array types + pub(crate) count: usize, + count_as_time: bool, + + /// Name of the variable pub name: String, pub description: String, + /// The unit with which the variables value(s) should be interpreted + /// + /// This may be a unit of measurement (e.g., "m/s") or describe one of the [`Bitfield`][crate::telemetry::bitfields::Bitfield] or + /// [`Enum`][crate::telemetry::enums::Enum] types. A [`Sample`][crate::telemetry::Sample] will + /// decode known types correctly. pub unit: String, } impl VarHeader { - pub fn from_raw(raw: &raw::VarHeader) -> Self { + pub(crate) fn from_raw(raw: &raw::VarHeader) -> Self { let ty = raw .ty .try_into() -- 2.51.2