Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: CI

on:
push:
branches: [main]
pull_request:

# Single sequential job: `cargo test` generates the ts-rs bindings into
# shared/ (gitignored), which the dashboard build consumes — same order
# as build-release.sh.
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

# rust-embed's #[folder = "../dashboard/dist"] must exist at compile
# time; tests and clippy don't need the real assets.
- run: mkdir -p dashboard/dist && touch dashboard/dist/index.html

- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
with:
workspaces: bridge

- name: Bridge tests (also exports shared/ ts-rs bindings)
run: cargo test
working-directory: bridge
- name: Bridge lints
run: cargo clippy --all-targets -- -D warnings
working-directory: bridge

- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: dashboard/package-lock.json
- name: Dashboard install
run: npm ci
working-directory: dashboard
- name: Dashboard lint
run: npm run lint
working-directory: dashboard
- name: Dashboard build (tsc + vite)
run: npm run build
working-directory: dashboard
2 changes: 1 addition & 1 deletion bridge/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion bridge/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "bridge"
version = "0.2.5"
version = "0.2.7"
edition = "2021"
rust-version = "1.80"

Expand Down
4 changes: 0 additions & 4 deletions bridge/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
#[derive(Debug, Clone)]
pub struct Config {
pub ws_port: u16,
pub telemetry_hz: u32,
pub standings_hz: u32,
/// Disables auto-shutdown (BRIDGE_KEEP_ALIVE=1).
pub keep_alive: bool,
/// Seconds without a client after the first connect before shutdown (BRIDGE_SHUTDOWN_GRACE_SEC).
Expand All @@ -20,8 +18,6 @@ impl Config {
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(8765),
telemetry_hz: 60,
standings_hz: 4,
keep_alive: std::env::var("BRIDGE_KEEP_ALIVE")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false),
Expand Down
16 changes: 0 additions & 16 deletions bridge/src/iracing_sdk/broadcast.rs

This file was deleted.

19 changes: 0 additions & 19 deletions bridge/src/iracing_sdk/events.rs

This file was deleted.

9 changes: 1 addition & 8 deletions bridge/src/iracing_sdk/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,15 @@
//! Architecture:
//! - `header`: irsdk_header layout and parser
//! - `var_header`: variable-descriptor array, indexing
//! - `reader`: open the MMF, triple-buffer read loop
//! - `events`: WaitForSingleObject on Local\IRSDKDataValidEvent
//! - `reader`: open the MMF, event wait, triple-buffer read loop
//! - `yaml`: ISO-8859-1 decode + serde_yaml parse
//! - `types`: SessionInfo, DriverInfo, ResultsPositions (ts-rs derive)
//! - `broadcast`: PostMessageW wrapper for Pit/Camera/Replay (later)

pub mod broadcast;
pub mod events;
pub mod header;
pub mod reader;
pub mod synthetic_id;
pub mod types;
pub mod var_header;
pub mod yaml;

// Re-export for the public API. `allow(unused_imports)` because
// main.rs only uses the client once the implementation is in place.
#[allow(unused_imports)]
pub use reader::IRacingClient;
110 changes: 43 additions & 67 deletions bridge/src/iracing_sdk/reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,10 +185,6 @@ impl IRacingClient {
self.header.session_info_update
}

pub fn var_index(&self) -> &VarIndex {
&self.var_index
}

/// No-op: var_index is already built during `connect()`. Kept for API symmetry.
pub fn parse_var_index(&mut self) -> Result<()> {
Ok(())
Expand Down Expand Up @@ -277,55 +273,60 @@ impl IRacingClient {
}

// ── Typed getters ────────────────────────────────────────────────────────
//
// All getters decode via `from_le_bytes` on the raw frame bytes. The frame
// is a plain `Vec<u8>`, so no alignment can be assumed — a `&[u8] → &[T]`
// cast would be UB whenever the allocation or the SDK offset isn't aligned
// for `T`. Scalars decode allocation-free; arrays collect into a `Vec`
// (≤ 64 elements, negligible at our tick rates).

pub fn get_f32(&self, name: &str) -> Result<f32> {
self.read::<f32>(name, VarType::Float).map(|s| s[0])
self.var_bytes(name, VarType::Float)
.map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
}

pub fn get_f64(&self, name: &str) -> Result<f64> {
self.read::<f64>(name, VarType::Double).map(|s| s[0])
self.var_bytes(name, VarType::Double).map(|b| {
f64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]])
})
}

pub fn get_i32(&self, name: &str) -> Result<i32> {
self.read::<i32>(name, VarType::Int).map(|s| s[0])
self.var_bytes(name, VarType::Int)
.map(|b| i32::from_le_bytes([b[0], b[1], b[2], b[3]]))
}

pub fn get_bool(&self, name: &str) -> Result<bool> {
let desc = self
.var_index
.get(name)
.ok_or_else(|| BridgeError::SdkRead(format!("unknown var '{name}'")))?;
if desc.var_type != VarType::Bool {
return Err(BridgeError::SdkRead(format!(
"var '{name}': expected Bool, got {:?}",
desc.var_type
)));
}
let end = desc.offset + 1;
if end > self.frame.len() {
return Err(BridgeError::SdkRead(format!(
"var '{name}': out of bounds (offset={} frame={})",
desc.offset,
self.frame.len()
)));
}
Ok(self.frame[desc.offset] != 0)
self.var_bytes(name, VarType::Bool).map(|b| b[0] != 0)
}

pub fn get_bitfield(&self, name: &str) -> Result<u32> {
self.read::<u32>(name, VarType::BitField).map(|s| s[0])
self.var_bytes(name, VarType::BitField)
.map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
}

pub fn get_f32_array(&self, name: &str) -> Result<&[f32]> {
self.read::<f32>(name, VarType::Float)
pub fn get_f32_array(&self, name: &str) -> Result<Vec<f32>> {
self.var_bytes(name, VarType::Float).map(|b| {
b.chunks_exact(4)
.map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect()
})
}

pub fn get_i32_array(&self, name: &str) -> Result<&[i32]> {
self.read::<i32>(name, VarType::Int)
pub fn get_i32_array(&self, name: &str) -> Result<Vec<i32>> {
self.var_bytes(name, VarType::Int).map(|b| {
b.chunks_exact(4)
.map(|c| i32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect()
})
}

pub fn get_bitfield_array(&self, name: &str) -> Result<&[u32]> {
self.read::<u32>(name, VarType::BitField)
pub fn get_bitfield_array(&self, name: &str) -> Result<Vec<u32>> {
self.var_bytes(name, VarType::BitField).map(|b| {
b.chunks_exact(4)
.map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect()
})
}

/// Dumps every variable known to the SDK with its current value(s), formatted
Expand Down Expand Up @@ -398,36 +399,16 @@ impl IRacingClient {
}

pub fn get_bool_array(&self, name: &str) -> Result<Vec<bool>> {
let desc = self
.var_index
.get(name)
.ok_or_else(|| BridgeError::SdkRead(format!("unknown var '{name}'")))?;
if desc.var_type != VarType::Bool {
return Err(BridgeError::SdkRead(format!(
"var '{name}': expected Bool, got {:?}",
desc.var_type
)));
}
let end = desc.offset + desc.count;
if end > self.frame.len() {
return Err(BridgeError::SdkRead(format!(
"var '{name}': out of bounds (offset={} count={} frame={})",
desc.offset,
desc.count,
self.frame.len()
)));
}
Ok(self.frame[desc.offset..end]
.iter()
.map(|&b| b != 0)
.collect())
self.var_bytes(name, VarType::Bool)
.map(|b| b.iter().map(|&v| v != 0).collect())
}

// ── Private helper ───────────────────────────────────────────────────────

/// Returns a `&[T]` view into `self.frame` for the named variable.
/// Verifies the stored `VarType` matches `expected`.
fn read<T: Copy>(&self, name: &str, expected: VarType) -> Result<&[T]> {
/// Returns the raw frame bytes for the named variable (all array elements).
/// Verifies the stored `VarType` matches `expected` and that the variable
/// (including a scalar's single element) lies within the frame buffer.
fn var_bytes(&self, name: &str, expected: VarType) -> Result<&[u8]> {
let desc = self
.var_index
.get(name)
Expand All @@ -438,9 +419,10 @@ impl IRacingClient {
desc.var_type
)));
}
let elem_size = std::mem::size_of::<T>();
let elem_size = expected.size_bytes();
let byte_len = desc
.count
.max(1)
.checked_mul(elem_size)
.ok_or_else(|| BridgeError::SdkRead(format!("var '{name}': byte length overflow")))?;
let end = desc
Expand All @@ -455,13 +437,7 @@ impl IRacingClient {
self.frame.len()
)));
}
let bytes = &self.frame[desc.offset..end];
// SAFETY: The iRacing SDK lays out all variables at naturally aligned offsets
// (multiples of the type's size). Vec<u8> on Windows x86_64 is allocated with
// ≥16-byte alignment by the global allocator, so bytes.as_ptr() is properly
// aligned for any T with align ≤ 8 (all iRacing types). The slice covers
// exactly `count` elements of size `elem_size`.
Ok(unsafe { std::slice::from_raw_parts(bytes.as_ptr() as *const T, desc.count) })
Ok(&self.frame[desc.offset..end])
}
}

Expand Down
9 changes: 5 additions & 4 deletions bridge/src/iracing_sdk/var_header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,11 @@ pub fn parse_var_index(raw: &[u8], num_vars: usize) -> crate::error::Result<VarI
))
})?;

// Extract strings (trimmed at null bytes)
let name = cstr_from_bytes(&record[16..48])?;
let desc = cstr_from_bytes(&record[48..112])?;
let unit = cstr_from_bytes(&record[112..144])?;
// Extract strings (trimmed at null bytes).
// Layout after the 16-byte numeric block: name[32] desc[64] unit[32].
let name = cstr_from_bytes(&record[16..16 + IRSDK_MAX_STRING])?;
let desc = cstr_from_bytes(&record[48..48 + IRSDK_MAX_DESC])?;
let unit = cstr_from_bytes(&record[112..112 + IRSDK_MAX_STRING])?;

// Skip padding records (empty name)
if name.is_empty() {
Expand Down
2 changes: 1 addition & 1 deletion bridge/src/iracing_sdk/yaml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ fn keep_sections(yaml: &str, keep: &[&str]) -> String {
let is_top_level = matches!(first, Some(c) if !c.is_whitespace() && c != '#' && c != '-' && c != '.');
if is_top_level {
let section_name = line.split(':').next().unwrap_or("").trim();
in_section = keep.iter().any(|k| *k == section_name);
in_section = keep.contains(&section_name);
}
if in_section {
out.push_str(line);
Expand Down
9 changes: 7 additions & 2 deletions bridge/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
//! Tokio runtime, HTTP+WebSocket server, iRacing SDK reader.

#![cfg_attr(all(target_os = "windows", not(debug_assertions)), windows_subsystem = "windows")]
#![allow(dead_code)]
// On non-Windows hosts the whole SDK consumer chain hangs off the
// cfg(windows) sdk_loop, so dead-code analysis would flag it all.
// Windows builds (the shipped artifact) still get real dead-code checks.
#![cfg_attr(not(windows), allow(dead_code))]

mod config;
mod error;
Expand Down Expand Up @@ -90,7 +93,9 @@ async fn main() -> Result<()> {
log::info!("update available: v{}", i.latest_version);
let _ = upd_tx.send(Some(i));
}
std::future::pending::<()>().await;
// Task ends here. The WS handler tolerates the closed channel, and
// late-connecting clients still see the last value via the initial
// `borrow_and_update()` replay.
});

let lan_url = detect_lan_ip().map(|ip| format!("http://{}:{}/", ip, cfg.ws_port));
Expand Down
3 changes: 0 additions & 3 deletions bridge/src/race_engineer/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ pub const PIPER_VERSION: &str = "2023.11.14-2";
pub const PIPER_DOWNLOAD_URL: &str =
"https://github.com/rhasspy/piper/releases/download/2023.11.14-2/piper_windows_amd64.zip";

pub const RADIO_CHECK_PHRASE: &str =
"Radio check, radio check. Box this lap, box this lap. Push now, you have good pace.";

/// A voice available for download and use with Piper.
#[derive(Debug, Clone)]
pub struct VoiceDefinition {
Expand Down
Loading
Loading