From 275ad31bad13d5ddf0311fc4b4094907e40ca089 Mon Sep 17 00:00:00 2001 From: Scott Velez <162245270+SVappsLAB@users.noreply.github.com> Date: Mon, 21 Jul 2025 07:01:30 -0700 Subject: [PATCH] v1.0 release --- .gitignore | 5 +- AI_CONTEXT.md | 858 --------- MIGRATION_GUIDE.md | 289 +++ NUGET.md | 180 +- README.md | 415 +++- .../DumpVariables_DumpSessionInfo.csproj | 2 +- .../DumpVariables_DumpSessionInfo/Program.cs | 59 +- .../LocationAndWarnings.csproj | 2 +- Samples/LocationAndWarnings/Program.cs | 53 +- Samples/MinimalExample/MinimalExample.csproj | 15 + Samples/MinimalExample/Program.cs | 58 + .../Properties/launchSettings.json | 8 + Samples/SpeedRPMGear/Program.cs | 77 +- Samples/SpeedRPMGear/SpeedRPMGear.csproj | 6 +- .../CodeGen.cs | 79 +- .../iRacingData.cs | 804 ++++---- .../TelemetryClient_Enums.cs | 2 +- .../TelemetryClient_Flags.cs | 2 +- .../TelemetryVar.cs | 427 +++++ Sdk/SVappsLAB.iRacingTelemetrySDK.sln | 29 +- .../Constants.cs} | 10 +- .../DataProviders/DataProviderBase.cs | 85 +- .../DataProviders/IBTDataProvider.cs | 29 +- .../DataProviders/IDataProvider.cs | 14 +- .../DataProviders/LiveDataProvider.cs | 44 +- .../IBTPlayback/Governor.cs | 8 +- .../ITelemetryClient.cs | 204 +- .../Metrics/MetricsService.cs | 131 ++ .../Metrics/ScopeLambda.cs} | 19 +- .../Metrics/ScopeTimeSpanTimer.cs | 36 + .../Models/CameraInfo.cs | 4 +- .../Models/DriverInfo.cs | 6 +- .../Models/QualifyResultsInfo.cs | 4 +- .../Models/RadioInfo.cs | 4 +- .../Models/SessionInfo.cs | 4 +- .../Models/SplitTimeInfo.cs | 4 +- .../Models/TelemetrySessionInfo.cs | 6 +- .../Models/WeekendInfo.cs | 6 +- Sdk/SVappsLAB.iRacingTelemetrySDK/PInvoke.cs | 2 +- .../SVappsLAB.iRacingTelemetrySDK.csproj | 21 +- .../SubscriptionExtensions.cs | 191 ++ .../TelemetryClient.cs | 756 ++++++-- .../TelemetryDataAccessor.cs | 192 ++ .../YamlParser.cs | 8 +- .../build/SVappsLAB.iRacingTelemetrySDK.props | 20 + .../contents/docs/AI_USAGE.md | 1664 +++++++++++++++++ .../irSDK_defines.cs | 5 +- Sdk/tests/Directory.Build.props | 6 + Sdk/tests/IBT_Tests/Files/ReadFile.cs | 60 - Sdk/tests/IBT_Tests/Fixture.cs | 72 - Sdk/tests/IBT_Tests/IBT_Tests.csproj | 46 - .../IBT_Tests/SessionInfo/SessionInfoTests.cs | 161 -- .../IBT_Tests/Variables/TelemetryVariables.cs | 57 - .../data/race_oval/latemodel_southboston.ibt | 3 - .../data/race_oval/latemodel_southboston.yaml | 546 ------ .../race_test/lamborghinievogt3_spa up.ibt | 3 - .../race_test/lamborghinievogt3_spa up.yaml | 860 --------- Sdk/tests/Live_Tests/Fixture.cs | 69 - Sdk/tests/Live_Tests/Live_Tests.csproj | 33 - .../Live_Tests/SessionInfo/ModelYamlMatch.cs | 147 -- .../SessionInfo/SessionInfoTests.cs | 83 - Sdk/tests/Live_Tests/Telemetry.cs | 142 -- Sdk/tests/SmokeTests/Base/Base.Models.cs | 145 ++ Sdk/tests/SmokeTests/Base/Base.Variables.cs | 71 + Sdk/tests/SmokeTests/Base/Base.cs | 133 ++ Sdk/tests/SmokeTests/Base/Summaries.cs | 73 + Sdk/tests/SmokeTests/Base/TestLogger.cs | 48 + Sdk/tests/SmokeTests/IBT/IBT.cs | 103 + Sdk/tests/SmokeTests/Live/Live.cs | 60 + .../SmokeTests/Properties/launchSettings.json | 8 + Sdk/tests/SmokeTests/SmokeTests.csproj | 67 + Sdk/tests/SmokeTests/VarsToTest.cs | 35 + .../data/ibt}/audir8lmsevo2gt3_spa up.ibt | 0 ...inistock_mtwashington climb 2025-03-28.ibt | 3 + .../ibt/mx5 cup_okayama full 2011-05-13.ibt | 3 + .../raygr22_roadatlanta full 2025-07-15.ibt | 3 + .../data/yaml}/audir8lmsevo2gt3_spa up.yaml | 0 .../data/yaml/invalid_data.yaml | 0 Sdk/tests/UnitTests/IBTPlaybackGovernor.cs | 4 +- Sdk/tests/UnitTests/UnitTests.csproj | 79 +- Sdk/tests/UnitTests/YamlParsing.cs | 6 +- 81 files changed, 5806 insertions(+), 4170 deletions(-) delete mode 100644 AI_CONTEXT.md create mode 100644 MIGRATION_GUIDE.md create mode 100644 Samples/MinimalExample/MinimalExample.csproj create mode 100644 Samples/MinimalExample/Program.cs create mode 100644 Samples/MinimalExample/Properties/launchSettings.json create mode 100644 Sdk/SVappsLAB.iRacingTelemetrySDK.EnumsAndFlags/TelemetryVar.cs rename Sdk/{tests/Live_Tests/GlobalUsings.cs => SVappsLAB.iRacingTelemetrySDK/Constants.cs} (74%) create mode 100644 Sdk/SVappsLAB.iRacingTelemetrySDK/Metrics/MetricsService.cs rename Sdk/{tests/IBT_Tests/GlobalUsings.cs => SVappsLAB.iRacingTelemetrySDK/Metrics/ScopeLambda.cs} (61%) create mode 100644 Sdk/SVappsLAB.iRacingTelemetrySDK/Metrics/ScopeTimeSpanTimer.cs create mode 100644 Sdk/SVappsLAB.iRacingTelemetrySDK/SubscriptionExtensions.cs create mode 100644 Sdk/SVappsLAB.iRacingTelemetrySDK/TelemetryDataAccessor.cs create mode 100644 Sdk/SVappsLAB.iRacingTelemetrySDK/build/SVappsLAB.iRacingTelemetrySDK.props create mode 100644 Sdk/SVappsLAB.iRacingTelemetrySDK/contents/docs/AI_USAGE.md create mode 100644 Sdk/tests/Directory.Build.props delete mode 100644 Sdk/tests/IBT_Tests/Files/ReadFile.cs delete mode 100644 Sdk/tests/IBT_Tests/Fixture.cs delete mode 100644 Sdk/tests/IBT_Tests/IBT_Tests.csproj delete mode 100644 Sdk/tests/IBT_Tests/SessionInfo/SessionInfoTests.cs delete mode 100644 Sdk/tests/IBT_Tests/Variables/TelemetryVariables.cs delete mode 100644 Sdk/tests/IBT_Tests/data/race_oval/latemodel_southboston.ibt delete mode 100644 Sdk/tests/IBT_Tests/data/race_oval/latemodel_southboston.yaml delete mode 100644 Sdk/tests/IBT_Tests/data/race_test/lamborghinievogt3_spa up.ibt delete mode 100644 Sdk/tests/IBT_Tests/data/race_test/lamborghinievogt3_spa up.yaml delete mode 100644 Sdk/tests/Live_Tests/Fixture.cs delete mode 100644 Sdk/tests/Live_Tests/Live_Tests.csproj delete mode 100644 Sdk/tests/Live_Tests/SessionInfo/ModelYamlMatch.cs delete mode 100644 Sdk/tests/Live_Tests/SessionInfo/SessionInfoTests.cs delete mode 100644 Sdk/tests/Live_Tests/Telemetry.cs create mode 100644 Sdk/tests/SmokeTests/Base/Base.Models.cs create mode 100644 Sdk/tests/SmokeTests/Base/Base.Variables.cs create mode 100644 Sdk/tests/SmokeTests/Base/Base.cs create mode 100644 Sdk/tests/SmokeTests/Base/Summaries.cs create mode 100644 Sdk/tests/SmokeTests/Base/TestLogger.cs create mode 100644 Sdk/tests/SmokeTests/IBT/IBT.cs create mode 100644 Sdk/tests/SmokeTests/Live/Live.cs create mode 100644 Sdk/tests/SmokeTests/Properties/launchSettings.json create mode 100644 Sdk/tests/SmokeTests/SmokeTests.csproj create mode 100644 Sdk/tests/SmokeTests/VarsToTest.cs rename Sdk/tests/{IBT_Tests/data/race_road => SmokeTests/data/ibt}/audir8lmsevo2gt3_spa up.ibt (100%) create mode 100644 Sdk/tests/SmokeTests/data/ibt/ministock_mtwashington climb 2025-03-28.ibt create mode 100644 Sdk/tests/SmokeTests/data/ibt/mx5 cup_okayama full 2011-05-13.ibt create mode 100644 Sdk/tests/SmokeTests/data/ibt/raygr22_roadatlanta full 2025-07-15.ibt rename Sdk/tests/{IBT_Tests/data/race_road => SmokeTests/data/yaml}/audir8lmsevo2gt3_spa up.yaml (100%) rename Sdk/tests/{IBT_Tests => SmokeTests}/data/yaml/invalid_data.yaml (100%) diff --git a/.gitignore b/.gitignore index 77337af..efecccd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,8 @@ -.user +*.user .vs +Samples/**/docs/ artifacts/ bin/ dist/ -node_modules/ obj/ -upstream/ diff --git a/AI_CONTEXT.md b/AI_CONTEXT.md deleted file mode 100644 index 007eb75..0000000 --- a/AI_CONTEXT.md +++ /dev/null @@ -1,858 +0,0 @@ -# iRacing Telemetry SDK - AI Context & Implementation Guide - -> **AI Assistant Instructions**: This document provides comprehensive guidance for AI coding assistants to understand and implement applications using the iRacing Telemetry SDK. The SDK uses source code generation and requires specific patterns for proper operation. - -## SDK Overview - -The **TelemetryClient** is the core component of the iRacing Telemetry SDK that provides high-performance access to iRacing simulator telemetry data. It supports both live telemetry streaming from active iRacing sessions and playback of IBT (iRacing Binary Telemetry) files with strongly-typed data structures generated at compile time. - -## Critical Requirements for AI Tools - -โš ๏ธ **Essential Constraints**: -- **Source Generation Dependency**: The `[RequiredTelemetryVars]` attribute triggers compile-time code generation. The `TelemetryData` struct is NOT manually created. -- **Target Framework**: .NET 8.0+ required -- **Package Dependencies**: `Microsoft.Extensions.Logging` and `Microsoft.Extensions.Logging.Console` are required -- **Windows Dependency**: Live telemetry requires Windows (iRacing memory-mapped files). IBT playback works cross-platform. -- **Generic Type Parameter**: `TelemetryClient` where `T` is the generated `TelemetryData` struct - -## Project Setup - -### Required NuGet Packages -```xml - - - -``` - -### Project File Requirements -```xml - - - Exe - net8.0 - enable - enable - - -``` - -## Key Features - -- **Generic Type Safety**: Uses source code generation to create strongly-typed telemetry data structures -- **Dual Data Sources**: Works with live iRacing sessions or IBT file playback -- **High Performance**: Optimized with `ref struct`, `ReadOnlySpan`, and unsafe code for zero-allocation processing -- **Event-Driven Architecture**: Subscribe to telemetry updates, connection changes, and session info events -- **Asynchronous Operations**: Non-blocking operations throughout using async/await patterns - -## Quick Reference for AI Tools - -### Core Pattern (Always Required) -```csharp -// 1. Define telemetry variables (triggers source generation) -[RequiredTelemetryVars(["Speed", "RPM", "Gear"])] -public class Program -{ - // 2. Create logger and client - var logger = LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger("App"); - using var client = TelemetryClient.Create(logger); - - // 3. Subscribe to events - client.OnTelemetryUpdate += (sender, data) => { - // data.Speed, data.RPM, data.Gear are now available - }; - - // 4. Start monitoring - await client.Monitor(cancellationToken); -} -``` - -### Common Variable Categories -| Category | Variables | Notes | -|----------|-----------|-------| -| **Basic Vehicle** | `Speed`, `RPM`, `Gear`, `Throttle`, `Brake` | Core driving metrics | -| **Position** | `LapDistPct`, `IsOnTrack`, `PlayerTrackSurface` | Track position | -| **Safety** | `PlayerIncidents`, `EngineWarnings` | Warnings and penalties | -| **Session** | `SessionTime`, `SessionNum`, `IsOnTrackCar` | Session state | - -### Data Modes -```csharp -// Live mode (Windows only, requires iRacing running) -var client = TelemetryClient.Create(logger); - -// IBT file mode (cross-platform) -var ibtOptions = new IBTOptions("file.ibt", playBackSpeedMultiplier: 1); -var client = TelemetryClient.Create(logger, ibtOptions); -``` - -### Essential Event Handlers -```csharp -client.OnTelemetryUpdate += (sender, data) => { /* 60Hz telemetry data */ }; -client.OnConnectStateChanged += (sender, args) => { /* Connection status */ }; -client.OnError += (sender, args) => { /* Handle errors */ }; -client.OnSessionInfoUpdate += (sender, info) => { /* Parsed YAML */ }; -``` - -## Basic Usage - -### 1. Define Required Telemetry Variables - -Use the `[RequiredTelemetryVars]` attribute to specify which telemetry variables your application needs. The source generator will create a strongly-typed `TelemetryData` struct with these properties. - -```csharp -using SVappsLAB.iRacingTelemetrySDK; - -[RequiredTelemetryVars(["Speed", "RPM", "Gear"])] -public class Program -{ - // Your application code here -} -``` - -### 2. Create and Configure the Client - -```csharp -using Microsoft.Extensions.Logging; -using SVappsLAB.iRacingTelemetrySDK; - -// Set up logging -var logger = LoggerFactory - .Create(builder => builder - .SetMinimumLevel(LogLevel.Information) - .AddConsole()) - .CreateLogger("TelemetryApp"); - -// Create client for live data -using var client = TelemetryClient.Create(logger); - -// OR create client for IBT file playback -var ibtOptions = new IBTOptions("path/to/file.ibt", playBackSpeedMultiplier: 1); -using var client = TelemetryClient.Create(logger, ibtOptions); -``` - -### 3. Subscribe to Events - -```csharp -// Subscribe to telemetry data updates -client.OnTelemetryUpdate += (sender, telemetryData) => -{ - Console.WriteLine($"Speed: {telemetryData.Speed:F1} m/s, RPM: {telemetryData.RPM:F0}, Gear: {telemetryData.Gear}"); -}; - -// Subscribe to connection state changes -client.OnConnectStateChanged += (sender, args) => -{ - Console.WriteLine($"Connection state: {args.State}"); -}; - -// Subscribe to session info updates -client.OnSessionInfoUpdate += (sender, sessionInfo) => -{ - Console.WriteLine($"Track: {sessionInfo.WeekendInfo.TrackDisplayName}"); -}; - -// Subscribe to raw session info (YAML format) -client.OnRawSessionInfoUpdate += (sender, yamlData) => -{ - Console.WriteLine("Raw session info updated"); -}; - -// Subscribe to errors -client.OnError += (sender, args) => -{ - Console.WriteLine($"Error: {args.Exception.Message}"); -}; -``` - -### 4. Start Monitoring - -```csharp -using var cts = new CancellationTokenSource(); - -// Start monitoring (this will run until cancelled) -await client.Monitor(cts.Token); -``` - -## Complete Examples - -### Example 1: Basic Speed, RPM, and Gear Display - -```csharp -using Microsoft.Extensions.Logging; -using SVappsLAB.iRacingTelemetrySDK; - -namespace BasicTelemetryApp -{ - [RequiredTelemetryVars(["Speed", "RPM", "Gear", "IsOnTrackCar"])] - internal class Program - { - static async Task Main(string[] args) - { - var logger = LoggerFactory - .Create(builder => builder - .SetMinimumLevel(LogLevel.Information) - .AddConsole()) - .CreateLogger("BasicApp"); - - // Support both live and IBT file modes - IBTOptions? ibtOptions = args.Length == 1 ? new IBTOptions(args[0]) : null; - - using var client = TelemetryClient.Create(logger, ibtOptions); - - var counter = 0; - client.OnTelemetryUpdate += (sender, data) => - { - // Limit logging output to once per second - if ((counter++ % 60) != 0 || !data.IsOnTrackCar) return; - - var speedMph = data.Speed * 2.23694f; // Convert m/s to mph - logger.LogInformation($"Gear: {data.Gear}, RPM: {data.RPM:F0}, Speed: {speedMph:F0} mph"); - }; - - using var cts = new CancellationTokenSource(); - Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); }; - - await client.Monitor(cts.Token); - } - } -} -``` - -### Example 2: Track Position and Surface Analysis - -```csharp -using Microsoft.Extensions.Logging; -using SVappsLAB.iRacingTelemetrySDK; - -namespace TrackAnalysisApp -{ - [RequiredTelemetryVars(["IsOnTrack", "PlayerTrackSurface", "PlayerTrackSurfaceMaterial", "EngineWarnings", "PlayerIncidents", "LapDistPct"])] - internal class Program - { - static async Task Main(string[] args) - { - var logger = LoggerFactory - .Create(builder => builder - .SetMinimumLevel(LogLevel.Information) - .AddConsole()) - .CreateLogger("TrackAnalysis"); - - IBTOptions? ibtOptions = args.Length == 1 ? new IBTOptions(args[0]) : null; - using var client = TelemetryClient.Create(logger, ibtOptions); - - var counter = 0; - client.OnTelemetryUpdate += (sender, data) => - { - if ((counter++ % 120) != 0) return; // Output every 2 seconds - - var trackSurface = Enum.GetName(data.PlayerTrackSurface) ?? "Unknown"; - var surfaceMaterial = Enum.GetName(data.PlayerTrackSurfaceMaterial) ?? "Unknown"; - var warnings = GetEngineWarningsList(data.EngineWarnings); - var incidents = GetIncidentInfo(data.PlayerIncidents); - - logger.LogInformation($"Lap: {data.LapDistPct:P1}, OnTrack: {data.IsOnTrack}, " + - $"Surface: {trackSurface}, Material: {surfaceMaterial}, " + - $"Warnings: {warnings}, Incidents: {incidents}"); - }; - - using var cts = new CancellationTokenSource(); - Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); }; - - await client.Monitor(cts.Token); - } - - static string GetEngineWarningsList(EngineWarnings warnings) - { - var activeWarnings = new List(); - foreach (var flag in Enum.GetValues()) - { - if (warnings.HasFlag(flag) && flag != EngineWarnings.None) - { - activeWarnings.Add(Enum.GetName(flag) ?? flag.ToString()); - } - } - return activeWarnings.Count > 0 ? string.Join(", ", activeWarnings) : "None"; - } - - static string GetIncidentInfo(IncidentFlags incidents) - { - // Extract incident report and penalty separately - var incidentReport = (int)(incidents & IncidentFlags.IncidentRepMask); - var incidentPenalty = (int)(incidents & IncidentFlags.IncidentPenMask); - - var reportType = incidentReport switch - { - 0x0001 => "Loss of Control", - 0x0002 => "Off Track", - 0x0004 => "Contact", - 0x0005 => "Collision", - 0x0007 => "Car Contact", - 0x0008 => "Car Collision", - _ => incidentReport > 0 ? $"Unknown({incidentReport:X})" : "None" - }; - - var penaltyType = incidentPenalty switch - { - 0x0100 => "0x", - 0x0200 => "1x", - 0x0300 => "2x", - 0x0400 => "4x", - _ => incidentPenalty > 0 ? $"Unknown({incidentPenalty:X})" : "None" - }; - - return $"{reportType} ({penaltyType})"; - } - } -} -``` - -### Example 3: Data Export and Analysis - -```csharp -using Microsoft.Extensions.Logging; -using SVappsLAB.iRacingTelemetrySDK; -using SVappsLAB.iRacingTelemetrySDK.Models; - -namespace DataExportApp -{ - [RequiredTelemetryVars(["Speed", "RPM", "SteeringWheelAngle", "Throttle", "Brake"])] - internal class Program - { - static async Task Main(string[] args) - { - var logger = LoggerFactory - .Create(builder => builder - .SetMinimumLevel(LogLevel.Information) - .AddConsole()) - .CreateLogger("DataExport"); - - IBTOptions? ibtOptions = args.Length == 1 ? new IBTOptions(args[0]) : null; - using var client = TelemetryClient.Create(logger, ibtOptions); - - var dataPoints = new List(); - var sessionInfo = ""; - - // Collect telemetry data - client.OnTelemetryUpdate += (sender, data) => - { - dataPoints.Add(data); - if (dataPoints.Count % 3600 == 0) // Log once a minute (60 Hz * 60 sec) - { - logger.LogInformation($"Collected {dataPoints.Count} data points..."); - } - }; - - // Capture session information - client.OnRawSessionInfoUpdate += (sender, yaml) => - { - if (string.IsNullOrEmpty(sessionInfo)) - { - sessionInfo = yaml; - logger.LogInformation("Session info captured"); - } - }; - - // Get available telemetry variables - client.OnConnectStateChanged += async (sender, args) => - { - if (args.State == ConnectState.Connected) - { - var variables = await client.GetTelemetryVariables(); - logger.LogInformation($"Available variables: {variables.Count()}"); - - // Export variable definitions - await ExportVariableDefinitions(variables); - } - }; - - using var cts = new CancellationTokenSource(); - Console.CancelKeyPress += (_, e) => - { - e.Cancel = true; - cts.Cancel(); - ExportCollectedData(dataPoints, sessionInfo, logger); - }; - - await client.Monitor(cts.Token); - } - - static async Task ExportVariableDefinitions(IEnumerable variables) - { - var timestamp = DateTime.Now.ToString("yyyyMMdd-HHmmss"); - var filename = $"TelemetryVariables-{timestamp}.csv"; - - await using var writer = new StreamWriter(filename); - await writer.WriteLineAsync("Name,Type,Length,IsTimeValue,Description,Units"); - - foreach (var variable in variables.OrderBy(v => v.Name)) - { - await writer.WriteLineAsync($"{variable.Name},{variable.Type.Name}," + - $"{variable.Length},{variable.IsTimeValue}," + - $"\"{variable.Desc}\",\"{variable.Units}\""); - } - } - - static void ExportCollectedData(List dataPoints, string sessionInfo, ILogger logger) - { - var timestamp = DateTime.Now.ToString("yyyyMMdd-HHmmss"); - - // Export telemetry data - using (var writer = new StreamWriter($"TelemetryData-{timestamp}.csv")) - { - writer.WriteLine("Speed,RPM,SteeringWheelAngle,Throttle,Brake"); - foreach (var data in dataPoints) - { - writer.WriteLine($"{data.Speed},{data.RPM},{data.SteeringWheelAngle}," + - $"{data.Throttle},{data.Brake}"); - } - } - - // Export session info - if (!string.IsNullOrEmpty(sessionInfo)) - { - File.WriteAllText($"SessionInfo-{timestamp}.yaml", sessionInfo); - } - - logger.LogInformation($"Exported {dataPoints.Count} data points"); - } - } -} -``` - -## Advanced Usage - -### IBT File Options - -```csharp -// Play at normal speed (1x) -var ibtOptions = new IBTOptions("replay.ibt", playBackSpeedMultiplier: 1); - -// Play at 10x speed -var ibtOptions = new IBTOptions("replay.ibt", playBackSpeedMultiplier: 10); - -// Play as fast as possible (default) -var ibtOptions = new IBTOptions("replay.ibt", playBackSpeedMultiplier: int.MaxValue); -``` - -### Pause and Resume - -```csharp -// Pause telemetry event firing (processing continues) -client.Pause(); - -// Resume telemetry events -client.Resume(); -``` - -### Connection Status Monitoring - -```csharp -client.OnConnectStateChanged += (sender, args) => -{ - switch (args.State) - { - case ConnectState.Connected: - Console.WriteLine("Connected to iRacing"); - break; - case ConnectState.Disconnected: - Console.WriteLine("Disconnected from iRacing"); - break; - } -}; - -// Check connection status at any time -if (client.IsConnected()) -{ - Console.WriteLine("Currently connected"); -} -``` - -### Error Notification - -```csharp -client.OnError += (sender, args) => -{ - Console.WriteLine($"Telemetry error: {args.Exception.Message}"); - - // Log full exception details - logger.LogError(args.Exception, "Telemetry client error occurred"); -}; -``` - -## Comprehensive Telemetry Variables Reference - -The SDK provides access to 200+ telemetry variables from iRacing. Here are the most commonly used: - -### ๐Ÿš— Vehicle Dynamics & Control -| Variable | Type | Units | Description | -|----------|------|-------|-------------| -| `Speed` | `float` | m/s | Vehicle speed | -| `RPM` | `float` | rpm | Engine RPM | -| `Gear` | `int` | - | Current gear (-1=reverse, 0=neutral, 1+=forward) | -| `Throttle` | `float` | 0.0-1.0 | Throttle pedal position | -| `Brake` | `float` | 0.0-1.0 | Brake pedal position | -| `Clutch` | `float` | 0.0-1.0 | Clutch pedal position | -| `SteeringWheelAngle` | `float` | rad | Steering wheel angle | -| `SteeringWheelTorque` | `float` | Nยทm | Force feedback torque | -| `LongAccel` | `float` | m/sยฒ | Longitudinal, lateral, vertical G-forces | - -### ๐Ÿ Track Position & Timing -| Variable | Type | Units | Description | -|----------|------|-------|-------------| -| `LapDistPct` | `float` | 0.0-1.0 | Distance around current lap | -| `LapCurrentLapTime` | `float` | s | Current lap time | -| `LapBestLapTime` | `float` | s | Best lap time this session | -| `LapLastLapTime` | `float` | s | Last completed lap time | -| `IsOnTrack` | `bool` | - | Whether car is on track surface | -| `IsOnTrackCar` | `bool` | - | Whether player's car is on track | -| `PlayerTrackSurface` | `int` | enum | Track surface type (asphalt, concrete, etc.) | -| `PlayerTrackSurfaceMaterial` | `int` | enum | Surface material properties | - -### โš ๏ธ Safety & Incidents -| Variable | Type | Units | Description | -|----------|------|-------|-------------| -| `PlayerIncidents` | `IncidentFlags` | flags | Incident type and penalty level | -| `EngineWarnings` | `EngineWarnings` | flags | Engine warning indicators | -| `SessionFlags` | `SessionFlags` | flags | Yellow, red, checkered flags | -| `CarIdxTrackSurface` | `int[]` | enum | Track surface for each car | - -**IncidentFlags Usage**: -```csharp -var reportType = (int)(data.PlayerIncidents & IncidentFlags.IncidentRepMask); -var penaltyLevel = (int)(data.PlayerIncidents & IncidentFlags.IncidentPenMask); -``` - -### ๐Ÿ”ง Vehicle Systems & Status -| Variable | Type | Units | Description | -|----------|------|-------|-------------| -| `FuelLevel` | `float` | L | Current fuel level | -| `FuelUsePerHour` | `float` | L/h | Fuel consumption rate | -| `WaterTemp` | `float` | ยฐC | Engine coolant temperature | -| `OilTemp` | `float` | ยฐC | Engine oil temperature | -| `OilPress` | `float` | bar | Engine oil pressure | -| `Voltage` | `float` | V | Electrical system voltage | -| `ManifoldPress` | `float` | bar | Intake manifold pressure | - -### ๐Ÿ† Session & Race Information -| Variable | Type | Units | Description | -|----------|------|-------|-------------| -| `SessionTime` | `double` | s | Current session time | -| `SessionTimeRemain` | `double` | s | Time remaining in session | -| `SessionNum` | `int` | - | Current session number | -| `SessionState` | `int` | enum | Session state (practice, qualifying, race) | -| `SessionLapsRemain` | `int` | - | Laps remaining (if applicable) | -| `SessionLapsTotal` | `int` | - | Total laps in session | - -### ๐ŸŒก๏ธ Environment & Track Conditions -| Variable | Type | Units | Description | -|----------|------|-------|-------------| -| `AirTemp` | `float` | ยฐC | Ambient air temperature | -| `TrackTemp` | `float` | ยฐC | Track surface temperature | -| `RelativeHumidity` | `float` | % | Relative humidity | -| `WindVel` | `float` | m/s | Wind speed | -| `WindDir` | `float` | rad | Wind direction | -| `TrackWetness` | `int` | enum | Track wetness level | - -### ๐Ÿšฆ Multi-Car Data (Arrays) -| Variable | Type | Description | -|----------|------|-------------| -| `CarIdxLapDistPct` | `float[]` | Lap distance for each car | -| `CarIdxPosition` | `int[]` | Race position for each car | -| `CarIdxClassPosition` | `int[]` | Class position for each car | -| `CarIdxF2Time` | `float[]` | Time behind leader for each car | -| `CarIdxOnPitRoad` | `bool[]` | Pit road status for each car | - -To discover all available variables, use: - -```csharp -var variables = await client.GetTelemetryVariables(); -foreach (var variable in variables.OrderBy(v => v.Name)) -{ - Console.WriteLine($"{variable.Name}: {variable.Desc} ({variable.Units})"); -} -``` - -## โš ๏ธ Critical Anti-Patterns & Common Pitfalls - -### โŒ DO NOT: Manually Create TelemetryData Struct -```csharp -// WRONG - This will cause compilation errors -public struct TelemetryData -{ - public float Speed { get; set; } - public float RPM { get; set; } -} -``` -**Why**: The `TelemetryData` struct is generated by the source generator based on the `[RequiredTelemetryVars]` attribute. - -### โŒ DO NOT: Use TelemetryClient Without Generic Parameter -```csharp -// WRONG - Missing generic type parameter -var client = TelemetryClient.Create(logger); -``` -**Correct**: -```csharp -var client = TelemetryClient.Create(logger); -``` - -### โŒ DO NOT: Perform Heavy Operations in Event Handlers -```csharp -// WRONG - Blocking telemetry processing thread -client.OnTelemetryUpdate += (sender, data) => -{ - Thread.Sleep(100); // Blocks telemetry processing - SaveToDatabase(data); // Synchronous I/O - ComplexCalculation(); // CPU-intensive work -}; -``` -**Why**: Telemetry arrives at 60Hz. Heavy operations block the processing thread. - -**Correct**: -```csharp -client.OnTelemetryUpdate += (sender, data) => -{ - // Queue data for background processing - telemetryQueue.Enqueue(data); -}; -``` - -### โŒ DO NOT: Forget Resource Disposal -```csharp -// WRONG - Memory leaks -var client = TelemetryClient.Create(logger); -// Client never disposed -``` -**Correct**: -```csharp -using var client = TelemetryClient.Create(logger); -// or -client.Dispose(); -``` - -### โŒ DO NOT: Access Non-Declared Variables -```csharp -[RequiredTelemetryVars(["Speed", "RPM"])] -public class Program -{ - client.OnTelemetryUpdate += (sender, data) => - { - var gear = data.Gear; // COMPILATION ERROR - Gear not declared - }; -} -``` - -### โŒ DO NOT: Use IBT Files on Non-Existent Paths -```csharp -// WRONG - Will throw FileNotFoundException immediately -var ibtOptions = new IBTOptions("nonexistent.ibt"); -var client = TelemetryClient.Create(logger, ibtOptions); -``` - -### โŒ DO NOT: Use Blocking Calls in Async Context -```csharp -// WRONG - Blocking async context -await Task.Run(() => -{ - client.Monitor(cancellationToken).Wait(); // Blocks thread pool thread -}); -``` -**Correct**: -```csharp -await client.Monitor(cancellationToken); -``` - -## Performance Considerations - -1. **Throttle Output**: Telemetry data arrives at 60 Hz. Consider throttling console output or file writes -2. **Memory Usage**: For long-running applications, be mindful of data collection growth -3. **Event Handlers**: Keep event handlers lightweight to avoid blocking the telemetry processing loop -4. **IBT Playback**: Large IBT files can consume significant memory during processing - -## Integration Patterns & Data Flow - -### Database Integration Pattern -```csharp -[RequiredTelemetryVars(["Speed", "RPM", "Gear", "LapDistPct", "SessionTime"])] -public class DatabaseLogger -{ - private readonly ConcurrentQueue _dataQueue = new(); - private readonly CancellationTokenSource _backgroundCts = new(); - - public async Task StartAsync() - { - using var client = TelemetryClient.Create(logger); - - // Queue telemetry data (non-blocking) - client.OnTelemetryUpdate += (sender, data) => _dataQueue.Enqueue(data); - - // Background database writer - var writerTask = Task.Run(async () => - { - while (!_backgroundCts.Token.IsCancellationRequested) - { - if (_dataQueue.TryDequeue(out var data)) - { - await SaveToDatabase(data); - } - await Task.Delay(10); // Prevent busy waiting - } - }); - - await client.Monitor(cancellationToken); - _backgroundCts.Cancel(); - } -} -``` - -### Real-Time Dashboard Pattern -```csharp -[RequiredTelemetryVars(["Speed", "RPM", "Gear", "Throttle", "Brake"])] -public class DashboardService -{ - private TelemetryData _latestData; - private readonly Timer _updateTimer; - - public event Action OnDashboardUpdate; - - public DashboardService() - { - // Update dashboard at 10Hz (lower than telemetry rate) - _updateTimer = new Timer(SendDashboardUpdate, null, 0, 100); - } - - public async Task StartTelemetry() - { - using var client = TelemetryClient.Create(logger); - - client.OnTelemetryUpdate += (sender, data) => - { - _latestData = data; // Just store latest, don't process here - }; - - await client.Monitor(cancellationToken); - } - - private void SendDashboardUpdate(object state) - { - var dashData = new DashboardData - { - SpeedMph = _latestData.Speed * 2.23694f, - RPM = _latestData.RPM, - Gear = _latestData.Gear, - ThrottlePercent = _latestData.Throttle * 100f, - BrakePercent = _latestData.Brake * 100f - }; - OnDashboardUpdate?.Invoke(dashData); - } -} -``` - - -## Source Generation Details & Constraints - -### How Source Generation Works -The SDK uses Roslyn source generators to create the `TelemetryData` struct at compile time: - -1. **Attribute Processing**: The `[RequiredTelemetryVars]` attribute is processed during compilation -2. **Code Generation**: A struct is generated with properties matching the specified variable names -3. **Type Safety**: The generated struct provides compile-time type checking and IntelliSense - -### Generated Code Structure -Given this attribute: -```csharp -[RequiredTelemetryVars(["Speed", "RPM", "Gear", "IsOnTrack"])] -``` - -The source generator creates: -```csharp -// Generated automatically - DO NOT MODIFY -public readonly struct TelemetryData -{ - public readonly float Speed; - public readonly float RPM; - public readonly int Gear; - public readonly bool IsOnTrack; - - public TelemetryData(float speed, float rpm, int gear, bool isOnTrack) - { - Speed = speed; - RPM = rpm; - Gear = gear; - IsOnTrack = isOnTrack; - } -} -``` - -### Variable Name Resolution -- **Case Insensitive**: `"speed"`, `"Speed"`, `"SPEED"` all resolve to the same variable -- **Exact Matching**: Variable names must match iRacing's internal names exactly -- **Type Inference**: Types are determined from iRacing's variable definitions -- **Array Support**: Array variables like `"CarIdxLapDistPct"` become `float[]` properties - -### Compilation Requirements -- **Build Order**: Source generation happens during compilation, before your code is compiled -- **Clean Builds**: Sometimes required after changing `[RequiredTelemetryVars]` attributes -- **IDE Support**: Modern IDEs show generated code in "Dependencies > Analyzers" - -### Source Generator Constraints -```csharp -// โœ… VALID: String array literals -[RequiredTelemetryVars(["Speed", "RPM", "Gear"])] - -// โœ… VALID: Constant string arrays -private const string[] REQUIRED_VARS = ["Speed", "RPM"]; -[RequiredTelemetryVars(REQUIRED_VARS)] - -// โŒ INVALID: Runtime-determined arrays -[RequiredTelemetryVars(GetVariablesFromConfig())] // Compilation error - -// โŒ INVALID: Variables from other assemblies -[RequiredTelemetryVars(ExternalClass.Variables)] // May not work -``` - -### Multiple Attribute Support -```csharp -// Each class can have its own set of variables -[RequiredTelemetryVars(["Speed", "RPM"])] -public class BasicMonitor { } - -[RequiredTelemetryVars(["Speed", "RPM", "Gear", "Throttle", "Brake"])] -public class DetailedMonitor { } - -// Different TelemetryData structs are generated for each -``` - - -### Debugging Generated Code -View generated code in your IDE: -1. **Visual Studio**: Solution Explorer > Dependencies > Analyzers > SVappsLAB.iRacingTelemetrySDK.CodeGen -2. **VS Code**: Use "Go to Definition" on `TelemetryData` -3. **Build Output**: Check `obj/Generated/` folder - -### Variable Validation -The source generator validates variable names at compile time: -```csharp -// โœ… Valid iRacing variable -[RequiredTelemetryVars(["Speed"])] // Compiles successfully - -// โŒ Invalid variable name -[RequiredTelemetryVars(["InvalidVar"])] // Compilation warning/error -``` - -## Threading and Async Patterns - -The TelemetryClient is designed for async/await usage: - -```csharp -// Proper async pattern -await client.Monitor(cancellationToken); - -// Multiple concurrent operations -var monitorTask = client.Monitor(cancellationToken); -var keyboardTask = MonitorKeyboardInput(); - -await Task.WhenAny(monitorTask, keyboardTask); -``` - -## License - -This SDK is licensed under the Apache License, Version 2.0. See the LICENSE file for details. diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md new file mode 100644 index 0000000..8a5f65a --- /dev/null +++ b/MIGRATION_GUIDE.md @@ -0,0 +1,289 @@ +# iRacing Telemetry SDK Migration Guide + +The iRacing Telemetry SDK underwent major architectural redesign to address performance, type safety, and maintainability concerns. + +## โœจ What's New in v1.0 + +- **Strongly-typed Variables**: IntelliSense support with compile-time validation using `TelemetryVar` enums +- **Nullable Properties**: Better represents iRacing's dynamic variable availability +- **Modern Async Patterns**: Async stream-based data flows with asynchronous non-blocking processing +- **High Performance**: Lock-free architecture delivering 600,000+ telemetry records/second +- **Performance Monitoring**: Built-in metrics system for production diagnostics +- **Flow Control**: Pause/Resume functionality for temporary data suppression + +## Before You Begin + +Update your references to the latest version: `dotnet add package SVappsLAB.iRacingTelemetrySDK` + +Check out the **MinimalExample** sample project in `Samples/MinimalExample/` for a complete working example of the new API. + +Migration requires updating five key areas: +1. **Variable Declaration**: String arrays โ†’ Enum arrays +2. **Data Consumption**: Events โ†’ Async stream consumption or extension methods +3. **Disposal Pattern**: `using` โ†’ `await using` +4. **Namespaces**: Remove `.Models` from imports +5. **Null Handling**: Direct access โ†’ Nullable-aware patterns + +The extension methods like `SubscribeToAllStreams` provide the smoothest migration path for simple applications, +while direct async stream consumption offer maximum performance and flexibility when speed is a concern. + +## Breaking Changes Overview + +### Variable Name Mapping + +Most variables map directly from string to enum: + +```csharp +"Speed" โ†’ TelemetryVar.Speed +"RPM" โ†’ TelemetryVar.RPM +"LFtempL" โ†’ TelemetryVar.LFtempL +"CarIdxLap" โ†’ TelemetryVar.CarIdxLap +``` +### Breaking Changes Summary + +| Component | Legacy | Modern | Migration Impact | +|-----------|--------|--------|------------------| +| Variable Declaration | `string[]` | `TelemetryVar[]` | Compile-time validation using enums | +| Type System | Non-nullable values | Nullable properties | Explicit null handling required | +| Data Delivery | .NET Events | Async streams (`IAsyncEnumerable`) | Performance boost, backpressure handling | +| Async Patterns | Event handlers | `await foreach` | Modern async consumption patterns | +| Disposal Pattern | `IDisposable` | `IAsyncDisposable` | `using` โ†’ `await using` | +| Model Namespace | `SVappsLAB.iRacingTelemetrySDK.Models` | `SVappsLAB.iRacingTelemetrySDK` | Update using statements | +| GetTelemetryVariables | `Task>` | `IReadOnlyList` | Remove await, update return type | + +## Migration Workflow + +1. Switch telemetry variable declarations to enums (`Migration Patterns โ†’ 1. Enums for Variables`). +2. Replace event-driven consumption with async streams or extension methods (`Migration Patterns โ†’ 2. Event Handlers to Async Stream Consumption`). +3. Move connection and error handling onto the provided streams (`Migration Patterns โ†’ 3. Connection and Error Handling Migration`). +4. Update disposal patterns and namespaces (`Migration Patterns โ†’ 4. Disposal Pattern and Namespace Changes`). +5. Update downstream code to respect nullable telemetry properties (`Nullable Property Handling`). + +## Quick Migration Checklist + +- [ ] Replace `[RequiredTelemetryVars(["Speed", "RPM"])]` with `[RequiredTelemetryVars([TelemetryVar.Speed, TelemetryVar.RPM])]` +- [ ] Replace event handlers with async stream consumption or `SubscribeToAllStreams` +- [ ] Update `using` to `await using` for client disposal +- [ ] Update namespace imports (remove `.Models` if used) +- [ ] Remove `await` from `GetTelemetryVariables()` calls +- [ ] Handle nullable properties explicitly (if needed) + +## Migration Patterns + +### 1. Enums for Variables + +Replace string arrays with `TelemetryVar` enum arrays: + +**Legacy:** +```csharp +[RequiredTelemetryVars(["Speed", "RPM", "Gear"])] +``` +**Modern:** +```csharp +[RequiredTelemetryVars([TelemetryVar.Speed, TelemetryVar.RPM, TelemetryVar.Gear])] +``` + +### 2. Event Handlers to Async Stream Consumption + +**Legacy (Events):** +```csharp +[RequiredTelemetryVars(["Speed", "RPM", "Gear"])] +public class SpeedMonitor +{ + public void Setup() + { + client.OnTelemetryUpdate += (sender, data) => { + Console.WriteLine($"Speed: {data.Speed:F0}, RPM: {data.RPM:F0}"); + }; + + client.OnSessionInfoUpdate += (sender, session) => { + Console.WriteLine($"Track: {session.WeekendInfo.TrackName}"); + }; + } +} +``` + +**Modern (Async Streams - Extension Method Approach):** + +Extension methods provide the easiest migration path. Call `SubscribeToAllStreams` with async Task delegates to handle updates. + +```csharp +// Consume all streams with async Func delegate handlers +await client.SubscribeToAllStreams( + onTelemetryUpdate: async data => { + Console.WriteLine($"Speed: {data.Speed?.ToString("F0") ?? "N/A"}, RPM: {data.RPM?.ToString("F0") ?? "N/A"}"); + }, + onSessionInfoUpdate: async session => { + Console.WriteLine($"Track: {session.WeekendInfo.TrackName}"); + }, + onRawSessionInfoUpdate: async yaml => { + Console.WriteLine($"Session YAML updated: {yaml.Length} chars"); + }, + onConnectStateChanged: async state => { + Console.WriteLine($"Connection: {state}"); + }, + onError: async error => { + logger.LogError(error, "Error occurred"); + }, + cancellationToken: cancellationToken +); +``` + + +**Modern (Async Streams - Manual Approach):** + +Use async foreach to consume streaming data, or use Tasks with modern async patterns to consume channels. + +```csharp +public async Task SetupAsync(CancellationToken cancellationToken) +{ + var telemetryTask = Task.Run(async () => { + await foreach (var data in client.TelemetryData.WithCancellation(cancellationToken)) { + Console.WriteLine($"Speed: {data.Speed?.ToString("F0") ?? "N/A"}, RPM: {data.RPM?.ToString("F0") ?? "N/A"}"); + } + }, cancellationToken); + + var sessionTask = Task.Run(async () => { + await foreach (var session in client.SessionData.WithCancellation(cancellationToken)) { + Console.WriteLine($"Track: {session.WeekendInfo.TrackName}"); + } + }, cancellationToken); + + var monitorTask = client.Monitor(cancellationToken); + + await Task.WhenAny(monitorTask, telemetryTask, sessionTask); +} +``` + +### 3. Connection and Error Handling Migration + +Connection state changes and runtime exceptions are streamed through channels, just like telemetry and session data. + +**Legacy:** +```csharp +client.OnError += (sender, args) => { + logger.LogError(args.Exception, "Telemetry error occurred"); +}; +``` + +**Modern:** + +Errors are streamed via `Errors`. + +```csharp +var errorTask = Task.Run(async () => { + await foreach (var error in client.Errors.WithCancellation(cancellationToken)) { + logger.LogError(error, "Telemetry error occurred"); + } +}, cancellationToken); +``` + +### 4. Disposal Pattern and Namespace Changes + +**Disposal Pattern Change:** +```csharp +// โŒ Legacy: IDisposable +using var client = TelemetryClient.Create(logger); + +// โœ… Modern: IAsyncDisposable +await using var client = TelemetryClient.Create(logger); +``` + +**Namespace Changes:** +```csharp +// โŒ Legacy: Models in separate namespace +using SVappsLAB.iRacingTelemetrySDK.Models; + +// โœ… Modern: Models in main namespace +using SVappsLAB.iRacingTelemetrySDK; // All types now in main namespace +``` + +**GetTelemetryVariables Change:** +```csharp +// โŒ Legacy: Async method +var variables = await client.GetTelemetryVariables(); + +// โœ… Modern: Synchronous method +var variables = client.GetTelemetryVariables(); +``` + +## Events to Async Streams Reference + +| Legacy Event | Modern Async Stream | Type | +|--------------|----------------------|------| +| `OnTelemetryUpdate` | `TelemetryData` | `IAsyncEnumerable` | +| `OnSessionInfoUpdate` | `SessionData` | `IAsyncEnumerable` | +| `OnRawSessionInfoUpdate` | `SessionDataYaml` | `IAsyncEnumerable` | +| `OnConnectStateChanged` | `ConnectStates` | `IAsyncEnumerable` | +| `OnError` | `Errors` | `IAsyncEnumerable` | + +## Nullable Property Handling + +Some iRacing variables are only available in certain sessions. Rather than throwing a runtime error when a variable is unavailable, the SDK returns null values using nullable types. + +```csharp +// โœ… Direct arithmetic (preserves null semantics) +var speedMph = data.Speed * 2.23694f; + +// โœ… Null-conditional formatting +var display = $"RPM: {data.RPM?.ToString("F0") ?? "N/A"}"; + +// โœ… Explicit null handling +var speed = data.Speed ?? 0f; +var speed = data.Speed.GetValueOrDefault(); + +// โœ… Boolean checks +if (data.IsOnTrackCar == true) { /* ... */ } +if (data.IsOnTrackCar.GetValueOrDefault()) { /* ... */ } + +// โŒ Avoid - potential NullReferenceException +var display = $"RPM: {data.RPM.Value:F0}"; +``` + +## Common Migration Issues & Solutions + +### Issue: Compilation Error - String to Enum Conversion +```csharp +// โŒ Error: Cannot convert from 'string[]' to 'TelemetryVar[]' +[RequiredTelemetryVars(["Speed", "RPM"])] + +// โœ… Solution: Use enum values +[RequiredTelemetryVars([TelemetryVar.Speed, TelemetryVar.RPM])] +``` + +### Issue: Null Reference Warnings +```csharp +// โŒ Warning: Possible null reference +var speed = data.Speed; + +// โœ… Solution: Handle nullability +var speed = data.Speed ?? 0f; +var speedText = data.Speed?.ToString("F1") ?? "N/A"; +``` + +### Issue: Boolean Logic Changes +```csharp +// โŒ Error: Cannot implicitly convert 'bool?' to 'bool' +if (data.IsOnTrackCar) { } + +// โœ… Solution: Explicit boolean check +if (data.IsOnTrackCar == true) { } +``` + +### Issue: Disposal Pattern Changes +```csharp +// โŒ Error: Cannot convert from 'using' to 'await using' +using var client = TelemetryClient.Create(logger); + +// โœ… Solution: Use async disposal +await using var client = TelemetryClient.Create(logger); +``` + +### Issue: Namespace Resolution +```csharp +// โŒ Error: Type 'SessionInfo' not found +using SVappsLAB.iRacingTelemetrySDK.Models; + +// โœ… Solution: Use main namespace +using SVappsLAB.iRacingTelemetrySDK; +``` diff --git a/NUGET.md b/NUGET.md index 1dc790e..bc77bba 100644 --- a/NUGET.md +++ b/NUGET.md @@ -1,74 +1,106 @@ -# iRacing Telemetry SDK for C# .NET - -High-performance .NET SDK for accessing **live telemetry data** from iRacing simulator and **IBT file playback**. Features compile-time code generation for strongly-typed telemetry access with zero-allocation performance optimizations. - -## Why Use This SDK? - -- **Live Telemetry**: Real-time access to speed, RPM, tire data, and 200+ other variables during iRacing sessions -- **IBT File Support**: Analyze historical telemetry data from saved iRacing IBT files -- **Type Safety**: Source code generation creates strongly-typed telemetry structs at compile time -- **High Performance**: Processes 300,000+ telemetry records/second with zero-allocation techniques -- **Simple API**: Event-driven architecture with easy-to-use async patterns - -Perfect for building **dashboards**, **data analysis tools**, **race engineering applications**, and **telemetry visualizations**. - -## Installation - -```bash -dotnet add package SVappsLAB.iRacingTelemetrySDK -``` - -## Quick Start - -```csharp -using Microsoft.Extensions.Logging; -using SVappsLAB.iRacingTelemetrySDK; - -// 1. Define the telemetry variables you want to track -[RequiredTelemetryVars(["Speed", "RPM", "Gear"])] -public class Program -{ - static async Task Main(string[] args) - { - // 2. Create logger - var logger = LoggerFactory.Create(builder => builder.AddConsole()) - .CreateLogger("TelemetryApp"); - - // 3. Use IBT file if provided, otherwise connect to live iRacing session - // If using an IBT file, you can specify the playback speed from 1x to max (depends on hardware) - // e.g. to specify 10x playback speed: new IBTOptions(args[0], playBackSpeedMultiplier: 10) - var ibtOptions = args.Length == 1 ? new IBTOptions(args[0]) : null; - - // 4. Create telemetry client - using var client = TelemetryClient.Create(logger, ibtOptions); - - // 5. Subscribe to telemetry updates (60Hz) - client.OnTelemetryUpdate += (sender, data) => - { - var speedMph = data.Speed * 2.23694f; // Convert m/s to mph - Console.WriteLine($"Speed: {speedMph:F0} mph, RPM: {data.RPM:F0}, Gear: {data.Gear}"); - }; - - // 6. Start monitoring - using var cts = new CancellationTokenSource(); - await client.Monitor(cts.Token); - } -} -``` - -## Requirements - -- **.NET 8.0+** -- **Windows** (for live telemetry) - IBT playback works cross-platform -- **Microsoft.Extensions.Logging** package - -## Documentation & Examples - -- **[Getting Started Guide](https://github.com/SVappsLAB/iRacingTelemetrySDK/blob/main/README.md)** - Quick setup and basic usage walkthrough -- **[AI Context Documentation](https://github.com/SVappsLAB/iRacingTelemetrySDK/blob/main/AI_CONTEXT.md)** - Comprehensive reference for AI coding assistants -- **[Sample Projects](https://github.com/SVappsLAB/iRacingTelemetrySDK/tree/main/Samples)** - Ready-to-run examples: basic monitoring, data export, track analysis -- **[GitHub Repository](https://github.com/SVappsLAB/iRacingTelemetrySDK)** - Full source code and releases - -## License - -Apache License 2.0 - See [LICENSE](https://github.com/SVappsLAB/iRacingTelemetrySDK/blob/main/LICENSE) for details. +# iRacing Telemetry SDK for C# .NET + +High-performance .NET SDK for accessing **live telemetry data** from iRacing simulator and **IBT file playback**. Features compile-time code generation for strongly-typed telemetry access with lock-free performance optimizations. + +## Why Use This SDK? + +- **Type Safety**: Enum-based telemetry variables with IntelliSense/Copilot support and compile-time validation +- **High Performance**: Processes 600,000+ telemetry records/second with lock-free data streaming architecture +- **Background Processing**: Dedicated threads for telemetry collection and processing - your app's processing speed never blocks the streaming telemetry data +- **Modern Async API**: Async data streams with async/await patterns and automatic backpressure handling +- **Live Telemetry**: Real-time access to speed, RPM, tire data, and 200+ variables during iRacing sessions +- **IBT File Support**: Cross-platform playback of historical telemetry using the same strongly-typed API +- **Robust Buffering**: 60-sample ring buffer (1 second at 60Hz) ensures reliable data delivery even when your app can't keep pace + +## Architecture Benefits + +**Production-Ready Data Streaming:** +- **60-sample ring buffer** provides 1 second of buffering at iRacing's 60Hz update rate +- **FIFO with drop-oldest** strategy automatically handles backpressure when your processing can't keep up +- **Never blocks** the iRacing data stream - always prioritizes the most recent telemetry +- **Prevents memory exhaustion** - bounded buffers protect against slow consumer scenarios +- **Zero data loss** when your app processes faster than 60Hz (~16ms per sample) + +This architecture means your dashboard or analysis tool stays responsive and current, even during CPU-intensive operations like rendering charts or writing to disk. + +## Use Cases + +Perfect for building: +- **Real-time Dashboards** - Display live telemetry on secondary screens +- **Data Analysis Tools** - Analyze racing performance from IBT files +- **Race Engineering Apps** - Track tire wear, fuel consumption, lap times +- **Telemetry Visualizations** - Create charts and graphs from historical data + + +## Installation + +```bash +dotnet add package SVappsLAB.iRacingTelemetrySDK +``` + +## Quick Start + +```csharp +using Microsoft.Extensions.Logging; +using SVappsLAB.iRacingTelemetrySDK; + +// 1. Define the telemetry variables you want to track +// Source generator creates strongly-typed TelemetryData struct at compile-time +[RequiredTelemetryVars([TelemetryVar.Speed, TelemetryVar.RPM])] + +public class Program +{ + public static async Task Main(string[] args) + { + // 2. Create logger + var logger = LoggerFactory.Create(builder => builder.AddConsole()) + .CreateLogger("Simple.Program"); + + // 3. Choose data source + IBTOptions? ibtOptions = null; // null for live telemetry from iRacing + // = new IBTOptions("gt3_spa.ibt"); // IBT file path for playback + + // 4. Create telemetry client + await using var client = TelemetryClient.Create(logger, ibtOptions); + using var cts = new CancellationTokenSource(); + + // 5. Subscribe to telemetry and sessionInfo streams + var subscriptionTask = client.SubscribeToAllStreams( + onTelemetryUpdate: async data => + { + Console.WriteLine($"Speed: {data.Speed}, RPM: {data.RPM}"); + }, + onSessionInfoUpdate: async session => + { + Console.WriteLine($"Track: {session.WeekendInfo.TrackName}, Drivers: {session.DriverInfo.Drivers.Count}"); + }, + onConnectStateChanged: async state => + { + Console.WriteLine($"Connection state: {state}"); // Connected, Disconnected + }, + cancellationToken: cts.Token + ); + + // 6. Start monitoring (iRacing data will be processed by your subscription handlers) + var monitorTask = client.Monitor(cts.Token); + + await Task.WhenAny(monitorTask, subscriptionTask); + } +} +``` + +## Requirements + +- **.NET 8.0+** + +## Documentation & Examples + +- **[Getting Started Guide](https://github.com/SVappsLAB/iRacingTelemetrySDK#readme)** - Setup and basic usage +- **[Sample Projects](https://github.com/SVappsLAB/iRacingTelemetrySDK/tree/main/Samples)** - Basic monitoring, data export, track analysis +- **[Implementation Guide](https://github.com/SVappsLAB/iRacingTelemetrySDK/blob/main/Sdk/SVappsLAB.iRacingTelemetrySDK/contents/docs/AI_USAGE.md)** - Deep dive into telemetry APIs and code-generation requirements +- **[Migration Guide](https://github.com/SVappsLAB/iRacingTelemetrySDK/blob/main/MIGRATION_GUIDE.md)** - Upgrading from previous versions +- **[GitHub Repository](https://github.com/SVappsLAB/iRacingTelemetrySDK)** - Source code and releases + +## License + +Apache License 2.0 - See [LICENSE](https://github.com/SVappsLAB/iRacingTelemetrySDK/blob/main/LICENSE) for details. diff --git a/README.md b/README.md index e301ee5..d587bac 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,79 @@ -# iRacingTelemetrySDK +# iRacing Telemetry SDK for C# .NET -**iRacingTelemetrySDK** is a .Net SDK tailored for C# developers aiming to incorporate telemetry data from the iRacing simulator into their software projects. +High-performance .NET SDK for accessing **live telemetry data** from iRacing simulator and **IBT file playback**. Features compile-time code generation for strongly-typed telemetry access with lock-free performance optimizations. + +[![NuGet](https://img.shields.io/nuget/v/SVappsLAB.iRacingTelemetrySDK)](https://www.nuget.org/packages/SVappsLAB.iRacingTelemetrySDK) +[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE) + +Perfect for building **real-time dashboards**, **data analysis tools**, **race engineering applications**, and **telemetry visualizations**. + +## Table of Contents + +- [Features](#features) +- [Requirements](#requirements) +- [Quick Example](#quick-example) +- [Getting Started](#getting-started) +- [Understanding Telemetry Variables](#understanding-telemetry-variables) +- [Performance and Design](#performance-and-design) +- [Performance Monitoring](#performance-monitoring) +- [Samples](#samples) +- [Documentation](#documentation) +- [License](#license) ## Features -- **Live Telemetry Data**: facilitates easy retrieval of telemetry information generated during iRacing sessions, including vehicle speed, engine RPM, tire temperatures, and more. +- **Type Safety**: Enum-based telemetry variables with IntelliSense support and compile-time validation +- **High Performance**: Processes 600,000+ telemetry records/second with lock-free data streaming architecture +- **Background Processing**: Dedicated threads for telemetry collection and processing - your app's processing speed never blocks the streaming telemetry data +- **Live Telemetry**: Real-time access to 200+ variables including speed, RPM, tire data during iRacing sessions +- **IBT File Playback**: Analyze historical telemetry using the same API as live data +- **Modern Async API**: Async data streams with automatic backpressure handling +- **Built-in Metrics**: Integrated performance monitoring via System.Diagnostics.Metrics +- **Pause and Resume**: Control data flow while background processing continues -- **Playback of saved IBT Telemetry Data:** In addition to live data access, the SDK can read previously saved iRacing IBT files and play them back as if it were live data, using the same API. -This allows you to analyze and process historical telemetry data, the same way you would with live data. +## Requirements -- **Strongly Typed Telemetry Data**: Source Generation is used to create strongly typed iRacing variables, such as Floats (speed, rpm) and Enums (track surface, pit service state). +- **.NET 8.0+** +- **Windows** for live iRacing telemetry +- **Cross-platform** for IBT file playback -- **Optimized Performance:** The SDK uses techniques such as asynchronous Task's, ref struct's and ReadOnlySpan's to minimize memory allocations and maximize performance. -When processing IBT files for example, the SDK is able to process 1 hour of saved telemetry data in under 1/2 second. A rate of over 300,000 telemetry records/sec. +## Quick Example -- **Pause and Resume:** Control telemetry event firing with `Pause()` and `Resume()` methods. Background processing continues while paused, but events are suppressed until resumed. +```csharp +using Microsoft.Extensions.Logging; +using SVappsLAB.iRacingTelemetrySDK; -## Telemetry Variables +[RequiredTelemetryVars([TelemetryVar.Speed, TelemetryVar.RPM])] -The iRacing simulator generates extensive telemetry data. This SDK lets you select which telemetry data you want to track and generates a strongly-typed struct with named variables you can access directly in your project. +public class Program +{ + public static async Task Main() + { + var logger = LoggerFactory.Create(b => b.AddConsole()).CreateLogger("App"); + await using var client = TelemetryClient.Create(logger); -### Availability + using var cts = new CancellationTokenSource(); -iRacing outputs different variables depending on the context. Some variables available in live sessions might not be available in offline IBT files, and vice versa. + var subscriptionTask = client.SubscribeToAllStreams( + onTelemetryUpdate: async data => + Console.WriteLine($"Speed: {data.Speed}, RPM: {data.RPM}"), + cancellationToken: cts.Token + ); -To check variable availability, use the [./Samples/DumpVariables_DumpSessionInfo](https://github.com/SVappsLAB/iRacingTelemetrySDK/tree/main/Samples/DumpVariables_DumpSessionInfo) utility. This will generate a CSV file listing available variables and a YAML file with complete session info. + var monitorTask = client.Monitor(cts.Token); -Once you know what variables are available and you have the list of which ones you want to use, you're ready to start using the SDK. + await Task.WhenAny(monitorTask, subscriptionTask); + } +} +``` ## Getting Started +> โš ๏ธ **v1.0+ Breaking Changes**: This 1.0 version introduces significant API changes with enum-based variable identification and async data streaming. If upgrading from a previous version, see [MIGRATION_GUIDE.md](./MIGRATION_GUIDE.md) for detailed migration instructions. + To incorporate **iRacingTelemetrySDK** into your projects, follow these steps: -> ๐Ÿ“š **For comprehensive documentation and advanced usage patterns**, see [AI_CONTEXT.md](./AI_CONTEXT.md) - a detailed guide designed for developers and AI coding assistants. +> ๐Ÿค– **For AI-assisted development**, see [AI_USAGE.md](./Sdk/SVappsLAB.iRacingTelemetrySDK/contents/docs/AI_USAGE.md) - a comprehensive guide with tips, best practices, and examples specifically designed for AI agents to help you build telemetry applications more effectively. 1. **Install the Package:** Add the **iRacingTelemetrySDK** NuGet package to your project using your preferred package manager. @@ -42,11 +83,11 @@ To incorporate **iRacingTelemetrySDK** into your projects, follow these steps: 1. Add the **RequiredTelemetryVars** attribute to the main class of your project - The attribute takes an array of strings. The string values, are the name of the iRacing telemetry variables you want to use in your program. + The attribute takes an array of TelemetryVar enum values. These enum values identify the iRacing telemetry variables you want to use in your program. ```csharp // these are the telemetry variables we want to track - [RequiredTelemetryVars(["isOnTrackCar", "rpm", "speed", "PlayerTrackSurface"])] + [RequiredTelemetryVars([TelemetryVar.IsOnTrackCar, TelemetryVar.RPM, TelemetryVar.Speed, TelemetryVar.PlayerTrackSurface])] internal class Program { @@ -57,97 +98,313 @@ To incorporate **iRacingTelemetrySDK** into your projects, follow these steps: A source generator will be leveraged to create a new .Net `TelemetryData` type you can use in your code. For the attribute above, the created type will look like ```csharp - public record struct TelemetryData(Boolean IsOnTrackCar,Single RPM,Single Speed, irsdk_TrkLoc PlayerTrackSurface); + public record struct TelemetryData + { + public bool? IsOnTrackCar { get; init; } + public float? RPM { get; init; } + public float? Speed { get; init; } + public TrackLocation? PlayerTrackSurface { get; init; } + } ``` 1. Create an instance of the TelemetryClient + The TelemetryClient implements `IAsyncDisposable` and should be used with `await using` for proper resource cleanup. + The TelemetryClient runs in one of two modes: Live or IBT file playback. - For live telemetry, you only need to provide a logger. + **For live telemetry**, you only need to provide a logger: - ```csharp - // live telemetry - using var tc = TelemetryClient.Create(logger); - ``` + ```csharp + // Live telemetry from iRacing + await using var tc = TelemetryClient.Create(logger); + ``` - For IBT playback, provide the path to the IBT file and an optional playback speed multiplier. The speed multiplier will speed up or slow down the playback of the IBT file.
- A speed of `1` will play the IBT file at the normal speed iRacing output the file (60 records/sec). A speed of `20` will playback the file at 20x speed (20*60=1200 records/rec).
- To play the file at maximum speed, use `int.MaxValue` as the multiplier value. + **For IBT playback**, provide the path to the IBT file and an optional playback speed multiplier: - ```csharp - // process the IBT file at 10x speed + ```csharp + // Process IBT file at 10x speed var ibtOptions = new IBTOptions(@"C:\path\to\file.ibt", 10); - using var tc = TelemetryClient.Create(logger, ibtOptions); - ``` + await using var tc = TelemetryClient.Create(logger, ibtOptions); -1. Add an event handler + // Maximum speed processing (the default) + var fastOptions = new IBTOptions(@"C:\path\to\file.ibt", int.MaxValue); + await using var fastTc = TelemetryClient.Create(logger, fastOptions); + ``` + + **Speed multiplier values:** + - `1` = Normal speed (60 records/sec) + - `20` = 20x speed (1,200 records/sec) + - `int.MaxValue` = Maximum speed processing + +1. Subscribe to data streams - The event handler will be called with the latest telemetry data. - + The async streaming API provides two consumption patterns: + + **Option A: Extension Method (Can be used for simple scenarios)** ```csharp - // event handler - void OnTelemetryUpdate(object? _sender, TelemetryData e) + // Subscribe to all data streams with async delegate methods for simplified consumption + var subscriptionTask = tc.SubscribeToAllStreams( + onTelemetryUpdate: OnTelemetryUpdate, + onSessionInfoUpdate: OnSessionInfoUpdate, + onConnectStateChanged: OnConnectStateChanged, + onError: OnError, + cancellationToken: cts.Token); + + // Async event handler methods + Task OnTelemetryUpdate(TelemetryData data) + { + // Properties are nullable - handle accordingly + var speed = data.Speed?.ToString("F1") ?? "N/A"; + var rpm = data.RPM?.ToString("F0") ?? "N/A"; + + logger.LogInformation("Speed: {speed} mph, RPM: {rpm}", speed, rpm); + return Task.CompletedTask; + } + + Task OnSessionInfoUpdate(TelemetrySessionInfo session) { - // do something with the telemetry data - logger.LogInformation("rpm: {rpm}, speed: {speed}, track surface: {trksuf}", e.RPM, e.Speed, e.PlayerTrackSurface); + var driverCount = session.DriverInfo?.Drivers?.Count ?? 0; + logger.LogInformation("Drivers in session: {count}", driverCount); + return Task.CompletedTask; } ``` -1. Monitor for telemetry data changes + **Option B: Direct Stream Access (For advanced scenarios)** + ```csharp + // Consume data streams directly for maximum performance and flexibility + var telemetryTask = Task.Run(async () => + { + await foreach (var data in tc.TelemetryData.WithCancellation(cts.Token)) + { + // Handle nullable properties explicitly + if (data.Speed.HasValue && data.RPM.HasValue) + { + logger.LogInformation("Speed: {speed:F1}, RPM: {rpm:F0}", + data.Speed.Value, data.RPM.Value); + } + } + }, cts.Token); + + var sessionTask = Task.Run(async () => + { + await foreach (var session in tc.SessionData.WithCancellation(cts.Token)) + { + var trackName = session.WeekendInfo?.TrackName ?? "Unknown"; + logger.LogInformation("Track: {track}", trackName); + } + }, cts.Token); + ``` + +1. Monitor for data changes - Once monitoring is initiated, the events will fire and your event handlers will be called.
- Monitoring is stopped, when the `CancellationToken` is cancelled, or the when the end-of-file is reached when processing a IBT file. + The client uses multiple tasks (multi-threading) to monitor all iRacing data. Monitoring stops when the `CancellationToken` is cancelled (or when end-of-file is reached for IBT files). + **For Extension Method approach:** ```csharp - CancellationTokenSource cts = new new CancellationTokenSource(); + using var cts = new CancellationTokenSource(); + + // cancel on Ctrl+C + Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); }; + + // Start both monitoring and subscription tasks concurrently + var monitorTask = tc.Monitor(cts.Token); - // start monitoring the telemetry - await tc.Monitor(cts.Token); + await Task.WhenAny(monitorTask, subscriptionTask); ``` -## Samples + **For Direct Stream approach:** + ```csharp + using var cts = new CancellationTokenSource(); + Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); }; + + // Start monitoring and all consumption tasks concurrently + var monitorTask = tc.Monitor(cts.Token); -See [./Samples/README.md](https://github.com/SVappsLAB/iRacingTelemetrySDK/tree/main/Samples/README.md) for a list of example projects using the SDK + await Task.WhenAny(monitorTask, telemetryTask, sessionTask); + ``` + +## Understanding Telemetry Variables + +### Telemetry Variables + +The iRacing simulator generates extensive telemetry data. This SDK lets you select which telemetry data you want to track and generates a strongly-typed struct with named variables you can access directly in your project. + +#### Availability + +iRacing outputs different variables depending on the context. Some variables available in live sessions might not be available in offline IBT files, and vice versa. + +To check variable availability, use the [./Samples/DumpVariables_DumpSessionInfo](https://github.com/SVappsLAB/iRacingTelemetrySDK/tree/main/Samples/DumpVariables_DumpSessionInfo) utility. This will generate a CSV file listing available variables and a YAML file with complete session info. + +Once you know what variables are available and you have the list of which ones you want to use, you're ready to start using the SDK. -## Event Sequence Diagram +#### Nullable Properties -The following shows how the events are +All telemetry properties are nullable (`float?`, `int?`, `bool?`) to accurately represent iRacing's variable availability model. Some variables are only available in certain sessions or contexts. + +**Recommended patterns for handling null values:** + +```csharp +// โœ… Null-conditional formatting +var speedDisplay = $"Speed: {data.Speed?.ToString("F1") ?? "N/A"}"; + +// โœ… Direct arithmetic (preserves null semantics) +var speedMph = data.Speed * 2.23694f; // Result is null if Speed is null + +// โœ… Explicit null handling +var speed = data.Speed ?? 0f; +var hasValue = data.Speed.HasValue; + +// โœ… Boolean checks +if (data.IsOnTrackCar == true) { /* ... */ } +``` + +## Performance and Design + +The SDK is designed for high performance with zero data loss through async data streaming architecture. + +### Data Streaming Benefits + +**Data Safety:** +- **60-sample ring buffer** with FIFO (First-In-First-Out) and destructive-read semantics +- Provides **up to 1 second** of buffering at iRacing's 60Hz update rate +- When the buffer fills, **oldest unread samples are automatically dropped** to make room for new data +- **Drop-oldest strategy** ensures the SDK never blocks and prioritizes the most recent telemetry + +**Performance:** +- **Lock-free operations** eliminate blocking and contention +- **Asynchronous processing** keeps your main thread responsive +- **~2x performance improvement** over traditional event-based approaches +- **600,000+ records/sec** processing capability for IBT files + +**Flexibility:** +- **Two consumption patterns**: Simple extension methods or direct stream access +- **Independent streams**: Consume only the data types you need +- **Cancellation support**: Graceful shutdown with `CancellationToken` + +### Architecture Overview + +**Key Design Principles:** + +- **Strongly Typed Telemetry Data**: Use strong type checking throughout +- **Non-blocking Streams**: All real-time data flows through asynchronous data streams +- **Separation of Concerns**: Telemetry data (high-frequency, time-critical) and Session Info processing (low frequency, CPU-intensive) run independently +- **Background Processing**: CPU-intensive YAML parsing runs on a separate thread to prevent any telemetry data drops. ```mermaid -sequenceDiagram - participant C as Client - create participant S as SDK - - C--)S: created - C->>S: startup() - - create participant R as monitoring telemetry - S--)R: - note over R: events start to fire - activate R - R-->>C: EVENT: "connected" - - loop sessionInfo - note right of S: when sessionInfo changes - R-->>C: EVENT: "sessionInfo" +graph TB + subgraph "Data Sources" + iRacing[iRacing Simulator
60Hz Live Data] + IBT[IBT File
Historical Data] + end + + subgraph "Task1 - Telemetry Data" + MainTask[Telemetry Data Stream] + end + + subgraph "Task2 - Session Info" + SessionTask[Session Info Processing
YAML Parsing] end - - loop telemetry - note right of S: when telemetry updates - R-->>C: EVENT: "telemetry" + + subgraph "High Performance Data Streams" + TelemetryStream[TelemetryData] + SessionStream[SessionData] + RawStream[SessionDataYaml] end - - R-->C: EVENT: "disconnected" - C->>S: shutdown() - deactivate R - note over R: events stop - destroy R - - R--)S: - destroy S - C--)S: destroyed + + iRacing --> MainTask + IBT --> MainTask + + MainTask --> TelemetryStream + MainTask --> SessionTask + SessionTask --> SessionStream + SessionTask --> RawStream + + classDef dataSource fill:#e1f5fe + classDef processing fill:#f3e5f5 + classDef stream fill:#e8f5e8 + + class iRacing,IBT dataSource + class MainTask,SessionTask processing + class TelemetryStream,SessionStream,RawStream stream ``` + +### Performance Monitoring + +The SDK includes built-in support for System.Diagnostics.Metrics to help monitor performance and diagnose issues. + +#### Available Metrics + +The SDK exposes several metrics automatically: + +- **telemetry_records_processed_total**: Counter of processed telemetry records +- **telemetry_records_dropped_total**: Counter of dropped records (when channels are full) +- **telemetry_processing_duration_microseconds**: Histogram of telemetry processing time +- **sessioninfo_records_processed_total**: Counter of session info updates processed +- **sessioninfo_processing_duration_milliseconds**: Histogram of session info processing time + +#### Enabling Metrics with Dependency Injection + +```csharp +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +[RequiredTelemetryVars([TelemetryVar.Speed, TelemetryVar.RPM])] +public class Program +{ + public static async Task Main(string[] args) + { + var host = Host.CreateDefaultBuilder(args) + .ConfigureServices((context, services) => + { + services.AddMetrics(); // Enable metrics support + services.AddLogging(logging => logging.AddConsole()); + }) + .Build(); + + var logger = host.Services.GetRequiredService>(); + var meterFactory = host.Services.GetRequiredService(); + + // Create client with metrics support + var clientOptions = new ClientOptions { MeterFactory = meterFactory }; + await using var client = TelemetryClient.Create(logger, null, clientOptions); + + // Your telemetry code here... + } +} +``` + +#### Monitoring with dotnet-counters + +Use any monitoring tool that supports System.Diagnostics.Metrics, like OpenTelemetry or the free Microsoft provided `dotnet-counters` tool to monitor SDK performance in real-time: + +```bash +# Monitor all SDK metrics for a running application named "YourApp" +dotnet-counters monitor --name "YourApp" --counters SVappsLAB.iRacingTelemetrySDK + +# Sample output: +# [SVappsLAB.iRacingTelemetrySDK] +# telemetry_records_processed_total 45,231 +# telemetry_records_dropped_total 0 +# sessioninfo_records_processed_total 12 +``` + +This helps identify performance bottlenecks, monitor processing rates, and detect if records are being dropped due to slow consumption. + +## Samples + +See [Samples Directory](./Samples/README.md) for ready-to-run example projects including: +- Basic telemetry monitoring +- IBT file analysis +- Data export utilities +- Track analysis tools + +## Documentation + +- **[Migration Guide](./MIGRATION_GUIDE.md)** - Upgrading from previous (pre-release) versions +- **[AI Assistant Instructions](./Sdk/SVappsLAB.iRacingTelemetrySDK/contents/docs/AI_USAGE.md)** - Documentation for AI Assistants + ## License -This project is licensed under the Apache License. Refer to the [LICENSE](https://github.com/SVappsLAB/iRacingTelemetrySDK/blob/main/LICENSE) file for details. +This project is licensed under the Apache License 2.0. See [LICENSE](./LICENSE) file for details. diff --git a/Samples/DumpVariables_DumpSessionInfo/DumpVariables_DumpSessionInfo.csproj b/Samples/DumpVariables_DumpSessionInfo/DumpVariables_DumpSessionInfo.csproj index 1a5dc5e..eaca6c7 100644 --- a/Samples/DumpVariables_DumpSessionInfo/DumpVariables_DumpSessionInfo.csproj +++ b/Samples/DumpVariables_DumpSessionInfo/DumpVariables_DumpSessionInfo.csproj @@ -9,7 +9,7 @@ - + diff --git a/Samples/DumpVariables_DumpSessionInfo/Program.cs b/Samples/DumpVariables_DumpSessionInfo/Program.cs index 7a8f47b..16a0288 100644 --- a/Samples/DumpVariables_DumpSessionInfo/Program.cs +++ b/Samples/DumpVariables_DumpSessionInfo/Program.cs @@ -11,16 +11,15 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; + * limitations under the License. **/ using Microsoft.Extensions.Logging; using SVappsLAB.iRacingTelemetrySDK; -using SVappsLAB.iRacingTelemetrySDK.Models; namespace DumpVariables_DumpSessionInfo { - [RequiredTelemetryVars(["rpm"])] + [RequiredTelemetryVars([TelemetryVar.RPM])] internal class Program { @@ -47,12 +46,22 @@ static async Task Main(string[] args) logger.LogInformation("pulling data from \'{source}\'", ibtOptions != null ? "IBT file session" : "Live iRacing session"); - // create telemetry client and subscribe - using var tc = TelemetryClient.Create(logger, ibtOptions); - tc.OnConnectStateChanged += OnConnectStateChanged; - tc.OnRawSessionInfoUpdate += OnRawSessionInfoUpdate; + // create telemetry client + await using var tc = TelemetryClient.Create(logger, ibtOptions); - // startTime monitoring - exit when we receive the session info event + // Note: ConnectStateStream is not available in published NuGet package yet + // For now, we'll get telemetry variables when we successfully get data + + // Start raw session info consumption + var rawSessionTask = Task.Run(async () => + { + await foreach (var rawYaml in tc.SessionDataYaml.WithCancellation(cts.Token)) + { + OnRawSessionInfoUpdate(rawYaml); + } + }, cts.Token); + + // Start monitoring - exit when we receive the session info var monitorTask = tc.Monitor(cts.Token); DateTime startTime = DateTime.Now; @@ -63,7 +72,7 @@ static async Task Main(string[] args) // save telemetryVariables to file writeVariablesFile(telemetryVariables); - // save sessinInfo yaml to file + // save sessionInfo yaml to file writeSessionInfoFile(rawSessionInfoYaml); // now that we have both 'telemetryVariables' and 'sessionInfo' @@ -72,27 +81,33 @@ static async Task Main(string[] args) break; } + // Get telemetry variables if we haven't already and client is connected + if (telemetryVariables == null && tc.IsConnected) + { + try + { + telemetryVariables = tc.GetTelemetryVariables(); + } + catch + { + // Ignore errors, we'll try again + } + } + logger.LogInformation("waiting for telemetry data... {elapsed} secs", (DateTime.Now - startTime).TotalSeconds.ToString("F1")); await Task.Delay(TimeSpan.FromSeconds(1)); } // wait for 2 seconds to exit - bool success = monitorTask.Wait(2 * 1000); - logger.LogInformation("Done. Status: {status}", success ? "successful" : "timeout-" + monitorTask.Status); + bool success = await Task.WhenAny(monitorTask, rawSessionTask) == monitorTask ? + monitorTask.Wait(2 * 1000) : + rawSessionTask.Wait(2 * 1000); + logger.LogInformation("Done. Status: {status}", success ? "successful" : "timeout"); - // connection event handler - void OnConnectStateChanged(object? _sender, ConnectStateChangedEventArgs e) - { - // if we already have the telemetryVariables, no more work to do - if (telemetryVariables != null) - return; - - telemetryVariables = tc.GetTelemetryVariables().Result; - } - // raw sessionInfo event handler - void OnRawSessionInfoUpdate(object? sender, string sessionInfoYaml) + // raw sessionInfo handler + void OnRawSessionInfoUpdate(string sessionInfoYaml) { // if we already have the rawSessionInfoYaml, no more work to do if (rawSessionInfoYaml != null) diff --git a/Samples/LocationAndWarnings/LocationAndWarnings.csproj b/Samples/LocationAndWarnings/LocationAndWarnings.csproj index 1a5dc5e..eaca6c7 100644 --- a/Samples/LocationAndWarnings/LocationAndWarnings.csproj +++ b/Samples/LocationAndWarnings/LocationAndWarnings.csproj @@ -9,7 +9,7 @@ - + diff --git a/Samples/LocationAndWarnings/Program.cs b/Samples/LocationAndWarnings/Program.cs index 8d39f8d..1cad2d8 100644 --- a/Samples/LocationAndWarnings/Program.cs +++ b/Samples/LocationAndWarnings/Program.cs @@ -11,7 +11,7 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; + * limitations under the License. **/ using Microsoft.Extensions.Logging; @@ -21,7 +21,12 @@ namespace LocationAndWarnings { // these are the telemetry variables we want to track - [RequiredTelemetryVars(["enginewarnings", "IsOnTrack", "PlayerTrackSurface", "PlayerTrackSurfaceMaterial"])] + [RequiredTelemetryVars([ + TelemetryVar.EngineWarnings, + TelemetryVar.IsOnTrack, + TelemetryVar.PlayerTrackSurface, + TelemetryVar.PlayerTrackSurfaceMaterial + ])] internal class Program { // pass the IBT file you want to analyze @@ -41,45 +46,59 @@ public static async Task Main(string[] args) logger.LogInformation("processing data from \"{source}\"", ibtOptions == null ? "online LIVE session" : "offline IBT file"); // create telemetry client - using var tc = TelemetryClient.Create(logger, ibtOptions); + await using var tc = TelemetryClient.Create(logger, ibtOptions); - // subscribe to telemetry updates - tc.OnTelemetryUpdate += OnTelemetryUpdate; + // use cancellation token for proper shutdown + using var cts = new CancellationTokenSource(); + + // start telemetry consumption + var telemetryTask = Task.Run(async () => + { + await foreach (var data in tc.TelemetryData.WithCancellation(cts.Token)) + { + OnTelemetryUpdate(data); + } + }, cts.Token); // start monitoring telemetry - press ctrl-c to exit - await tc.Monitor(CancellationToken.None); + var monitorTask = tc.Monitor(cts.Token); + + // wait for either task to complete + await Task.WhenAny(monitorTask, telemetryTask); logger.LogInformation("done"); - void OnTelemetryUpdate(object? sender, TelemetryData e) + void OnTelemetryUpdate(TelemetryData e) { // slow things down, only output information every 2 seconds if ((counter++ % (2 * 60f)) != 0) return; // figure out where the car is, and what the track surface is - var trackSurface = Enum.GetName(e.PlayerTrackSurface); - var trackSurfaceMaterial = Enum.GetName(e.PlayerTrackSurfaceMaterial); + var trackSurface = e.PlayerTrackSurface.ToString(); + var trackSurfaceMaterial = e.PlayerTrackSurfaceMaterial.ToString(); // engine warnings var engineWarnings = GetEngineWarnings(e.EngineWarnings); - // EngineWarnings,Boolean IsOnTrack,Int32 PlayerTrackSurface,Int32 PlayerTrackSurfaceMaterial,Int32 TrackWetness); var message = $"OnTrack:({e.IsOnTrack}), TrackSurface:({trackSurface}), TrackSurfaceMaterial:({trackSurfaceMaterial}), EngineWarnings:({engineWarnings})"; logger.LogInformation(message); } - string GetEngineWarnings(EngineWarnings engineWarnings) + string GetEngineWarnings(EngineWarnings? engineWarnings) { var warnings = new List(); - foreach (var flag in Enum.GetValues()) + if (engineWarnings.HasValue) { - // check if the flag is set - if (engineWarnings.HasFlag(flag)) + foreach (var flag in Enum.GetValues()) { - var flagName = Enum.GetName(flag); - if (!string.IsNullOrEmpty(flagName)) - warnings.Add(flagName); + // check if the flag is set + if ((engineWarnings.Value & flag) == flag) + { + var flagName = flag.ToString(); + if (!string.IsNullOrEmpty(flagName)) + warnings.Add(flagName); + } } } return string.Join(",", warnings); diff --git a/Samples/MinimalExample/MinimalExample.csproj b/Samples/MinimalExample/MinimalExample.csproj new file mode 100644 index 0000000..edc6580 --- /dev/null +++ b/Samples/MinimalExample/MinimalExample.csproj @@ -0,0 +1,15 @@ +๏ปฟ + + + Exe + net8.0 + enable + enable + + + + + + + + diff --git a/Samples/MinimalExample/Program.cs b/Samples/MinimalExample/Program.cs new file mode 100644 index 0000000..727d2da --- /dev/null +++ b/Samples/MinimalExample/Program.cs @@ -0,0 +1,58 @@ +using Microsoft.Extensions.Logging; +using SVappsLAB.iRacingTelemetrySDK; + +namespace MinimalExample +{ + // 1. Define the telemetry variables you want to track + [RequiredTelemetryVars([TelemetryVar.Speed, TelemetryVar.RPM])] + internal class Program + { + public static async Task Main(string[] args) + { + // 2. Create logger + var logger = LoggerFactory.Create(builder => builder.AddConsole()) + .CreateLogger("MinimalExample"); + + // 3. Choose data source + IBTOptions? ibtOptions = null; // null for live telemetry from iRacing + // = new IBTOptions("gt3_spa.ibt"); IBT filepath for file playback + ibtOptions = new IBTOptions(args[0]); + + // 4. Create telemetry client + await using var client = TelemetryClient.Create(logger, ibtOptions); + + // 5. Use cancellation token for proper shutdown + using var cts = new CancellationTokenSource(); + + // 6. Subscribe to all telemetryData streams using async delegate methods for simplified consumption + // note: SubscribeToAllStreams handles single-reader channel limitation internally - safe for multiple callbacks + var subscriptionTask = client.SubscribeToAllStreams( + onTelemetryUpdate: async telemetryData => + { + Console.WriteLine($"Speed: {telemetryData.Speed}, RPM: {telemetryData.RPM}"); + }, + onSessionInfoUpdate: async session => + { + var driverCount = session.DriverInfo?.Drivers?.Count ?? 0; + Console.WriteLine($"Drivers: {driverCount}"); + }, + onConnectStateChanged: async state => + { + Console.WriteLine($"Connection: {state}"); + }, + onError: async error => + { + Console.WriteLine($"Error: {error.Message}"); + }, + cancellationToken: cts.Token); + + // 7. Enable graceful shutdown with Ctrl+C + Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); }; + + // 8. Start monitoring and wait for completion + var monitorTask = client.Monitor(cts.Token); + + await Task.WhenAny(monitorTask, subscriptionTask); + } + } +} diff --git a/Samples/MinimalExample/Properties/launchSettings.json b/Samples/MinimalExample/Properties/launchSettings.json new file mode 100644 index 0000000..267b794 --- /dev/null +++ b/Samples/MinimalExample/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "MinimalExample": { + "commandName": "Project", + "commandLineArgs": "\"..\\..\\..\\..\\data\\formulair04_tsukuba 2kfull 2024-01-09 17-26-10.ibt\"" + } + } +} \ No newline at end of file diff --git a/Samples/SpeedRPMGear/Program.cs b/Samples/SpeedRPMGear/Program.cs index d91409c..4a0e756 100644 --- a/Samples/SpeedRPMGear/Program.cs +++ b/Samples/SpeedRPMGear/Program.cs @@ -11,44 +11,73 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; + * limitations under the License. **/ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using SVappsLAB.iRacingTelemetrySDK; namespace SpeedRPMGear { // these are the telemetry variables we want to track - [RequiredTelemetryVars(["gear", "isOnTrackCar", "rpm", "speed"])] + [RequiredTelemetryVars([TelemetryVar.Gear, TelemetryVar.IsOnTrackCar, TelemetryVar.RPM, TelemetryVar.Speed])] internal class Program { static async Task Main(string[] args) { - var counter = 0; - var logger = LoggerFactory - .Create(builder => builder - .SetMinimumLevel(LogLevel.Debug) - .AddConsole()) - .CreateLogger("logger"); + // Create host builder with dependency injection + var builder = Host.CreateDefaultBuilder(args) + .ConfigureServices((context, services) => + { + // Add metrics support + services.AddMetrics(); - // if you pass in a IBT filename, we'll use that, otherwise default to LIVE mode - IBTOptions? ibtOptions = null; - if (args.Length == 1) - ibtOptions = new IBTOptions(args[0]); + // Configure logging + services.AddLogging(logging => + { + logging.SetMinimumLevel(LogLevel.Debug); + logging.AddConsole(); + }); - logger.LogInformation("Press Ctrl-C to exit..."); + // Register TelemetryClient as a singleton + services.AddSingleton>(provider => + { + var logger = provider.GetRequiredService>(); + var meterFactory = provider.GetRequiredService(); + + // if you pass in a IBT filename, we'll use that, otherwise default to LIVE mode + IBTOptions? ibtOptions = null; + if (args.Length == 1) + ibtOptions = new IBTOptions(args[0]); + + var clientOptions = new ClientOptions { MeterFactory = meterFactory }; + return TelemetryClient.Create(logger, ibtOptions); + }); + }); - // create telemetry client - using var tc = TelemetryClient.Create(logger, ibtOptions); + using var host = builder.Build(); - // subscribe to telemetry updates - tc.OnTelemetryUpdate += OnTelemetryUpdate; + var logger = host.Services.GetRequiredService>(); + var tc = host.Services.GetRequiredService>(); + + var counter = 0; + logger.LogInformation("Press Ctrl-C to exit..."); // use this cancellation token to end processing using var cts = new CancellationTokenSource(); var cancellationToken = cts.Token; + // start telemetry consumption + var telemetryTask = Task.Run(async () => + { + await foreach (var data in tc.TelemetryData.WithCancellation(cancellationToken)) + { + OnTelemetryUpdate(data); + } + }, cancellationToken); + // start keyboard monitoring // - pause telemetry events when 'p' key is pressed // - resume telemetry events when 'r' key is pressed @@ -58,21 +87,21 @@ static async Task Main(string[] args) // start iRacing monitoring var monitorTask = tc.Monitor(cancellationToken); - // wait for either task to complete + // wait for any task to complete // - when 'live', the keyboard task (Ctrl-C) is most likely to complete first (before the iRacing session ends) // - when playing 'IBT' files, the monitoring task is most likely to complete first (at end-of-file) - await Task.WhenAny(keyboardTask, monitorTask); + await Task.WhenAny(keyboardTask, monitorTask, telemetryTask); // regardless of which task completes first, - // set the cancellation token so the other task can complete + // set the cancellation token so the other tasks can complete cts.Cancel(); // await for all tasks to complete - await Task.WhenAll(monitorTask, keyboardTask); + await Task.WhenAll(monitorTask, keyboardTask, telemetryTask); - // event handler - void OnTelemetryUpdate(object? sender, TelemetryData e) + // telemetry data handler + async Task OnTelemetryUpdate(TelemetryData e) { // to reduce logging, only log every 60th update (once a second) if ((counter++ % 60f) != 0) @@ -80,7 +109,7 @@ void OnTelemetryUpdate(object? sender, TelemetryData e) // convert speed from m/s to mph var mph = e.Speed * 2.23694f; - logger.LogInformation("gear: {gear}, rpm: {rpm}, speed: {speed}", e.Gear, e.RPM.ToString("F0"), mph.ToString("F0")); + logger.LogInformation("gear: {gear}, rpm: {rpm}, speed: {speed}", e.Gear, e.RPM?.ToString("F0"), mph?.ToString("F0")); } async Task MonitorKeyboardAsync() { diff --git a/Samples/SpeedRPMGear/SpeedRPMGear.csproj b/Samples/SpeedRPMGear/SpeedRPMGear.csproj index 1a5dc5e..c9aef6f 100644 --- a/Samples/SpeedRPMGear/SpeedRPMGear.csproj +++ b/Samples/SpeedRPMGear/SpeedRPMGear.csproj @@ -1,4 +1,4 @@ - +๏ปฟ Exe @@ -8,8 +8,10 @@ + + - + diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK.CodeGen/CodeGen.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK.CodeGen/CodeGen.cs index 7caa51f..127acdd 100644 --- a/Sdk/SVappsLAB.iRacingTelemetrySDK.CodeGen/CodeGen.cs +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK.CodeGen/CodeGen.cs @@ -11,7 +11,7 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; + * limitations under the License. **/ using System; @@ -28,8 +28,8 @@ namespace SVappsLAB.iRacingTelemetrySDK { - public record struct VarsToGenerate(VarType[] vars, (string name, Location location)[] duplicates); - public record struct VarType(string name, Type type, Location location); + public record struct VarsToGenerate(VarType[] vars, (TelemetryVar variable, Location location)[] duplicates); + public record struct VarType(TelemetryVar variable, Type type, Location location); [Generator(LanguageNames.CSharp)] public class CodeGenerator : IIncrementalGenerator @@ -44,9 +44,9 @@ namespace SVappsLAB.iRacingTelemetrySDK [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] internal sealed class RequiredTelemetryVarsAttribute : Attribute { - private readonly string[]? _vars; + private readonly TelemetryVar[]? _vars; - public RequiredTelemetryVarsAttribute(string[]? vars) + public RequiredTelemetryVarsAttribute(TelemetryVar[]? vars) { _vars = vars; } @@ -122,9 +122,9 @@ private bool predMatcher(SyntaxNode sn, CancellationToken _ct) return attr; } - private List<(string name, Location location)> ExtractVariableLocations(GeneratorAttributeSyntaxContext context, AttributeData attribute) + private List<(TelemetryVar variable, Location location)> ExtractVariableLocations(GeneratorAttributeSyntaxContext context, AttributeData attribute) { - var variableLocations = new List<(string name, Location location)>(); + var variableLocations = new List<(TelemetryVar variable, Location location)>(); for (int argIndex = 0; argIndex < attribute.ConstructorArguments.Length; argIndex++) { @@ -132,10 +132,11 @@ private bool predMatcher(SyntaxNode sn, CancellationToken _ct) for (int valueIndex = 0; valueIndex < arg.Values.Length; valueIndex++) { var value = arg.Values[valueIndex]; - if (value.Value?.ToString() is string varName && !string.IsNullOrEmpty(varName)) + if (value.Value is int enumValue && Enum.IsDefined(typeof(TelemetryVar), enumValue)) { + var telemetryVar = (TelemetryVar)enumValue; var location = GetVariableLocation(context, argIndex, valueIndex); - variableLocations.Add((varName, location)); + variableLocations.Add((telemetryVar, location)); } } } @@ -143,10 +144,10 @@ private bool predMatcher(SyntaxNode sn, CancellationToken _ct) return variableLocations; } - private (string name, Location location)[] FindDuplicateVariables(List<(string name, Location location)> variableLocations) + private (TelemetryVar variable, Location location)[] FindDuplicateVariables(List<(TelemetryVar variable, Location location)> variableLocations) { var duplicateGroups = variableLocations - .GroupBy(x => x.name, StringComparer.OrdinalIgnoreCase) + .GroupBy(x => x.variable) .Where(g => g.Count() > 1) .ToArray(); @@ -163,30 +164,30 @@ private bool predMatcher(SyntaxNode sn, CancellationToken _ct) return duplicatesWithLocations; } - private VarType[] ProcessVariables(List<(string name, Location location)> variableLocations) + private VarType[] ProcessVariables(List<(TelemetryVar variable, Location location)> variableLocations) { var varList = new VarType[variableLocations.Count]; for (int i = 0; i < variableLocations.Count; i++) { - var (name, location) = variableLocations[i]; - varList[i] = ProcessSingleVariable(name, location); + var (variable, location) = variableLocations[i]; + varList[i] = ProcessSingleVariable(variable, location); } return varList; } - private VarType ProcessSingleVariable(string rawVariableName, Location location) + private VarType ProcessSingleVariable(TelemetryVar variable, Location location) { - if (_iRacingData.Value.Vars.TryGetValue(rawVariableName, out var varItem)) + if (_iRacingData.Value.Vars.TryGetValue(variable, out var varItem)) { var type = GetVariableType(varItem); - return new VarType(varItem.Name, type, location); + return new VarType(variable, type, location); } else { IncrementCounter(TelemetryConstants.COUNTER_UNKNOWN_VARIABLES); - return new VarType(rawVariableName, typeof(Exception), location); + return new VarType(variable, typeof(Exception), location); } } @@ -257,20 +258,20 @@ private static void Execute(SourceProductionContext spc, VarsToGenerate? vars) } } - private static VarType[] FilterValidVariables(VarType[] values, (string name, Location location)[] duplicates) + private static VarType[] FilterValidVariables(VarType[] values, (TelemetryVar variable, Location location)[] duplicates) { - var duplicateNames = new HashSet(duplicates.Select(d => d.name), StringComparer.OrdinalIgnoreCase); - var seenNames = new HashSet(StringComparer.OrdinalIgnoreCase); + var duplicateVariables = new HashSet(duplicates.Select(d => d.variable)); + var seenVariables = new HashSet(); var validVariables = new List(); foreach (var variable in values) { // skip if it's a duplicate or unknown variable - if (duplicateNames.Contains(variable.name) || variable.type == typeof(Exception)) + if (duplicateVariables.Contains(variable.variable) || variable.type == typeof(Exception)) continue; - // skip if we've already seen this name (case-insensitive) - if (!seenNames.Add(variable.name)) + // skip if we've already seen this variable + if (!seenVariables.Add(variable.variable)) continue; validVariables.Add(variable); @@ -279,11 +280,11 @@ private static VarType[] FilterValidVariables(VarType[] values, (string name, Lo return validVariables.ToArray(); } - private static void ReportDuplicateVariableDiagnostics(SourceProductionContext spc, (string name, Location location)[] duplicates) + private static void ReportDuplicateVariableDiagnostics(SourceProductionContext spc, (TelemetryVar variable, Location location)[] duplicates) { - foreach (var (name, location) in duplicates) + foreach (var (variable, location) in duplicates) { - var diagnostic = CreateDuplicateVariableDiagnostic(name, location); + var diagnostic = CreateDuplicateVariableDiagnostic(variable.ToString(), location); spc.ReportDiagnostic(diagnostic); } } @@ -294,7 +295,7 @@ private static void ReportUnknownVariableDiagnostics(SourceProductionContext spc { if (item.type == typeof(Exception)) { - var diagnostic = CreateUnknownVariableDiagnostic(item.name, item.location); + var diagnostic = CreateUnknownVariableDiagnostic(item.variable.ToString(), item.location); spc.ReportDiagnostic(diagnostic); } } @@ -308,11 +309,12 @@ private static string GenerateVariableList(VarType[] values) var item = values[i]; if (i > 0) - sb.Append(","); + sb.AppendLine(); - var friendlyTypeName = GetFriendlyTypeName(item.type); - var varDeclaration = $"{friendlyTypeName} {item.name}"; - sb.Append(varDeclaration); + // Generate property declarations with nullable types + var nullableFriendlyTypeName = GetNullableFriendlyTypeName(item.type); + var propertyDeclaration = $" public {nullableFriendlyTypeName} {item.variable} {{ get; init; }}"; + sb.Append(propertyDeclaration); } return sb.ToString(); } @@ -320,6 +322,7 @@ private static string GenerateVariableList(VarType[] values) private static string GenerateSourceCode(string varList) { return $$""" + #nullable enable using System; namespace SVappsLAB.iRacingTelemetrySDK @@ -327,8 +330,12 @@ namespace SVappsLAB.iRacingTelemetrySDK /// /// Represents iRacing telemetry data with strongly-typed properties. /// This record is generated based on the RequiredTelemetryVarsAttribute usage. + /// Properties return null when the corresponding telemetry variable is not available. /// - public record struct TelemetryData({{varList}}); + public record struct TelemetryData + { + {{varList}} + } } """; } @@ -478,6 +485,12 @@ private static string GetFriendlyTypeName(Type type) return TypeNameMappings.TryGetValue(type.Name, out var friendlyName) ? friendlyName : type.Name; } + private static string GetNullableFriendlyTypeName(Type type) + { + var baseName = GetFriendlyTypeName(type); + return $"{baseName}?"; + } + // telemetry helpers private static void IncrementCounter(string key, long increment = 1) => _performanceCounters.AddOrUpdate(key, increment, (k, v) => v + increment); private static void RecordTime(string key, TimeSpan elapsed) => _performanceTimes.AddOrUpdate(key, elapsed, (k, v) => v + elapsed); diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK.CodeGen/iRacingData.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK.CodeGen/iRacingData.cs index d609514..74dcab9 100644 --- a/Sdk/SVappsLAB.iRacingTelemetrySDK.CodeGen/iRacingData.cs +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK.CodeGen/iRacingData.cs @@ -11,7 +11,7 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; + * limitations under the License. **/ /* @@ -20,7 +20,6 @@ * **/ -using System; using System.Collections.Generic; namespace SVappsLAB.iRacingTelemetrySDK @@ -30,403 +29,412 @@ public class iRacingVars { // a dictionary of all known variables from the iRacing SDK - both live sessions and offline ibt files // this data is used by the code generation to generate typed variables for your code. - public Dictionary Vars = new(StringComparer.OrdinalIgnoreCase) + public Dictionary Vars = new() { - { "AirDensity", new VarItem("AirDensity", 4, 1, false, "Density of air at start/finish line", "kg/m^3") }, - { "AirPressure", new VarItem("AirPressure", 4, 1, false, "Pressure of air at start/finish line", "Pa") }, - { "AirTemp", new VarItem("AirTemp", 4, 1, false, "Temperature of air at start/finish line", "C") }, - { "Alt", new VarItem("Alt", 4, 1, false, "Altitude in meters", "m") }, - { "Brake", new VarItem("Brake", 4, 1, false, "0=brake released to 1=max pedal force", "%") }, - { "BrakeABSactive", new VarItem("BrakeABSactive", 1, 1, false, "true if abs is currently reducing brake force pressure", "") }, - { "BrakeABScutPct", new VarItem("BrakeABScutPct", 4, 1, false, "Percent of brake force reduction caused by ABS system", "%") }, - { "BrakeRaw", new VarItem("BrakeRaw", 4, 1, false, "Raw brake input 0=brake released to 1=max pedal force", "%") }, - { "CamCameraNumber", new VarItem("CamCameraNumber", 2, 1, false, "Active camera number", "") }, - { "CamCameraState", new VarItem("CamCameraState", 3, 1, false, "State of camera system", "CameraState") }, - { "CamCarIdx", new VarItem("CamCarIdx", 2, 1, false, "Active camera's focus car index", "") }, - { "CamGroupNumber", new VarItem("CamGroupNumber", 2, 1, false, "Active camera group number", "") }, - { "CarDistAhead", new VarItem("CarDistAhead", 4, 1, false, "Distance to first car in front of player in meters", "m") }, - { "CarDistBehind", new VarItem("CarDistBehind", 4, 1, false, "Distance to first car behind player in meters", "m") }, - { "CarIdxBestLapNum", new VarItem("CarIdxBestLapNum", 2, 64, false, "Cars best lap number", "") }, - { "CarIdxBestLapTime", new VarItem("CarIdxBestLapTime", 4, 64, false, "Cars best lap time", "s") }, - { "CarIdxClass", new VarItem("CarIdxClass", 2, 64, false, "Cars class id by car index", "") }, - { "CarIdxClassPosition", new VarItem("CarIdxClassPosition", 2, 64, false, "Cars class position in race by car index", "") }, - { "CarIdxEstTime", new VarItem("CarIdxEstTime", 4, 64, false, "Estimated time to reach current location on track", "s") }, - { "CarIdxF2Time", new VarItem("CarIdxF2Time", 4, 64, false, "Race time behind leader or fastest lap time otherwise", "s") }, - { "CarIdxFastRepairsUsed", new VarItem("CarIdxFastRepairsUsed", 2, 64, false, "How many fast repairs each car has used", "") }, - { "CarIdxGear", new VarItem("CarIdxGear", 2, 64, false, "-1=reverse 0=neutral 1..n=current gear by car index", "") }, - { "CarIdxLap", new VarItem("CarIdxLap", 2, 64, false, "Laps started by car index", "") }, - { "CarIdxLapCompleted", new VarItem("CarIdxLapCompleted", 2, 64, false, "Laps completed by car index", "") }, - { "CarIdxLapDistPct", new VarItem("CarIdxLapDistPct", 4, 64, false, "Percentage distance around lap by car index", "%") }, - { "CarIdxLastLapTime", new VarItem("CarIdxLastLapTime", 4, 64, false, "Cars last lap time", "s") }, - { "CarIdxOnPitRoad", new VarItem("CarIdxOnPitRoad", 1, 64, false, "On pit road between the cones by car index", "") }, - { "CarIdxP2P_Count", new VarItem("CarIdxP2P_Count", 2, 64, false, "Push2Pass count of usage (or remaining in Race)", "") }, - { "CarIdxP2P_Status", new VarItem("CarIdxP2P_Status", 1, 64, false, "Push2Pass active or not", "") }, - { "CarIdxPaceFlags", new VarItem("CarIdxPaceFlags", 3, 64, false, "Pacing status flags for each car", "PaceFlags") }, - { "CarIdxPaceLine", new VarItem("CarIdxPaceLine", 2, 64, false, "What line cars are pacing in or -1 if not pacing", "") }, - { "CarIdxPaceRow", new VarItem("CarIdxPaceRow", 2, 64, false, "What row cars are pacing in or -1 if not pacing", "") }, - { "CarIdxPosition", new VarItem("CarIdxPosition", 2, 64, false, "Cars position in race by car index", "") }, - { "CarIdxQualTireCompound", new VarItem("CarIdxQualTireCompound", 2, 64, false, "Cars Qual tire compound", "") }, - { "CarIdxQualTireCompoundLocked", new VarItem("CarIdxQualTireCompoundLocked", 1, 64, false, "Cars Qual tire compound is locked-in", "") }, - { "CarIdxRPM", new VarItem("CarIdxRPM", 4, 64, false, "Engine rpm by car index", "revs/min") }, - { "CarIdxSessionFlags", new VarItem("CarIdxSessionFlags", 3, 64, false, "Session flags for each player", "SessionFlags") }, - { "CarIdxSteer", new VarItem("CarIdxSteer", 4, 64, false, "Steering wheel angle by car index", "rad") }, - { "CarIdxTireCompound", new VarItem("CarIdxTireCompound", 2, 64, false, "Cars current tire compound", "") }, - { "CarIdxTrackSurface", new VarItem("CarIdxTrackSurface", 2, 64, false, "Track surface type by car index", "TrackLocation") }, - { "CarIdxTrackSurfaceMaterial", new VarItem("CarIdxTrackSurfaceMaterial", 2, 64, false, "Track surface material type by car index", "TrackSurface") }, - { "CarLeftRight", new VarItem("CarLeftRight", 2, 1, false, "Notify if car is to the left or right of driver", "CarLeftRight") }, - { "CFshockDefl", new VarItem("CFshockDefl", 4, 1, false, "CF shock deflection", "m") }, - { "CFshockDefl_ST", new VarItem("CFshockDefl_ST", 4, 6, true, "CF shock deflection at 360 Hz", "m") }, - { "CFshockVel", new VarItem("CFshockVel", 4, 1, false, "CF shock velocity", "m/s") }, - { "CFshockVel_ST", new VarItem("CFshockVel_ST", 4, 6, true, "CF shock velocity at 360 Hz", "m/s") }, - { "ChanAvgLatency", new VarItem("ChanAvgLatency", 4, 1, false, "Communications average latency", "s") }, - { "ChanClockSkew", new VarItem("ChanClockSkew", 4, 1, false, "Communications server clock skew", "s") }, - { "ChanLatency", new VarItem("ChanLatency", 4, 1, false, "Communications latency", "s") }, - { "ChanPartnerQuality", new VarItem("ChanPartnerQuality", 4, 1, false, "Partner communications quality", "%") }, - { "ChanQuality", new VarItem("ChanQuality", 4, 1, false, "Communications quality", "%") }, - { "Clutch", new VarItem("Clutch", 4, 1, false, "0=disengaged to 1=fully engaged", "%") }, - { "ClutchRaw", new VarItem("ClutchRaw", 4, 1, false, "Raw clutch input 0=disengaged to 1=fully engaged", "%") }, - { "CpuUsageBG", new VarItem("CpuUsageBG", 4, 1, false, "Percent of available tim bg thread took with a 1 sec avg", "%") }, - { "CpuUsageFG", new VarItem("CpuUsageFG", 4, 1, false, "Percent of available tim fg thread took with a 1 sec avg", "%") }, - { "CRshockDefl", new VarItem("CRshockDefl", 4, 1, false, "CR shock deflection", "m") }, - { "CRshockDefl_ST", new VarItem("CRshockDefl_ST", 4, 6, true, "CR shock deflection at 360 Hz", "m") }, - { "CRshockVel", new VarItem("CRshockVel", 4, 1, false, "CR shock velocity", "m/s") }, - { "CRshockVel_ST", new VarItem("CRshockVel_ST", 4, 6, true, "CR shock velocity at 360 Hz", "m/s") }, - { "dcABS", new VarItem("dcABS", 4, 1, false, "In car abs adjustment", "") }, - { "dcAntiRollFront", new VarItem("dcAntiRollFront", 4, 1, false, "In car front anti roll bar adjustment", "") }, - { "dcAntiRollRear", new VarItem("dcAntiRollRear", 4, 1, false, "In car rear anti roll bar adjustment", "") }, - { "dcBrakeBias", new VarItem("dcBrakeBias", 4, 1, false, "In car brake bias adjustment", "") }, - { "dcDashPage", new VarItem("dcDashPage", 4, 1, false, "In car dash display page adjustment", "") }, - { "DCDriversSoFar", new VarItem("DCDriversSoFar", 2, 1, false, "Number of team drivers who have run a stint", "") }, - { "dcFuelMixture", new VarItem("dcFuelMixture", 4, 1, false, "In car fuel mixture adjustment", "") }, - { "dcHeadlightFlash", new VarItem("dcHeadlightFlash", 1, 1, false, "In car headlight flash control active", "") }, - { "DCLapStatus", new VarItem("DCLapStatus", 2, 1, false, "Status of driver change lap requirements", "") }, - { "dcLaunchRPM", new VarItem("dcLaunchRPM", 4, 1, false, "In car launch rpm adjustment", "") }, - { "dcPitSpeedLimiterToggle", new VarItem("dcPitSpeedLimiterToggle", 1, 1, false, "In car traction control active", "") }, - { "dcPushToPass", new VarItem("dcPushToPass", 1, 1, false, "In car trigger push to pass", "") }, - { "dcRFBrakeAttachedToggle", new VarItem("dcRFBrakeAttachedToggle", 1, 1, false, "In car Right Front Brake attached(1) or detached(0)", "") }, - { "dcStarter", new VarItem("dcStarter", 1, 1, false, "In car trigger car starter", "") }, - { "dcTearOffVisor", new VarItem("dcTearOffVisor", 1, 1, false, "In car tear off visor film", "") }, - { "dcThrottleShape", new VarItem("dcThrottleShape", 4, 1, false, "In car throttle shape adjustment", "") }, - { "dcToggleWindshieldWipers", new VarItem("dcToggleWindshieldWipers", 1, 1, false, "In car turn wipers on or off", "") }, - { "dcTractionControl", new VarItem("dcTractionControl", 4, 1, false, "In car traction control adjustment", "") }, - { "dcTriggerWindshieldWipers", new VarItem("dcTriggerWindshieldWipers", 1, 1, false, "In car momentarily turn on wipers", "") }, - { "dcWeightJackerRight", new VarItem("dcWeightJackerRight", 4, 1, false, "In car right wedge/weight jacker adjustment", "") }, - { "DisplayUnits", new VarItem("DisplayUnits", 2, 1, false, "Default units for the user interface 0 = english 1 = metric", "") }, - { "dpFastRepair", new VarItem("dpFastRepair", 4, 1, false, "Pitstop fast repair set", "") }, - { "dpFuelAddKg", new VarItem("dpFuelAddKg", 4, 1, false, "Pitstop fuel add amount", "kg") }, - { "dpFuelAutoFillActive", new VarItem("dpFuelAutoFillActive", 4, 1, false, "Pitstop auto fill fuel next stop flag", "") }, - { "dpFuelAutoFillEnabled", new VarItem("dpFuelAutoFillEnabled", 4, 1, false, "Pitstop auto fill fuel system enabled", "") }, - { "dpFuelFill", new VarItem("dpFuelFill", 4, 1, false, "Pitstop fuel fill flag", "") }, - { "dpLFTireChange", new VarItem("dpLFTireChange", 4, 1, false, "Pitstop lf tire change request", "") }, - { "dpLFTireColdPress", new VarItem("dpLFTireColdPress", 4, 1, false, "Pitstop lf tire cold pressure adjustment", "Pa") }, - { "dpLRTireChange", new VarItem("dpLRTireChange", 4, 1, false, "Pitstop lr tire change request", "") }, - { "dpLRTireColdPress", new VarItem("dpLRTireColdPress", 4, 1, false, "Pitstop lr tire cold pressure adjustment", "Pa") }, - { "dpLTireChange", new VarItem("dpLTireChange", 4, 1, false, "Pitstop left tire change request", "") }, - { "dpRFTireChange", new VarItem("dpRFTireChange", 4, 1, false, "Pitstop rf tire change request", "") }, - { "dpRFTireColdPress", new VarItem("dpRFTireColdPress", 4, 1, false, "Pitstop rf cold tire pressure adjustment", "Pa") }, - { "dpRRTireChange", new VarItem("dpRRTireChange", 4, 1, false, "Pitstop rr tire change request", "") }, - { "dpRRTireColdPress", new VarItem("dpRRTireColdPress", 4, 1, false, "Pitstop rr cold tire pressure adjustment", "Pa") }, - { "dpRTireChange", new VarItem("dpRTireChange", 4, 1, false, "Pitstop right tire change request", "") }, - { "dpTireChange", new VarItem("dpTireChange", 4, 1, false, "Pitstop all tire change request", "") }, - { "dpWindshieldTearoff", new VarItem("dpWindshieldTearoff", 4, 1, false, "Pitstop windshield tearoff", "") }, - { "dpWingFront", new VarItem("dpWingFront", 4, 1, false, "Pitstop front wing adjustment", "") }, - { "dpWingRear", new VarItem("dpWingRear", 4, 1, false, "Pitstop rear wing adjustment", "") }, - { "DriverMarker", new VarItem("DriverMarker", 1, 1, false, "Driver activated flag", "") }, - { "Engine0_RPM", new VarItem("Engine0_RPM", 4, 1, false, "Engine0Engine rpm", "revs/min") }, - { "EngineWarnings", new VarItem("EngineWarnings", 3, 1, false, "Bitfield for warning lights", "EngineWarnings") }, - { "EnterExitReset", new VarItem("EnterExitReset", 2, 1, false, "Indicate action the reset key will take 0 enter 1 exit 2 reset", "") }, - { "FastRepairAvailable", new VarItem("FastRepairAvailable", 2, 1, false, "How many fast repairs left 255 is unlimited", "") }, - { "FastRepairUsed", new VarItem("FastRepairUsed", 2, 1, false, "How many fast repairs used so far", "") }, - { "FogLevel", new VarItem("FogLevel", 4, 1, false, "Fog level at start/finish line", "%") }, - { "FrameRate", new VarItem("FrameRate", 4, 1, false, "Average frames per second", "fps") }, - { "FrontTireSetsAvailable", new VarItem("FrontTireSetsAvailable", 2, 1, false, "How many front tire sets are remaining 255 is unlimited", "") }, - { "FrontTireSetsUsed", new VarItem("FrontTireSetsUsed", 2, 1, false, "How many front tire sets used so far", "") }, - { "FuelLevel", new VarItem("FuelLevel", 4, 1, false, "Liters of fuel remaining", "l") }, - { "FuelLevelPct", new VarItem("FuelLevelPct", 4, 1, false, "Percent fuel remaining", "%") }, - { "FuelPress", new VarItem("FuelPress", 4, 1, false, "Engine fuel pressure", "bar") }, - { "FuelUsePerHour", new VarItem("FuelUsePerHour", 4, 1, false, "Engine fuel used instantaneous", "kg/h") }, - { "Gear", new VarItem("Gear", 2, 1, false, "-1=reverse 0=neutral 1..n=current gear", "") }, - { "GpuUsage", new VarItem("GpuUsage", 4, 1, false, "Percent of available tim gpu took with a 1 sec avg", "%") }, - { "HandbrakeRaw", new VarItem("HandbrakeRaw", 4, 1, false, "Raw handbrake input 0=handbrake released to 1=max force", "%") }, - { "IsDiskLoggingActive", new VarItem("IsDiskLoggingActive", 1, 1, false, "0=disk based telemetry file not being written 1=being written", "") }, - { "IsDiskLoggingEnabled", new VarItem("IsDiskLoggingEnabled", 1, 1, false, "0=disk based telemetry turned off 1=turned on", "") }, - { "IsGarageVisible", new VarItem("IsGarageVisible", 1, 1, false, "1=Garage screen is visible", "") }, - { "IsInGarage", new VarItem("IsInGarage", 1, 1, false, "1=Car in garage physics running", "") }, - { "IsOnTrack", new VarItem("IsOnTrack", 1, 1, false, "1=Car on track physics running with player in car", "") }, - { "IsOnTrackCar", new VarItem("IsOnTrackCar", 1, 1, false, "1=Car on track physics running", "") }, - { "IsReplayPlaying", new VarItem("IsReplayPlaying", 1, 1, false, "0=replay not playing 1=replay playing", "") }, - { "Lap", new VarItem("Lap", 2, 1, false, "Laps started count", "") }, - { "LapBestLap", new VarItem("LapBestLap", 2, 1, false, "Players best lap number", "") }, - { "LapBestLapTime", new VarItem("LapBestLapTime", 4, 1, false, "Players best lap time", "s") }, - { "LapBestNLapLap", new VarItem("LapBestNLapLap", 2, 1, false, "Player last lap in best N average lap time", "") }, - { "LapBestNLapTime", new VarItem("LapBestNLapTime", 4, 1, false, "Player best N average lap time", "s") }, - { "LapCompleted", new VarItem("LapCompleted", 2, 1, false, "Laps completed count", "") }, - { "LapCurrentLapTime", new VarItem("LapCurrentLapTime", 4, 1, false, "Estimate of players current lap time as shown in F3 box", "s") }, - { "LapDeltaToBestLap", new VarItem("LapDeltaToBestLap", 4, 1, false, "Delta time for best lap", "s") }, - { "LapDeltaToBestLap_DD", new VarItem("LapDeltaToBestLap_DD", 4, 1, false, "Rate of change of delta time for best lap", "s/s") }, - { "LapDeltaToBestLap_OK", new VarItem("LapDeltaToBestLap_OK", 1, 1, false, "Delta time for best lap is valid", "") }, - { "LapDeltaToOptimalLap", new VarItem("LapDeltaToOptimalLap", 4, 1, false, "Delta time for optimal lap", "s") }, - { "LapDeltaToOptimalLap_DD", new VarItem("LapDeltaToOptimalLap_DD", 4, 1, false, "Rate of change of delta time for optimal lap", "s/s") }, - { "LapDeltaToOptimalLap_OK", new VarItem("LapDeltaToOptimalLap_OK", 1, 1, false, "Delta time for optimal lap is valid", "") }, - { "LapDeltaToSessionBestLap", new VarItem("LapDeltaToSessionBestLap", 4, 1, false, "Delta time for session best lap", "s") }, - { "LapDeltaToSessionBestLap_DD", new VarItem("LapDeltaToSessionBestLap_DD", 4, 1, false, "Rate of change of delta time for session best lap", "s/s") }, - { "LapDeltaToSessionBestLap_OK", new VarItem("LapDeltaToSessionBestLap_OK", 1, 1, false, "Delta time for session best lap is valid", "") }, - { "LapDeltaToSessionLastlLap", new VarItem("LapDeltaToSessionLastlLap", 4, 1, false, "Delta time for session last lap", "s") }, - { "LapDeltaToSessionLastlLap_DD", new VarItem("LapDeltaToSessionLastlLap_DD", 4, 1, false, "Rate of change of delta time for session last lap", "s/s") }, - { "LapDeltaToSessionLastlLap_OK", new VarItem("LapDeltaToSessionLastlLap_OK", 1, 1, false, "Delta time for session last lap is valid", "") }, - { "LapDeltaToSessionOptimalLap", new VarItem("LapDeltaToSessionOptimalLap", 4, 1, false, "Delta time for session optimal lap", "s") }, - { "LapDeltaToSessionOptimalLap_DD", new VarItem("LapDeltaToSessionOptimalLap_DD", 4, 1, false, "Rate of change of delta time for session optimal lap", "s/s") }, - { "LapDeltaToSessionOptimalLap_OK", new VarItem("LapDeltaToSessionOptimalLap_OK", 1, 1, false, "Delta time for session optimal lap is valid", "") }, - { "LapDist", new VarItem("LapDist", 4, 1, false, "Meters traveled from S/F this lap", "m") }, - { "LapDistPct", new VarItem("LapDistPct", 4, 1, false, "Percentage distance around lap", "%") }, - { "LapLasNLapSeq", new VarItem("LapLasNLapSeq", 2, 1, false, "Player num consecutive clean laps completed for N average", "") }, - { "LapLastLapTime", new VarItem("LapLastLapTime", 4, 1, false, "Players last lap time", "s") }, - { "LapLastNLapTime", new VarItem("LapLastNLapTime", 4, 1, false, "Player last N average lap time", "s") }, - { "Lat", new VarItem("Lat", 5, 1, false, "Latitude in decimal degrees", "deg") }, - { "LatAccel", new VarItem("LatAccel", 4, 1, false, "Lateral acceleration (including gravity)", "m/s^2") }, - { "LatAccel_ST", new VarItem("LatAccel_ST", 4, 6, true, "Lateral acceleration (including gravity) at 360 Hz", "m/s^2") }, - { "LeftTireSetsAvailable", new VarItem("LeftTireSetsAvailable", 2, 1, false, "How many left tire sets are remaining 255 is unlimited", "") }, - { "LeftTireSetsUsed", new VarItem("LeftTireSetsUsed", 2, 1, false, "How many left tire sets used so far", "") }, - { "LFbrakeLinePress", new VarItem("LFbrakeLinePress", 4, 1, false, "LF brake line pressure", "bar") }, - { "LFcoldPressure", new VarItem("LFcoldPressure", 4, 1, false, "LF tire cold pressure as set in the garage", "kPa") }, - { "LFodometer", new VarItem("LFodometer", 4, 1, false, "LF distance tire traveled since being placed on car", "m") }, - { "LFpressure", new VarItem("LFpressure", 4, 1, false, "LF tire pressure", "kPa") }, - { "LFrideHeight", new VarItem("LFrideHeight", 4, 1, false, "LF ride height", "m") }, - { "LFshockDefl", new VarItem("LFshockDefl", 4, 1, false, "LF shock deflection", "m") }, - { "LFshockDefl_ST", new VarItem("LFshockDefl_ST", 4, 6, true, "LF shock deflection at 360 Hz", "m") }, - { "LFshockVel", new VarItem("LFshockVel", 4, 1, false, "LF shock velocity", "m/s") }, - { "LFshockVel_ST", new VarItem("LFshockVel_ST", 4, 6, true, "LF shock velocity at 360 Hz", "m/s") }, - { "LFSHshockDefl", new VarItem("LFSHshockDefl", 4, 1, false, "LFSH shock deflection", "m") }, - { "LFSHshockDefl_ST", new VarItem("LFSHshockDefl_ST", 4, 6, true, "LFSH shock deflection at 360 Hz", "m") }, - { "LFSHshockVel", new VarItem("LFSHshockVel", 4, 1, false, "LFSH shock velocity", "m/s") }, - { "LFSHshockVel_ST", new VarItem("LFSHshockVel_ST", 4, 6, true, "LFSH shock velocity at 360 Hz", "m/s") }, - { "LFspeed", new VarItem("LFspeed", 4, 1, false, "LF wheel speed", "m/s") }, - { "LFtempCL", new VarItem("LFtempCL", 4, 1, false, "LF tire left carcass temperature", "C") }, - { "LFtempCM", new VarItem("LFtempCM", 4, 1, false, "LF tire middle carcass temperature", "C") }, - { "LFtempCR", new VarItem("LFtempCR", 4, 1, false, "LF tire right carcass temperature", "C") }, - { "LFtempL", new VarItem("LFtempL", 4, 1, false, "LF tire left surface temperature", "C") }, - { "LFtempM", new VarItem("LFtempM", 4, 1, false, "LF tire middle surface temperature", "C") }, - { "LFtempR", new VarItem("LFtempR", 4, 1, false, "LF tire right surface temperature", "C") }, - { "LFTiresAvailable", new VarItem("LFTiresAvailable", 2, 1, false, "How many left front tires are remaining 255 is unlimited", "") }, - { "LFTiresUsed", new VarItem("LFTiresUsed", 2, 1, false, "How many left front tires used so far", "") }, - { "LFwearL", new VarItem("LFwearL", 4, 1, false, "LF tire left percent tread remaining", "%") }, - { "LFwearM", new VarItem("LFwearM", 4, 1, false, "LF tire middle percent tread remaining", "%") }, - { "LFwearR", new VarItem("LFwearR", 4, 1, false, "LF tire right percent tread remaining", "%") }, - { "LoadNumTextures", new VarItem("LoadNumTextures", 1, 1, false, "True if the car_num texture will be loaded", "") }, - { "Lon", new VarItem("Lon", 5, 1, false, "Longitude in decimal degrees", "deg") }, - { "LongAccel", new VarItem("LongAccel", 4, 1, false, "Longitudinal acceleration (including gravity)", "m/s^2") }, - { "LongAccel_ST", new VarItem("LongAccel_ST", 4, 6, true, "Longitudinal acceleration (including gravity) at 360 Hz", "m/s^2") }, - { "LR2shockDefl", new VarItem("LR2shockDefl", 4, 1, false, "LR2 shock deflection", "m") }, - { "LR2shockDefl_ST", new VarItem("LR2shockDefl_ST", 4, 6, true, "LR2 shock deflection at 360 Hz", "m") }, - { "LR2shockVel", new VarItem("LR2shockVel", 4, 1, false, "LR2 shock velocity", "m/s") }, - { "LR2shockVel_ST", new VarItem("LR2shockVel_ST", 4, 6, true, "LR2 shock velocity at 360 Hz", "m/s") }, - { "LRbrakeLinePress", new VarItem("LRbrakeLinePress", 4, 1, false, "LR brake line pressure", "bar") }, - { "LRcoldPressure", new VarItem("LRcoldPressure", 4, 1, false, "LR tire cold pressure as set in the garage", "kPa") }, - { "LRodometer", new VarItem("LRodometer", 4, 1, false, "LR distance tire traveled since being placed on car", "m") }, - { "LRpressure", new VarItem("LRpressure", 4, 1, false, "LR tire pressure", "kPa") }, - { "LRrideHeight", new VarItem("LRrideHeight", 4, 1, false, "LR ride height", "m") }, - { "LRshockDefl", new VarItem("LRshockDefl", 4, 1, false, "LR shock deflection", "m") }, - { "LRshockDefl_ST", new VarItem("LRshockDefl_ST", 4, 6, true, "LR shock deflection at 360 Hz", "m") }, - { "LRshockVel", new VarItem("LRshockVel", 4, 1, false, "LR shock velocity", "m/s") }, - { "LRshockVel_ST", new VarItem("LRshockVel_ST", 4, 6, true, "LR shock velocity at 360 Hz", "m/s") }, - { "LRspeed", new VarItem("LRspeed", 4, 1, false, "LR wheel speed", "m/s") }, - { "LRtempCL", new VarItem("LRtempCL", 4, 1, false, "LR tire left carcass temperature", "C") }, - { "LRtempCM", new VarItem("LRtempCM", 4, 1, false, "LR tire middle carcass temperature", "C") }, - { "LRtempCR", new VarItem("LRtempCR", 4, 1, false, "LR tire right carcass temperature", "C") }, - { "LRtempL", new VarItem("LRtempL", 4, 1, false, "LR tire left surface temperature", "C") }, - { "LRtempM", new VarItem("LRtempM", 4, 1, false, "LR tire middle surface temperature", "C") }, - { "LRtempR", new VarItem("LRtempR", 4, 1, false, "LR tire right surface temperature", "C") }, - { "LRTiresAvailable", new VarItem("LRTiresAvailable", 2, 1, false, "How many left rear tires are remaining 255 is unlimited", "") }, - { "LRTiresUsed", new VarItem("LRTiresUsed", 2, 1, false, "How many left rear tires used so far", "") }, - { "LRwearL", new VarItem("LRwearL", 4, 1, false, "LR tire left percent tread remaining", "%") }, - { "LRwearM", new VarItem("LRwearM", 4, 1, false, "LR tire middle percent tread remaining", "%") }, - { "LRwearR", new VarItem("LRwearR", 4, 1, false, "LR tire right percent tread remaining", "%") }, - { "ManifoldPress", new VarItem("ManifoldPress", 4, 1, false, "Engine manifold pressure", "bar") }, - { "ManualBoost", new VarItem("ManualBoost", 1, 1, false, "Hybrid manual boost state", "") }, - { "ManualNoBoost", new VarItem("ManualNoBoost", 1, 1, false, "Hybrid manual no boost state", "") }, - { "MemPageFaultSec", new VarItem("MemPageFaultSec", 4, 1, false, "Memory page faults per second", "") }, - { "MemSoftPageFaultSec", new VarItem("MemSoftPageFaultSec", 4, 1, false, "Memory soft page faults per second", "") }, - { "OilLevel", new VarItem("OilLevel", 4, 1, false, "Engine oil level", "l") }, - { "OilPress", new VarItem("OilPress", 4, 1, false, "Engine oil pressure", "bar") }, - { "OilTemp", new VarItem("OilTemp", 4, 1, false, "Engine oil temperature", "C") }, - { "OkToReloadTextures", new VarItem("OkToReloadTextures", 1, 1, false, "True if it is ok to reload car textures at this time", "") }, - { "OnPitRoad", new VarItem("OnPitRoad", 1, 1, false, "Is the player car on pit road between the cones", "") }, - { "P2P_Count", new VarItem("P2P_Count", 2, 1, false, "Push2Pass count of usage (or remaining in Race) on your car", "") }, - { "P2P_Status", new VarItem("P2P_Status", 1, 1, false, "Push2Pass active or not on your car", "") }, - { "PaceMode", new VarItem("PaceMode", 2, 1, false, "Are we pacing or not", "PaceMode") }, - { "Pitch", new VarItem("Pitch", 4, 1, false, "Pitch orientation", "rad") }, - { "PitchRate", new VarItem("PitchRate", 4, 1, false, "Pitch rate", "rad/s") }, - { "PitchRate_ST", new VarItem("PitchRate_ST", 4, 6, true, "Pitch rate at 360 Hz", "rad/s") }, - { "PitOptRepairLeft", new VarItem("PitOptRepairLeft", 4, 1, false, "Time left for optional repairs if repairs are active", "s") }, - { "PitRepairLeft", new VarItem("PitRepairLeft", 4, 1, false, "Time left for mandatory pit repairs if repairs are active", "s") }, - { "PitServiceFlags", new VarItem("PitServiceFlags", 3, 1, false, "Bitfield of pit service checkboxes", "PitServiceFlags") }, - { "PitsOpen", new VarItem("PitsOpen", 1, 1, false, "True if pit stop is allowed for the current player", "") }, - { "PitstopActive", new VarItem("PitstopActive", 1, 1, false, "Is the player getting pit stop service", "") }, - { "PitSvFlags", new VarItem("PitSvFlags", 3, 1, false, "Bitfield of pit service checkboxes", "PitSvFlags") }, - { "PitSvFuel", new VarItem("PitSvFuel", 4, 1, false, "Pit service fuel add amount", "l or kWh") }, - { "PitSvLFP", new VarItem("PitSvLFP", 4, 1, false, "Pit service left front tire pressure", "kPa") }, - { "PitSvLRP", new VarItem("PitSvLRP", 4, 1, false, "Pit service left rear tire pressure", "kPa") }, - { "PitSvRFP", new VarItem("PitSvRFP", 4, 1, false, "Pit service right front tire pressure", "kPa") }, - { "PitSvRRP", new VarItem("PitSvRRP", 4, 1, false, "Pit service right rear tire pressure", "kPa") }, - { "PitSvTireCompound", new VarItem("PitSvTireCompound", 2, 1, false, "Pit service pending tire compound", "") }, - { "PlayerCarClass", new VarItem("PlayerCarClass", 2, 1, false, "Player car class id", "") }, - { "PlayerCarClassPosition", new VarItem("PlayerCarClassPosition", 2, 1, false, "Players class position in race", "") }, - { "PlayerCarDriverIncidentCount", new VarItem("PlayerCarDriverIncidentCount", 2, 1, false, "Teams current drivers incident count for this session", "") }, - { "PlayerCarDryTireSetLimit", new VarItem("PlayerCarDryTireSetLimit", 2, 1, false, "Players dry tire set limit", "") }, - { "PlayerCarIdx", new VarItem("PlayerCarIdx", 2, 1, false, "Players carIdx", "") }, - { "PlayerCarInPitStall", new VarItem("PlayerCarInPitStall", 1, 1, false, "Players car is properly in their pitstall", "") }, - { "PlayerCarMyIncidentCount", new VarItem("PlayerCarMyIncidentCount", 2, 1, false, "Players own incident count for this session", "") }, - { "PlayerCarPitSvStatus", new VarItem("PlayerCarPitSvStatus", 2, 1, false, "Players car pit service status bits", "PitServiceStatus") }, - { "PlayerCarPosition", new VarItem("PlayerCarPosition", 2, 1, false, "Players position in race", "") }, - { "PlayerCarPowerAdjust", new VarItem("PlayerCarPowerAdjust", 4, 1, false, "Players power adjust", "%") }, - { "PlayerCarSLBlinkRPM", new VarItem("PlayerCarSLBlinkRPM", 4, 1, false, "Shift light blink rpm", "revs/min") }, - { "PlayerCarSLFirstRPM", new VarItem("PlayerCarSLFirstRPM", 4, 1, false, "Shift light first light rpm", "revs/min") }, - { "PlayerCarSLLastRPM", new VarItem("PlayerCarSLLastRPM", 4, 1, false, "Shift light last light rpm", "revs/min") }, - { "PlayerCarSLShiftRPM", new VarItem("PlayerCarSLShiftRPM", 4, 1, false, "Shift light shift rpm", "revs/min") }, - { "PlayerCarTeamIncidentCount", new VarItem("PlayerCarTeamIncidentCount", 2, 1, false, "Players team incident count for this session", "") }, - { "PlayerCarTowTime", new VarItem("PlayerCarTowTime", 4, 1, false, "Players car is being towed if time is greater than zero", "s") }, - { "PlayerCarWeightPenalty", new VarItem("PlayerCarWeightPenalty", 4, 1, false, "Players weight penalty", "kg") }, - { "PlayerFastRepairsUsed", new VarItem("PlayerFastRepairsUsed", 2, 1, false, "Players car number of fast repairs used", "") }, - { "PlayerIncidents", new VarItem("PlayerIncidents", 2, 1, false, "Log incidents that the player recieved", "irsdk_IncidentFlags") }, - { "PlayerTireCompound", new VarItem("PlayerTireCompound", 2, 1, false, "Players car current tire compound", "") }, - { "PlayerTrackSurface", new VarItem("PlayerTrackSurface", 2, 1, false, "Players car track surface type", "TrackLocation") }, - { "PlayerTrackSurfaceMaterial", new VarItem("PlayerTrackSurfaceMaterial", 2, 1, false, "Players car track surface material type", "TrackSurface") }, - { "Precipitation", new VarItem("Precipitation", 4, 1, false, "Precipitation at start/finish line", "%") }, - { "PushToPass", new VarItem("PushToPass", 1, 1, false, "Push to pass button state", "") }, - { "PushToTalk", new VarItem("PushToTalk", 1, 1, false, "Push to talk button state", "") }, - { "RaceLaps", new VarItem("RaceLaps", 2, 1, false, "Laps completed in race", "") }, - { "RadioTransmitCarIdx", new VarItem("RadioTransmitCarIdx", 2, 1, false, "The car index of the current person speaking on the radio", "") }, - { "RadioTransmitFrequencyIdx", new VarItem("RadioTransmitFrequencyIdx", 2, 1, false, "The frequency index of the current person speaking on the radio", "") }, - { "RadioTransmitRadioIdx", new VarItem("RadioTransmitRadioIdx", 2, 1, false, "The radio index of the current person speaking on the radio", "") }, - { "RearTireSetsAvailable", new VarItem("RearTireSetsAvailable", 2, 1, false, "How many rear tire sets are remaining 255 is unlimited", "") }, - { "RearTireSetsUsed", new VarItem("RearTireSetsUsed", 2, 1, false, "How many rear tire sets used so far", "") }, - { "RelativeHumidity", new VarItem("RelativeHumidity", 4, 1, false, "Relative Humidity at start/finish line", "%") }, - { "ReplayFrameNum", new VarItem("ReplayFrameNum", 2, 1, false, "Integer replay frame number (60 per second)", "") }, - { "ReplayFrameNumEnd", new VarItem("ReplayFrameNumEnd", 2, 1, false, "Integer replay frame number from end of tape", "") }, - { "ReplayPlaySlowMotion", new VarItem("ReplayPlaySlowMotion", 1, 1, false, "0=not slow motion 1=replay is in slow motion", "") }, - { "ReplayPlaySpeed", new VarItem("ReplayPlaySpeed", 2, 1, false, "Replay playback speed", "") }, - { "ReplaySessionNum", new VarItem("ReplaySessionNum", 2, 1, false, "Replay session number", "") }, - { "ReplaySessionTime", new VarItem("ReplaySessionTime", 5, 1, false, "Seconds since replay session start", "s") }, - { "RFbrakeLinePress", new VarItem("RFbrakeLinePress", 4, 1, false, "RF brake line pressure", "bar") }, - { "RFcoldPressure", new VarItem("RFcoldPressure", 4, 1, false, "RF tire cold pressure as set in the garage", "kPa") }, - { "RFodometer", new VarItem("RFodometer", 4, 1, false, "RF distance tire traveled since being placed on car", "m") }, - { "RFpressure", new VarItem("RFpressure", 4, 1, false, "RF tire pressure", "kPa") }, - { "RFrideHeight", new VarItem("RFrideHeight", 4, 1, false, "RF ride height", "m") }, - { "RFshockDefl", new VarItem("RFshockDefl", 4, 1, false, "RF shock deflection", "m") }, - { "RFshockDefl_ST", new VarItem("RFshockDefl_ST", 4, 6, true, "RF shock deflection at 360 Hz", "m") }, - { "RFshockVel", new VarItem("RFshockVel", 4, 1, false, "RF shock velocity", "m/s") }, - { "RFshockVel_ST", new VarItem("RFshockVel_ST", 4, 6, true, "RF shock velocity at 360 Hz", "m/s") }, - { "RFSHshockDefl", new VarItem("RFSHshockDefl", 4, 1, false, "RFSH shock deflection", "m") }, - { "RFSHshockDefl_ST", new VarItem("RFSHshockDefl_ST", 4, 6, true, "RFSH shock deflection at 360 Hz", "m") }, - { "RFSHshockVel", new VarItem("RFSHshockVel", 4, 1, false, "RFSH shock velocity", "m/s") }, - { "RFSHshockVel_ST", new VarItem("RFSHshockVel_ST", 4, 6, true, "RFSH shock velocity at 360 Hz", "m/s") }, - { "RFspeed", new VarItem("RFspeed", 4, 1, false, "RF wheel speed", "m/s") }, - { "RFtempCL", new VarItem("RFtempCL", 4, 1, false, "RF tire left carcass temperature", "C") }, - { "RFtempCM", new VarItem("RFtempCM", 4, 1, false, "RF tire middle carcass temperature", "C") }, - { "RFtempCR", new VarItem("RFtempCR", 4, 1, false, "RF tire right carcass temperature", "C") }, - { "RFtempL", new VarItem("RFtempL", 4, 1, false, "RF tire left surface temperature", "C") }, - { "RFtempM", new VarItem("RFtempM", 4, 1, false, "RF tire middle surface temperature", "C") }, - { "RFtempR", new VarItem("RFtempR", 4, 1, false, "RF tire right surface temperature", "C") }, - { "RFTiresAvailable", new VarItem("RFTiresAvailable", 2, 1, false, "How many right front tires are remaining 255 is unlimited", "") }, - { "RFTiresUsed", new VarItem("RFTiresUsed", 2, 1, false, "How many right front tires used so far", "") }, - { "RFwearL", new VarItem("RFwearL", 4, 1, false, "RF tire left percent tread remaining", "%") }, - { "RFwearM", new VarItem("RFwearM", 4, 1, false, "RF tire middle percent tread remaining", "%") }, - { "RFwearR", new VarItem("RFwearR", 4, 1, false, "RF tire right percent tread remaining", "%") }, - { "RightTireSetsAvailable", new VarItem("RightTireSetsAvailable", 2, 1, false, "How many right tire sets are remaining 255 is unlimited", "") }, - { "RightTireSetsUsed", new VarItem("RightTireSetsUsed", 2, 1, false, "How many right tire sets used so far", "") }, - { "Roll", new VarItem("Roll", 4, 1, false, "Roll orientation", "rad") }, - { "RollRate", new VarItem("RollRate", 4, 1, false, "Roll rate", "rad/s") }, - { "RollRate_ST", new VarItem("RollRate_ST", 4, 6, true, "Roll rate at 360 Hz", "rad/s") }, - { "RPM", new VarItem("RPM", 4, 1, false, "Engine rpm", "revs/min") }, - { "RRbrakeLinePress", new VarItem("RRbrakeLinePress", 4, 1, false, "RR brake line pressure", "bar") }, - { "RRcoldPressure", new VarItem("RRcoldPressure", 4, 1, false, "RR tire cold pressure as set in the garage", "kPa") }, - { "RRodometer", new VarItem("RRodometer", 4, 1, false, "RR distance tire traveled since being placed on car", "m") }, - { "RRpressure", new VarItem("RRpressure", 4, 1, false, "RR tire pressure", "kPa") }, - { "RRrideHeight", new VarItem("RRrideHeight", 4, 1, false, "RR ride height", "m") }, - { "RRshockDefl", new VarItem("RRshockDefl", 4, 1, false, "RR shock deflection", "m") }, - { "RRshockDefl_ST", new VarItem("RRshockDefl_ST", 4, 6, true, "RR shock deflection at 360 Hz", "m") }, - { "RRshockVel", new VarItem("RRshockVel", 4, 1, false, "RR shock velocity", "m/s") }, - { "RRshockVel_ST", new VarItem("RRshockVel_ST", 4, 6, true, "RR shock velocity at 360 Hz", "m/s") }, - { "RRspeed", new VarItem("RRspeed", 4, 1, false, "RR wheel speed", "m/s") }, - { "RRtempCL", new VarItem("RRtempCL", 4, 1, false, "RR tire left carcass temperature", "C") }, - { "RRtempCM", new VarItem("RRtempCM", 4, 1, false, "RR tire middle carcass temperature", "C") }, - { "RRtempCR", new VarItem("RRtempCR", 4, 1, false, "RR tire right carcass temperature", "C") }, - { "RRtempL", new VarItem("RRtempL", 4, 1, false, "RR tire left surface temperature", "C") }, - { "RRtempM", new VarItem("RRtempM", 4, 1, false, "RR tire middle surface temperature", "C") }, - { "RRtempR", new VarItem("RRtempR", 4, 1, false, "RR tire right surface temperature", "C") }, - { "RRTiresAvailable", new VarItem("RRTiresAvailable", 2, 1, false, "How many right rear tires are remaining 255 is unlimited", "") }, - { "RRTiresUsed", new VarItem("RRTiresUsed", 2, 1, false, "How many right rear tires used so far", "") }, - { "RRwearL", new VarItem("RRwearL", 4, 1, false, "RR tire left percent tread remaining", "%") }, - { "RRwearM", new VarItem("RRwearM", 4, 1, false, "RR tire middle percent tread remaining", "%") }, - { "RRwearR", new VarItem("RRwearR", 4, 1, false, "RR tire right percent tread remaining", "%") }, - { "SessionFlags", new VarItem("SessionFlags", 3, 1, false, "Session flags", "SessionFlags") }, - { "SessionJokerLapsRemain", new VarItem("SessionJokerLapsRemain", 2, 1, false, "Joker laps remaining to be taken", "") }, - { "SessionLapsRemain", new VarItem("SessionLapsRemain", 2, 1, false, "Old laps left till session ends use SessionLapsRemainEx", "") }, - { "SessionLapsRemainEx", new VarItem("SessionLapsRemainEx", 2, 1, false, "New improved laps left till session ends", "") }, - { "SessionLapsTotal", new VarItem("SessionLapsTotal", 2, 1, false, "Total number of laps in session", "") }, - { "SessionNum", new VarItem("SessionNum", 2, 1, false, "Session number", "") }, - { "SessionOnJokerLap", new VarItem("SessionOnJokerLap", 1, 1, false, "Player is currently completing a joker lap", "") }, - { "SessionState", new VarItem("SessionState", 2, 1, false, "Session state", "SessionState") }, - { "SessionTick", new VarItem("SessionTick", 2, 1, false, "Current update number", "") }, - { "SessionTime", new VarItem("SessionTime", 5, 1, false, "Seconds since session start", "s") }, - { "SessionTimeOfDay", new VarItem("SessionTimeOfDay", 4, 1, false, "Time of day in seconds", "s") }, - { "SessionTimeRemain", new VarItem("SessionTimeRemain", 5, 1, false, "Seconds left till session ends", "s") }, - { "SessionTimeTotal", new VarItem("SessionTimeTotal", 5, 1, false, "Total number of seconds in session", "s") }, - { "SessionUniqueID", new VarItem("SessionUniqueID", 2, 1, false, "Session ID", "") }, - { "Shifter", new VarItem("Shifter", 2, 1, false, "Log inputs from the players shifter control", "") }, - { "ShiftGrindRPM", new VarItem("ShiftGrindRPM", 4, 1, false, "RPM of shifter grinding noise", "RPM") }, - { "ShiftIndicatorPct", new VarItem("ShiftIndicatorPct", 4, 1, false, "DEPRECATED use DriverCarSLBlinkRPM instead", "%") }, - { "ShiftPowerPct", new VarItem("ShiftPowerPct", 4, 1, false, "Friction torque applied to gears when shifting or grinding", "%") }, - { "Skies", new VarItem("Skies", 2, 1, false, "Skies (0=clear/1=p cloudy/2=m cloudy/3=overcast)", "") }, - { "SolarAltitude", new VarItem("SolarAltitude", 4, 1, false, "Sun angle above horizon in radians", "rad") }, - { "SolarAzimuth", new VarItem("SolarAzimuth", 4, 1, false, "Sun angle clockwise from north in radians", "rad") }, - { "Speed", new VarItem("Speed", 4, 1, false, "GPS vehicle speed", "m/s") }, - { "SteeringFFBEnabled", new VarItem("SteeringFFBEnabled", 1, 1, false, "Force feedback is enabled", "") }, - { "SteeringWheelAngle", new VarItem("SteeringWheelAngle", 4, 1, false, "Steering wheel angle", "rad") }, - { "SteeringWheelAngleMax", new VarItem("SteeringWheelAngleMax", 4, 1, false, "Steering wheel max angle", "rad") }, - { "SteeringWheelLimiter", new VarItem("SteeringWheelLimiter", 4, 1, false, "Force feedback limiter strength limits impacts and oscillation", "%") }, - { "SteeringWheelMaxForceNm", new VarItem("SteeringWheelMaxForceNm", 4, 1, false, "Value of strength or max force slider in Nm for FFB", "N*m") }, - { "SteeringWheelPctDamper", new VarItem("SteeringWheelPctDamper", 4, 1, false, "Force feedback % max damping", "%") }, - { "SteeringWheelPctIntensity", new VarItem("SteeringWheelPctIntensity", 4, 1, false, "Force feedback % max intensity", "%") }, - { "SteeringWheelPctSmoothing", new VarItem("SteeringWheelPctSmoothing", 4, 1, false, "Force feedback % max smoothing", "%") }, - { "SteeringWheelPctTorque", new VarItem("SteeringWheelPctTorque", 4, 1, false, "Force feedback % max torque on steering shaft unsigned", "%") }, - { "SteeringWheelPctTorqueSign", new VarItem("SteeringWheelPctTorqueSign", 4, 1, false, "Force feedback % max torque on steering shaft signed", "%") }, - { "SteeringWheelPctTorqueSignStops", new VarItem("SteeringWheelPctTorqueSignStops", 4, 1, false, "Force feedback % max torque on steering shaft signed stops", "%") }, - { "SteeringWheelPeakForceNm", new VarItem("SteeringWheelPeakForceNm", 4, 1, false, "Peak torque mapping to direct input units for FFB", "N*m") }, - { "SteeringWheelTorque", new VarItem("SteeringWheelTorque", 4, 1, false, "Output torque on steering shaft", "N*m") }, - { "SteeringWheelTorque_ST", new VarItem("SteeringWheelTorque_ST", 4, 6, true, "Output torque on steering shaft at 360 Hz", "N*m") }, - { "SteeringWheelUseLinear", new VarItem("SteeringWheelUseLinear", 1, 1, false, "True if steering wheel force is using linear mode", "") }, - { "Throttle", new VarItem("Throttle", 4, 1, false, "0=off throttle to 1=full throttle", "%") }, - { "ThrottleRaw", new VarItem("ThrottleRaw", 4, 1, false, "Raw throttle input 0=off throttle to 1=full throttle", "%") }, - { "TireLF_RumblePitch", new VarItem("TireLF_RumblePitch", 4, 1, false, "Players LF Tire Sound rumblestrip pitch", "Hz") }, - { "TireLR_RumblePitch", new VarItem("TireLR_RumblePitch", 4, 1, false, "Players LR Tire Sound rumblestrip pitch", "Hz") }, - { "TireRF_RumblePitch", new VarItem("TireRF_RumblePitch", 4, 1, false, "Players RF Tire Sound rumblestrip pitch", "Hz") }, - { "TireRR_RumblePitch", new VarItem("TireRR_RumblePitch", 4, 1, false, "Players RR Tire Sound rumblestrip pitch", "Hz") }, - { "TireSetsAvailable", new VarItem("TireSetsAvailable", 2, 1, false, "How many tire sets are remaining 255 is unlimited", "") }, - { "TireSetsUsed", new VarItem("TireSetsUsed", 2, 1, false, "How many tire sets used so far", "") }, - { "TrackTemp", new VarItem("TrackTemp", 4, 1, false, "Deprecated set to TrackTempCrew", "C") }, - { "TrackTempCrew", new VarItem("TrackTempCrew", 4, 1, false, "Temperature of track measured by crew around track", "C") }, - { "TrackWetness", new VarItem("TrackWetness", 2, 1, false, "How wet is the average track surface", "TrackWetness") }, - { "VelocityX", new VarItem("VelocityX", 4, 1, false, "X velocity", "m/s") }, - { "VelocityX_ST", new VarItem("VelocityX_ST", 4, 6, true, "X velocity", "m/s at 360 Hz") }, - { "VelocityY", new VarItem("VelocityY", 4, 1, false, "Y velocity", "m/s") }, - { "VelocityY_ST", new VarItem("VelocityY_ST", 4, 6, true, "Y velocity", "m/s at 360 Hz") }, - { "VelocityZ", new VarItem("VelocityZ", 4, 1, false, "Z velocity", "m/s") }, - { "VelocityZ_ST", new VarItem("VelocityZ_ST", 4, 6, true, "Z velocity", "m/s at 360 Hz") }, - { "VertAccel", new VarItem("VertAccel", 4, 1, false, "Vertical acceleration (including gravity)", "m/s^2") }, - { "VertAccel_ST", new VarItem("VertAccel_ST", 4, 6, true, "Vertical acceleration (including gravity) at 360 Hz", "m/s^2") }, - { "VidCapActive", new VarItem("VidCapActive", 1, 1, false, "True if video currently being captured", "") }, - { "VidCapEnabled", new VarItem("VidCapEnabled", 1, 1, false, "True if video capture system is enabled", "") }, - { "Voltage", new VarItem("Voltage", 4, 1, false, "Engine voltage", "V") }, - { "WaterLevel", new VarItem("WaterLevel", 4, 1, false, "Engine coolant level", "l") }, - { "WaterTemp", new VarItem("WaterTemp", 4, 1, false, "Engine coolant temp", "C") }, - { "WeatherDeclaredWet", new VarItem("WeatherDeclaredWet", 1, 1, false, "The steward says rain tires can be used", "") }, - { "WindDir", new VarItem("WindDir", 4, 1, false, "Wind direction at start/finish line", "rad") }, - { "WindVel", new VarItem("WindVel", 4, 1, false, "Wind velocity at start/finish line", "m/s") }, - { "Yaw", new VarItem("Yaw", 4, 1, false, "Yaw orientation", "rad") }, - { "YawNorth", new VarItem("YawNorth", 4, 1, false, "Yaw orientation relative to north", "rad") }, - { "YawRate", new VarItem("YawRate", 4, 1, false, "Yaw rate", "rad/s") }, - { "YawRate_ST", new VarItem("YawRate_ST", 4, 6, true, "Yaw rate at 360 Hz", "rad/s") }, + { TelemetryVar.AirDensity, new VarItem("AirDensity", 4, 1, false, "Density of air at start/finish line", "kg/m^3") }, + { TelemetryVar.AirPressure, new VarItem("AirPressure", 4, 1, false, "Pressure of air at start/finish line", "Pa") }, + { TelemetryVar.AirTemp, new VarItem("AirTemp", 4, 1, false, "Temperature of air at start/finish line", "C") }, + { TelemetryVar.Alt, new VarItem("Alt", 4, 1, false, "Altitude in meters", "m") }, + { TelemetryVar.Brake, new VarItem("Brake", 4, 1, false, "0=brake released to 1=max pedal force", "%") }, + { TelemetryVar.BrakeABSactive, new VarItem("BrakeABSactive", 1, 1, false, "true if abs is currently reducing brake force pressure", "") }, + { TelemetryVar.BrakeABScutPct, new VarItem("BrakeABScutPct", 4, 1, false, "Percent of brake force reduction caused by ABS system", "%") }, + { TelemetryVar.BrakeRaw, new VarItem("BrakeRaw", 4, 1, false, "Raw brake input 0=brake released to 1=max pedal force", "%") }, + { TelemetryVar.CamCameraNumber, new VarItem("CamCameraNumber", 2, 1, false, "Active camera number", "") }, + { TelemetryVar.CamCameraState, new VarItem("CamCameraState", 3, 1, false, "State of camera system", "CameraState") }, + { TelemetryVar.CamCarIdx, new VarItem("CamCarIdx", 2, 1, false, "Active camera's focus car index", "") }, + { TelemetryVar.CamGroupNumber, new VarItem("CamGroupNumber", 2, 1, false, "Active camera group number", "") }, + { TelemetryVar.CarDistAhead, new VarItem("CarDistAhead", 4, 1, false, "Distance to first car in front of player in meters", "m") }, + { TelemetryVar.CarDistBehind, new VarItem("CarDistBehind", 4, 1, false, "Distance to first car behind player in meters", "m") }, + { TelemetryVar.CarIdxBestLapNum, new VarItem("CarIdxBestLapNum", 2, 64, false, "Cars best lap number", "") }, + { TelemetryVar.CarIdxBestLapTime, new VarItem("CarIdxBestLapTime", 4, 64, false, "Cars best lap time", "s") }, + { TelemetryVar.CarIdxClass, new VarItem("CarIdxClass", 2, 64, false, "Cars class id by car index", "") }, + { TelemetryVar.CarIdxClassPosition, new VarItem("CarIdxClassPosition", 2, 64, false, "Cars class position in race by car index", "") }, + { TelemetryVar.CarIdxEstTime, new VarItem("CarIdxEstTime", 4, 64, false, "Estimated time to reach current location on track", "s") }, + { TelemetryVar.CarIdxF2Time, new VarItem("CarIdxF2Time", 4, 64, false, "Race time behind leader or fastest lap time otherwise", "s") }, + { TelemetryVar.CarIdxFastRepairsUsed, new VarItem("CarIdxFastRepairsUsed", 2, 64, false, "How many fast repairs each car has used", "") }, + { TelemetryVar.CarIdxGear, new VarItem("CarIdxGear", 2, 64, false, "-1=reverse 0=neutral 1..n=current gear by car index", "") }, + { TelemetryVar.CarIdxLap, new VarItem("CarIdxLap", 2, 64, false, "Laps started by car index", "") }, + { TelemetryVar.CarIdxLapCompleted, new VarItem("CarIdxLapCompleted", 2, 64, false, "Laps completed by car index", "") }, + { TelemetryVar.CarIdxLapDistPct, new VarItem("CarIdxLapDistPct", 4, 64, false, "Percentage distance around lap by car index", "%") }, + { TelemetryVar.CarIdxLastLapTime, new VarItem("CarIdxLastLapTime", 4, 64, false, "Cars last lap time", "s") }, + { TelemetryVar.CarIdxOnPitRoad, new VarItem("CarIdxOnPitRoad", 1, 64, false, "On pit road between the cones by car index", "") }, + { TelemetryVar.CarIdxP2P_Count, new VarItem("CarIdxP2P_Count", 2, 64, false, "Push2Pass count of usage (or remaining in Race)", "") }, + { TelemetryVar.CarIdxP2P_Status, new VarItem("CarIdxP2P_Status", 1, 64, false, "Push2Pass active or not", "") }, + { TelemetryVar.CarIdxPaceFlags, new VarItem("CarIdxPaceFlags", 3, 64, false, "Pacing status flags for each car", "PaceFlags") }, + { TelemetryVar.CarIdxPaceLine, new VarItem("CarIdxPaceLine", 2, 64, false, "What line cars are pacing in or -1 if not pacing", "") }, + { TelemetryVar.CarIdxPaceRow, new VarItem("CarIdxPaceRow", 2, 64, false, "What row cars are pacing in or -1 if not pacing", "") }, + { TelemetryVar.CarIdxPosition, new VarItem("CarIdxPosition", 2, 64, false, "Cars position in race by car index", "") }, + { TelemetryVar.CarIdxQualTireCompound, new VarItem("CarIdxQualTireCompound", 2, 64, false, "Cars Qual tire compound", "") }, + { TelemetryVar.CarIdxQualTireCompoundLocked, new VarItem("CarIdxQualTireCompoundLocked", 1, 64, false, "Cars Qual tire compound is locked-in", "") }, + { TelemetryVar.CarIdxRPM, new VarItem("CarIdxRPM", 4, 64, false, "Engine rpm by car index", "revs/min") }, + { TelemetryVar.CarIdxSessionFlags, new VarItem("CarIdxSessionFlags", 3, 64, false, "Session flags for each player", "SessionFlags") }, + { TelemetryVar.CarIdxSteer, new VarItem("CarIdxSteer", 4, 64, false, "Steering wheel angle by car index", "rad") }, + { TelemetryVar.CarIdxTireCompound, new VarItem("CarIdxTireCompound", 2, 64, false, "Cars current tire compound", "") }, + { TelemetryVar.CarIdxTrackSurface, new VarItem("CarIdxTrackSurface", 2, 64, false, "Track surface type by car index", "TrackLocation") }, + { TelemetryVar.CarIdxTrackSurfaceMaterial, new VarItem("CarIdxTrackSurfaceMaterial", 2, 64, false, "Track surface material type by car index", "TrackSurface") }, + { TelemetryVar.CarLeftRight, new VarItem("CarLeftRight", 2, 1, false, "Notify if car is to the left or right of driver", "CarLeftRight") }, + { TelemetryVar.CFshockDefl, new VarItem("CFshockDefl", 4, 1, false, "CF shock deflection", "m") }, + { TelemetryVar.CFshockDefl_ST, new VarItem("CFshockDefl_ST", 4, 6, true, "CF shock deflection at 360 Hz", "m") }, + { TelemetryVar.CFshockVel, new VarItem("CFshockVel", 4, 1, false, "CF shock velocity", "m/s") }, + { TelemetryVar.CFshockVel_ST, new VarItem("CFshockVel_ST", 4, 6, true, "CF shock velocity at 360 Hz", "m/s") }, + { TelemetryVar.CFSRrideHeight, new VarItem("CFSRrideHeight", 4, 1, false, "CFSR ride height", "m") }, + { TelemetryVar.ChanAvgLatency, new VarItem("ChanAvgLatency", 4, 1, false, "Communications average latency", "s") }, + { TelemetryVar.ChanClockSkew, new VarItem("ChanClockSkew", 4, 1, false, "Communications server clock skew", "s") }, + { TelemetryVar.ChanLatency, new VarItem("ChanLatency", 4, 1, false, "Communications latency", "s") }, + { TelemetryVar.ChanPartnerQuality, new VarItem("ChanPartnerQuality", 4, 1, false, "Partner communications quality", "%") }, + { TelemetryVar.ChanQuality, new VarItem("ChanQuality", 4, 1, false, "Communications quality", "%") }, + { TelemetryVar.Clutch, new VarItem("Clutch", 4, 1, false, "0=disengaged to 1=fully engaged", "%") }, + { TelemetryVar.ClutchRaw, new VarItem("ClutchRaw", 4, 1, false, "Raw clutch input 0=disengaged to 1=fully engaged", "%") }, + { TelemetryVar.CpuUsageBG, new VarItem("CpuUsageBG", 4, 1, false, "Percent of available tim bg thread took with a 1 sec avg", "%") }, + { TelemetryVar.CpuUsageFG, new VarItem("CpuUsageFG", 4, 1, false, "Percent of available tim fg thread took with a 1 sec avg", "%") }, + { TelemetryVar.CRshockDefl, new VarItem("CRshockDefl", 4, 1, false, "CR shock deflection", "m") }, + { TelemetryVar.CRshockDefl_ST, new VarItem("CRshockDefl_ST", 4, 6, true, "CR shock deflection at 360 Hz", "m") }, + { TelemetryVar.CRshockVel, new VarItem("CRshockVel", 4, 1, false, "CR shock velocity", "m/s") }, + { TelemetryVar.CRshockVel_ST, new VarItem("CRshockVel_ST", 4, 6, true, "CR shock velocity at 360 Hz", "m/s") }, + { TelemetryVar.dcABS, new VarItem("dcABS", 4, 1, false, "In car abs adjustment", "") }, + { TelemetryVar.dcAntiRollFront, new VarItem("dcAntiRollFront", 4, 1, false, "In car front anti roll bar adjustment", "") }, + { TelemetryVar.dcAntiRollRear, new VarItem("dcAntiRollRear", 4, 1, false, "In car rear anti roll bar adjustment", "") }, + { TelemetryVar.dcBrakeBias, new VarItem("dcBrakeBias", 4, 1, false, "In car brake bias adjustment", "") }, + { TelemetryVar.dcDashPage, new VarItem("dcDashPage", 4, 1, false, "In car dash display page adjustment", "") }, + { TelemetryVar.DCDriversSoFar, new VarItem("DCDriversSoFar", 2, 1, false, "Number of team drivers who have run a stint", "") }, + { TelemetryVar.dcFuelMixture, new VarItem("dcFuelMixture", 4, 1, false, "In car fuel mixture adjustment", "") }, + { TelemetryVar.dcHeadlightFlash, new VarItem("dcHeadlightFlash", 1, 1, false, "In car headlight flash control active", "") }, + { TelemetryVar.DCLapStatus, new VarItem("DCLapStatus", 2, 1, false, "Status of driver change lap requirements", "") }, + { TelemetryVar.dcLaunchRPM, new VarItem("dcLaunchRPM", 4, 1, false, "In car launch rpm adjustment", "") }, + { TelemetryVar.dcLowFuelAccept, new VarItem("dcLowFuelAccept", 1, 1, false, "In car low fuel accept button", "") }, + { TelemetryVar.dcPitSpeedLimiterToggle, new VarItem("dcPitSpeedLimiterToggle", 1, 1, false, "In car traction control active", "") }, + { TelemetryVar.dcPowerSteering, new VarItem("dcPowerSteering", 1, 1, false, "In car power steering toggle", "") }, + { TelemetryVar.dcPushToPass, new VarItem("dcPushToPass", 1, 1, false, "In car trigger push to pass", "") }, + { TelemetryVar.dcRFBrakeAttachedToggle, new VarItem("dcRFBrakeAttachedToggle", 1, 1, false, "In car Right Front Brake attached(1) or detached(0)", "") }, + { TelemetryVar.dcStarter, new VarItem("dcStarter", 1, 1, false, "In car trigger car starter", "") }, + { TelemetryVar.dcTearOffVisor, new VarItem("dcTearOffVisor", 1, 1, false, "In car tear off visor film", "") }, + { TelemetryVar.dcThrottleShape, new VarItem("dcThrottleShape", 4, 1, false, "In car throttle shape adjustment", "") }, + { TelemetryVar.dcToggleWindshieldWipers, new VarItem("dcToggleWindshieldWipers", 1, 1, false, "In car turn wipers on or off", "") }, + { TelemetryVar.dcTractionControl, new VarItem("dcTractionControl", 4, 1, false, "In car traction control adjustment", "") }, + { TelemetryVar.dcTractionControlToggle, new VarItem("dcTractionControlToggle", 1, 1, false, "In car traction control toggle", "") }, + { TelemetryVar.dcTriggerWindshieldWipers, new VarItem("dcTriggerWindshieldWipers", 1, 1, false, "In car momentarily turn on wipers", "") }, + { TelemetryVar.dcWeightJackerRight, new VarItem("dcWeightJackerRight", 4, 1, false, "In car right wedge/weight jacker adjustment", "") }, + { TelemetryVar.DisplayUnits, new VarItem("DisplayUnits", 2, 1, false, "Default units for the user interface 0 = english 1 = metric", "") }, + { TelemetryVar.dpFastRepair, new VarItem("dpFastRepair", 4, 1, false, "Pitstop fast repair set", "") }, + { TelemetryVar.dpFuelAddKg, new VarItem("dpFuelAddKg", 4, 1, false, "Pitstop fuel add amount", "kg") }, + { TelemetryVar.dpFuelAutoFillActive, new VarItem("dpFuelAutoFillActive", 4, 1, false, "Pitstop auto fill fuel next stop flag", "") }, + { TelemetryVar.dpFuelAutoFillEnabled, new VarItem("dpFuelAutoFillEnabled", 4, 1, false, "Pitstop auto fill fuel system enabled", "") }, + { TelemetryVar.dpFuelFill, new VarItem("dpFuelFill", 4, 1, false, "Pitstop fuel fill flag", "") }, + { TelemetryVar.dpLFTireChange, new VarItem("dpLFTireChange", 4, 1, false, "Pitstop lf tire change request", "") }, + { TelemetryVar.dpLFTireColdPress, new VarItem("dpLFTireColdPress", 4, 1, false, "Pitstop lf tire cold pressure adjustment", "Pa") }, + { TelemetryVar.dpLRTireChange, new VarItem("dpLRTireChange", 4, 1, false, "Pitstop lr tire change request", "") }, + { TelemetryVar.dpLRTireColdPress, new VarItem("dpLRTireColdPress", 4, 1, false, "Pitstop lr tire cold pressure adjustment", "Pa") }, + { TelemetryVar.dpLTireChange, new VarItem("dpLTireChange", 4, 1, false, "Pitstop left tire change request", "") }, + { TelemetryVar.dpRFTireChange, new VarItem("dpRFTireChange", 4, 1, false, "Pitstop rf tire change request", "") }, + { TelemetryVar.dpRFTireColdPress, new VarItem("dpRFTireColdPress", 4, 1, false, "Pitstop rf cold tire pressure adjustment", "Pa") }, + { TelemetryVar.dpRRTireChange, new VarItem("dpRRTireChange", 4, 1, false, "Pitstop rr tire change request", "") }, + { TelemetryVar.dpRRTireColdPress, new VarItem("dpRRTireColdPress", 4, 1, false, "Pitstop rr cold tire pressure adjustment", "Pa") }, + { TelemetryVar.dpRTireChange, new VarItem("dpRTireChange", 4, 1, false, "Pitstop right tire change request", "") }, + { TelemetryVar.dpTireChange, new VarItem("dpTireChange", 4, 1, false, "Pitstop all tire change request", "") }, + { TelemetryVar.dpWindshieldTearoff, new VarItem("dpWindshieldTearoff", 4, 1, false, "Pitstop windshield tearoff", "") }, + { TelemetryVar.dpWingFront, new VarItem("dpWingFront", 4, 1, false, "Pitstop front wing adjustment", "") }, + { TelemetryVar.dpWingRear, new VarItem("dpWingRear", 4, 1, false, "Pitstop rear wing adjustment", "") }, + { TelemetryVar.DriverMarker, new VarItem("DriverMarker", 1, 1, false, "Driver activated flag", "") }, + { TelemetryVar.Engine0_RPM, new VarItem("Engine0_RPM", 4, 1, false, "Engine0Engine rpm", "revs/min") }, + { TelemetryVar.EngineWarnings, new VarItem("EngineWarnings", 3, 1, false, "Bitfield for warning lights", "EngineWarnings") }, + { TelemetryVar.EnterExitReset, new VarItem("EnterExitReset", 2, 1, false, "Indicate action the reset key will take 0 enter 1 exit 2 reset", "") }, + { TelemetryVar.FastRepairAvailable, new VarItem("FastRepairAvailable", 2, 1, false, "How many fast repairs left 255 is unlimited", "") }, + { TelemetryVar.FastRepairUsed, new VarItem("FastRepairUsed", 2, 1, false, "How many fast repairs used so far", "") }, + { TelemetryVar.FogLevel, new VarItem("FogLevel", 4, 1, false, "Fog level at start/finish line", "%") }, + { TelemetryVar.FrameRate, new VarItem("FrameRate", 4, 1, false, "Average frames per second", "fps") }, + { TelemetryVar.FrontTireSetsAvailable, new VarItem("FrontTireSetsAvailable", 2, 1, false, "How many front tire sets are remaining 255 is unlimited", "") }, + { TelemetryVar.FrontTireSetsUsed, new VarItem("FrontTireSetsUsed", 2, 1, false, "How many front tire sets used so far", "") }, + { TelemetryVar.FuelLevel, new VarItem("FuelLevel", 4, 1, false, "Liters of fuel remaining", "l") }, + { TelemetryVar.FuelLevelPct, new VarItem("FuelLevelPct", 4, 1, false, "Percent fuel remaining", "%") }, + { TelemetryVar.FuelPress, new VarItem("FuelPress", 4, 1, false, "Engine fuel pressure", "bar") }, + { TelemetryVar.FuelUsePerHour, new VarItem("FuelUsePerHour", 4, 1, false, "Engine fuel used instantaneous", "kg/h") }, + { TelemetryVar.Gear, new VarItem("Gear", 2, 1, false, "-1=reverse 0=neutral 1..n=current gear", "") }, + { TelemetryVar.GpuUsage, new VarItem("GpuUsage", 4, 1, false, "Percent of available tim gpu took with a 1 sec avg", "%") }, + { TelemetryVar.HandbrakeRaw, new VarItem("HandbrakeRaw", 4, 1, false, "Raw handbrake input 0=handbrake released to 1=max force", "%") }, + { TelemetryVar.IsDiskLoggingActive, new VarItem("IsDiskLoggingActive", 1, 1, false, "0=disk based telemetry file not being written 1=being written", "") }, + { TelemetryVar.IsDiskLoggingEnabled, new VarItem("IsDiskLoggingEnabled", 1, 1, false, "0=disk based telemetry turned off 1=turned on", "") }, + { TelemetryVar.IsGarageVisible, new VarItem("IsGarageVisible", 1, 1, false, "1=Garage screen is visible", "") }, + { TelemetryVar.IsInGarage, new VarItem("IsInGarage", 1, 1, false, "1=Car in garage physics running", "") }, + { TelemetryVar.IsOnTrack, new VarItem("IsOnTrack", 1, 1, false, "1=Car on track physics running with player in car", "") }, + { TelemetryVar.IsOnTrackCar, new VarItem("IsOnTrackCar", 1, 1, false, "1=Car on track physics running", "") }, + { TelemetryVar.IsReplayPlaying, new VarItem("IsReplayPlaying", 1, 1, false, "0=replay not playing 1=replay playing", "") }, + { TelemetryVar.Lap, new VarItem("Lap", 2, 1, false, "Laps started count", "") }, + { TelemetryVar.LapBestLap, new VarItem("LapBestLap", 2, 1, false, "Players best lap number", "") }, + { TelemetryVar.LapBestLapTime, new VarItem("LapBestLapTime", 4, 1, false, "Players best lap time", "s") }, + { TelemetryVar.LapBestNLapLap, new VarItem("LapBestNLapLap", 2, 1, false, "Player last lap in best N average lap time", "") }, + { TelemetryVar.LapBestNLapTime, new VarItem("LapBestNLapTime", 4, 1, false, "Player best N average lap time", "s") }, + { TelemetryVar.LapCompleted, new VarItem("LapCompleted", 2, 1, false, "Laps completed count", "") }, + { TelemetryVar.LapCurrentLapTime, new VarItem("LapCurrentLapTime", 4, 1, false, "Estimate of players current lap time as shown in F3 box", "s") }, + { TelemetryVar.LapDeltaToBestLap, new VarItem("LapDeltaToBestLap", 4, 1, false, "Delta time for best lap", "s") }, + { TelemetryVar.LapDeltaToBestLap_DD, new VarItem("LapDeltaToBestLap_DD", 4, 1, false, "Rate of change of delta time for best lap", "s/s") }, + { TelemetryVar.LapDeltaToBestLap_OK, new VarItem("LapDeltaToBestLap_OK", 1, 1, false, "Delta time for best lap is valid", "") }, + { TelemetryVar.LapDeltaToOptimalLap, new VarItem("LapDeltaToOptimalLap", 4, 1, false, "Delta time for optimal lap", "s") }, + { TelemetryVar.LapDeltaToOptimalLap_DD, new VarItem("LapDeltaToOptimalLap_DD", 4, 1, false, "Rate of change of delta time for optimal lap", "s/s") }, + { TelemetryVar.LapDeltaToOptimalLap_OK, new VarItem("LapDeltaToOptimalLap_OK", 1, 1, false, "Delta time for optimal lap is valid", "") }, + { TelemetryVar.LapDeltaToSessionBestLap, new VarItem("LapDeltaToSessionBestLap", 4, 1, false, "Delta time for session best lap", "s") }, + { TelemetryVar.LapDeltaToSessionBestLap_DD, new VarItem("LapDeltaToSessionBestLap_DD", 4, 1, false, "Rate of change of delta time for session best lap", "s/s") }, + { TelemetryVar.LapDeltaToSessionBestLap_OK, new VarItem("LapDeltaToSessionBestLap_OK", 1, 1, false, "Delta time for session best lap is valid", "") }, + { TelemetryVar.LapDeltaToSessionLastlLap, new VarItem("LapDeltaToSessionLastlLap", 4, 1, false, "Delta time for session last lap", "s") }, + { TelemetryVar.LapDeltaToSessionLastlLap_DD, new VarItem("LapDeltaToSessionLastlLap_DD", 4, 1, false, "Rate of change of delta time for session last lap", "s/s") }, + { TelemetryVar.LapDeltaToSessionLastlLap_OK, new VarItem("LapDeltaToSessionLastlLap_OK", 1, 1, false, "Delta time for session last lap is valid", "") }, + { TelemetryVar.LapDeltaToSessionOptimalLap, new VarItem("LapDeltaToSessionOptimalLap", 4, 1, false, "Delta time for session optimal lap", "s") }, + { TelemetryVar.LapDeltaToSessionOptimalLap_DD, new VarItem("LapDeltaToSessionOptimalLap_DD", 4, 1, false, "Rate of change of delta time for session optimal lap", "s/s") }, + { TelemetryVar.LapDeltaToSessionOptimalLap_OK, new VarItem("LapDeltaToSessionOptimalLap_OK", 1, 1, false, "Delta time for session optimal lap is valid", "") }, + { TelemetryVar.LapDist, new VarItem("LapDist", 4, 1, false, "Meters traveled from S/F this lap", "m") }, + { TelemetryVar.LapDistPct, new VarItem("LapDistPct", 4, 1, false, "Percentage distance around lap", "%") }, + { TelemetryVar.LapLasNLapSeq, new VarItem("LapLasNLapSeq", 2, 1, false, "Player num consecutive clean laps completed for N average", "") }, + { TelemetryVar.LapLastLapTime, new VarItem("LapLastLapTime", 4, 1, false, "Players last lap time", "s") }, + { TelemetryVar.LapLastNLapTime, new VarItem("LapLastNLapTime", 4, 1, false, "Player last N average lap time", "s") }, + { TelemetryVar.Lat, new VarItem("Lat", 5, 1, false, "Latitude in decimal degrees", "deg") }, + { TelemetryVar.LatAccel, new VarItem("LatAccel", 4, 1, false, "Lateral acceleration (including gravity)", "m/s^2") }, + { TelemetryVar.LatAccel_ST, new VarItem("LatAccel_ST", 4, 6, true, "Lateral acceleration (including gravity) at 360 Hz", "m/s^2") }, + { TelemetryVar.LeftTireSetsAvailable, new VarItem("LeftTireSetsAvailable", 2, 1, false, "How many left tire sets are remaining 255 is unlimited", "") }, + { TelemetryVar.LeftTireSetsUsed, new VarItem("LeftTireSetsUsed", 2, 1, false, "How many left tire sets used so far", "") }, + { TelemetryVar.LFbrakeLinePress, new VarItem("LFbrakeLinePress", 4, 1, false, "LF brake line pressure", "bar") }, + { TelemetryVar.LFcoldPressure, new VarItem("LFcoldPressure", 4, 1, false, "LF tire cold pressure as set in the garage", "kPa") }, + { TelemetryVar.LFodometer, new VarItem("LFodometer", 4, 1, false, "LF distance tire traveled since being placed on car", "m") }, + { TelemetryVar.LFpressure, new VarItem("LFpressure", 4, 1, false, "LF tire pressure", "kPa") }, + { TelemetryVar.LFrideHeight, new VarItem("LFrideHeight", 4, 1, false, "LF ride height", "m") }, + { TelemetryVar.LFshockDefl, new VarItem("LFshockDefl", 4, 1, false, "LF shock deflection", "m") }, + { TelemetryVar.LFshockDefl_ST, new VarItem("LFshockDefl_ST", 4, 6, true, "LF shock deflection at 360 Hz", "m") }, + { TelemetryVar.LFshockVel, new VarItem("LFshockVel", 4, 1, false, "LF shock velocity", "m/s") }, + { TelemetryVar.LFshockVel_ST, new VarItem("LFshockVel_ST", 4, 6, true, "LF shock velocity at 360 Hz", "m/s") }, + { TelemetryVar.LFSHshockDefl, new VarItem("LFSHshockDefl", 4, 1, false, "LFSH shock deflection", "m") }, + { TelemetryVar.LFSHshockDefl_ST, new VarItem("LFSHshockDefl_ST", 4, 6, true, "LFSH shock deflection at 360 Hz", "m") }, + { TelemetryVar.LFSHshockVel, new VarItem("LFSHshockVel", 4, 1, false, "LFSH shock velocity", "m/s") }, + { TelemetryVar.LFSHshockVel_ST, new VarItem("LFSHshockVel_ST", 4, 6, true, "LFSH shock velocity at 360 Hz", "m/s") }, + { TelemetryVar.LFspeed, new VarItem("LFspeed", 4, 1, false, "LF wheel speed", "m/s") }, + { TelemetryVar.LFtempCL, new VarItem("LFtempCL", 4, 1, false, "LF tire left carcass temperature", "C") }, + { TelemetryVar.LFtempCM, new VarItem("LFtempCM", 4, 1, false, "LF tire middle carcass temperature", "C") }, + { TelemetryVar.LFtempCR, new VarItem("LFtempCR", 4, 1, false, "LF tire right carcass temperature", "C") }, + { TelemetryVar.LFtempL, new VarItem("LFtempL", 4, 1, false, "LF tire left surface temperature", "C") }, + { TelemetryVar.LFtempM, new VarItem("LFtempM", 4, 1, false, "LF tire middle surface temperature", "C") }, + { TelemetryVar.LFtempR, new VarItem("LFtempR", 4, 1, false, "LF tire right surface temperature", "C") }, + { TelemetryVar.LFTiresAvailable, new VarItem("LFTiresAvailable", 2, 1, false, "How many left front tires are remaining 255 is unlimited", "") }, + { TelemetryVar.LFTiresUsed, new VarItem("LFTiresUsed", 2, 1, false, "How many left front tires used so far", "") }, + { TelemetryVar.LFwearL, new VarItem("LFwearL", 4, 1, false, "LF tire left percent tread remaining", "%") }, + { TelemetryVar.LFwearM, new VarItem("LFwearM", 4, 1, false, "LF tire middle percent tread remaining", "%") }, + { TelemetryVar.LFwearR, new VarItem("LFwearR", 4, 1, false, "LF tire right percent tread remaining", "%") }, + { TelemetryVar.LoadNumTextures, new VarItem("LoadNumTextures", 1, 1, false, "True if the car_num texture will be loaded", "") }, + { TelemetryVar.Lon, new VarItem("Lon", 5, 1, false, "Longitude in decimal degrees", "deg") }, + { TelemetryVar.LongAccel, new VarItem("LongAccel", 4, 1, false, "Longitudinal acceleration (including gravity)", "m/s^2") }, + { TelemetryVar.LongAccel_ST, new VarItem("LongAccel_ST", 4, 6, true, "Longitudinal acceleration (including gravity) at 360 Hz", "m/s^2") }, + { TelemetryVar.LR2shockDefl, new VarItem("LR2shockDefl", 4, 1, false, "LR2 shock deflection", "m") }, + { TelemetryVar.LR2shockDefl_ST, new VarItem("LR2shockDefl_ST", 4, 6, true, "LR2 shock deflection at 360 Hz", "m") }, + { TelemetryVar.LR2shockVel, new VarItem("LR2shockVel", 4, 1, false, "LR2 shock velocity", "m/s") }, + { TelemetryVar.LR2shockVel_ST, new VarItem("LR2shockVel_ST", 4, 6, true, "LR2 shock velocity at 360 Hz", "m/s") }, + { TelemetryVar.LRbrakeLinePress, new VarItem("LRbrakeLinePress", 4, 1, false, "LR brake line pressure", "bar") }, + { TelemetryVar.LRcoldPressure, new VarItem("LRcoldPressure", 4, 1, false, "LR tire cold pressure as set in the garage", "kPa") }, + { TelemetryVar.LRodometer, new VarItem("LRodometer", 4, 1, false, "LR distance tire traveled since being placed on car", "m") }, + { TelemetryVar.LRpressure, new VarItem("LRpressure", 4, 1, false, "LR tire pressure", "kPa") }, + { TelemetryVar.LRrideHeight, new VarItem("LRrideHeight", 4, 1, false, "LR ride height", "m") }, + { TelemetryVar.LRshockDefl, new VarItem("LRshockDefl", 4, 1, false, "LR shock deflection", "m") }, + { TelemetryVar.LRshockDefl_ST, new VarItem("LRshockDefl_ST", 4, 6, true, "LR shock deflection at 360 Hz", "m") }, + { TelemetryVar.LRshockVel, new VarItem("LRshockVel", 4, 1, false, "LR shock velocity", "m/s") }, + { TelemetryVar.LRshockVel_ST, new VarItem("LRshockVel_ST", 4, 6, true, "LR shock velocity at 360 Hz", "m/s") }, + { TelemetryVar.LRSHshockDefl, new VarItem("LRSHshockDefl", 4, 1, false, "LRSH shock deflection", "m") }, + { TelemetryVar.LRSHshockVel, new VarItem("LRSHshockVel", 4, 1, false, "LRSH shock velocity", "m/s") }, + { TelemetryVar.LRspeed, new VarItem("LRspeed", 4, 1, false, "LR wheel speed", "m/s") }, + { TelemetryVar.LRtempCL, new VarItem("LRtempCL", 4, 1, false, "LR tire left carcass temperature", "C") }, + { TelemetryVar.LRtempCM, new VarItem("LRtempCM", 4, 1, false, "LR tire middle carcass temperature", "C") }, + { TelemetryVar.LRtempCR, new VarItem("LRtempCR", 4, 1, false, "LR tire right carcass temperature", "C") }, + { TelemetryVar.LRtempL, new VarItem("LRtempL", 4, 1, false, "LR tire left surface temperature", "C") }, + { TelemetryVar.LRtempM, new VarItem("LRtempM", 4, 1, false, "LR tire middle surface temperature", "C") }, + { TelemetryVar.LRtempR, new VarItem("LRtempR", 4, 1, false, "LR tire right surface temperature", "C") }, + { TelemetryVar.LRTiresAvailable, new VarItem("LRTiresAvailable", 2, 1, false, "How many left rear tires are remaining 255 is unlimited", "") }, + { TelemetryVar.LRTiresUsed, new VarItem("LRTiresUsed", 2, 1, false, "How many left rear tires used so far", "") }, + { TelemetryVar.LRwearL, new VarItem("LRwearL", 4, 1, false, "LR tire left percent tread remaining", "%") }, + { TelemetryVar.LRwearM, new VarItem("LRwearM", 4, 1, false, "LR tire middle percent tread remaining", "%") }, + { TelemetryVar.LRwearR, new VarItem("LRwearR", 4, 1, false, "LR tire right percent tread remaining", "%") }, + { TelemetryVar.ManifoldPress, new VarItem("ManifoldPress", 4, 1, false, "Engine manifold pressure", "bar") }, + { TelemetryVar.ManualBoost, new VarItem("ManualBoost", 1, 1, false, "Hybrid manual boost state", "") }, + { TelemetryVar.ManualNoBoost, new VarItem("ManualNoBoost", 1, 1, false, "Hybrid manual no boost state", "") }, + { TelemetryVar.MemPageFaultSec, new VarItem("MemPageFaultSec", 4, 1, false, "Memory page faults per second", "") }, + { TelemetryVar.MemSoftPageFaultSec, new VarItem("MemSoftPageFaultSec", 4, 1, false, "Memory soft page faults per second", "") }, + { TelemetryVar.OilLevel, new VarItem("OilLevel", 4, 1, false, "Engine oil level", "l") }, + { TelemetryVar.OilPress, new VarItem("OilPress", 4, 1, false, "Engine oil pressure", "bar") }, + { TelemetryVar.OilTemp, new VarItem("OilTemp", 4, 1, false, "Engine oil temperature", "C") }, + { TelemetryVar.OkToReloadTextures, new VarItem("OkToReloadTextures", 1, 1, false, "True if it is ok to reload car textures at this time", "") }, + { TelemetryVar.OnPitRoad, new VarItem("OnPitRoad", 1, 1, false, "Is the player car on pit road between the cones", "") }, + { TelemetryVar.P2P_Count, new VarItem("P2P_Count", 2, 1, false, "Push2Pass count of usage (or remaining in Race) on your car", "") }, + { TelemetryVar.P2P_Status, new VarItem("P2P_Status", 1, 1, false, "Push2Pass active or not on your car", "") }, + { TelemetryVar.PaceMode, new VarItem("PaceMode", 2, 1, false, "Are we pacing or not", "PaceMode") }, + { TelemetryVar.Pitch, new VarItem("Pitch", 4, 1, false, "Pitch orientation", "rad") }, + { TelemetryVar.PitchRate, new VarItem("PitchRate", 4, 1, false, "Pitch rate", "rad/s") }, + { TelemetryVar.PitchRate_ST, new VarItem("PitchRate_ST", 4, 6, true, "Pitch rate at 360 Hz", "rad/s") }, + { TelemetryVar.PitOptRepairLeft, new VarItem("PitOptRepairLeft", 4, 1, false, "Time left for optional repairs if repairs are active", "s") }, + { TelemetryVar.PitRepairLeft, new VarItem("PitRepairLeft", 4, 1, false, "Time left for mandatory pit repairs if repairs are active", "s") }, + { TelemetryVar.PitsOpen, new VarItem("PitsOpen", 1, 1, false, "True if pit stop is allowed for the current player", "") }, + { TelemetryVar.PitstopActive, new VarItem("PitstopActive", 1, 1, false, "Is the player getting pit stop service", "") }, + { TelemetryVar.PitSvFlags, new VarItem("PitSvFlags", 3, 1, false, "Bitfield of pit service checkboxes", "PitSvFlags") }, + { TelemetryVar.PitSvFuel, new VarItem("PitSvFuel", 4, 1, false, "Pit service fuel add amount", "l or kWh") }, + { TelemetryVar.PitSvLFP, new VarItem("PitSvLFP", 4, 1, false, "Pit service left front tire pressure", "kPa") }, + { TelemetryVar.PitSvLRP, new VarItem("PitSvLRP", 4, 1, false, "Pit service left rear tire pressure", "kPa") }, + { TelemetryVar.PitSvRFP, new VarItem("PitSvRFP", 4, 1, false, "Pit service right front tire pressure", "kPa") }, + { TelemetryVar.PitSvRRP, new VarItem("PitSvRRP", 4, 1, false, "Pit service right rear tire pressure", "kPa") }, + { TelemetryVar.PitSvTireCompound, new VarItem("PitSvTireCompound", 2, 1, false, "Pit service pending tire compound", "") }, + { TelemetryVar.PlayerCarClass, new VarItem("PlayerCarClass", 2, 1, false, "Player car class id", "") }, + { TelemetryVar.PlayerCarClassPosition, new VarItem("PlayerCarClassPosition", 2, 1, false, "Players class position in race", "") }, + { TelemetryVar.PlayerCarDriverIncidentCount, new VarItem("PlayerCarDriverIncidentCount", 2, 1, false, "Teams current drivers incident count for this session", "") }, + { TelemetryVar.PlayerCarDryTireSetLimit, new VarItem("PlayerCarDryTireSetLimit", 2, 1, false, "Players dry tire set limit", "") }, + { TelemetryVar.PlayerCarIdx, new VarItem("PlayerCarIdx", 2, 1, false, "Players carIdx", "") }, + { TelemetryVar.PlayerCarInPitStall, new VarItem("PlayerCarInPitStall", 1, 1, false, "Players car is properly in their pitstall", "") }, + { TelemetryVar.PlayerCarMyIncidentCount, new VarItem("PlayerCarMyIncidentCount", 2, 1, false, "Players own incident count for this session", "") }, + { TelemetryVar.PlayerCarPitSvStatus, new VarItem("PlayerCarPitSvStatus", 2, 1, false, "Players car pit service status bits", "PitServiceStatus") }, + { TelemetryVar.PlayerCarPosition, new VarItem("PlayerCarPosition", 2, 1, false, "Players position in race", "") }, + { TelemetryVar.PlayerCarPowerAdjust, new VarItem("PlayerCarPowerAdjust", 4, 1, false, "Players power adjust", "%") }, + { TelemetryVar.PlayerCarSLBlinkRPM, new VarItem("PlayerCarSLBlinkRPM", 4, 1, false, "Shift light blink rpm", "revs/min") }, + { TelemetryVar.PlayerCarSLFirstRPM, new VarItem("PlayerCarSLFirstRPM", 4, 1, false, "Shift light first light rpm", "revs/min") }, + { TelemetryVar.PlayerCarSLLastRPM, new VarItem("PlayerCarSLLastRPM", 4, 1, false, "Shift light last light rpm", "revs/min") }, + { TelemetryVar.PlayerCarSLShiftRPM, new VarItem("PlayerCarSLShiftRPM", 4, 1, false, "Shift light shift rpm", "revs/min") }, + { TelemetryVar.PlayerCarTeamIncidentCount, new VarItem("PlayerCarTeamIncidentCount", 2, 1, false, "Players team incident count for this session", "") }, + { TelemetryVar.PlayerCarTowTime, new VarItem("PlayerCarTowTime", 4, 1, false, "Players car is being towed if time is greater than zero", "s") }, + { TelemetryVar.PlayerCarWeightPenalty, new VarItem("PlayerCarWeightPenalty", 4, 1, false, "Players weight penalty", "kg") }, + { TelemetryVar.PlayerFastRepairsUsed, new VarItem("PlayerFastRepairsUsed", 2, 1, false, "Players car number of fast repairs used", "") }, + { TelemetryVar.PlayerIncidents, new VarItem("PlayerIncidents", 2, 1, false, "Log incidents that the player received", "IncidentFlags") }, + { TelemetryVar.PlayerTireCompound, new VarItem("PlayerTireCompound", 2, 1, false, "Players car current tire compound", "") }, + { TelemetryVar.PlayerTrackSurface, new VarItem("PlayerTrackSurface", 2, 1, false, "Players car track surface type", "TrackLocation") }, + { TelemetryVar.PlayerTrackSurfaceMaterial, new VarItem("PlayerTrackSurfaceMaterial", 2, 1, false, "Players car track surface material type", "TrackSurface") }, + { TelemetryVar.Precipitation, new VarItem("Precipitation", 4, 1, false, "Precipitation at start/finish line", "%") }, + { TelemetryVar.PushToPass, new VarItem("PushToPass", 1, 1, false, "Push to pass button state", "") }, + { TelemetryVar.PushToTalk, new VarItem("PushToTalk", 1, 1, false, "Push to talk button state", "") }, + { TelemetryVar.RaceLaps, new VarItem("RaceLaps", 2, 1, false, "Laps completed in race", "") }, + { TelemetryVar.RadioTransmitCarIdx, new VarItem("RadioTransmitCarIdx", 2, 1, false, "The car index of the current person speaking on the radio", "") }, + { TelemetryVar.RadioTransmitFrequencyIdx, new VarItem("RadioTransmitFrequencyIdx", 2, 1, false, "The frequency index of the current person speaking on the radio", "") }, + { TelemetryVar.RadioTransmitRadioIdx, new VarItem("RadioTransmitRadioIdx", 2, 1, false, "The radio index of the current person speaking on the radio", "") }, + { TelemetryVar.RearTireSetsAvailable, new VarItem("RearTireSetsAvailable", 2, 1, false, "How many rear tire sets are remaining 255 is unlimited", "") }, + { TelemetryVar.RearTireSetsUsed, new VarItem("RearTireSetsUsed", 2, 1, false, "How many rear tire sets used so far", "") }, + { TelemetryVar.RelativeHumidity, new VarItem("RelativeHumidity", 4, 1, false, "Relative Humidity at start/finish line", "%") }, + { TelemetryVar.ReplayFrameNum, new VarItem("ReplayFrameNum", 2, 1, false, "Integer replay frame number (60 per second)", "") }, + { TelemetryVar.ReplayFrameNumEnd, new VarItem("ReplayFrameNumEnd", 2, 1, false, "Integer replay frame number from end of tape", "") }, + { TelemetryVar.ReplayPlaySlowMotion, new VarItem("ReplayPlaySlowMotion", 1, 1, false, "0=not slow motion 1=replay is in slow motion", "") }, + { TelemetryVar.ReplayPlaySpeed, new VarItem("ReplayPlaySpeed", 2, 1, false, "Replay playback speed", "") }, + { TelemetryVar.ReplaySessionNum, new VarItem("ReplaySessionNum", 2, 1, false, "Replay session number", "") }, + { TelemetryVar.ReplaySessionTime, new VarItem("ReplaySessionTime", 5, 1, false, "Seconds since replay session start", "s") }, + { TelemetryVar.RFbrakeLinePress, new VarItem("RFbrakeLinePress", 4, 1, false, "RF brake line pressure", "bar") }, + { TelemetryVar.RFcoldPressure, new VarItem("RFcoldPressure", 4, 1, false, "RF tire cold pressure as set in the garage", "kPa") }, + { TelemetryVar.RFodometer, new VarItem("RFodometer", 4, 1, false, "RF distance tire traveled since being placed on car", "m") }, + { TelemetryVar.RFpressure, new VarItem("RFpressure", 4, 1, false, "RF tire pressure", "kPa") }, + { TelemetryVar.RFrideHeight, new VarItem("RFrideHeight", 4, 1, false, "RF ride height", "m") }, + { TelemetryVar.RFshockDefl, new VarItem("RFshockDefl", 4, 1, false, "RF shock deflection", "m") }, + { TelemetryVar.RFshockDefl_ST, new VarItem("RFshockDefl_ST", 4, 6, true, "RF shock deflection at 360 Hz", "m") }, + { TelemetryVar.RFshockVel, new VarItem("RFshockVel", 4, 1, false, "RF shock velocity", "m/s") }, + { TelemetryVar.RFshockVel_ST, new VarItem("RFshockVel_ST", 4, 6, true, "RF shock velocity at 360 Hz", "m/s") }, + { TelemetryVar.RFSHshockDefl, new VarItem("RFSHshockDefl", 4, 1, false, "RFSH shock deflection", "m") }, + { TelemetryVar.RFSHshockDefl_ST, new VarItem("RFSHshockDefl_ST", 4, 6, true, "RFSH shock deflection at 360 Hz", "m") }, + { TelemetryVar.RFSHshockVel, new VarItem("RFSHshockVel", 4, 1, false, "RFSH shock velocity", "m/s") }, + { TelemetryVar.RFSHshockVel_ST, new VarItem("RFSHshockVel_ST", 4, 6, true, "RFSH shock velocity at 360 Hz", "m/s") }, + { TelemetryVar.RFspeed, new VarItem("RFspeed", 4, 1, false, "RF wheel speed", "m/s") }, + { TelemetryVar.RFtempCL, new VarItem("RFtempCL", 4, 1, false, "RF tire left carcass temperature", "C") }, + { TelemetryVar.RFtempCM, new VarItem("RFtempCM", 4, 1, false, "RF tire middle carcass temperature", "C") }, + { TelemetryVar.RFtempCR, new VarItem("RFtempCR", 4, 1, false, "RF tire right carcass temperature", "C") }, + { TelemetryVar.RFtempL, new VarItem("RFtempL", 4, 1, false, "RF tire left surface temperature", "C") }, + { TelemetryVar.RFtempM, new VarItem("RFtempM", 4, 1, false, "RF tire middle surface temperature", "C") }, + { TelemetryVar.RFtempR, new VarItem("RFtempR", 4, 1, false, "RF tire right surface temperature", "C") }, + { TelemetryVar.RFTiresAvailable, new VarItem("RFTiresAvailable", 2, 1, false, "How many right front tires are remaining 255 is unlimited", "") }, + { TelemetryVar.RFTiresUsed, new VarItem("RFTiresUsed", 2, 1, false, "How many right front tires used so far", "") }, + { TelemetryVar.RFwearL, new VarItem("RFwearL", 4, 1, false, "RF tire left percent tread remaining", "%") }, + { TelemetryVar.RFwearM, new VarItem("RFwearM", 4, 1, false, "RF tire middle percent tread remaining", "%") }, + { TelemetryVar.RFwearR, new VarItem("RFwearR", 4, 1, false, "RF tire right percent tread remaining", "%") }, + { TelemetryVar.RightTireSetsAvailable, new VarItem("RightTireSetsAvailable", 2, 1, false, "How many right tire sets are remaining 255 is unlimited", "") }, + { TelemetryVar.RightTireSetsUsed, new VarItem("RightTireSetsUsed", 2, 1, false, "How many right tire sets used so far", "") }, + { TelemetryVar.Roll, new VarItem("Roll", 4, 1, false, "Roll orientation", "rad") }, + { TelemetryVar.RollRate, new VarItem("RollRate", 4, 1, false, "Roll rate", "rad/s") }, + { TelemetryVar.RollRate_ST, new VarItem("RollRate_ST", 4, 6, true, "Roll rate at 360 Hz", "rad/s") }, + { TelemetryVar.RPM, new VarItem("RPM", 4, 1, false, "Engine rpm", "revs/min") }, + { TelemetryVar.RRbrakeLinePress, new VarItem("RRbrakeLinePress", 4, 1, false, "RR brake line pressure", "bar") }, + { TelemetryVar.RRcoldPressure, new VarItem("RRcoldPressure", 4, 1, false, "RR tire cold pressure as set in the garage", "kPa") }, + { TelemetryVar.RRodometer, new VarItem("RRodometer", 4, 1, false, "RR distance tire traveled since being placed on car", "m") }, + { TelemetryVar.RRpressure, new VarItem("RRpressure", 4, 1, false, "RR tire pressure", "kPa") }, + { TelemetryVar.RRrideHeight, new VarItem("RRrideHeight", 4, 1, false, "RR ride height", "m") }, + { TelemetryVar.RRshockDefl, new VarItem("RRshockDefl", 4, 1, false, "RR shock deflection", "m") }, + { TelemetryVar.RRshockDefl_ST, new VarItem("RRshockDefl_ST", 4, 6, true, "RR shock deflection at 360 Hz", "m") }, + { TelemetryVar.RRshockVel, new VarItem("RRshockVel", 4, 1, false, "RR shock velocity", "m/s") }, + { TelemetryVar.RRshockVel_ST, new VarItem("RRshockVel_ST", 4, 6, true, "RR shock velocity at 360 Hz", "m/s") }, + { TelemetryVar.RRSHshockDefl, new VarItem("RRSHshockDefl", 4, 1, false, "RRSH shock deflection", "m") }, + { TelemetryVar.RRSHshockVel, new VarItem("RRSHshockVel", 4, 1, false, "RRSH shock velocity", "m/s") }, + { TelemetryVar.RRspeed, new VarItem("RRspeed", 4, 1, false, "RR wheel speed", "m/s") }, + { TelemetryVar.RRtempCL, new VarItem("RRtempCL", 4, 1, false, "RR tire left carcass temperature", "C") }, + { TelemetryVar.RRtempCM, new VarItem("RRtempCM", 4, 1, false, "RR tire middle carcass temperature", "C") }, + { TelemetryVar.RRtempCR, new VarItem("RRtempCR", 4, 1, false, "RR tire right carcass temperature", "C") }, + { TelemetryVar.RRtempL, new VarItem("RRtempL", 4, 1, false, "RR tire left surface temperature", "C") }, + { TelemetryVar.RRtempM, new VarItem("RRtempM", 4, 1, false, "RR tire middle surface temperature", "C") }, + { TelemetryVar.RRtempR, new VarItem("RRtempR", 4, 1, false, "RR tire right surface temperature", "C") }, + { TelemetryVar.RRTiresAvailable, new VarItem("RRTiresAvailable", 2, 1, false, "How many right rear tires are remaining 255 is unlimited", "") }, + { TelemetryVar.RRTiresUsed, new VarItem("RRTiresUsed", 2, 1, false, "How many right rear tires used so far", "") }, + { TelemetryVar.RRwearL, new VarItem("RRwearL", 4, 1, false, "RR tire left percent tread remaining", "%") }, + { TelemetryVar.RRwearM, new VarItem("RRwearM", 4, 1, false, "RR tire middle percent tread remaining", "%") }, + { TelemetryVar.RRwearR, new VarItem("RRwearR", 4, 1, false, "RR tire right percent tread remaining", "%") }, + { TelemetryVar.SessionFlags, new VarItem("SessionFlags", 3, 1, false, "Session flags", "SessionFlags") }, + { TelemetryVar.SessionJokerLapsRemain, new VarItem("SessionJokerLapsRemain", 2, 1, false, "Joker laps remaining to be taken", "") }, + { TelemetryVar.SessionLapsRemain, new VarItem("SessionLapsRemain", 2, 1, false, "Old laps left till session ends use SessionLapsRemainEx", "") }, + { TelemetryVar.SessionLapsRemainEx, new VarItem("SessionLapsRemainEx", 2, 1, false, "New improved laps left till session ends", "") }, + { TelemetryVar.SessionLapsTotal, new VarItem("SessionLapsTotal", 2, 1, false, "Total number of laps in session", "") }, + { TelemetryVar.SessionNum, new VarItem("SessionNum", 2, 1, false, "Session number", "") }, + { TelemetryVar.SessionOnJokerLap, new VarItem("SessionOnJokerLap", 1, 1, false, "Player is currently completing a joker lap", "") }, + { TelemetryVar.SessionState, new VarItem("SessionState", 2, 1, false, "Session state", "SessionState") }, + { TelemetryVar.SessionTick, new VarItem("SessionTick", 2, 1, false, "Current update number", "") }, + { TelemetryVar.SessionTime, new VarItem("SessionTime", 5, 1, false, "Seconds since session start", "s") }, + { TelemetryVar.SessionTimeOfDay, new VarItem("SessionTimeOfDay", 4, 1, false, "Time of day in seconds", "s") }, + { TelemetryVar.SessionTimeRemain, new VarItem("SessionTimeRemain", 5, 1, false, "Seconds left till session ends", "s") }, + { TelemetryVar.SessionTimeTotal, new VarItem("SessionTimeTotal", 5, 1, false, "Total number of seconds in session", "s") }, + { TelemetryVar.SessionUniqueID, new VarItem("SessionUniqueID", 2, 1, false, "Session ID", "") }, + { TelemetryVar.Shifter, new VarItem("Shifter", 2, 1, false, "Log inputs from the players shifter control", "") }, + { TelemetryVar.ShiftGrindRPM, new VarItem("ShiftGrindRPM", 4, 1, false, "RPM of shifter grinding noise", "RPM") }, + { TelemetryVar.ShiftIndicatorPct, new VarItem("ShiftIndicatorPct", 4, 1, false, "DEPRECATED use DriverCarSLBlinkRPM instead", "%") }, + { TelemetryVar.ShiftPowerPct, new VarItem("ShiftPowerPct", 4, 1, false, "Friction torque applied to gears when shifting or grinding", "%") }, + { TelemetryVar.Skies, new VarItem("Skies", 2, 1, false, "Skies (0=clear/1=p cloudy/2=m cloudy/3=overcast)", "") }, + { TelemetryVar.SolarAltitude, new VarItem("SolarAltitude", 4, 1, false, "Sun angle above horizon in radians", "rad") }, + { TelemetryVar.SolarAzimuth, new VarItem("SolarAzimuth", 4, 1, false, "Sun angle clockwise from north in radians", "rad") }, + { TelemetryVar.Speed, new VarItem("Speed", 4, 1, false, "GPS vehicle speed", "m/s") }, + { TelemetryVar.SteeringFFBEnabled, new VarItem("SteeringFFBEnabled", 1, 1, false, "Force feedback is enabled", "") }, + { TelemetryVar.SteeringWheelAngle, new VarItem("SteeringWheelAngle", 4, 1, false, "Steering wheel angle", "rad") }, + { TelemetryVar.SteeringWheelAngleMax, new VarItem("SteeringWheelAngleMax", 4, 1, false, "Steering wheel max angle", "rad") }, + { TelemetryVar.SteeringWheelLimiter, new VarItem("SteeringWheelLimiter", 4, 1, false, "Force feedback limiter strength limits impacts and oscillation", "%") }, + { TelemetryVar.SteeringWheelMaxForceNm, new VarItem("SteeringWheelMaxForceNm", 4, 1, false, "Value of strength or max force slider in Nm for FFB", "N*m") }, + { TelemetryVar.SteeringWheelPctDamper, new VarItem("SteeringWheelPctDamper", 4, 1, false, "Force feedback % max damping", "%") }, + { TelemetryVar.SteeringWheelPctIntensity, new VarItem("SteeringWheelPctIntensity", 4, 1, false, "Force feedback % max intensity", "%") }, + { TelemetryVar.SteeringWheelPctSmoothing, new VarItem("SteeringWheelPctSmoothing", 4, 1, false, "Force feedback % max smoothing", "%") }, + { TelemetryVar.SteeringWheelPctTorque, new VarItem("SteeringWheelPctTorque", 4, 1, false, "Force feedback % max torque on steering shaft unsigned", "%") }, + { TelemetryVar.SteeringWheelPctTorqueSign, new VarItem("SteeringWheelPctTorqueSign", 4, 1, false, "Force feedback % max torque on steering shaft signed", "%") }, + { TelemetryVar.SteeringWheelPctTorqueSignStops, new VarItem("SteeringWheelPctTorqueSignStops", 4, 1, false, "Force feedback % max torque on steering shaft signed stops", "%") }, + { TelemetryVar.SteeringWheelPeakForceNm, new VarItem("SteeringWheelPeakForceNm", 4, 1, false, "Peak torque mapping to direct input units for FFB", "N*m") }, + { TelemetryVar.SteeringWheelTorque, new VarItem("SteeringWheelTorque", 4, 1, false, "Output torque on steering shaft", "N*m") }, + { TelemetryVar.SteeringWheelTorque_ST, new VarItem("SteeringWheelTorque_ST", 4, 6, true, "Output torque on steering shaft at 360 Hz", "N*m") }, + { TelemetryVar.SteeringWheelUseLinear, new VarItem("SteeringWheelUseLinear", 1, 1, false, "True if steering wheel force is using linear mode", "") }, + { TelemetryVar.Throttle, new VarItem("Throttle", 4, 1, false, "0=off throttle to 1=full throttle", "%") }, + { TelemetryVar.ThrottleRaw, new VarItem("ThrottleRaw", 4, 1, false, "Raw throttle input 0=off throttle to 1=full throttle", "%") }, + { TelemetryVar.TireLF_RumblePitch, new VarItem("TireLF_RumblePitch", 4, 1, false, "Players LF Tire Sound rumblestrip pitch", "Hz") }, + { TelemetryVar.TireLR_RumblePitch, new VarItem("TireLR_RumblePitch", 4, 1, false, "Players LR Tire Sound rumblestrip pitch", "Hz") }, + { TelemetryVar.TireRF_RumblePitch, new VarItem("TireRF_RumblePitch", 4, 1, false, "Players RF Tire Sound rumblestrip pitch", "Hz") }, + { TelemetryVar.TireRR_RumblePitch, new VarItem("TireRR_RumblePitch", 4, 1, false, "Players RR Tire Sound rumblestrip pitch", "Hz") }, + { TelemetryVar.TireSetsAvailable, new VarItem("TireSetsAvailable", 2, 1, false, "How many tire sets are remaining 255 is unlimited", "") }, + { TelemetryVar.TireSetsUsed, new VarItem("TireSetsUsed", 2, 1, false, "How many tire sets used so far", "") }, + { TelemetryVar.TrackTemp, new VarItem("TrackTemp", 4, 1, false, "Deprecated set to TrackTempCrew", "C") }, + { TelemetryVar.TrackTempCrew, new VarItem("TrackTempCrew", 4, 1, false, "Temperature of track measured by crew around track", "C") }, + { TelemetryVar.TrackWetness, new VarItem("TrackWetness", 2, 1, false, "How wet is the average track surface", "TrackWetness") }, + { TelemetryVar.VelocityX, new VarItem("VelocityX", 4, 1, false, "X velocity", "m/s") }, + { TelemetryVar.VelocityX_ST, new VarItem("VelocityX_ST", 4, 6, true, "X velocity", "m/s at 360 Hz") }, + { TelemetryVar.VelocityY, new VarItem("VelocityY", 4, 1, false, "Y velocity", "m/s") }, + { TelemetryVar.VelocityY_ST, new VarItem("VelocityY_ST", 4, 6, true, "Y velocity", "m/s at 360 Hz") }, + { TelemetryVar.VelocityZ, new VarItem("VelocityZ", 4, 1, false, "Z velocity", "m/s") }, + { TelemetryVar.VelocityZ_ST, new VarItem("VelocityZ_ST", 4, 6, true, "Z velocity", "m/s at 360 Hz") }, + { TelemetryVar.VertAccel, new VarItem("VertAccel", 4, 1, false, "Vertical acceleration (including gravity)", "m/s^2") }, + { TelemetryVar.VertAccel_ST, new VarItem("VertAccel_ST", 4, 6, true, "Vertical acceleration (including gravity) at 360 Hz", "m/s^2") }, + { TelemetryVar.VidCapActive, new VarItem("VidCapActive", 1, 1, false, "True if video currently being captured", "") }, + { TelemetryVar.VidCapEnabled, new VarItem("VidCapEnabled", 1, 1, false, "True if video capture system is enabled", "") }, + { TelemetryVar.Voltage, new VarItem("Voltage", 4, 1, false, "Engine voltage", "V") }, + { TelemetryVar.WaterLevel, new VarItem("WaterLevel", 4, 1, false, "Engine coolant level", "l") }, + { TelemetryVar.WaterTemp, new VarItem("WaterTemp", 4, 1, false, "Engine coolant temp", "C") }, + { TelemetryVar.WeatherDeclaredWet, new VarItem("WeatherDeclaredWet", 1, 1, false, "The steward says rain tires can be used", "") }, + { TelemetryVar.WeatherType, new VarItem("WeatherType", 2, 1, false, "Weather dynamics type", "WeatherDynamics") }, + { TelemetryVar.WeatherVersion, new VarItem("WeatherVersion", 2, 1, false, "Weather version", "WeatherVersion") }, + { TelemetryVar.WindDir, new VarItem("WindDir", 4, 1, false, "Wind direction at start/finish line", "rad") }, + { TelemetryVar.WindVel, new VarItem("WindVel", 4, 1, false, "Wind velocity at start/finish line", "m/s") }, + { TelemetryVar.Yaw, new VarItem("Yaw", 4, 1, false, "Yaw orientation", "rad") }, + { TelemetryVar.YawNorth, new VarItem("YawNorth", 4, 1, false, "Yaw orientation relative to north", "rad") }, + { TelemetryVar.YawRate, new VarItem("YawRate", 4, 1, false, "Yaw rate", "rad/s") }, + { TelemetryVar.YawRate_ST, new VarItem("YawRate_ST", 4, 6, true, "Yaw rate at 360 Hz", "rad/s") }, }; } } diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK.EnumsAndFlags/TelemetryClient_Enums.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK.EnumsAndFlags/TelemetryClient_Enums.cs index df649f1..acc470b 100644 --- a/Sdk/SVappsLAB.iRacingTelemetrySDK.EnumsAndFlags/TelemetryClient_Enums.cs +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK.EnumsAndFlags/TelemetryClient_Enums.cs @@ -11,7 +11,7 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; + * limitations under the License. **/ diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK.EnumsAndFlags/TelemetryClient_Flags.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK.EnumsAndFlags/TelemetryClient_Flags.cs index 63bfa87..01f5f91 100644 --- a/Sdk/SVappsLAB.iRacingTelemetrySDK.EnumsAndFlags/TelemetryClient_Flags.cs +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK.EnumsAndFlags/TelemetryClient_Flags.cs @@ -11,7 +11,7 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; + * limitations under the License. **/ // bit fields diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK.EnumsAndFlags/TelemetryVar.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK.EnumsAndFlags/TelemetryVar.cs new file mode 100644 index 0000000..8ec5662 --- /dev/null +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK.EnumsAndFlags/TelemetryVar.cs @@ -0,0 +1,427 @@ +/** + * Copyright (C) 2024-2025 Scott Velez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +**/ + +namespace SVappsLAB.iRacingTelemetrySDK +{ + // This enum is used to select which telemetry variables to include in the telemetry data. + public enum TelemetryVar + { + AirDensity, + AirPressure, + AirTemp, + Alt, + Brake, + BrakeABSactive, + BrakeABScutPct, + BrakeRaw, + CamCameraNumber, + CamCameraState, + CamCarIdx, + CamGroupNumber, + CarDistAhead, + CarDistBehind, + CarIdxBestLapNum, + CarIdxBestLapTime, + CarIdxClass, + CarIdxClassPosition, + CarIdxEstTime, + CarIdxF2Time, + CarIdxFastRepairsUsed, + CarIdxGear, + CarIdxLap, + CarIdxLapCompleted, + CarIdxLapDistPct, + CarIdxLastLapTime, + CarIdxOnPitRoad, + CarIdxP2P_Count, + CarIdxP2P_Status, + CarIdxPaceFlags, + CarIdxPaceLine, + CarIdxPaceRow, + CarIdxPosition, + CarIdxQualTireCompound, + CarIdxQualTireCompoundLocked, + CarIdxRPM, + CarIdxSessionFlags, + CarIdxSteer, + CarIdxTireCompound, + CarIdxTrackSurface, + CarIdxTrackSurfaceMaterial, + CarLeftRight, + CFshockDefl, + CFshockDefl_ST, + CFshockVel, + CFshockVel_ST, + CFSRrideHeight, + ChanAvgLatency, + ChanClockSkew, + ChanLatency, + ChanPartnerQuality, + ChanQuality, + Clutch, + ClutchRaw, + CpuUsageBG, + CpuUsageFG, + CRshockDefl, + CRshockDefl_ST, + CRshockVel, + CRshockVel_ST, + dcAntiRollFront, + dcAntiRollRear, + dcBrakeBias, + dcDashPage, + DCDriversSoFar, + dcFuelMixture, + dcHeadlightFlash, + DCLapStatus, + dcLaunchRPM, + dcLowFuelAccept, + dcPitSpeedLimiterToggle, + dcPowerSteering, + dcPushToPass, + dcRFBrakeAttachedToggle, + dcStarter, + dcTearOffVisor, + dcThrottleShape, + dcToggleWindshieldWipers, + dcTractionControl, + dcTractionControlToggle, + dcTriggerWindshieldWipers, + dcWeightJackerRight, + DisplayUnits, + dpFastRepair, + dpFuelAddKg, + dpFuelAutoFillActive, + dpFuelAutoFillEnabled, + dpFuelFill, + dpLFTireChange, + dpLFTireColdPress, + dpLRTireChange, + dpLRTireColdPress, + dpLTireChange, + dpRFTireChange, + dpRFTireColdPress, + dpRRTireChange, + dpRRTireColdPress, + dpRTireChange, + dpTireChange, + dpWindshieldTearoff, + dpWingFront, + dpWingRear, + DriverMarker, + Engine0_RPM, + EngineWarnings, + EnterExitReset, + FastRepairAvailable, + FastRepairUsed, + FogLevel, + FrameRate, + FrontTireSetsAvailable, + FrontTireSetsUsed, + FuelLevel, + FuelLevelPct, + FuelPress, + FuelUsePerHour, + Gear, + GpuUsage, + HandbrakeRaw, + IsDiskLoggingActive, + IsDiskLoggingEnabled, + IsGarageVisible, + IsInGarage, + IsOnTrack, + IsOnTrackCar, + IsReplayPlaying, + Lap, + LapBestLap, + LapBestLapTime, + LapBestNLapLap, + LapBestNLapTime, + LapCompleted, + LapCurrentLapTime, + LapDeltaToBestLap, + LapDeltaToBestLap_DD, + LapDeltaToBestLap_OK, + LapDeltaToOptimalLap, + LapDeltaToOptimalLap_DD, + LapDeltaToOptimalLap_OK, + LapDeltaToSessionBestLap, + LapDeltaToSessionBestLap_DD, + LapDeltaToSessionBestLap_OK, + LapDeltaToSessionLastlLap, + LapDeltaToSessionLastlLap_DD, + LapDeltaToSessionLastlLap_OK, + LapDeltaToSessionOptimalLap, + LapDeltaToSessionOptimalLap_DD, + LapDeltaToSessionOptimalLap_OK, + LapDist, + LapDistPct, + LapLasNLapSeq, + LapLastLapTime, + LapLastNLapTime, + Lat, + LatAccel, + LatAccel_ST, + LeftTireSetsAvailable, + LeftTireSetsUsed, + LFbrakeLinePress, + LFcoldPressure, + LFpressure, + LFrideHeight, + LFshockDefl, + LFshockDefl_ST, + LFshockVel, + LFshockVel_ST, + LFSHshockDefl, + LFSHshockDefl_ST, + LFSHshockVel, + LFSHshockVel_ST, + LFspeed, + LFtempCL, + LFtempCM, + LFtempCR, + LFtempL, + LFtempM, + LFtempR, + LFTiresAvailable, + LFTiresUsed, + LFwearL, + LFwearM, + LFwearR, + LFodometer, + LoadNumTextures, + Lon, + LongAccel, + LongAccel_ST, + LR2shockDefl, + LR2shockDefl_ST, + LR2shockVel, + LR2shockVel_ST, + LRbrakeLinePress, + LRcoldPressure, + LRpressure, + LRrideHeight, + LRshockDefl, + LRshockDefl_ST, + LRshockVel, + LRshockVel_ST, + LRSHshockDefl, + LRSHshockVel, + LRspeed, + LRtempCL, + LRtempCM, + LRtempCR, + LRtempL, + LRtempM, + LRtempR, + LRTiresAvailable, + LRTiresUsed, + LRwearL, + LRwearM, + LRwearR, + LRodometer, + ManifoldPress, + ManualBoost, + ManualNoBoost, + MemPageFaultSec, + MemSoftPageFaultSec, + OilLevel, + OilPress, + OilTemp, + OkToReloadTextures, + OnPitRoad, + P2P_Count, + P2P_Status, + PaceMode, + Pitch, + PitchRate, + PitchRate_ST, + PitOptRepairLeft, + PitRepairLeft, + PitsOpen, + PitstopActive, + PitSvFlags, + PitSvFuel, + PitSvLFP, + PitSvLRP, + PitSvRFP, + PitSvRRP, + PitSvTireCompound, + PlayerCarClass, + PlayerCarClassPosition, + PlayerCarDriverIncidentCount, + PlayerCarDryTireSetLimit, + PlayerCarIdx, + PlayerCarInPitStall, + PlayerCarMyIncidentCount, + PlayerCarPitSvStatus, + PlayerCarPosition, + PlayerCarPowerAdjust, + PlayerCarSLBlinkRPM, + PlayerCarSLFirstRPM, + PlayerCarSLLastRPM, + PlayerCarSLShiftRPM, + PlayerCarTeamIncidentCount, + PlayerCarTowTime, + PlayerCarWeightPenalty, + PlayerFastRepairsUsed, + PlayerIncidents, + PlayerTireCompound, + PlayerTrackSurface, + PlayerTrackSurfaceMaterial, + Precipitation, + PushToPass, + PushToTalk, + RaceLaps, + RadioTransmitCarIdx, + RadioTransmitFrequencyIdx, + RadioTransmitRadioIdx, + RearTireSetsAvailable, + RearTireSetsUsed, + RelativeHumidity, + ReplayFrameNum, + ReplayFrameNumEnd, + ReplayPlaySlowMotion, + ReplayPlaySpeed, + ReplaySessionNum, + ReplaySessionTime, + RFbrakeLinePress, + RFcoldPressure, + RFpressure, + RFrideHeight, + RFshockDefl, + RFshockDefl_ST, + RFshockVel, + RFshockVel_ST, + RFSHshockDefl, + RFSHshockDefl_ST, + RFSHshockVel, + RFSHshockVel_ST, + RFspeed, + RFtempCL, + RFtempCM, + RFtempCR, + RFtempL, + RFtempM, + RFtempR, + RFTiresAvailable, + RFTiresUsed, + RFwearL, + RFwearM, + RFwearR, + RFodometer, + RightTireSetsAvailable, + RightTireSetsUsed, + Roll, + RollRate, + RollRate_ST, + RPM, + RRbrakeLinePress, + RRcoldPressure, + RRpressure, + RRrideHeight, + RRshockDefl, + RRshockDefl_ST, + RRshockVel, + RRshockVel_ST, + RRSHshockDefl, + RRSHshockVel, + RRspeed, + RRtempCL, + RRtempCM, + RRtempCR, + RRtempL, + RRtempM, + RRtempR, + RRTiresAvailable, + RRTiresUsed, + RRwearL, + RRwearM, + RRwearR, + RRodometer, + SessionFlags, + SessionJokerLapsRemain, + SessionLapsRemain, + SessionLapsRemainEx, + SessionLapsTotal, + SessionNum, + SessionOnJokerLap, + SessionState, + SessionTick, + SessionTime, + SessionTimeOfDay, + SessionTimeRemain, + SessionTimeTotal, + SessionUniqueID, + ShiftGrindRPM, + ShiftIndicatorPct, + ShiftPowerPct, + Shifter, + Skies, + SolarAltitude, + SolarAzimuth, + Speed, + SteeringFFBEnabled, + SteeringWheelAngle, + SteeringWheelAngleMax, + SteeringWheelLimiter, + SteeringWheelMaxForceNm, + SteeringWheelPctDamper, + SteeringWheelPctIntensity, + SteeringWheelPctSmoothing, + SteeringWheelPctTorque, + SteeringWheelPctTorqueSign, + SteeringWheelPctTorqueSignStops, + SteeringWheelPeakForceNm, + SteeringWheelTorque, + SteeringWheelTorque_ST, + SteeringWheelUseLinear, + Throttle, + ThrottleRaw, + TireLF_RumblePitch, + TireLR_RumblePitch, + TireRF_RumblePitch, + TireRR_RumblePitch, + TireSetsAvailable, + TireSetsUsed, + TrackTemp, + TrackTempCrew, + TrackWetness, + VelocityX, + VelocityX_ST, + VelocityY, + VelocityY_ST, + VelocityZ, + VelocityZ_ST, + VertAccel, + VertAccel_ST, + VidCapActive, + VidCapEnabled, + Voltage, + WaterLevel, + WaterTemp, + WeatherDeclaredWet, + WeatherType, + WeatherVersion, + WindDir, + WindVel, + Yaw, + YawNorth, + YawRate, + YawRate_ST, + dcABS, + } +} diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK.sln b/Sdk/SVappsLAB.iRacingTelemetrySDK.sln index e650729..533ad29 100644 --- a/Sdk/SVappsLAB.iRacingTelemetrySDK.sln +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK.sln @@ -1,7 +1,6 @@ -๏ปฟ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.10.35027.167 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.11018.127 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SVappsLAB.iRacingTelemetrySDK", "SVappsLAB.iRacingTelemetrySDK\SVappsLAB.iRacingTelemetrySDK.csproj", "{30E3D703-1A4D-46A3-BD8E-60C6F618851C}" EndProject @@ -11,18 +10,19 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{B72EA0DF EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests", "tests\UnitTests\UnitTests.csproj", "{21625842-A56A-4F10-8F12-E6E27C5A167B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IBT_Tests", "tests\IBT_Tests\IBT_Tests.csproj", "{C15621B4-C2E5-4A3C-A36F-F252E823EA72}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Live_Tests", "tests\Live_Tests\Live_Tests.csproj", "{42A7E161-8B51-4D06-9FEF-2430A6E27567}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SVappsLAB.iRacingTelemetrySDK.EnumsAndFlags", "SVappsLAB.iRacingTelemetrySDK.EnumsAndFlags\SVappsLAB.iRacingTelemetrySDK.EnumsAndFlags.csproj", "{33B96CF1-348F-4143-BD67-CB842762B07B}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extras", "Extras", "{09B1AB62-FEF3-472D-BD82-C02514C84177}" ProjectSection(SolutionItems) = preProject + ..\Directory.Build.props = ..\Directory.Build.props ..\LICENSE = ..\LICENSE + ..\MIGRATION_GUIDE.md = ..\MIGRATION_GUIDE.md + ..\NUGET.md = ..\NUGET.md ..\README.md = ..\README.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmokeTests", "tests\SmokeTests\SmokeTests.csproj", "{55338C68-90C6-4731-A419-DCECB10F5B31}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -41,26 +41,21 @@ Global {21625842-A56A-4F10-8F12-E6E27C5A167B}.Debug|Any CPU.Build.0 = Debug|Any CPU {21625842-A56A-4F10-8F12-E6E27C5A167B}.Release|Any CPU.ActiveCfg = Release|Any CPU {21625842-A56A-4F10-8F12-E6E27C5A167B}.Release|Any CPU.Build.0 = Release|Any CPU - {C15621B4-C2E5-4A3C-A36F-F252E823EA72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C15621B4-C2E5-4A3C-A36F-F252E823EA72}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C15621B4-C2E5-4A3C-A36F-F252E823EA72}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C15621B4-C2E5-4A3C-A36F-F252E823EA72}.Release|Any CPU.Build.0 = Release|Any CPU - {42A7E161-8B51-4D06-9FEF-2430A6E27567}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {42A7E161-8B51-4D06-9FEF-2430A6E27567}.Debug|Any CPU.Build.0 = Debug|Any CPU - {42A7E161-8B51-4D06-9FEF-2430A6E27567}.Release|Any CPU.ActiveCfg = Release|Any CPU - {42A7E161-8B51-4D06-9FEF-2430A6E27567}.Release|Any CPU.Build.0 = Release|Any CPU {33B96CF1-348F-4143-BD67-CB842762B07B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {33B96CF1-348F-4143-BD67-CB842762B07B}.Debug|Any CPU.Build.0 = Debug|Any CPU {33B96CF1-348F-4143-BD67-CB842762B07B}.Release|Any CPU.ActiveCfg = Release|Any CPU {33B96CF1-348F-4143-BD67-CB842762B07B}.Release|Any CPU.Build.0 = Release|Any CPU + {55338C68-90C6-4731-A419-DCECB10F5B31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {55338C68-90C6-4731-A419-DCECB10F5B31}.Debug|Any CPU.Build.0 = Debug|Any CPU + {55338C68-90C6-4731-A419-DCECB10F5B31}.Release|Any CPU.ActiveCfg = Release|Any CPU + {55338C68-90C6-4731-A419-DCECB10F5B31}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {21625842-A56A-4F10-8F12-E6E27C5A167B} = {B72EA0DF-4C9E-46DA-B581-3E5A456E711C} - {C15621B4-C2E5-4A3C-A36F-F252E823EA72} = {B72EA0DF-4C9E-46DA-B581-3E5A456E711C} - {42A7E161-8B51-4D06-9FEF-2430A6E27567} = {B72EA0DF-4C9E-46DA-B581-3E5A456E711C} + {55338C68-90C6-4731-A419-DCECB10F5B31} = {B72EA0DF-4C9E-46DA-B581-3E5A456E711C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A774B726-6C97-4ECE-AB77-C788463CA995} diff --git a/Sdk/tests/Live_Tests/GlobalUsings.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK/Constants.cs similarity index 74% rename from Sdk/tests/Live_Tests/GlobalUsings.cs rename to Sdk/SVappsLAB.iRacingTelemetrySDK/Constants.cs index b78209c..932f562 100644 --- a/Sdk/tests/Live_Tests/GlobalUsings.cs +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/Constants.cs @@ -11,7 +11,13 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; + * limitations under the License. **/ -global using Xunit; +namespace SVappsLAB.iRacingTelemetrySDK +{ + internal static class Constants + { + public const string SDK_NAME = "SVappsLAB.iRacingTelemetrySDK"; + } +} \ No newline at end of file diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/DataProviderBase.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/DataProviderBase.cs index a95f3e1..9fb9277 100644 --- a/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/DataProviderBase.cs +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/DataProviderBase.cs @@ -11,7 +11,7 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; + * limitations under the License. **/ using System; @@ -19,6 +19,8 @@ using System.IO.MemoryMappedFiles; using System.Runtime.InteropServices; using System.Text; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using SVappsLAB.iRacingTelemetrySDK.irSDKDefines; @@ -38,7 +40,7 @@ public VarHeaderDictionary() : base(StringComparer.InvariantCultureIgnoreCase) } } - internal abstract unsafe class DataProviderBase : IDisposable + internal abstract unsafe class DataProviderBase : IAsyncDisposable { private static readonly Encoding TelemetryEncoding = Encoding.GetEncoding("ISO-8859-1"); @@ -56,12 +58,25 @@ internal abstract unsafe class DataProviderBase : IDisposable public DataProviderBase(ILogger logger) { _logger = logger; - _logger.LogInformation($"Initializing {GetType().Name}."); + _logger.LogDebug($"Initializing {GetType().Name}."); } - public void Dispose() + + public virtual ValueTask DisposeAsync() { - Dispose(true); + if (_viewAccessor != null) + { + _viewAccessor.SafeMemoryMappedViewHandle.ReleasePointer(); + _viewAccessor.Dispose(); + _viewAccessor = null; + } + if (_mmFile != null) + { + _mmFile.Dispose(); + _mmFile = null; + } + GC.SuppressFinalize(this); + return ValueTask.CompletedTask; } public void OpenDataSource(string ibtFilename) @@ -110,11 +125,12 @@ public string GetSessionInfoYaml() return sessInfo; } - public object GetVarValue(string varName) + public object? GetVarValue(string varName) { if (!_varHeaders!.TryGetValue(varName, out irsdk_varHeader vh)) { - throw new Exception($"the telemetry value '{varName}' does not exist"); + _logger.LogDebug("Telemetry variable [{varName}] not found in data provider", varName); + return null; } var rosBuffer = _telemetryDataBuffer.AsSpan(); @@ -139,23 +155,23 @@ public object GetVarValue(string varName) break; case irsdk_VarType.irsdk_bool: { - val = GetValue(rosBuffer, vh.offset, vh.count); + val = GetValue(rosBuffer, vh.offset, vh.count, vh.type); } break; case irsdk_VarType.irsdk_int: case irsdk_VarType.irsdk_bitField: { - val = GetValue(rosBuffer, vh.offset, vh.count); + val = GetValue(rosBuffer, vh.offset, vh.count, vh.type); } break; case irsdk_VarType.irsdk_float: { - val = GetValue(rosBuffer, vh.offset, vh.count); + val = GetValue(rosBuffer, vh.offset, vh.count, vh.type); } break; case irsdk_VarType.irsdk_double: { - val = GetValue(rosBuffer, vh.offset, vh.count); + val = GetValue(rosBuffer, vh.offset, vh.count, vh.type); } break; default: @@ -166,7 +182,7 @@ public object GetVarValue(string varName) } // wait for iRacing to signal there is new data - public abstract bool WaitForDataReady(TimeSpan timeSpan); + public abstract Task WaitForDataReady(TimeSpan timeSpan, CancellationToken cancellationToken = default); public VarHeaderDictionary? GetVarHeaders() { @@ -182,27 +198,6 @@ protected void CopyNewTelemetryDataToBuffer(int recNum = 0) ros.CopyTo(_telemetryDataBuffer); } - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - if (_viewAccessor != null) - { - _viewAccessor.SafeMemoryMappedViewHandle.ReleasePointer(); - _viewAccessor.Dispose(); - _viewAccessor = null; - } - if (_mmFile != null) - { - _mmFile.Dispose(); - _mmFile = null; - } - } - } - ~DataProviderBase() - { - Dispose(false); - } VarHeaderDictionary ReadVarHeaders() { var ros = new ReadOnlySpan(_dataPtr + _header.varHeaderOffset, _header.numVars); @@ -244,15 +239,29 @@ private string ExtractNullTerminatedString(Span data, int expectedLength) return TelemetryEncoding.GetString(data.Slice(0, actualLength)); } - object GetValue(ReadOnlySpan span, int offset, int count) where T : struct + object GetValue(ReadOnlySpan span, int offset, int count, irsdk_VarType type) where T : struct { - var ros = MemoryMarshal.Cast(span.Slice(offset, count * Marshal.SizeOf(typeof(T)))); - var data = ros.ToArray(); - object val = count == 1 ? data[0] : data; - return val; + int elementSizeInBytes = type switch + { + irsdk_VarType.irsdk_bool => sizeof(bool), + irsdk_VarType.irsdk_int => sizeof(int), + irsdk_VarType.irsdk_bitField => sizeof(int), + irsdk_VarType.irsdk_float => sizeof(float), + irsdk_VarType.irsdk_double => sizeof(double), + _ => throw new NotImplementedException($"{type} size not implemented") + }; + + var ros = MemoryMarshal.Cast(span.Slice(offset, count * elementSizeInBytes)); + + // optimize memory allocation: avoid array allocation for single values + if (count == 1) + return ros[0]; + else + return ros.ToArray(); } } + } diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/IBTDataProvider.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/IBTDataProvider.cs index 43ee8b9..86b2b79 100644 --- a/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/IBTDataProvider.cs +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/IBTDataProvider.cs @@ -11,13 +11,15 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; + * limitations under the License. **/ using System; using System.IO; using System.IO.MemoryMappedFiles; using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using SVappsLAB.iRacingTelemetrySDK.IBTPlayback; using SVappsLAB.iRacingTelemetrySDK.irSDKDefines; @@ -68,11 +70,13 @@ public int GetNumRecordsInIBTFile() } // wait for iRacing to signal there is new data - public override bool WaitForDataReady(TimeSpan _timeSpan) + public override Task WaitForDataReady(TimeSpan _timeSpan, CancellationToken cancellationToken = default) { - // throttle playback speed - _governor.GovernSpeed(_currentRecord).Wait(); + return IBTDataProviderAsyncHelper.WaitForDataReady(_governor, _currentRecord, this); + } + internal bool ProcessNextRecord() + { CopyNewTelemetryDataToBuffer(_currentRecord); _currentRecord++; @@ -91,9 +95,22 @@ irsdk_diskSubHeader GetDiskSubHeader() return diskSubHeader; } - protected override void Dispose(bool disposing) + public override ValueTask DisposeAsync() + { + // IBTDataProvider doesn't have additional resources to dispose + return base.DisposeAsync(); + } + } + + // Helper class to handle async operations outside unsafe context + internal static class IBTDataProviderAsyncHelper + { + public static async Task WaitForDataReady(IPlaybackGovernor governor, int currentRecord, IBTDataProvider provider) { - base.Dispose(disposing); + // throttle playback speed + await governor.GovernSpeed(currentRecord).ConfigureAwait(false); + + return provider.ProcessNextRecord(); } } } diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/IDataProvider.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/IDataProvider.cs index db6a29a..ec8049d 100644 --- a/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/IDataProvider.cs +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/IDataProvider.cs @@ -11,16 +11,18 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; + * limitations under the License. **/ using System; using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using SVappsLAB.iRacingTelemetrySDK.irSDKDefines; namespace SVappsLAB.iRacingTelemetrySDK.DataProviders { - internal interface IDataProvider : IDisposable + internal interface IDataProvider : IAsyncDisposable { void OpenDataSource(); /// @@ -58,17 +60,17 @@ internal interface IDataProvider : IDisposable /// Gets the value of a telemetry variable by name. /// /// The name of the variable to retrieve. - /// The value of the variable, which could be a scalar or an array. + /// The value of the variable, which could be a scalar or an array. Returns null if the variable is not found. /// Thrown when variable headers or telemetry buffer are not initialized. - /// Thrown when the specified variable name does not exist. /// Thrown when the variable's data would exceed buffer boundaries. - object GetVarValue(string varName); + object? GetVarValue(string varName); /// /// Waits for new telemetry data to become available. /// /// Maximum time to wait for new data. + /// Cancellation token to cancel the wait operation. /// True if new data is available; otherwise, false (timeout). - bool WaitForDataReady(TimeSpan timeout); + Task WaitForDataReady(TimeSpan timeout, CancellationToken cancellationToken = default); } } diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/LiveDataProvider.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/LiveDataProvider.cs index f5f18a7..bb89ce7 100644 --- a/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/LiveDataProvider.cs +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/LiveDataProvider.cs @@ -11,12 +11,13 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; + * limitations under the License. **/ using System; using System.IO.MemoryMappedFiles; using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using SVappsLAB.iRacingTelemetrySDK.irSDKDefines; @@ -50,15 +51,13 @@ public override void OpenDataSource() _dataReadyEvent = new AutoResetEvent(false) { SafeWaitHandle = new Microsoft.Win32.SafeHandles.SafeWaitHandle(rawEvent, true) }; } - public override bool WaitForDataReady(TimeSpan timeSpan) + public override Task WaitForDataReady(TimeSpan timeSpan, CancellationToken cancellationToken = default) { - var signaled = _dataReadyEvent!.WaitOne(timeSpan); - if (!signaled) - { - _logger.LogDebug("timeout waiting for data ready event"); - return false; - } + return LiveDataProviderAsyncHelper.WaitForDataReady(_dataReadyEvent!, timeSpan, cancellationToken, _logger, this); + } + internal bool ProcessNewData() + { var latestTickCount = GetLatestVarBuff().tickCount; // if we missed any telemetry data, log that it happened @@ -101,17 +100,30 @@ irsdk_varBuf GetLatestVarBuff() return vb; } - protected override void Dispose(bool disposing) + public override ValueTask DisposeAsync() { - if (disposing) + if (_dataReadyEvent != null) { - if (_dataReadyEvent != null) - { - _dataReadyEvent.Dispose(); - _dataReadyEvent = null; - } + _dataReadyEvent.Dispose(); + _dataReadyEvent = null; + } + return base.DisposeAsync(); + } + } + + // Helper class to handle async operations outside unsafe context + internal static class LiveDataProviderAsyncHelper + { + public static async Task WaitForDataReady(AutoResetEvent dataReadyEvent, TimeSpan timeSpan, CancellationToken cancellationToken, ILogger logger, LiveDataProvider provider) + { + var signaled = await Task.Run(() => dataReadyEvent.WaitOne(timeSpan), cancellationToken).ConfigureAwait(false); + if (!signaled) + { + logger.LogDebug("timeout waiting for data ready event"); + return false; } - base.Dispose(disposing); + + return provider.ProcessNewData(); } } } diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/IBTPlayback/Governor.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK/IBTPlayback/Governor.cs index 2a31ffd..d0bf1cc 100644 --- a/Sdk/SVappsLAB.iRacingTelemetrySDK/IBTPlayback/Governor.cs +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/IBTPlayback/Governor.cs @@ -11,7 +11,7 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; + * limitations under the License. **/ using System; @@ -21,16 +21,16 @@ namespace SVappsLAB.iRacingTelemetrySDK.IBTPlayback { - public record GovernorStats(long elapsedMs, int CurrentRecord, int TargetRecord, double OldDelay, double NewDelay); + internal record GovernorStats(long elapsedMs, int CurrentRecord, int TargetRecord, double OldDelay, double NewDelay); - public interface IPlaybackGovernor + internal interface IPlaybackGovernor { public void StartPlayback(); public Task GovernSpeed(int recNum); public GovernorStats GetStats(); } - public class SimpleGovernor : IPlaybackGovernor + internal class SimpleGovernor : IPlaybackGovernor { const int STANDARD_HZ = 60; const double STANDARD_MS_PER_RECORD = 1000d / STANDARD_HZ; diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/ITelemetryClient.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK/ITelemetryClient.cs index 4159a86..bb4f40c 100644 --- a/Sdk/SVappsLAB.iRacingTelemetrySDK/ITelemetryClient.cs +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/ITelemetryClient.cs @@ -11,14 +11,14 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; + * limitations under the License. **/ using System; using System.Collections.Generic; using System.Threading; +using System.Threading.Channels; using System.Threading.Tasks; -using SVappsLAB.iRacingTelemetrySDK.Models; namespace SVappsLAB.iRacingTelemetrySDK { @@ -26,68 +26,212 @@ namespace SVappsLAB.iRacingTelemetrySDK /// Represents a telemetry client for iRacing. /// /// The 'TelemetryData' type generated by the 'RequiredTelemetryVars' attribute - public interface ITelemetryClient : IDisposable where T : struct + public interface ITelemetryClient : IAsyncDisposable where T : struct { /// - /// Event that is raised when the connection state changes. + /// Gets an async stream for connection state changes. /// - event EventHandler OnConnectStateChanged; + /// + /// + /// Cancellation: Use .WithCancellation(cancellationToken) to enable + /// cooperative cancellation when consuming this stream. + /// + /// + /// Single Reader Limitation: This async stream is optimized for single concurrent reader. + /// Attempting multiple concurrent enumerations (e.g., multiple await foreach loops) is not supported + /// and may cause undefined behavior. For multiple subscribers, use the SubscribeToAllStreams extension + /// method or create separate subscriptions. + /// + /// + /// + /// + /// await foreach (var state in client.ConnectStates.WithCancellation(ct)) + /// { + /// // Handle connection state change (Connected/Disconnected) + /// } + /// + /// + IAsyncEnumerable ConnectStates { get; } /// - /// Event that is raised when the raw session info is updated. + /// Gets an async stream for error notifications. /// - event EventHandler OnRawSessionInfoUpdate; + /// + /// + /// Cancellation: Use .WithCancellation(cancellationToken) to enable + /// cooperative cancellation when consuming this stream. + /// + /// + /// Single Reader Limitation: This async stream is optimized for single concurrent reader. + /// Attempting multiple concurrent enumerations (e.g., multiple await foreach loops) is not supported + /// and may cause undefined behavior. For multiple subscribers, use the SubscribeToAllStreams extension + /// method or create separate subscriptions. + /// + /// + /// + /// + /// await foreach (var error in client.Errors.WithCancellation(ct)) + /// { + /// // Handle error exception + /// } + /// + /// + IAsyncEnumerable Errors { get; } /// - /// Event that is raised when the session info model has been parsed. + /// Gets an async stream for raw session info updates in YAML format. /// - event EventHandler OnSessionInfoUpdate; + /// + /// + /// Cancellation: Use .WithCancellation(cancellationToken) to enable + /// cooperative cancellation when consuming this stream. + /// + /// + /// Single Reader Limitation: This async stream is optimized for single concurrent reader. + /// Attempting multiple concurrent enumerations (e.g., multiple await foreach loops) is not supported + /// and may cause undefined behavior. For multiple subscribers, use the SubscribeToAllStreams extension + /// method or create separate subscriptions. + /// + /// + /// + /// + /// await foreach (var rawYaml in client.SessionDataYaml.WithCancellation(ct)) + /// { + /// // Process raw YAML session data + /// } + /// + /// + IAsyncEnumerable SessionDataYaml { get; } /// - /// Event that is raised when the telemetry data is updated. + /// Gets an async stream for parsed session info updates. /// - event EventHandler OnTelemetryUpdate; + /// + /// + /// Cancellation: Use .WithCancellation(cancellationToken) to enable + /// cooperative cancellation when consuming this stream. + /// + /// + /// Channel Behavior: Uses a 60-sample ring buffer with FIFO (First-In-First-Out) + /// semantics and destructive reads. Session info updates are infrequent (typically only at + /// session state changes), but if your application cannot consume updates fast enough and the + /// buffer fills, the oldest unread updates are automatically discarded to make room for new data. + /// + /// + /// Single Reader Limitation: This async stream is optimized for single concurrent reader. + /// Attempting multiple concurrent enumerations (e.g., multiple await foreach loops) is not supported + /// and may cause undefined behavior. For multiple subscribers, use the SubscribeToAllStreams extension + /// method or create separate subscriptions. + /// + /// + /// + /// + /// await foreach (var sessionInfo in client.SessionData.WithCancellation(ct)) + /// { + /// // Process parsed session information + /// } + /// + /// + IAsyncEnumerable SessionData { get; } /// - /// Event that is raised when an error occurs. + /// Gets an async stream for telemetry data updates at 60Hz. /// - event EventHandler OnError; + /// + /// + /// Cancellation: Use .WithCancellation(cancellationToken) to enable + /// cooperative cancellation when consuming this stream. + /// + /// + /// Channel Behavior: Uses a 60-sample ring buffer with FIFO (First-In-First-Out) + /// semantics and destructive reads. At iRacing's 60Hz update rate, this buffer provides up to + /// 1 second of telemetry data buffering. If your application cannot consume samples fast enough + /// and the buffer fills to capacity, the oldest unread samples are automatically discarded to + /// make room for new incoming data. This 'drop-oldest' strategy ensures the SDK never blocks + /// iRacing's data stream and prioritizes delivering the most recent telemetry to your application. + /// + /// + /// Single Reader Limitation: This async stream is optimized for single concurrent reader. + /// Attempting multiple concurrent enumerations (e.g., multiple await foreach loops) is not supported + /// and may cause undefined behavior. For multiple subscribers, use the SubscribeToAllStreams extension + /// method or create separate subscriptions. + /// + /// + /// + /// + /// await foreach (var telemetry in client.TelemetryData.WithCancellation(ct)) + /// { + /// // Process telemetry data (Speed, RPM, etc.) + /// } + /// + /// + IAsyncEnumerable TelemetryData { get; } /// /// Retrieves the telemetry variables available for the current session. /// - /// A task that represents the asynchronous operation. The task result contains the telemetry variables. - Task> GetTelemetryVariables(); + /// + /// A read-only list of telemetry variables. Returns an empty list if called before + /// the data provider has been initialized (typically during or after the first + /// Monitor() call in live mode, or immediately after construction for IBT mode). + /// + /// + /// Initialization Timing: In live mode, variable headers become + /// available only after iRacing is running and the first data update occurs. In IBT mode, + /// they're available immediately after construction. + /// Thread Safety: This method is thread-safe and can be called + /// concurrently with Monitor(), though results may vary depending on initialization state. + /// + /// Thrown if the client has been disposed. + IReadOnlyList GetTelemetryVariables(); - /// - /// Retrieves the raw telemetry session info in YAML format. - /// This can be useful if you want to parse the data yourself. - /// - /// The raw telemetry session info in YAML format. - string GetRawTelemetrySessionInfoYaml(); /// - /// Checks if the client is connected to the telemetry server. + /// Monitors the telemetry data and writes data to streams when new data is available. /// - /// True if the client is connected; otherwise, false. - bool IsConnected(); + /// The cancellation token to cancel the monitoring operation. + /// + /// A task that represents the asynchronous monitoring operation. + /// The result is the number of telemetry data records processed during the monitoring session. + /// For live sessions, this represents the count of telemetry updates received. + /// For IBT playback, this represents the total number of records processed from the file. + /// + /// + /// IMPORTANT: This method can only be called ONCE per TelemetryClient instance. + /// After Monitor() completes (via cancellation or IBT file EOF), the client cannot be restarted. + /// Create a new client instance for subsequent monitoring sessions. + /// Concurrent Calls: Calling Monitor() while already running will throw + /// InvalidOperationException. Ensure the previous Monitor() call has completed before starting + /// a new client instance. + /// + Task Monitor(CancellationToken ct); /// - /// Monitors the telemetry data and raises the OnTelemetryUpdate event when new data is available. + /// Gets a value indicating whether the client is connected to the telemetry source. /// - /// The cancellation token to cancel the monitoring operation. - /// A task that represents the asynchronous monitoring operation. - Task Monitor(CancellationToken ct); + /// + /// true if connected to iRacing (live mode) or an IBT file is open; otherwise false. + /// + /// + /// This property is safe to call from any thread and will return false if the client + /// has been disposed or is not yet initialized. + /// + bool IsConnected { get; } /// - /// Pauses telemetry event firing. Processing continues, but events are suppressed. + /// Pauses stream data writing. Processing continues, but stream writes are suppressed. /// void Pause(); /// - /// Resumes telemetry event firing. + /// Resumes stream data writing. /// void Resume(); + + /// + /// Gets a value indicating whether stream writes are currently paused. + /// + bool IsPaused { get; } } } diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/Metrics/MetricsService.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK/Metrics/MetricsService.cs new file mode 100644 index 0000000..752a77f --- /dev/null +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/Metrics/MetricsService.cs @@ -0,0 +1,131 @@ +/** + * Copyright (C) 2024-2025 Scott Velez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +**/ + +using System; +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using System.Threading.Tasks; + +namespace SVappsLAB.iRacingTelemetrySDK.Metrics +{ + internal class TelemetryMeters + { + public TelemetryMeters(Meter meter) + { + Processed = meter.CreateCounter( + "telemetry_records_processed_total", + description: "Total number of telemetry records processed"); + + Dropped = meter.CreateCounter( + "telemetry_records_dropped_total", + description: "Total number of telemetry records dropped"); + + Duration = meter.CreateHistogram( + "telemetry_processing_duration_microseconds", + unit: "us", + description: "Time spent processing individual telemetry records"); + } + public Counter Processed { get; private set; } + public Counter Dropped { get; private set; } + public Histogram Duration { get; private set; } + + internal void RecordsProcessed(long count) => Processed.Add(count); + internal void RecordsDropped(long count) => Dropped.Add(count); + internal void ProcessingDuration(TimeSpan ts) => Duration.Record(ts.TotalMicroseconds); + } + internal class SessionInfoMeters + { + public SessionInfoMeters(Meter meter) + { + Processed = meter.CreateCounter( + "sessioninfo_records_processed_total", + description: "Total number of session info records processed"); + + Duration = meter.CreateHistogram( + "sessioninfo_processing_duration_milliseconds", + unit: "ms", + description: "Time spent processing individual session info records"); + } + public Counter Processed { get; private set; } + public Histogram Duration { get; private set; } + + internal void RecordsProcessed(long count) => Processed.Add(count); + internal void ProcessingDuration(TimeSpan ts) => Duration.Record(ts.TotalMilliseconds); + } + + internal interface IMetricsService : IAsyncDisposable + { + TelemetryMeters Telemetry { get; } + SessionInfoMeters SessionInfo { get; } + } + + internal class MetricsService : IMetricsService + { + private readonly Meter _meter; + public TelemetryMeters Telemetry { get; private set; } + public SessionInfoMeters SessionInfo { get; private set; } + + public MetricsService(IMeterFactory? meterFactory, string dataSourceType) + { + var tags = new List> + { + new("data_source", dataSourceType) + }; + + _meter = meterFactory != null ? + meterFactory.Create(Constants.SDK_NAME, null, tags) : + new Meter(Constants.SDK_NAME, null, tags); + + Telemetry = new TelemetryMeters(_meter); + SessionInfo = new SessionInfoMeters(_meter); + + //// Connection metrics + //_connectionsTotal = _meter.CreateCounter( + // "telemetry_connections_total", + // description: "Total number of telemetry connections established"); + + //_disconnectionsTotal = _meter.CreateCounter( + // "telemetry_disconnections_total", + // description: "Total number of telemetry disconnections"); + + //_connectionStatus = _meter.CreateGauge( + // "telemetry_connection_status", + // description: "Current connection status (1 = connected, 0 = disconnected)"); + + //_connectionDuration = _meter.CreateHistogram( + // "telemetry_connection_duration_seconds", + // unit: "s", + // description: "Duration of telemetry connections", + // advice: new InstrumentAdvice { HistogramBucketBoundaries = [1, 5, 10, 30, 60, 300, 600, 1800, 3600] }); + + + //// Observable metrics for current state + //_meter.CreateObservableGauge( + // "telemetry_connection_uptime_seconds", + // () => _isConnected && _connectionStartTime.HasValue ? + // (DateTime.UtcNow - _connectionStartTime.Value).TotalSeconds : 0.0, + // unit: "s", + // description: "Current connection uptime in seconds"); + } + + public ValueTask DisposeAsync() + { + _meter.Dispose(); + GC.SuppressFinalize(this); + return ValueTask.CompletedTask; + } + } +} diff --git a/Sdk/tests/IBT_Tests/GlobalUsings.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK/Metrics/ScopeLambda.cs similarity index 61% rename from Sdk/tests/IBT_Tests/GlobalUsings.cs rename to Sdk/SVappsLAB.iRacingTelemetrySDK/Metrics/ScopeLambda.cs index a4b194e..aca64fe 100644 --- a/Sdk/tests/IBT_Tests/GlobalUsings.cs +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/Metrics/ScopeLambda.cs @@ -11,8 +11,23 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; + * limitations under the License. **/ -global using Xunit; +using System; +namespace SVappsLAB.iRacingTelemetrySDK.Metrics +{ + internal class ScopeLambda : IDisposable + { + readonly Action _lambda; + public ScopeLambda(Action lambda) + { + _lambda = lambda; + } + public void Dispose() + { + _lambda(); + } + } +} diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/Metrics/ScopeTimeSpanTimer.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK/Metrics/ScopeTimeSpanTimer.cs new file mode 100644 index 0000000..53f7133 --- /dev/null +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/Metrics/ScopeTimeSpanTimer.cs @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2024-2025 Scott Velez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +**/ + +using System; + +namespace SVappsLAB.iRacingTelemetrySDK.Metrics +{ + internal class ScopeTimeSpanTimer : IDisposable + { + long _timeStamp; + readonly Action _lambda; + public ScopeTimeSpanTimer(Action lambda) + { + _lambda = lambda; + _timeStamp = TimeProvider.System.GetTimestamp(); + } + public void Dispose() + { + var elapsed = TimeProvider.System.GetElapsedTime(_timeStamp); + _lambda(elapsed); + } + } +} diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/CameraInfo.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/CameraInfo.cs index 6ccabec..28a9454 100644 --- a/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/CameraInfo.cs +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/CameraInfo.cs @@ -11,13 +11,13 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; + * limitations under the License. **/ using System.Collections.Generic; #nullable disable -namespace SVappsLAB.iRacingTelemetrySDK.Models +namespace SVappsLAB.iRacingTelemetrySDK { public class CameraInfo diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/DriverInfo.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/DriverInfo.cs index ecbb708..6965c6d 100644 --- a/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/DriverInfo.cs +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/DriverInfo.cs @@ -11,13 +11,13 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; + * limitations under the License. **/ using System.Collections.Generic; #nullable disable -namespace SVappsLAB.iRacingTelemetrySDK.Models +namespace SVappsLAB.iRacingTelemetrySDK { public class DriverInfo { @@ -118,7 +118,7 @@ public class Driver public class DriverTire { public int TireIndex { get; set; } // 0 - public string TireCompoundType { get; set; } // "Hard" + public string TireCompoundType { get; set; } // "Hard, Soft, Qualifying, Wet, AllPurpose" } } #nullable enable diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/QualifyResultsInfo.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/QualifyResultsInfo.cs index 9579cf8..62e8344 100644 --- a/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/QualifyResultsInfo.cs +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/QualifyResultsInfo.cs @@ -11,13 +11,13 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; + * limitations under the License. **/ using System.Collections.Generic; #nullable disable -namespace SVappsLAB.iRacingTelemetrySDK.Models +namespace SVappsLAB.iRacingTelemetrySDK { public class QualifyResultsInfo { diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/RadioInfo.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/RadioInfo.cs index f4108f6..19a1544 100644 --- a/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/RadioInfo.cs +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/RadioInfo.cs @@ -11,13 +11,13 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; + * limitations under the License. **/ using System.Collections.Generic; #nullable disable -namespace SVappsLAB.iRacingTelemetrySDK.Models +namespace SVappsLAB.iRacingTelemetrySDK { public class RadioInfo { diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/SessionInfo.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/SessionInfo.cs index 3a1d8df..7511b26 100644 --- a/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/SessionInfo.cs +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/SessionInfo.cs @@ -11,13 +11,13 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; + * limitations under the License. **/ using System.Collections.Generic; #nullable disable -namespace SVappsLAB.iRacingTelemetrySDK.Models +namespace SVappsLAB.iRacingTelemetrySDK { public class SessionInfo { diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/SplitTimeInfo.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/SplitTimeInfo.cs index 30b0b06..86d1576 100644 --- a/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/SplitTimeInfo.cs +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/SplitTimeInfo.cs @@ -11,13 +11,13 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; + * limitations under the License. **/ using System.Collections.Generic; #nullable disable -namespace SVappsLAB.iRacingTelemetrySDK.Models +namespace SVappsLAB.iRacingTelemetrySDK { public class SplitTimeInfo { diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/TelemetrySessionInfo.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/TelemetrySessionInfo.cs index 3aaf8a1..e870d9b 100644 --- a/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/TelemetrySessionInfo.cs +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/TelemetrySessionInfo.cs @@ -11,13 +11,13 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; + * limitations under the License. **/ using System.Collections.Generic; #nullable disable -namespace SVappsLAB.iRacingTelemetrySDK.Models +namespace SVappsLAB.iRacingTelemetrySDK { public class TelemetrySessionInfo { @@ -28,9 +28,7 @@ public class TelemetrySessionInfo public RadioInfo RadioInfo { get; set; } public DriverInfo DriverInfo { get; set; } public SplitTimeInfo SplitTimeInfo { get; set; } - //public dynamic CarSetup { get; set; } public Dictionary CarSetup { get; set; } - } } diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/WeekendInfo.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/WeekendInfo.cs index 67edfbc..663ae22 100644 --- a/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/WeekendInfo.cs +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/WeekendInfo.cs @@ -11,19 +11,19 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; + * limitations under the License. **/ using System; #nullable disable -namespace SVappsLAB.iRacingTelemetrySDK.Models +namespace SVappsLAB.iRacingTelemetrySDK { public class WeekendInfo { public string TrackName { get; set; } // spa up public int TrackID { get; set; } // 143 - public string TrackLength { get; set; } // 6.93 km + public string TrackLength { get; set; } // 6.93 km (10cm accuracy) public string TrackLengthOfficial { get; set; } // 7.00 km public string TrackDisplayName { get; set; } // Circuit de Spa-Francorchamps public string TrackDisplayShortName { get; set; } // Spa diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/PInvoke.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK/PInvoke.cs index cc502b7..4479a07 100644 --- a/Sdk/SVappsLAB.iRacingTelemetrySDK/PInvoke.cs +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/PInvoke.cs @@ -11,7 +11,7 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; + * limitations under the License. **/ using System; diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/SVappsLAB.iRacingTelemetrySDK.csproj b/Sdk/SVappsLAB.iRacingTelemetrySDK/SVappsLAB.iRacingTelemetrySDK.csproj index a59fe3f..f6c2eae 100644 --- a/Sdk/SVappsLAB.iRacingTelemetrySDK/SVappsLAB.iRacingTelemetrySDK.csproj +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/SVappsLAB.iRacingTelemetrySDK.csproj @@ -1,22 +1,35 @@ -๏ปฟ + net8.0 enable Latest true + true - $(NoWarn);CA1416 + $(NoWarn);CA1416;CS1591;CS1587 + + + <_Parameter1>UnitTests + + + <_Parameter1>SmokeTests + + + + + - + + @@ -35,7 +48,7 @@ iRacing iRacingSDK irsdk IBT Telemetry SDK API NUGET.md LICENSE - 0.9.8.3 + 1.0.0 Scott Velez SVappsLAB Copyright (c) 2025 SVappsLAB diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/SubscriptionExtensions.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK/SubscriptionExtensions.cs new file mode 100644 index 0000000..88631f9 --- /dev/null +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/SubscriptionExtensions.cs @@ -0,0 +1,191 @@ +/** + * Copyright (C) 2024-2025 Scott Velez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +**/ + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace SVappsLAB.iRacingTelemetrySDK +{ + /// + /// Extension methods for ITelemetryClient to help with async stream consumption patterns + /// + public static class SubscriptionExtensions + { + /// + /// Subscribes to all data streams concurrently + /// + /// The telemetry data type + /// The telemetry client + /// Async function to invoke for telemetry updates (optional) + /// Async function to invoke for session info updates (optional) + /// Async function to invoke for raw session info updates (optional) + /// Async function to invoke for connection state changes (optional) + /// Async function to invoke for error notifications (optional) + /// Cancellation token + /// Task that completes when all streams are closed or cancelled + /// + /// + /// Each stream processes items sequentially. Async callbacks are awaited before processing the next item + /// in that stream. For 60Hz telemetry, ensure callbacks complete quickly (under 16ms) to avoid + /// buffering/dropping frames. + /// + /// + /// Callbacks execute on the calling thread's SynchronizationContext. For UI applications, callbacks run + /// on the UI thread enabling direct control access. For long-running callbacks, use ConfigureAwait(false) + /// or offload work to avoid blocking. + /// + /// + /// Single-Reader Channels: This method properly handles the underlying single-reader channel + /// limitation by creating separate subscriptions for each stream. Multiple callbacks can be registered via + /// this method without causing undefined behavior. + /// + /// + public static async Task SubscribeToAllStreams(this ITelemetryClient client, + Func? onTelemetryUpdate = null, + Func? onSessionInfoUpdate = null, + Func? onRawSessionInfoUpdate = null, + Func? onConnectStateChanged = null, + Func? onError = null, + CancellationToken cancellationToken = default) where T : struct + { + var tasks = new List(); + + if (onTelemetryUpdate != null) + { + tasks.Add(client.SubscribeToTelemetry(onTelemetryUpdate, cancellationToken)); + } + + if (onSessionInfoUpdate != null) + { + tasks.Add(client.SubscribeToSessionInfo(onSessionInfoUpdate, cancellationToken)); + } + + if (onRawSessionInfoUpdate != null) + { + tasks.Add(client.SubscribeToRawSessionInfo(onRawSessionInfoUpdate, cancellationToken)); + } + + if (onConnectStateChanged != null) + { + tasks.Add(client.SubscribeToConnectState(onConnectStateChanged, cancellationToken)); + } + + if (onError != null) + { + tasks.Add(client.SubscribeToErrors(onError, cancellationToken)); + } + + if (tasks.Count > 0) + { + await Task.WhenAll(tasks); + } + } + + private static async Task SubscribeToTelemetry(this ITelemetryClient client, + Func onTelemetryUpdate, + CancellationToken cancellationToken = default) where T : struct + { + await foreach (var data in client.TelemetryData.WithCancellation(cancellationToken).ConfigureAwait(false)) + { + try + { + await onTelemetryUpdate(data); + } + catch + { + // Re-throw to fault the subscription task + // Users can handle via try-catch around SubscribeToAllStreams or monitor task exceptions + throw; + } + } + } + + private static async Task SubscribeToSessionInfo(this ITelemetryClient client, + Func onSessionInfoUpdate, + CancellationToken cancellationToken = default) where T : struct + { + await foreach (var sessionInfo in client.SessionData.WithCancellation(cancellationToken).ConfigureAwait(false)) + { + try + { + await onSessionInfoUpdate(sessionInfo); + } + catch + { + // Re-throw to fault the subscription task + // Users can handle via try-catch around SubscribeToAllStreams or monitor task exceptions + throw; + } + } + } + private static async Task SubscribeToRawSessionInfo(this ITelemetryClient client, + Func onRawSessionInfoUpdate, + CancellationToken cancellationToken = default) where T : struct + { + await foreach (var rawSessionInfo in client.SessionDataYaml.WithCancellation(cancellationToken).ConfigureAwait(false)) + { + try + { + await onRawSessionInfoUpdate(rawSessionInfo); + } + catch + { + // Re-throw to fault the subscription task + // Users can handle via try-catch around SubscribeToAllStreams or monitor task exceptions + throw; + } + } + } + private static async Task SubscribeToErrors(this ITelemetryClient client, + Func onError, + CancellationToken cancellationToken = default) where T : struct + { + await foreach (var error in client.Errors.WithCancellation(cancellationToken).ConfigureAwait(false)) + { + try + { + await onError(error); + } + catch + { + // Re-throw to fault the subscription task + // Users can handle via try-catch around SubscribeToAllStreams or monitor task exceptions + throw; + } + } + } + private static async Task SubscribeToConnectState(this ITelemetryClient client, + Func onConnectStateChanged, + CancellationToken cancellationToken = default) where T : struct + { + await foreach (var connectState in client.ConnectStates.WithCancellation(cancellationToken).ConfigureAwait(false)) + { + try + { + await onConnectStateChanged(connectState); + } + catch + { + // Re-throw to fault the subscription task + // Users can handle via try-catch around SubscribeToAllStreams or monitor task exceptions + throw; + } + } + } + } +} diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/TelemetryClient.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK/TelemetryClient.cs index 9af1410..4b20cf3 100644 --- a/Sdk/SVappsLAB.iRacingTelemetrySDK/TelemetryClient.cs +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/TelemetryClient.cs @@ -1,31 +1,34 @@ /** * Copyright (C) 2024-2025 Scott Velez - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; + * limitations under the License. **/ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.Metrics; using System.IO; using System.Linq; using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; using System.Threading; +using System.Threading.Channels; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using SVappsLAB.iRacingTelemetrySDK.DataProviders; using SVappsLAB.iRacingTelemetrySDK.irSDKDefines; -using SVappsLAB.iRacingTelemetrySDK.Models; +using SVappsLAB.iRacingTelemetrySDK.Metrics; namespace SVappsLAB.iRacingTelemetrySDK { @@ -35,20 +38,6 @@ public enum ConnectState Connected } - public class ExceptionEventArgs : EventArgs - { - public ExceptionEventArgs(Exception exception) - { - Exception = exception; - } - - public Exception Exception { get; private set; } - } - - public class ConnectStateChangedEventArgs : EventArgs - { - public ConnectState State { get; set; } - } public record class TelemetryVariable { @@ -92,41 +81,144 @@ public IBTOptions(string ibtFilePath, int playBackSpeedMultiplier = int.MaxValue } } - - - public class TelemetryClient : ITelemetryClient where T : struct + /// + /// Configuration options that control various aspects of telemetry processing. + /// + public class ClientOptions { - private const int DATA_READY_TIMEOUT_MS = 30; - private const int INITIALIZATION_DELAY_MS = 1000; - - public event EventHandler? OnTelemetryUpdate; - public event EventHandler? OnRawSessionInfoUpdate; - public event EventHandler? OnSessionInfoUpdate; - public event EventHandler? OnError; - public event EventHandler? OnConnectStateChanged; - - Task? _task; - - public bool _lastConnectionStatus = false; + /// + /// Optional factory for creating metrics to monitor telemetry processing performance. + /// Tracks processing duration and record counts. + /// + public IMeterFactory? MeterFactory { get; init; } + } + public class TelemetryClient : ITelemetryClient, IAsyncDisposable where T : struct + { + const int DATA_READY_TIMEOUT_MS = 30; + const int INITIALIZATION_DELAY_MS = 1000; + private const int CHANNEL_SIZE = 60; + readonly Channel _connectStateChannel; + readonly Channel _errorChannel; + readonly Channel _rawSessionDataChannel; + readonly Channel _sessionDataChannel; + readonly Channel _telemetryDataChannel; + readonly Channel _internalSessionInfoChannel; + IMetricsService? _metricsService; + Task? _dataProcessingTask; + Task? _sessionInfoProcessorTask; + CancellationTokenSource? _internalCts; // internal cancellation token source - linked to external token, owned by TelemetryClient private ISessionInfoParser _sessionInfoParser; private ILogger _logger; - bool _isInitialized = false; - bool _isPaused = false; + private volatile bool _isInitialized = false; + private volatile bool _isPaused = false; + private bool _lastConnectionStatus = false; IBTOptions? _ibtOptions; IDataProvider _dataProvider; - private System.Reflection.ConstructorInfo _telemetryDataConstructorInfo; - private IEnumerable _telemetryDataConstructorParameters; + private readonly TelemetryDataAccessor _telemetryAccessor; + + // Disposal state tracking + private volatile bool _disposed = false; + + // Monitor state tracking - 0=idle, 1=running + private int _monitorState = 0; + + // telemetry variables caching + private IReadOnlyList? _cachedTelemetryVariables; + private readonly object _telemetryVariablesLock = new object(); + + /// + public IAsyncEnumerable ConnectStates => GetConnectStatesEnumerable(); + + /// + public IAsyncEnumerable Errors => GetErrorsEnumerable(); + + /// + public IAsyncEnumerable SessionDataYaml => GetRawSessionDataEnumerable(); + + /// + public IAsyncEnumerable SessionData => GetSessionDataEnumerable(); + + /// + public IAsyncEnumerable TelemetryData => GetTelemetryDataEnumerable(); + + public bool IsPaused => _isPaused; + + // Async stream wrapper methods that expose streams as IAsyncEnumerable + private async IAsyncEnumerable GetConnectStatesEnumerable([EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await foreach (var item in _connectStateChannel.Reader.ReadAllAsync(cancellationToken)) + yield return item; + } - // factory to create instances of the client - public static ITelemetryClient Create(ILogger logger, IBTOptions? ibtOptions = null) + private async IAsyncEnumerable GetErrorsEnumerable([EnumeratorCancellation] CancellationToken cancellationToken = default) { - return new TelemetryClient(logger, ibtOptions); + await foreach (var item in _errorChannel.Reader.ReadAllAsync(cancellationToken)) + yield return item; } - private TelemetryClient(ILogger logger, IBTOptions? ibtOptions = null) + + private async IAsyncEnumerable GetRawSessionDataEnumerable([EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await foreach (var item in _rawSessionDataChannel.Reader.ReadAllAsync(cancellationToken)) + yield return item; + } + + private async IAsyncEnumerable GetSessionDataEnumerable([EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await foreach (var item in _sessionDataChannel.Reader.ReadAllAsync(cancellationToken)) + yield return item; + } + + private async IAsyncEnumerable GetTelemetryDataEnumerable([EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await foreach (var item in _telemetryDataChannel.Reader.ReadAllAsync(cancellationToken)) + yield return item; + } + + /// + /// creates a new instance of the telemetry client. + /// + /// logger instance for diagnostic output. + /// + /// optional IBT file playback options. if null, client operates in live mode connecting to iRacing. + /// + /// a new telemetry client instance configured for the specified mode. + /// + /// Live Mode: when ibtOptions is null, the client connects to iRacing's shared memory. + /// IBT Mode: when ibtOptions is provided, the client plays back the specified IBT file. + /// + /// + /// thrown if ibtOptions specifies a file that doesn't exist. + /// + public static ITelemetryClient Create(ILogger logger, IBTOptions? ibtOptions = null) => + new TelemetryClient(logger, null, ibtOptions); + + /// + /// creates a new instance of the telemetry client with advanced configuration options. + /// + /// logger instance for diagnostic output. + /// + /// optional IBT file playback options. if null, client operates in live mode connecting to iRacing. + /// + /// + /// configuration options for metrics and other client behavior. + /// + /// a new telemetry client instance configured for the specified mode. + /// + /// Live Mode: when ibtOptions is null, the client connects to iRacing's shared memory. + /// IBT Mode: when ibtOptions is provided, the client plays back the specified IBT file. + /// Metrics: provide a MeterFactory via clientOptions to enable built-in diagnostic metrics. + /// + /// + /// thrown if ibtOptions specifies a file that doesn't exist. + /// + public static ITelemetryClient Create(ILogger logger, IBTOptions? ibtOptions, ClientOptions clientOptions) => + new TelemetryClient(logger, clientOptions, ibtOptions); + + private TelemetryClient(ILogger logger, ClientOptions? clientOptions = null, IBTOptions? ibtOptions = null) { // do a quick check for valid ibt file and bail immediately if not found if (ibtOptions != null && !File.Exists(ibtOptions.IbtFilePath)) @@ -137,34 +229,168 @@ private TelemetryClient(ILogger logger, IBTOptions? ibtOptions = null) _logger = logger; _ibtOptions = ibtOptions; + // initialize bounded channels + var boundedChannelOptions = new BoundedChannelOptions(CHANNEL_SIZE) + { + FullMode = BoundedChannelFullMode.DropOldest, + SingleReader = true, + SingleWriter = true + }; + _connectStateChannel = Channel.CreateBounded(boundedChannelOptions); + _errorChannel = Channel.CreateBounded(boundedChannelOptions); + _rawSessionDataChannel = Channel.CreateBounded(boundedChannelOptions); + _sessionDataChannel = Channel.CreateBounded(boundedChannelOptions); + _telemetryDataChannel = Channel.CreateBounded(boundedChannelOptions); + + // Internal session info processing channel + var sessionInfoChannelOptions = new BoundedChannelOptions(10) + { + FullMode = BoundedChannelFullMode.DropOldest, + SingleReader = true, + SingleWriter = false // Both Live and IBT can write + }; + _internalSessionInfoChannel = Channel.CreateBounded(sessionInfoChannelOptions); + + // initialize data provider _dataProvider = _ibtOptions == null ? new LiveDataProvider(_logger) : new IBTDataProvider(_logger, _ibtOptions); - // get constructor and parameters of the TelemetryData type - // we'll need them later when we create instances of the type - _telemetryDataConstructorInfo = typeof(T).GetConstructors()[0]; - _telemetryDataConstructorParameters = _telemetryDataConstructorInfo.GetParameters(); + _metricsService = new MetricsService(clientOptions?.MeterFactory, _ibtOptions == null ? "Live" : "IBT"); + _telemetryAccessor = new TelemetryDataAccessor(_logger); _sessionInfoParser = new YamlParser(); } - public Task Monitor(CancellationToken ct) + /// + /// starts monitoring telemetry data from either live iRacing or IBT file playback. + /// this method can only be called once per TelemetryClient instance. + /// + /// cancellation token to stop monitoring + /// number of telemetry records processed + /// thrown if Monitor() is called while already running + /// thrown if the client has been disposed + /// + /// IMPORTANT: This method can only be called ONCE per TelemetryClient instance. + /// After Monitor() completes (via cancellation or IBT file EOF), the client cannot be restarted. + /// Create a new client instance for subsequent monitoring sessions. + /// Concurrent Calls: Calling Monitor() while already running will throw + /// InvalidOperationException. Ensure the previous Monitor() call has completed before starting + /// a new client instance. + /// + public async Task Monitor(CancellationToken ct) { - _logger.LogDebug("monitoring '{mode}' data", IsOnlineMode ? "Live" : "IBT"); + // check if channels have already been completed from a previous run + if (_telemetryDataChannel.Reader.Completion.IsCompleted) + { + throw new InvalidOperationException( + "Monitor() has already completed on this instance. " + + "Create a new TelemetryClient instance to monitor again."); + } - _task = IsOnlineMode ? - Task.Run(() => ProcessLiveData(ct), ct) : - Task.Run(() => ProcessIbtData(ct), ct); + // atomically check and set monitor state to prevent re-entrancy + // compareExchange returns the original value - if it was 0 (idle), we successfully set it to 1 (running) + if (Interlocked.CompareExchange(ref _monitorState, 1, 0) != 0) + { + throw new InvalidOperationException("Monitor() is already running. concurrent calls to Monitor() are not allowed."); + } - return _task; + try + { + _logger.LogDebug("monitoring '{mode}' data", IsOnlineMode ? "Live" : "IBT"); + + // create internal CTS linked to external token + // this allows both external cancellation and internal control during disposal + _internalCts = CancellationTokenSource.CreateLinkedTokenSource(ct); + try + { + _sessionInfoProcessorTask = StartSessionInfoProcessorTask(_internalCts.Token); + _dataProcessingTask = StartDataProcessorTask(_internalCts.Token); + + await Task.WhenAll(_dataProcessingTask, _sessionInfoProcessorTask).ConfigureAwait(false); + var result = _dataProcessingTask.Result; // safe since task already completed + return result; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error starting processing tasks"); + throw; + } + } + finally + { + // reset monitor state to idle on any exit path (success, exception, cancellation) + // this ensures state is cleaned up even if Monitor() throws during startup + Interlocked.Exchange(ref _monitorState, 0); + } + } + + private Task StartSessionInfoProcessorTask(CancellationToken ct) + { + return ProcessSessionInfoChannel(ct); } - public Task> GetTelemetryVariables() + private async Task StartDataProcessorTask(CancellationToken ct) { - // use local unsafe function to get the varHeaders - unsafe IEnumerable unsafeInternal() + try { - var list = new List(); - foreach (var vh in _dataProvider.GetVarHeaders()!.Values) + _logger.LogDebug("{mode} data processor started", IsOnlineMode ? "Live" : "IBT"); + + var task = IsOnlineMode ? + ProcessLiveData(ct) : + ProcessIbtData(ct); + + return await task.ConfigureAwait(false); + } + finally + { + // complete the session info channel. there will be no more data + _internalSessionInfoChannel.Writer.Complete(); + } + } + + public IReadOnlyList GetTelemetryVariables() + { + // fast path - already cached + if (_cachedTelemetryVariables != null) + return _cachedTelemetryVariables; + + lock (_telemetryVariablesLock) + { + // double-check pattern + if (_cachedTelemetryVariables != null) + return _cachedTelemetryVariables; + + try + { + IReadOnlyList list = BuildTelemetryVariablesList(); + _cachedTelemetryVariables = list; + return list; + } + catch (ObjectDisposedException) + { + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving telemetry variables"); + throw; + } + } + } + + private unsafe IReadOnlyList BuildTelemetryVariablesList() + { + var list = new List(); + var varHeaders = _dataProvider.GetVarHeaders(); + + if (varHeaders == null) + { + _logger.LogWarning("Variable headers are null, returning empty list"); + return list; + } + + foreach (var vh in varHeaders.Values) + { + try { var tVar = new TelemetryVariable { @@ -176,7 +402,7 @@ unsafe IEnumerable unsafeInternal() irsdk_VarType.irsdk_bitField => vh.count > 1 ? typeof(uint[]) : typeof(uint), irsdk_VarType.irsdk_float => vh.count > 1 ? typeof(float[]) : typeof(float), irsdk_VarType.irsdk_double => vh.count > 1 ? typeof(double[]) : typeof(double), - _ => throw new NotImplementedException($"{vh.type}, not implemented") + _ => throw new NotImplementedException($"{vh.type} not implemented") }, Length = vh.count, @@ -187,92 +413,245 @@ unsafe IEnumerable unsafeInternal() }; list.Add(tVar); } - return list; + catch (Exception ex) + { + _logger.LogWarning(ex, "Error processing telemetry variable header"); + // continue processing other variables + } } + list.Sort((a, b) => string.CompareOrdinal(a.Name, b.Name)); + return list; + } - var sortedList = unsafeInternal().OrderBy(v => v.Name) as IEnumerable; - return Task.FromResult(sortedList); + /// + /// Gets a value indicating whether the client is connected to the telemetry source. + /// + /// + /// true if connected to iRacing (live mode) or an IBT file is open; otherwise false. + /// + /// + /// This property is safe to call from any thread and will return false if the client + /// has been disposed or is not yet initialized. + /// + public bool IsConnected + { + get + { + try + { + if (_isInitialized) + { + return _dataProvider.IsConnected; + } + return false; + } + catch (ObjectDisposedException) + { + return false; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error checking connection status"); + return false; + } + } } - public string GetRawTelemetrySessionInfoYaml() + /// + /// Pauses stream data writing. Processing continues, but stream writes are suppressed. + /// + /// + /// Thread Safety: This method is thread-safe and can be called from any thread. + /// Idempotency: Safe to call multiple times. Calling Pause() when already paused has no effect. + /// Eventual Consistency: Changes are not immediate. A few telemetry samples may be written + /// to streams before the pause takes effect (typically 1-2 samples, ~16-32ms at 60Hz). + /// + public void Pause() { - return _dataProvider.GetSessionInfoYaml(); + _isPaused = true; + _logger.LogDebug("Telemetry client paused"); } - public bool IsConnected() + + /// + /// Resumes stream data writing. + /// + /// + /// Thread Safety: This method is thread-safe and can be called from any thread. + /// Idempotency: Safe to call multiple times. Calling Resume() when not paused has no effect. + /// Eventual Consistency: Changes are not immediate. A few telemetry samples may be suppressed + /// before the resume takes effect (typically 1-2 samples, ~16-32ms at 60Hz). + /// + public void Resume() { - if (_isInitialized) - { - var isConnected = _dataProvider.IsConnected; - return isConnected; - } - return false; + _isPaused = false; + _logger.LogDebug("Telemetry client resumed"); } - public void Pause() => _isPaused = true; - public void Resume() => _isPaused = false; - public void Dispose() + public async ValueTask DisposeAsync() { - Dispose(true); - GC.SuppressFinalize(this); + if (_disposed) return; + try + { + await Shutdown().ConfigureAwait(false); + GC.SuppressFinalize(this); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during disposal"); + throw; + } + finally + { + _disposed = true; + } } + private async Task Shutdown() { _logger.LogDebug("Shutting down"); + + Exception? shutdownException = null; + try { - await _task!.ConfigureAwait(false); + _logger.LogDebug("Monitor completion - shutting down processing tasks"); - //Uninitialize(); + // cancel internal CTS to signal graceful shutdown to processing tasks + // this ensures tasks can exit their loops cleanly + if (_internalCts != null && !_internalCts.IsCancellationRequested) + { + _logger.LogDebug("Cancelling internal processing token"); + _internalCts.Cancel(); + } - if (_dataProvider != null) + // shut down processing tasks + var tasksToWait = new List(); + if (_dataProcessingTask != null) + tasksToWait.Add(_dataProcessingTask); + if (_sessionInfoProcessorTask != null) + tasksToWait.Add(_sessionInfoProcessorTask); + + if (tasksToWait.Count > 0) { - _dataProvider.Dispose(); + // tasks should exit quickly now that token is cancelled + // timeout is a safety net for unexpected hangs + var shutdownTimeoutToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token; + await Task.WhenAll(tasksToWait).WaitAsync(shutdownTimeoutToken).ConfigureAwait(false); } } - catch (Exception e) + catch (OperationCanceledException) { - _logger.LogError(e, "Shutdown() error"); + // Expected during cancellation + } + catch (Exception ex) + { + _logger.LogError(ex, "Error shutting down main processing task"); + shutdownException ??= ex; } - } - protected virtual async void Dispose(bool disposing) - { - if (disposing) + CompleteAllChannels(); + + // Dispose services + if (_dataProvider != null) { - await Shutdown().ConfigureAwait(false); + await _dataProvider.DisposeAsync().ConfigureAwait(false); + } + if (_metricsService != null) + { + await _metricsService.DisposeAsync().ConfigureAwait(false); + } + + // dispose internal CTS + _internalCts?.Dispose(); + _internalCts = null; + + _logger.LogDebug("Shutdown completed"); + + if (shutdownException != null) + { + throw shutdownException; } } - ~TelemetryClient() + + private void CompleteAllChannels() { - Dispose(false); + try + { + _connectStateChannel.Writer.Complete(); + _errorChannel.Writer.Complete(); + _rawSessionDataChannel.Writer.Complete(); + _sessionDataChannel.Writer.Complete(); + _telemetryDataChannel.Writer.Complete(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error completing channels"); + } } private bool IsOnlineMode => _ibtOptions == null; - Task UpdateSession(string rawSessionInfoYaml) + + private async Task ProcessSessionInfoChannel(CancellationToken ct) { - var task = Task.Run(() => UpdateSessionHelper(rawSessionInfoYaml)); - return task; + _logger.LogDebug("Session info processor started"); + + try + { + await foreach (var rawYaml in _internalSessionInfoChannel.Reader.ReadAllAsync(ct)) + { + try + { + _rawSessionDataChannel.Writer.TryWrite(rawYaml); + + var sessionInfo = ParseSessionInfo(rawYaml); + if (sessionInfo != null) + { + _sessionDataChannel.Writer.TryWrite(sessionInfo); + } + } + catch (Exception e) + { + _logger.LogError(e, "Error processing session info"); + _errorChannel.Writer.TryWrite(e); + } + } + } + catch (OperationCanceledException) when (ct.IsCancellationRequested) + { + _logger.LogDebug("Session info processor cancelled"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Unexpected error in session info processor"); + + // write error to channel + _errorChannel?.Writer.TryWrite(ex); + throw; + } + + _logger.LogDebug("Session info processor ended"); } - void UpdateSessionHelper(string rawSessionInfoYaml) + + private TelemetrySessionInfo? ParseSessionInfo(string rawSessionInfoYaml) { - var sw = new Stopwatch(); - sw.Start(); + using var timer = new ScopeTimeSpanTimer(elapsed => _metricsService?.SessionInfo.ProcessingDuration(elapsed)); + using var counter = new ScopeLambda(() => _metricsService?.SessionInfo.RecordsProcessed(1)); + TelemetrySessionInfo? sessionInfo = null; try { - var parseResult = _sessionInfoParser.Parse(rawSessionInfoYaml); - var sessionTelemetryInfo = parseResult.Model; - _logger.LogDebug("sessionInfo deserialize complete. required {attempts} attempts. ({elapsed}ms)", parseResult.ParseAttemptsRequired, sw.ElapsedMilliseconds); + Stopwatch sw = Stopwatch.StartNew(); - // send event - OnSessionInfoUpdate?.Invoke(this, sessionTelemetryInfo); + var parseResult = _sessionInfoParser.Parse(rawSessionInfoYaml); + sessionInfo = parseResult.Model; + _logger.LogDebug("sessionInfo deserialize complete. required {attempts} attempts. ({ScopeTimeSpanTimer}ms)", parseResult.ParseAttemptsRequired, sw.ElapsedMilliseconds); } catch (Exception e) { _logger.LogError(e, "error deserializing or sending sessionTelemetryInfo event"); - OnError?.Invoke(this, new ExceptionEventArgs(e)); + _errorChannel.Writer.TryWrite(e); } - _logger.LogDebug("UpdateSession complete ({elapsed}ms)", sw.ElapsedMilliseconds); + return sessionInfo; } private bool Initialize() @@ -292,36 +671,46 @@ private bool Initialize() private async Task ProcessLiveData(CancellationToken ct) { + int numUpdates = 0; + while (!ct.IsCancellationRequested) { try { - await WaitForData(ct).ConfigureAwait(false); + var telemetryProcessed = await WaitForData(ct).ConfigureAwait(false); + if (telemetryProcessed) + { + numUpdates++; + } } - catch (TaskCanceledException) + catch (OperationCanceledException) when (ct.IsCancellationRequested) { - _logger.LogDebug("monitoring cancelled"); - return -1; + _logger.LogDebug("Live data monitoring cancelled after {numUpdates} updates", numUpdates); + throw; // propagate cancellation to caller } catch (Exception e) { - _logger.LogError(e, "error processing live data"); - OnError?.Invoke(this, new ExceptionEventArgs(e)); - throw; + _logger.LogError(e, "Error processing live data"); + _errorChannel.Writer.TryWrite(e); + + // For other exceptions, wait a bit before retrying + await Task.Delay(1000).ConfigureAwait(false); } } - _logger.LogInformation("dataMonitor stopping"); - return -1; + + return numUpdates; } - Task _sessionInfoProcessingTask = Task.CompletedTask; - private async Task WaitForData(CancellationToken ct) + private async Task WaitForData(CancellationToken ct) { + // Check cancellation early + ct.ThrowIfCancellationRequested(); + // ensure initialization if (!_isInitialized && !Initialize()) { await Task.Delay(INITIALIZATION_DELAY_MS, ct).ConfigureAwait(false); - return; + return false; } // check connection @@ -332,8 +721,8 @@ private async Task WaitForData(CancellationToken ct) { _logger.LogDebug("isConnected changed from {lastState} to {currState}", _lastConnectionStatus, isConnected); - // inform listeners of connection state change - OnConnectStateChanged?.Invoke(this, new ConnectStateChangedEventArgs { State = isConnected ? ConnectState.Connected : ConnectState.Disconnected }); + // write connection state change to channel if not disposed + _connectStateChannel.Writer.TryWrite(isConnected ? ConnectState.Connected : ConnectState.Disconnected); _lastConnectionStatus = isConnected; } @@ -342,65 +731,45 @@ private async Task WaitForData(CancellationToken ct) if (!isConnected) { await Task.Delay(INITIALIZATION_DELAY_MS, ct).ConfigureAwait(false); - return; + return false; } - // if new session info, send event + // if new session info, queue for processing if (_dataProvider.IsSessionInfoUpdated()) { - var rawSessionInfoYaml = _dataProvider.GetSessionInfoYaml(); - - // suppress events if paused - if (!_isPaused) + if (!IsPaused) { - // if anyone wants raw yaml, send it - if (OnRawSessionInfoUpdate != null) - { - OnRawSessionInfoUpdate.Invoke(this, rawSessionInfoYaml); - } - - if (OnSessionInfoUpdate != null) - { - if (!_sessionInfoProcessingTask.IsCompleted) - { - _logger.LogWarning("sessionInfo processing task is still running"); - } - _sessionInfoProcessingTask = UpdateSession(rawSessionInfoYaml); - } + var rawSessionInfoYaml = _dataProvider.GetSessionInfoYaml(); + _internalSessionInfoChannel.Writer.TryWrite(rawSessionInfoYaml); } } - // wait for new telemetry data - var signaled = _dataProvider.WaitForDataReady(TimeSpan.FromMilliseconds(DATA_READY_TIMEOUT_MS)); + // wait for new telemetry data with cancellation support + var signaled = await _dataProvider.WaitForDataReady(TimeSpan.FromMilliseconds(DATA_READY_TIMEOUT_MS), ct).ConfigureAwait(false); if (!signaled) { // no new data, return and try again - return; + return false; } - // suppress events if paused - if (!_isPaused) + // suppress events if paused or disposed + if (!IsPaused) { - // send event if we have a listener - if (OnTelemetryUpdate != null) - { - var telemetryData = GetTelemetryDataSample(); - - OnTelemetryUpdate.Invoke(this, telemetryData); - } + // write to channel + var telemetryData = GetTelemetryDataSample(); + _telemetryDataChannel.Writer.TryWrite(telemetryData); } + + return true; // data processed } private T GetTelemetryDataSample() { - var parameterValues = _telemetryDataConstructorParameters - .Select(p => - { - var val = _dataProvider.GetVarValue(p.Name!); - return val; - }); - var telemetryData = (T)_telemetryDataConstructorInfo.Invoke(parameterValues.ToArray()); - return telemetryData; + using var elapsedTimer = new ScopeTimeSpanTimer(elapsed => _metricsService?.Telemetry.ProcessingDuration(elapsed)); + using var counter = new ScopeLambda(() => _metricsService?.Telemetry.RecordsProcessed(1)); + + T val = _telemetryAccessor.CreateTelemetryDataSample(_dataProvider); + return val; } private async Task ProcessIbtData(CancellationToken token) { @@ -408,70 +777,85 @@ private async Task ProcessIbtData(CancellationToken token) try { + // Check cancellation before starting + token.ThrowIfCancellationRequested(); + var sw = new Stopwatch(); sw.Start(); - _dataProvider.OpenDataSource(); + if (!_isInitialized) + Initialize(); - // send connect event - OnConnectStateChanged?.Invoke(this, new ConnectStateChangedEventArgs { State = ConnectState.Connected }); + _connectStateChannel.Writer.TryWrite(ConnectState.Connected); // update and send session info event if (_dataProvider.IsSessionInfoUpdated()) { - // suppress events if paused - if (!_isPaused) + if (!IsPaused) { - var rawSessionInfoYaml = _dataProvider.GetSessionInfoYaml(); - if (OnRawSessionInfoUpdate != null) - { - OnRawSessionInfoUpdate.Invoke(this, rawSessionInfoYaml); - } - if (OnSessionInfoUpdate != null) - { - await UpdateSession(rawSessionInfoYaml).ConfigureAwait(false); - } + _internalSessionInfoChannel.Writer.TryWrite(rawSessionInfoYaml); } } - // loop until we are at eof or cancelled + // Process with respect for playback speed multiplier + var playbackDelay = _ibtOptions?.PlayBackSpeedMultiplier == int.MaxValue ? TimeSpan.Zero : + TimeSpan.FromMilliseconds(16.67 / (_ibtOptions?.PlayBackSpeedMultiplier ?? 1)); // ~60 FPS base rate + + // loop until we are at eof, cancelled, or shutdown requested for (numRecords = 0; !token.IsCancellationRequested; numRecords++) { - var dataAvailable = _dataProvider.WaitForDataReady(TimeSpan.Zero); + var dataAvailable = await _dataProvider.WaitForDataReady(TimeSpan.Zero, token).ConfigureAwait(false); if (!dataAvailable) { break; } - // suppress events if paused - if (!_isPaused) + // suppress events if paused or disposed + if (!IsPaused) { - // send event if we have a listener - if (OnTelemetryUpdate != null && !_isPaused) - { - var telemetryData = GetTelemetryDataSample(); - OnTelemetryUpdate.Invoke(this, telemetryData); - } + // write to channel + var telemetryData = GetTelemetryDataSample(); + _telemetryDataChannel.Writer.TryWrite(telemetryData); + } + + // Apply playback speed delay if configured + if (playbackDelay > TimeSpan.Zero) + { + await Task.Delay(playbackDelay, token).ConfigureAwait(false); + } + + // Yield periodically for responsive cancellation + if (numRecords % 100 == 0) + { + await Task.Yield(); + token.ThrowIfCancellationRequested(); } } sw.Stop(); - var recsPerSec = Math.Round(numRecords / sw.ElapsedMilliseconds * 1000f, 1); - var minsOfData = Math.Round(numRecords / 60f / 60f); - _logger.LogInformation("processed {_numRecords} IBT telemetry records ({minsOfData} mins worth of session data), in {milliseconds}ms. ({rate} recs/sec)", numRecords, minsOfData, sw.ElapsedMilliseconds, recsPerSec); + var recsPerSec = Math.Round(numRecords / (sw.ElapsedMilliseconds + 1) * 1000f, 1); // +1 to avoid division by zero + var minsOfData = Math.Round(numRecords / 60f / 60f, 2); + _logger.LogInformation("processed {numRecords} IBT telemetry records ({minsOfData} mins worth of session data), in {milliseconds}ms. ({rate} recs/sec)", numRecords, minsOfData, sw.ElapsedMilliseconds, recsPerSec); + } + catch (OperationCanceledException) when (token.IsCancellationRequested) + { + _logger.LogDebug("IBT data processing cancelled after {numRecords} records", numRecords); + throw; // propagate cancellation to caller } catch (Exception e) { - _logger.LogError(e, "error playing ibt file"); - OnError?.Invoke(this, new ExceptionEventArgs(e)); + _logger.LogError(e, "Error playing IBT file after {numRecords} records", numRecords); + _errorChannel.Writer.TryWrite(e); throw; } finally { - // send disconnect event - OnConnectStateChanged?.Invoke(this, new ConnectStateChangedEventArgs { State = ConnectState.Disconnected }); + _connectStateChannel.Writer.TryWrite(ConnectState.Disconnected); } + + // session info processor will complete naturally when _internalSessionInfoChannel is completed + // (which happens in StartDataProcessorTask's finally block) return numRecords; } diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/TelemetryDataAccessor.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK/TelemetryDataAccessor.cs new file mode 100644 index 0000000..2b321d9 --- /dev/null +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/TelemetryDataAccessor.cs @@ -0,0 +1,192 @@ +/** + * Copyright (C) 2024-2025 Scott Velez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +**/ + +using System; +using System.Collections.Concurrent; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; +using Microsoft.Extensions.Logging; +using SVappsLAB.iRacingTelemetrySDK.DataProviders; + +namespace SVappsLAB.iRacingTelemetrySDK +{ + /// + /// High-performance accessor for telemetry data that eliminates boxing/unboxing overhead + /// through ref-based operations and compiled expression trees. + /// + /// The telemetry data struct type + internal sealed class TelemetryDataAccessor where T : struct + { + private readonly ILogger _logger; + private readonly PropertyAccessor[] _propertyAccessors; + + public TelemetryDataAccessor(ILogger logger) + { + _logger = logger; + _propertyAccessors = CompilePropertyAccessors(); + } + + /// + /// Creates a new telemetry data sample with high performance, avoiding boxing/unboxing + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T CreateTelemetryDataSample(IDataProvider dataProvider) + { + var telemetryData = default(T); + + foreach (var accessor in _propertyAccessors) + { + var rawValue = dataProvider.GetVarValue(accessor.PropertyName); + if (rawValue == null) continue; + + try + { + var convertedValue = accessor.ConvertValue(rawValue); + accessor.SetValue(ref telemetryData, convertedValue); + } + catch (Exception ex) + { + _logger.LogWarning("Failed to set property {PropertyName} with value '{RawValue}': {Error}", + accessor.PropertyName, rawValue, ex.Message); + } + } + + return telemetryData; + } + + + private static PropertyAccessor[] CompilePropertyAccessors() + { + var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance); + var accessors = new PropertyAccessor[properties.Length]; + + for (int i = 0; i < properties.Length; i++) + { + accessors[i] = new PropertyAccessor(properties[i]); + } + + return accessors; + } + + private delegate void RefSetter(ref TStruct instance, object value) where TStruct : struct; + + private sealed class PropertyAccessor + { + private static readonly ConcurrentDictionary> _converterCache + = new ConcurrentDictionary>(); + + public string PropertyName { get; } + public Type PropertyType { get; } + public Type UnderlyingType { get; } + public bool IsEnum { get; } + public bool IsNullable { get; } + + private readonly RefSetter _refSetter; + private readonly Func _converter; + + public PropertyAccessor(PropertyInfo property) + { + PropertyName = property.Name; + PropertyType = property.PropertyType; + UnderlyingType = Nullable.GetUnderlyingType(PropertyType) ?? PropertyType; + IsEnum = UnderlyingType.IsEnum; + IsNullable = PropertyType != UnderlyingType; + + _refSetter = CompileRefPropertySetter(property); + _converter = GetOrCompileConverter(PropertyType, UnderlyingType, IsEnum); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetValue(ref T instance, object value) + { + _refSetter(ref instance, value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public object ConvertValue(object value) + { + return _converter(value); + } + + private static RefSetter CompileRefPropertySetter(PropertyInfo property) + { + var instanceParam = Expression.Parameter(typeof(T).MakeByRefType(), "instance"); + var valueParam = Expression.Parameter(typeof(object), "value"); + + var valueCast = Expression.Convert(valueParam, property.PropertyType); + var propertyAccess = Expression.Property(instanceParam, property); + var assignment = Expression.Assign(propertyAccess, valueCast); + + var lambda = Expression.Lambda>( + assignment, instanceParam, valueParam); + + return lambda.Compile(); + } + + private static Func GetOrCompileConverter(Type targetType, Type underlyingType, bool isEnum) + { + return _converterCache.GetOrAdd(targetType, _ => CompileConverter(targetType, underlyingType, isEnum)); + } + + private static Func CompileConverter(Type targetType, Type underlyingType, bool isEnum) + { + var valueParam = Expression.Parameter(typeof(object), "value"); + Expression conversionExpr; + + if (isEnum) + { + // Handle enum conversion: if value is int, convert to enum + var valueAsInt = Expression.Convert(valueParam, typeof(int)); + conversionExpr = Expression.Call( + typeof(Enum).GetMethod(nameof(Enum.ToObject), new[] { typeof(Type), typeof(object) })!, + Expression.Constant(underlyingType), + Expression.Convert(valueAsInt, typeof(object))); + conversionExpr = Expression.Convert(conversionExpr, targetType); + } + else if (targetType.IsArray) + { + // Handle array types: direct cast, no conversion needed + conversionExpr = Expression.Convert(valueParam, targetType); + } + else if (targetType == typeof(object)) + { + // No conversion needed + conversionExpr = valueParam; + } + else if (Nullable.GetUnderlyingType(targetType) != null) + { + // Handle nullable types: convert to underlying type first, then to nullable + var underlyingConvertMethod = typeof(Convert).GetMethod(nameof(Convert.ChangeType), new[] { typeof(object), typeof(Type) }); + var underlyingConversion = Expression.Call(underlyingConvertMethod!, valueParam, Expression.Constant(underlyingType)); + conversionExpr = Expression.Convert(underlyingConversion, targetType); + } + else + { + // Standard type conversion + var convertMethod = typeof(Convert).GetMethod(nameof(Convert.ChangeType), new[] { typeof(object), typeof(Type) }); + conversionExpr = Expression.Call(convertMethod!, valueParam, Expression.Constant(targetType)); + conversionExpr = Expression.Convert(conversionExpr, targetType); + } + + var lambda = Expression.Lambda>( + Expression.Convert(conversionExpr, typeof(object)), valueParam); + + return lambda.Compile(); + } + } + } +} diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/YamlParser.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK/YamlParser.cs index ad61cd9..e73334b 100644 --- a/Sdk/SVappsLAB.iRacingTelemetrySDK/YamlParser.cs +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/YamlParser.cs @@ -11,7 +11,7 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; + * limitations under the License. **/ using System; @@ -20,13 +20,13 @@ namespace SVappsLAB.iRacingTelemetrySDK { - public record struct ParseResult(T Model, int ParseAttemptsRequired); - public interface ISessionInfoParser + internal record struct ParseResult(T Model, int ParseAttemptsRequired); + internal interface ISessionInfoParser { public ParseResult Parse(string sessionInfo); } - public class YamlParser : ISessionInfoParser + internal class YamlParser : ISessionInfoParser { readonly IDeserializer _yamlDeserializer; diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/build/SVappsLAB.iRacingTelemetrySDK.props b/Sdk/SVappsLAB.iRacingTelemetrySDK/build/SVappsLAB.iRacingTelemetrySDK.props new file mode 100644 index 0000000..fb575be --- /dev/null +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/build/SVappsLAB.iRacingTelemetrySDK.props @@ -0,0 +1,20 @@ + + + $(MSBuildThisFileDirectory)..\contentFiles\any\any\docs\SVappsLAB.iRacingTelemetrySDK\AI_USAGE.md + $(MSBuildProjectDirectory)\docs\SVappsLAB.iRacingTelemetrySDK\AI_USAGE.md + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/contents/docs/AI_USAGE.md b/Sdk/SVappsLAB.iRacingTelemetrySDK/contents/docs/AI_USAGE.md new file mode 100644 index 0000000..55a6cf4 --- /dev/null +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/contents/docs/AI_USAGE.md @@ -0,0 +1,1664 @@ +# iRacing Telemetry SDK - AI Context & Implementation Guide + +> **AI Assistant Instructions**: This document provides comprehensive guidance for AI coding assistants to understand and implement applications using the iRacing Telemetry SDK. The SDK uses source code generation and requires specific patterns for proper operation. + +## SDK Overview + +The **ITelemetryClient** is the core interface of the iRacing Telemetry SDK that provides high-performance access to iRacing simulator telemetry data using an **async data streaming architecture**. It supports both live telemetry streaming from active iRacing sessions and playback of IBT (iRacing Binary Telemetry) files with strongly-typed data structures generated at compile time. + +## Critical Requirements for AI Tools + +โš ๏ธ **Essential Constraints**: +- **Async Data Streaming**: Uses `System.Threading.Channels` internally for high-performance, lock-free data streaming +- **Source Generation Dependency**: The `[RequiredTelemetryVars]` attribute triggers compile-time code generation. The `TelemetryData` struct is NOT manually created. +- **Enum-Based Variable Identification**: Use `TelemetryVar` enum values instead of strings to identify telemetry variables (v1.0+ only) +- **Nullable Properties**: Generated `TelemetryData` struct has nullable properties (`float?`, `int?`, `bool?`) - handle appropriately +- **Target Framework**: .NET 8.0+ required +- **Package Dependencies**: `Microsoft.Extensions.Logging`, `Microsoft.Extensions.Hosting`, and dependency injection support recommended +- **Windows Dependency**: Live telemetry requires Windows (iRacing memory-mapped files). IBT playback works cross-platform. +- **Interface-Based**: Use `ITelemetryClient` interface, not concrete `TelemetryClient` class directly + +## Project Setup + +### Required NuGet Packages +```xml + + + + + +``` + +### Project File Requirements +```xml + + + Exe + net8.0 + enable + enable + + +``` + +## Key Features + +- **Async Data Streaming**: Uses `System.Threading.Channels` internally for high-performance, lock-free data streaming +- **Generic Type Safety**: Uses source code generation to create strongly-typed telemetry data structures +- **Dual Data Sources**: Works with live iRacing sessions or IBT file playback +- **High Performance**: Optimized with `ref struct`, `ReadOnlySpan`, and unsafe code for zero-allocation processing +- **Multiple Stream Types**: Separate channels for telemetry data, session info, connection state, and errors +- **Asynchronous Operations**: Non-blocking operations throughout using async/await patterns with `IAsyncEnumerable` + +## ๐Ÿšจ Critical: Nullable Properties Handling + +### Why Properties Are Nullable + +**All telemetry properties are nullable** (`float?`, `int?`, `bool?`) because iRacing variables have dynamic availability: +- Some variables only exist in live sessions (not in IBT files) +- Some variables only exist in specific car types or session types +- Variables may be unavailable during certain racing conditions + +### AI Agent Guidelines + +**โœ… ALWAYS use these patterns:** + +```csharp +// โœ… Null-conditional operator with fallback +var speedDisplay = $"Speed: {data.Speed?.ToString("F1") ?? "N/A"}"; + +// โœ… Direct arithmetic (preserves null semantics) +var speedMph = data.Speed * 2.23694f; // Result is null if Speed is null + +// โœ… Explicit null handling +var speed = data.Speed ?? 0f; +var speed = data.Speed.GetValueOrDefault(); +var hasValue = data.Speed.HasValue; + +// โœ… Boolean checks (essential for bool? properties) +if (data.IsOnTrackCar == true) { /* handle when explicitly true */ } +if (data.IsOnTrackCar.GetValueOrDefault()) { /* handle when true or null as false */ } + +// โœ… Safe conditional checks +if (data.Speed.HasValue && data.Speed.Value > 100) { /* process */ } +``` + +**โŒ NEVER use these patterns:** + +```csharp +// โŒ Direct .Value access (throws if null) +var speed = data.Speed.Value; // NullReferenceException if Speed is null + +// โŒ Implicit bool conversion (compilation error) +if (data.IsOnTrackCar) { } // Cannot convert bool? to bool + +// โŒ Direct math without null handling +var calculation = data.Speed + data.RPM; // May be null unexpectedly +``` + +### Common Nullable Scenarios + +```csharp +// โœ… Speed/Distance calculations +var speedKph = data.Speed.HasValue ? data.Speed.Value * 3.6f : (float?)null; +var speedMph = data.Speed * 2.23694f; // Preserves null + +// โœ… Boolean flag handling +var onTrack = data.IsOnTrackCar == true; +var hasIncidents = data.PlayerIncidents > 0; + +// โœ… Array/enum access +var gearText = data.Gear?.ToString() ?? "N"; +var surfaceType = data.PlayerTrackSurface?.ToString() ?? "Unknown"; + +// โœ… Display formatting +Console.WriteLine($"Speed: {data.Speed?.ToString("F1") ?? "---"} mph"); +Console.WriteLine($"Gear: {data.Gear?.ToString() ?? "N"}"); +Console.WriteLine($"RPM: {data.RPM?.ToString("F0") ?? "----"}"); +``` + +## Implementation Patterns + +The SDK offers **two main approaches** for consuming telemetry data: + +1. **๐ŸŸข SIMPLE APPROACH**: Use subscription extension methods for event-like patterns +2. **๐Ÿ”ด ADVANCED APPROACH**: Use direct stream consumption for maximum performance and control + +--- + +## ๐ŸŸข SIMPLE APPROACH: Subscription Extensions (Recommended) + +### Quick Start Pattern + +Use the `SubscribeToAllStreams` extension method for the easiest migration from event-based code: +```csharp +[RequiredTelemetryVars([TelemetryVar.Speed, TelemetryVar.RPM, TelemetryVar.Gear])] +public class Program +{ + public static async Task Main(string[] args) + { + var logger = LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger("App"); + await using var client = TelemetryClient.Create(logger); + using var cts = new CancellationTokenSource(); + + // Use extension method for simplified consumption with async callbacks + var subscriptionTask = client.SubscribeToAllStreams( + onTelemetryUpdate: async data => Console.WriteLine($"Speed: {data.Speed}, RPM: {data.RPM}, Gear: {data.Gear}"), + onSessionInfoUpdate: async session => Console.WriteLine($"Track: {session.WeekendInfo.TrackDisplayName}"), + onConnectStateChanged: async state => Console.WriteLine($"Connection: {state}"), + onError: async error => Console.WriteLine($"Error: {error.Message}"), + cancellationToken: cts.Token); + + var monitorTask = client.Monitor(cts.Token); + + await Task.WhenAny(monitorTask, subscriptionTask); + } +} +``` + +### Individual Stream Subscription + +For selective data consumption, use direct stream access: + +```csharp +[RequiredTelemetryVars([TelemetryVar.Speed, TelemetryVar.RPM])] +public class Program +{ + public static async Task Main(string[] args) + { + var logger = LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger("App"); + await using var client = TelemetryClient.Create(logger); + using var cts = new CancellationTokenSource(); + + // Subscribe to only the streams you need + var telemetryTask = Task.Run(async () => + { + await foreach (var data in client.TelemetryData.WithCancellation(cts.Token)) + { + Console.WriteLine($"Speed: {data.Speed}, RPM: {data.RPM}"); + } + }, cts.Token); + + var sessionTask = Task.Run(async () => + { + await foreach (var session in client.SessionData.WithCancellation(cts.Token)) + { + Console.WriteLine($"Track: {session.WeekendInfo?.TrackDisplayName ?? "Unknown"}"); + } + }, cts.Token); + + var monitorTask = client.Monitor(cts.Token); + + await Task.WhenAny(monitorTask, telemetryTask, sessionTask); + } +} +``` + +### Available Extension Methods + +**Important**: All delegates in `SubscribeToAllStreams` are **optional** - you only need to provide the callbacks you care about. + +| Extension Method | Purpose | Stream Type | Parameter Type | +|------------------|---------|-------------|----------------| +| `SubscribeToAllStreams` | All streams with optional delegates | Multiple | All delegates optional | +| Direct stream access | High-frequency telemetry data (60Hz) | `TelemetryData` | `T` (generated struct) | +| Direct stream access | Parsed session information | `SessionData` | `TelemetrySessionInfo` | +| Direct stream access | Raw YAML session data | `SessionDataYaml` | `string` | +| Direct stream access | Connection state changes | `ConnectStates` | `ConnectState` enum | +| Direct stream access | Error notifications | `Errors` | `Exception` | + +### Migration from Events (Pre-v1.0) + +If you're migrating from the old event-based API: + +```csharp +// OLD (Events - no longer available): +// client.OnTelemetryUpdate += (sender, data) => { /* handle */ }; +// client.OnSessionInfoUpdate += (sender, session) => { /* handle */ }; +// client.OnError += (sender, error) => { /* handle */ }; + +// NEW (Subscription Extensions - recommended with async callbacks): +// Note: All delegates are optional - only provide the ones you need +var subscriptionTask = client.SubscribeToAllStreams( + onTelemetryUpdate: async data => { /* handle - data is T (generated struct) */ }, + onSessionInfoUpdate: async session => { /* handle - session is TelemetrySessionInfo */ }, + onError: async error => { /* handle - error is Exception */ }, + cancellationToken: cancellationToken); +``` + +--- + +## ๐Ÿค– AI Agent Guide: Two Consumption Approaches + +### Overview for AI Agents + +The v1.0 SDK provides two distinct patterns for consuming telemetry data. Choose based on your application requirements: + +| Approach | When to Use | Performance | Complexity | +|----------|-------------|-------------|-------------| +| **Extension Method** | Simple applications, rapid prototyping | High (sufficient for most use cases) | Low | +| **Direct Stream Access** | Maximum performance, custom logic | Highest (650K+ records/sec) | Medium | + +### Extension Method Approach (Recommended for Most Cases) + +**Benefits:** +- Simplified API similar to events +- Automatic task management +- Built-in error handling +- Perfect for AI-generated code + +**Pattern:** +```csharp +// Single method handles all streams with async callbacks (all delegates are optional) +var subscriptionTask = client.SubscribeToAllStreams( + onTelemetryUpdate: async data => { /* process telemetry */ }, + onSessionInfoUpdate: async session => { /* process session */ }, + onRawSessionInfoUpdate: async yaml => { /* process raw YAML */ }, + onConnectStateChanged: async state => { /* handle connection - state is ConnectState enum */ }, + onError: async error => { /* handle errors - error is Exception */ }, + cancellationToken: cancellationToken +); + +var monitorTask = client.Monitor(cancellationToken); + +await Task.WhenAny(monitorTask, subscriptionTask); +``` + +### Direct Stream Access Approach (Maximum Performance) + +**Benefits:** +- Maximum performance and flexibility +- Custom backpressure handling +- Selective stream consumption +- Advanced async patterns + +**Pattern:** +```csharp +// Consume each stream independently +var telemetryTask = Task.Run(async () => +{ + await foreach (var data in client.TelemetryData.WithCancellation(cancellationToken)) + { + // Process telemetry data + } +}, cancellationToken); + +var sessionTask = Task.Run(async () => +{ + await foreach (var session in client.SessionData.WithCancellation(cancellationToken)) + { + // Process session info + } +}, cancellationToken); + +var monitorTask = client.Monitor(cancellationToken); + +await Task.WhenAny(monitorTask, telemetryTask, sessionTask); +``` + +### AI Agent Decision Tree + +When generating code, follow this decision tree: + +1. **Does the application need maximum performance (>500K records/sec)?** + - No โ†’ Use Extension Method Approach + - Yes โ†’ Use Direct Stream Access + +2. **Does the application need custom backpressure handling?** + - No โ†’ Use Extension Method Approach + - Yes โ†’ Use Direct Stream Access + +3. **Does the application only need specific streams?** + - No โ†’ Use Extension Method Approach (with null delegates for unused streams) + - Yes โ†’ Use Direct Stream Access + +4. **Is this a prototype or simple application?** + - Yes โ†’ Use Extension Method Approach + - No โ†’ Consider Direct Stream Access + +--- + +## ๐Ÿ”ด ADVANCED APPROACH: Direct Stream Consumption + +### When to Use Direct Streams + +- **Maximum Performance**: Need absolute best performance (650K+ records/sec) +- **Complex Processing**: Require advanced backpressure handling +- **Custom Patterns**: Need custom consumption logic beyond simple callbacks +- **Selective Consumption**: Only process data under specific conditions + +### Core Data Streaming Pattern + +```csharp +[RequiredTelemetryVars([TelemetryVar.Speed, TelemetryVar.RPM, TelemetryVar.Gear])] +public class Program +{ + public static async Task Main(string[] args) + { + var logger = LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger("App"); + await using var client = TelemetryClient.Create(logger); + using var cts = new CancellationTokenSource(); + + // Direct stream consumption for maximum performance + var telemetryTask = Task.Run(async () => + { + await foreach (var data in client.TelemetryData.WithCancellation(cts.Token)) + { + // High-performance processing + Console.WriteLine($"Speed: {data.Speed}, RPM: {data.RPM}, Gear: {data.Gear}"); + } + }, cts.Token); + + var sessionTask = Task.Run(async () => + { + await foreach (var session in client.SessionData.WithCancellation(cts.Token)) + { + Console.WriteLine($"Track: {session.WeekendInfo.TrackName}"); + } + }, cts.Token); + + var monitorTask = client.Monitor(cts.Token); + + await Task.WhenAny(monitorTask, telemetryTask, sessionTask); + } +} +``` + +### Available Async Streams + +```csharp +ITelemetryClient client; + +// Primary data streams (all are IAsyncEnumerable) +client.TelemetryData // IAsyncEnumerable - 60Hz telemetry +client.SessionData // IAsyncEnumerable - session updates +client.SessionDataYaml // IAsyncEnumerable - raw YAML session data +client.ConnectStates // IAsyncEnumerable - connection changes +client.Errors // IAsyncEnumerable - error notifications + +// Consume with await foreach pattern +await foreach (var data in client.TelemetryData.WithCancellation(cancellationToken)) +{ + // Process telemetry data at maximum speed +} +``` + +### Data Stream Buffer Behavior + +**Important: Understanding the Ring Buffer** + +All data streams use a **60-sample ring buffer** with FIFO (First-In-First-Out) semantics and destructive reads: + +- **Buffer Capacity**: 60 samples per stream +- **Buffering Duration**: At iRacing's 60Hz update rate, provides up to **1 second** of data buffering +- **Read Semantics**: Destructive - once consumed from the stream, data is removed from the buffer +- **Overflow Behavior**: When buffer fills to capacity, **oldest unread samples are automatically dropped** +- **Non-Blocking**: The SDK never blocks iRacing's data stream, ensuring continuous telemetry flow + +**What This Means for Your Application:** + +```csharp +// โœ… If your processing keeps up with 60Hz (~16ms per sample): +// - You receive every telemetry sample +// - No data loss occurs +// - Buffer typically holds only 1-2 samples + +// โš ๏ธ If your processing is slower than 60Hz: +// - Buffer accumulates up to 60 samples (1 second) +// - Once buffer fills, oldest samples are automatically discarded +// - You receive the most recent data, but some intermediate samples are lost +// - This prevents memory exhaustion and keeps your app responsive + +// Example: Expensive processing +await foreach (var data in client.TelemetryData.WithCancellation(ct)) +{ + // If this takes >16ms per iteration, samples will be dropped + await PerformExpensiveAnalysis(data); // โš ๏ธ May cause sample loss +} + +// โœ… Better: Keep consumption fast, offload heavy work +await foreach (var data in client.TelemetryData.WithCancellation(ct)) +{ + // Fast: Just capture the data + _ = Task.Run(() => PerformExpensiveAnalysis(data)); +} +``` + +**Design Principle**: The ring buffer with drop-oldest strategy ensures your application always receives the **most current telemetry** without blocking iRacing or risking memory issues, at the cost of potentially missing intermediate samples if processing cannot keep pace. + +### Advanced Streaming Patterns + +```csharp +// Pattern 1: Selective Processing with Conditions +await foreach (var data in client.TelemetryData.WithCancellation(cancellationToken)) +{ + // Only process when car is on track and above certain speed + if (data.IsOnTrackCar == true && (data.Speed ?? 0) > 50) + { + ProcessHighSpeedData(data); + } +} + +// Pattern 2: Batched Processing for Performance +var batch = new List(); +await foreach (var data in client.TelemetryData.WithCancellation(cancellationToken)) +{ + batch.Add(data); + if (batch.Count >= 60) // Process once per second at 60Hz + { + ProcessBatch(batch); + batch.Clear(); + } +} + +// Pattern 3: Multiple Stream Coordination +var tasks = new[] +{ + ConsumeStream(client.TelemetryData, ProcessTelemetry), + ConsumeStream(client.SessionData, ProcessSession), + ConsumeStream(client.Errors, ProcessError) +}; + +await Task.WhenAll(tasks); + +static async Task ConsumeStream(IAsyncEnumerable stream, Action processor) +{ + await foreach (var item in stream.WithCancellation(cancellationToken)) + { + processor(item); + } +} +``` + +--- + +## Common Usage Patterns + +### Variable Categories +| Category | Variables | Notes | +|----------|-----------|-------| +| **Basic Vehicle** | `Speed`, `RPM`, `Gear`, `Throttle`, `Brake` | Core driving metrics | +| **Position** | `LapDistPct`, `IsOnTrack`, `PlayerTrackSurface` | Track position | +| **Safety** | `PlayerIncidents`, `EngineWarnings` | Warnings and penalties | +| **Session** | `SessionTime`, `SessionNum`, `IsOnTrackCar` | Session state | + +### Data Modes and ClientOptions +```csharp +// Live mode (Windows only, requires iRacing running) +var client = TelemetryClient.Create(logger); + +// IBT file mode (cross-platform) +var ibtOptions = new IBTOptions("file.ibt", playBackSpeedMultiplier: 1); +var client = TelemetryClient.Create(logger, ibtOptions); + +// With ClientOptions (for metrics support, etc.) +// Note: There are only 2 Create method overloads: +// 1. Create(logger, ibtOptions = null) +// 2. Create(logger, ibtOptions, clientOptions) +var clientOptions = new ClientOptions { MeterFactory = meterFactory }; +var client = TelemetryClient.Create(logger, ibtOptions, clientOptions); +``` + + +### Dependency Injection Pattern (Recommended) +```csharp +// In Program.cs with Host Builder +var builder = Host.CreateDefaultBuilder(args) + .ConfigureServices((context, services) => + { + services.AddMetrics(); + services.AddLogging(logging => + { + logging.SetMinimumLevel(LogLevel.Debug); + logging.AddConsole(); + }); + + // Register TelemetryClient as singleton + services.AddSingleton>(provider => + { + var logger = provider.GetRequiredService>(); + var meterFactory = provider.GetRequiredService(); + + var clientOptions = new ClientOptions { MeterFactory = meterFactory }; + IBTOptions? ibtOptions = args.Length == 1 ? new IBTOptions(args[0]) : null; + + return TelemetryClient.Create(logger, clientOptions, ibtOptions); + }); + }); + +using var host = builder.Build(); +var client = host.Services.GetRequiredService>(); +``` + +## Core Setup (Required for Both Approaches) + +### 1. Define Required Telemetry Variables + +Use the `[RequiredTelemetryVars]` attribute to specify which telemetry variables your application needs using `TelemetryVar` enum values. The source generator will create a strongly-typed `TelemetryData` struct with nullable properties. + +```csharp +using SVappsLAB.iRacingTelemetrySDK; + +[RequiredTelemetryVars([TelemetryVar.Speed, TelemetryVar.RPM, TelemetryVar.Gear])] +public class Program +{ + // Generated TelemetryData will have: + // public float? Speed { get; init; } + // public float? RPM { get; init; } + // public int? Gear { get; init; } +} +``` + +### 2. Create and Configure the Client + +```csharp +using Microsoft.Extensions.Logging; +using SVappsLAB.iRacingTelemetrySDK; + +// Set up logging +var logger = LoggerFactory + .Create(builder => builder + .SetMinimumLevel(LogLevel.Information) + .AddConsole()) + .CreateLogger("TelemetryApp"); + +// Create client for live data (Windows only, requires iRacing running) +await using var client = TelemetryClient.Create(logger); + +// OR create client for IBT file playback (cross-platform) +var ibtOptions = new IBTOptions("path/to/file.ibt", playBackSpeedMultiplier: 1); +await using var client = TelemetryClient.Create(logger, ibtOptions); + +// OR with ClientOptions for metrics support +// Note: When using ClientOptions, BOTH ibtOptions and clientOptions must be provided +var clientOptions = new ClientOptions { MeterFactory = meterFactory }; +await using var client = TelemetryClient.Create(logger, ibtOptions, clientOptions); +``` + +### 3. Handle Nullable Properties (v1.0+ Critical) + +All telemetry properties in v1.0+ are nullable (`float?`, `int?`, `bool?`) to handle cases where data might not be available. + +```csharp +// โœ… Safe arithmetic with nullable values (preserves null semantics) +var speedMph = data.Speed * 2.23694f; // Result is float?, not float + +// โœ… Explicit null checking when needed +if (data.Speed.HasValue) +{ + var speed = data.Speed.Value * 2.23694f; +} + +// โœ… Boolean nullable comparisons +if (data.IsOnTrackCar == true) { /* car is on track */ } + +// โœ… String formatting with null-conditional operators +Console.WriteLine($"Speed: {data.Speed?.ToString("F1") ?? "N/A"}"); +``` + +## Complete Examples + +### Example 1: ๐ŸŸข Simple Approach - Speed, RPM, and Gear Display + +```csharp +using Microsoft.Extensions.Logging; +using SVappsLAB.iRacingTelemetrySDK; + +namespace BasicTelemetryApp +{ + [RequiredTelemetryVars([TelemetryVar.Speed, TelemetryVar.RPM, TelemetryVar.Gear, TelemetryVar.IsOnTrackCar])] + internal class Program + { + static async Task Main(string[] args) + { + var logger = LoggerFactory + .Create(builder => builder + .SetMinimumLevel(LogLevel.Information) + .AddConsole()) + .CreateLogger("BasicApp"); + + // Support both live and IBT file modes + IBTOptions? ibtOptions = args.Length == 1 ? new IBTOptions(args[0]) : null; + await using var client = TelemetryClient.Create(logger, ibtOptions); + using var cts = new CancellationTokenSource(); + + var counter = 0; + // Use extension method for simplified consumption with async callbacks + var subscriptionTask = client.SubscribeToAllStreams( + onTelemetryUpdate: async data => + { + // Limit logging output to once per second + if ((counter++ % 60) != 0 || data.IsOnTrackCar != true) return; + + var speedMph = data.Speed * 2.23694f; // Convert m/s to mph + logger.LogInformation($"Gear: {data.Gear}, RPM: {data.RPM?.ToString("F0") ?? "N/A"}, Speed: {speedMph?.ToString("F0") ?? "N/A"} mph"); + }, + onSessionInfoUpdate: async session => + { + logger.LogInformation($"Track: {session.WeekendInfo.TrackDisplayName}"); + }, + onConnectStateChanged: async state => + { + logger.LogInformation($"Connection: {state}"); + }, + onError: async error => + { + logger.LogError(error, "Telemetry error occurred"); + }, + cancellationToken: cts.Token); + + Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); }; + + var monitorTask = client.Monitor(cts.Token); + + await Task.WhenAny(monitorTask, subscriptionTask); + } + } +} +``` + +### Example 2: ๐Ÿ”ด Advanced Approach - High-Performance Direct Stream Consumption + +```csharp +using Microsoft.Extensions.Logging; +using SVappsLAB.iRacingTelemetrySDK; +using System.Collections.Generic; + +namespace HighPerformanceApp +{ + [RequiredTelemetryVars([TelemetryVar.Speed, TelemetryVar.RPM, TelemetryVar.Gear, TelemetryVar.IsOnTrackCar])] + internal class Program + { + private static readonly List _dataBuffer = new(); + private static int _processedCount = 0; + + static async Task Main(string[] args) + { + var logger = LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger("HighPerf"); + await using var client = TelemetryClient.Create(logger); + using var cts = new CancellationTokenSource(); + + // Direct stream consumption for maximum performance (650K+ records/sec) + var telemetryTask = Task.Run(async () => + { + await foreach (var data in client.TelemetryData.WithCancellation(cts.Token)) + { + // Selective processing - only when car is on track and above 50 m/s + if (data.IsOnTrackCar == true && (data.Speed ?? 0) > 50) + { + // Batch processing for efficiency + _dataBuffer.Add(data); + + if (_dataBuffer.Count >= 60) // Process once per second at 60Hz + { + ProcessBatch(_dataBuffer, logger); + _dataBuffer.Clear(); + } + } + + _processedCount++; + + // Performance monitoring + if (_processedCount % 6000 == 0) // Every 100 seconds at 60Hz + { + logger.LogInformation($"Processed {_processedCount} records at high speed"); + } + } + }, cts.Token); + + // Monitor session changes with direct stream access + var sessionTask = Task.Run(async () => + { + await foreach (var session in client.SessionData.WithCancellation(cts.Token)) + { + // Direct access to session data - no overhead from extension methods + logger.LogInformation($"Session Update - Track: {session.WeekendInfo.TrackDisplayName}, " + + $"Drivers: {session.DriverInfo?.Drivers?.Count ?? 0}"); + } + }, cts.Token); + + // Direct error handling + var errorTask = Task.Run(async () => + { + await foreach (var error in client.Errors.WithCancellation(cts.Token)) + { + logger.LogError(error, "High-performance telemetry error"); + } + }, cts.Token); + + Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); }; + + // Coordinate all tasks for maximum throughput + var monitorTask = client.Monitor(cts.Token); + + await Task.WhenAny(monitorTask, telemetryTask, sessionTask, errorTask); + + // Final batch processing if any data remains + if (_dataBuffer.Count > 0) + { + ProcessBatch(_dataBuffer, logger); + } + + logger.LogInformation($"Final count: {_processedCount} records processed"); + } + + private static void ProcessBatch(List batch, ILogger logger) + { + // High-performance batch processing + var avgSpeed = batch.Where(d => d.Speed.HasValue).Average(d => d.Speed!.Value) * 2.23694f; // m/s to mph + var maxRpm = batch.Where(d => d.RPM.HasValue).Max(d => d.RPM!.Value); + + logger.LogInformation($"Batch processed: {batch.Count} records, Avg Speed: {avgSpeed:F1} mph, Max RPM: {maxRpm:F0}"); + } + } +} +``` + +### Example 3: Track Position and Surface Analysis + +```csharp +using Microsoft.Extensions.Logging; +using SVappsLAB.iRacingTelemetrySDK; + +namespace TrackAnalysisApp +{ + [RequiredTelemetryVars([TelemetryVar.IsOnTrack, TelemetryVar.PlayerTrackSurface, TelemetryVar.PlayerTrackSurfaceMaterial, TelemetryVar.EngineWarnings, TelemetryVar.PlayerIncidents, TelemetryVar.LapDistPct])] + internal class Program + { + static async Task Main(string[] args) + { + var logger = LoggerFactory + .Create(builder => builder + .SetMinimumLevel(LogLevel.Information) + .AddConsole()) + .CreateLogger("TrackAnalysis"); + + IBTOptions? ibtOptions = args.Length == 1 ? new IBTOptions(args[0]) : null; + await using var client = TelemetryClient.Create(logger, ibtOptions); + + using var cts = new CancellationTokenSource(); + Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); }; + + var counter = 0; + var subscriptionTask = client.SubscribeToAllStreams( + onTelemetryUpdate: async data => + { + if ((counter++ % 120) != 0) return; // Output every 2 seconds + + var trackSurface = data.PlayerTrackSurface.HasValue ? Enum.GetName(data.PlayerTrackSurface.Value) ?? "Unknown" : "N/A"; + var surfaceMaterial = data.PlayerTrackSurfaceMaterial.HasValue ? Enum.GetName(data.PlayerTrackSurfaceMaterial.Value) ?? "Unknown" : "N/A"; + var warnings = data.EngineWarnings.HasValue ? GetEngineWarningsList(data.EngineWarnings.Value) : "N/A"; + var incidents = data.PlayerIncidents.HasValue ? GetIncidentInfo(data.PlayerIncidents.Value) : "N/A"; + + logger.LogInformation($"Lap: {data.LapDistPct?.ToString("P1") ?? "N/A"}, OnTrack: {data.IsOnTrack}, " + + $"Surface: {trackSurface}, Material: {surfaceMaterial}, " + + $"Warnings: {warnings}, Incidents: {incidents}"); + }, + cancellationToken: cts.Token + ); + + var monitorTask = client.Monitor(cts.Token); + + await Task.WhenAny(monitorTask, subscriptionTask); + } + + static string GetEngineWarningsList(EngineWarnings warnings) + { + var activeWarnings = new List(); + foreach (var flag in Enum.GetValues()) + { + if (warnings.HasFlag(flag) && flag != EngineWarnings.None) + { + activeWarnings.Add(Enum.GetName(flag) ?? flag.ToString()); + } + } + return activeWarnings.Count > 0 ? string.Join(", ", activeWarnings) : "None"; + } + + static string GetIncidentInfo(IncidentFlags incidents) + { + // Extract incident report and penalty separately + var incidentReport = (int)(incidents & IncidentFlags.IncidentRepMask); + var incidentPenalty = (int)(incidents & IncidentFlags.IncidentPenMask); + + var reportType = incidentReport switch + { + 0x0001 => "Loss of Control", + 0x0002 => "Off Track", + 0x0004 => "Contact", + 0x0005 => "Collision", + 0x0007 => "Car Contact", + 0x0008 => "Car Collision", + _ => incidentReport > 0 ? $"Unknown({incidentReport:X})" : "None" + }; + + var penaltyType = incidentPenalty switch + { + 0x0100 => "0x", + 0x0200 => "1x", + 0x0300 => "2x", + 0x0400 => "4x", + _ => incidentPenalty > 0 ? $"Unknown({incidentPenalty:X})" : "None" + }; + + return $"{reportType} ({penaltyType})"; + } + } +} +``` + +### Example 3: Data Export and Analysis + +```csharp +using Microsoft.Extensions.Logging; +using SVappsLAB.iRacingTelemetrySDK; + +namespace DataExportApp +{ + [RequiredTelemetryVars([TelemetryVar.Speed, TelemetryVar.RPM, TelemetryVar.SteeringWheelAngle, TelemetryVar.Throttle, TelemetryVar.Brake])] + internal class Program + { + static async Task Main(string[] args) + { + var logger = LoggerFactory + .Create(builder => builder + .SetMinimumLevel(LogLevel.Information) + .AddConsole()) + .CreateLogger("DataExport"); + + IBTOptions? ibtOptions = args.Length == 1 ? new IBTOptions(args[0]) : null; + await using var client = TelemetryClient.Create(logger, ibtOptions); + + var dataPoints = new List(); + var sessionInfo = ""; + + using var cts = new CancellationTokenSource(); + Console.CancelKeyPress += (_, e) => + { + e.Cancel = true; + cts.Cancel(); + ExportCollectedData(dataPoints, sessionInfo, logger); + }; + + var subscriptionTask = client.SubscribeToAllStreams( + onTelemetryUpdate: async data => + { + dataPoints.Add(data); + if (dataPoints.Count % 3600 == 0) // Log once a minute (60 Hz * 60 sec) + { + logger.LogInformation($"Collected {dataPoints.Count} data points..."); + } + }, + onRawSessionInfoUpdate: async yaml => + { + if (string.IsNullOrEmpty(sessionInfo)) + { + sessionInfo = yaml; + logger.LogInformation("Session info captured"); + } + }, + onConnectStateChanged: async state => + { + if (state == ConnectState.Connected) + { + var variables = client.GetTelemetryVariables(); + logger.LogInformation($"Available variables: {variables.Count}"); + + // Export variable definitions + await ExportVariableDefinitions(variables); + } + }, + cancellationToken: cts.Token + ); + + var monitorTask = client.Monitor(cts.Token); + + await Task.WhenAny(monitorTask, subscriptionTask); + } + + static async Task ExportVariableDefinitions(IEnumerable variables) + { + var timestamp = DateTime.Now.ToString("yyyyMMdd-HHmmss"); + var filename = $"TelemetryVariables-{timestamp}.csv"; + + await using var writer = new StreamWriter(filename); + await writer.WriteLineAsync("Name,Type,Length,IsTimeValue,Description,Units"); + + foreach (var variable in variables.OrderBy(v => v.Name)) + { + await writer.WriteLineAsync($"{variable.Name},{variable.Type.Name}," + + $"{variable.Length},{variable.IsTimeValue}," + + $"\"{variable.Desc}\",\"{variable.Units}\""); + } + } + + static void ExportCollectedData(List dataPoints, string sessionInfo, ILogger logger) + { + var timestamp = DateTime.Now.ToString("yyyyMMdd-HHmmss"); + + // Export telemetry data + using (var writer = new StreamWriter($"TelemetryData-{timestamp}.csv")) + { + writer.WriteLine("Speed,RPM,SteeringWheelAngle,Throttle,Brake"); + foreach (var data in dataPoints) + { + writer.WriteLine($"{data.Speed},{data.RPM},{data.SteeringWheelAngle}," + + $"{data.Throttle},{data.Brake}"); + } + } + + // Export session info + if (!string.IsNullOrEmpty(sessionInfo)) + { + File.WriteAllText($"SessionInfo-{timestamp}.yaml", sessionInfo); + } + + logger.LogInformation($"Exported {dataPoints.Count} data points"); + } + } +} +``` + +## Advanced Usage + +### IBT File Options + +```csharp +// Play at normal speed (1x) +var ibtOptions = new IBTOptions("replay.ibt", playBackSpeedMultiplier: 1); + +// Play at 10x speed +var ibtOptions = new IBTOptions("replay.ibt", playBackSpeedMultiplier: 10); + +// Play as fast as possible (default) +var ibtOptions = new IBTOptions("replay.ibt", playBackSpeedMultiplier: int.MaxValue); +``` + +### Pause and Resume + +```csharp +// Pause telemetry stream writes (processing continues in background) +client.Pause(); + +// Resume telemetry stream writes +client.Resume(); + +// Check pause state +if (client.IsPaused) +{ + Console.WriteLine("Client is paused"); +} +``` + +**Thread Safety and Behavior:** +- Both `Pause()` and `Resume()` are thread-safe and can be called from any thread +- Both methods are idempotent - safe to call multiple times without side effects +- Changes are not immediate due to eventual consistency (typically 1-2 samples, ~16-32ms at 60Hz) +- A few telemetry samples may pass through channels before pause/resume takes full effect + +### Connection Status Monitoring + +**Option A: Using Extension Method** +```csharp +using var cts = new CancellationTokenSource(); + +var subscriptionTask = client.SubscribeToAllStreams( + onConnectStateChanged: async state => // state is ConnectState enum + { + switch (state) + { + case ConnectState.Connected: + Console.WriteLine("Connected to iRacing"); + break; + case ConnectState.Disconnected: + Console.WriteLine("Disconnected from iRacing"); + break; + } + }, + cancellationToken: cts.Token +); + +// Check connection status at any time +if (client.IsConnected) +{ + Console.WriteLine("Currently connected"); +} +``` + +**Option B: Direct Stream Access** +```csharp +var connectionTask = Task.Run(async () => +{ + await foreach (var state in client.ConnectStates.WithCancellation(cancellationToken)) // state is ConnectState enum + { + switch (state) + { + case ConnectState.Connected: + Console.WriteLine("Connected to iRacing"); + break; + case ConnectState.Disconnected: + Console.WriteLine("Disconnected from iRacing"); + break; + } + } +}, cancellationToken); +``` + +### Error Notification + +**Option A: Using Extension Method** +```csharp +var subscriptionTask = client.SubscribeToAllStreams( + onError: async error => // error is Exception type + { + Console.WriteLine($"Telemetry error: {error.Message}"); + + // Log full exception details + logger.LogError(error, "Telemetry client error occurred"); + }, + cancellationToken: cts.Token +); +``` + +**Option B: Direct Stream Access** +```csharp +var errorTask = Task.Run(async () => +{ + await foreach (var error in client.Errors.WithCancellation(cancellationToken)) // error is Exception type + { + Console.WriteLine($"Telemetry error: {error.Message}"); + + // Log full exception details + logger.LogError(error, "Telemetry client error occurred"); + } +}, cancellationToken); +``` + +## Comprehensive Telemetry Variables Reference + +The SDK provides access to 200+ telemetry variables from iRacing. Here are the most commonly used: + +### ๐Ÿš— Vehicle Dynamics & Control +| Variable | Type | Units | Description | +|----------|------|-------|-------------| +| `Speed` | `float` | m/s | Vehicle speed | +| `RPM` | `float` | rpm | Engine RPM | +| `Gear` | `int` | - | Current gear (-1=reverse, 0=neutral, 1+=forward) | +| `Throttle` | `float` | 0.0-1.0 | Throttle pedal position | +| `Brake` | `float` | 0.0-1.0 | Brake pedal position | +| `Clutch` | `float` | 0.0-1.0 | Clutch pedal position | +| `SteeringWheelAngle` | `float` | rad | Steering wheel angle | +| `SteeringWheelTorque` | `float` | Nยทm | Force feedback torque | +| `LongAccel` | `float` | m/sยฒ | Longitudinal, lateral, vertical G-forces | + +### ๐Ÿ Track Position & Timing +| Variable | Type | Units | Description | +|----------|------|-------|-------------| +| `LapDistPct` | `float` | 0.0-1.0 | Distance around current lap | +| `LapCurrentLapTime` | `float` | s | Current lap time | +| `LapBestLapTime` | `float` | s | Best lap time this session | +| `LapLastLapTime` | `float` | s | Last completed lap time | +| `IsOnTrack` | `bool` | - | Whether car is on track surface | +| `IsOnTrackCar` | `bool` | - | Whether player's car is on track | +| `PlayerTrackSurface` | `int` | enum | Track surface type (asphalt, concrete, etc.) | +| `PlayerTrackSurfaceMaterial` | `int` | enum | Surface material properties | + +### โš ๏ธ Safety & Incidents +| Variable | Type | Units | Description | +|----------|------|-------|-------------| +| `PlayerIncidents` | `IncidentFlags` | flags | Incident type and penalty level | +| `EngineWarnings` | `EngineWarnings` | flags | Engine warning indicators | +| `SessionFlags` | `SessionFlags` | flags | Yellow, red, checkered flags | +| `CarIdxTrackSurface` | `int[]` | enum | Track surface for each car | + +**IncidentFlags Usage**: +```csharp +var reportType = (int)(data.PlayerIncidents & IncidentFlags.IncidentRepMask); +var penaltyLevel = (int)(data.PlayerIncidents & IncidentFlags.IncidentPenMask); +``` + +### ๐Ÿ”ง Vehicle Systems & Status +| Variable | Type | Units | Description | +|----------|------|-------|-------------| +| `FuelLevel` | `float` | L | Current fuel level | +| `FuelUsePerHour` | `float` | L/h | Fuel consumption rate | +| `WaterTemp` | `float` | ยฐC | Engine coolant temperature | +| `OilTemp` | `float` | ยฐC | Engine oil temperature | +| `OilPress` | `float` | bar | Engine oil pressure | +| `Voltage` | `float` | V | Electrical system voltage | +| `ManifoldPress` | `float` | bar | Intake manifold pressure | + +### ๐Ÿ† Session & Race Information +| Variable | Type | Units | Description | +|----------|------|-------|-------------| +| `SessionTime` | `double` | s | Current session time | +| `SessionTimeRemain` | `double` | s | Time remaining in session | +| `SessionNum` | `int` | - | Current session number | +| `SessionState` | `int` | enum | Session state (practice, qualifying, race) | +| `SessionLapsRemain` | `int` | - | Laps remaining (if applicable) | +| `SessionLapsTotal` | `int` | - | Total laps in session | + +### ๐ŸŒก๏ธ Environment & Track Conditions +| Variable | Type | Units | Description | +|----------|------|-------|-------------| +| `AirTemp` | `float` | ยฐC | Ambient air temperature | +| `TrackTemp` | `float` | ยฐC | Track surface temperature | +| `RelativeHumidity` | `float` | % | Relative humidity | +| `WindVel` | `float` | m/s | Wind speed | +| `WindDir` | `float` | rad | Wind direction | +| `TrackWetness` | `int` | enum | Track wetness level | + +### ๐Ÿšฆ Multi-Car Data (Arrays) +| Variable | Type | Description | +|----------|------|-------------| +| `CarIdxLapDistPct` | `float[]` | Lap distance for each car | +| `CarIdxPosition` | `int[]` | Race position for each car | +| `CarIdxClassPosition` | `int[]` | Class position for each car | +| `CarIdxF2Time` | `float[]` | Time behind leader for each car | +| `CarIdxOnPitRoad` | `bool[]` | Pit road status for each car | + +To discover all available variables, use: + +```csharp +var variables = client.GetTelemetryVariables(); +foreach (var variable in variables.OrderBy(v => v.Name)) +{ + Console.WriteLine($"{variable.Name}: {variable.Desc} ({variable.Units})"); +} +``` + +## โš ๏ธ Critical Anti-Patterns & Common Pitfalls + +### โŒ DO NOT: Manually Create TelemetryData Struct +```csharp +// WRONG - This will cause compilation errors +public struct TelemetryData +{ + public float Speed { get; set; } + public float RPM { get; set; } +} +``` +**Why**: The `TelemetryData` struct is generated by the source generator based on the `[RequiredTelemetryVars]` attribute. + +### โŒ DO NOT: Use String-Based Variable Names (v1.0+) +```csharp +// WRONG - v1.0+ uses enums, not strings +[RequiredTelemetryVars(["Speed", "RPM", "Gear"])] +public class Program { } +``` +**Correct v1.0+ syntax**: +```csharp +[RequiredTelemetryVars([TelemetryVar.Speed, TelemetryVar.RPM, TelemetryVar.Gear])] +public class Program { } +``` + +### โŒ DO NOT: Use TelemetryClient Without Generic Parameter +```csharp +// WRONG - Missing generic type parameter +var client = TelemetryClient.Create(logger); +``` +**Correct**: +```csharp +ITelemetryClient client = TelemetryClient.Create(logger); +``` + +### โŒ DO NOT: Perform Heavy Operations in Channel Readers +```csharp +// WRONG - Blocking channel consumption +await foreach (var data in client.TelemetryData.WithCancellation(cancellationToken)) +{ + Thread.Sleep(100); // Blocks stream consumption + await SaveToDatabase(data); // Slow I/O operations + ComplexCalculation(); // CPU-intensive work +} +``` +**Why**: Telemetry arrives at 60Hz. Heavy operations can cause buffer overflow and data loss. + +**Correct**: +```csharp +// Option 1: Queue for background processing +var dataQueue = new ConcurrentQueue(); +await foreach (var data in client.TelemetryData.WithCancellation(cancellationToken)) +{ + dataQueue.Enqueue(data); +} + +// Option 2: Use separate task for processing +await foreach (var data in client.TelemetryData.WithCancellation(cancellationToken)) +{ + _ = Task.Run(() => ProcessDataAsync(data)); // Fire and forget +} +``` + +### โŒ DO NOT: Forget Resource Disposal +```csharp +// WRONG - Memory leaks +var client = TelemetryClient.Create(logger); +// Client never disposed +``` +**Correct**: +```csharp +await using var client = TelemetryClient.Create(logger); +// or +await client.DisposeAsync(); +``` + +### โŒ DO NOT: Access Non-Declared Variables +```csharp +[RequiredTelemetryVars([TelemetryVar.Speed, TelemetryVar.RPM])] +public class Program +{ + client.OnTelemetryUpdate += (sender, data) => + { + var gear = data.Gear; // COMPILATION ERROR - Gear not declared + }; +} +``` + +### โŒ DO NOT: Ignore Nullable Properties (v1.0+) +```csharp +// WRONG - Will cause compilation errors with bool? +if (data.IsOnTrackCar) { } // Cannot convert bool? to bool + +// WRONG - May cause unexpected behavior +var speed = data.Speed; // speed is float?, not float +Console.WriteLine($"Speed: {speed:F1}"); // May not format as expected +``` +**Correct approaches**: +```csharp +// โœ… Explicit boolean comparison +if (data.IsOnTrackCar == true) { } + +// โœ… Handle nullable formatting +Console.WriteLine($"Speed: {data.Speed?.ToString("F1") ?? "N/A"}"); + +// โœ… Use GetValueOrDefault() only when zero is meaningful +var speed = data.Speed.GetValueOrDefault(); // Use sparingly +``` + +### โŒ DO NOT: Use IBT Files on Non-Existent Paths +```csharp +// WRONG - Will throw FileNotFoundException immediately +var ibtOptions = new IBTOptions("nonexistent.ibt"); +var client = TelemetryClient.Create(logger, ibtOptions); +``` + +### โŒ DO NOT: Use Blocking Calls in Async Context +```csharp +// WRONG - Blocking async context +await Task.Run(() => +{ + client.Monitor(cancellationToken).Wait(); // Blocks thread pool thread +}); +``` +**Correct**: +```csharp +await client.Monitor(cancellationToken); +``` + +## Performance Considerations + +1. **Throttle Output**: Telemetry data arrives at 60 Hz. Consider throttling console output or file writes +2. **Memory Usage**: For long-running applications, be mindful of data collection growth +3. **Stream Consumption**: Keep stream readers lightweight to avoid overflow and data loss +4. **IBT Playback**: Large IBT files can consume significant memory during processing +5. **Buffer Capacity**: 60-sample ring buffer (1 second at 60Hz) prevents memory issues but drops oldest data if consumption is too slow + +## Integration Patterns & Data Flow + +### Database Integration Pattern (Stream-Based) +```csharp +[RequiredTelemetryVars([TelemetryVar.Speed, TelemetryVar.RPM, TelemetryVar.Gear, TelemetryVar.LapDistPct, TelemetryVar.SessionTime])] +public class DatabaseLogger +{ + private readonly ConcurrentQueue _dataQueue = new(); + private readonly CancellationTokenSource _backgroundCts = new(); + + public async Task StartAsync(CancellationToken cancellationToken) + { + await using var client = TelemetryClient.Create(logger); + + // Background database writer + var writerTask = Task.Run(async () => + { + while (!_backgroundCts.Token.IsCancellationRequested) + { + if (_dataQueue.TryDequeue(out var data)) + { + await SaveToDatabase(data); + } + await Task.Delay(10); // Prevent busy waiting + } + }); + + // Stream consumer that queues data for background processing + var consumerTask = Task.Run(async () => + { + await foreach (var data in client.TelemetryData.WithCancellation(cancellationToken)) + { + _dataQueue.Enqueue(data); + } + }, cancellationToken); + + var monitorTask = client.Monitor(cancellationToken); + + await Task.WhenAny(monitorTask, consumerTask); + _backgroundCts.Cancel(); + await writerTask; + } +} +``` + +### Real-Time Dashboard Pattern (Stream-Based) +```csharp +[RequiredTelemetryVars([TelemetryVar.Speed, TelemetryVar.RPM, TelemetryVar.Gear, TelemetryVar.Throttle, TelemetryVar.Brake])] +public class DashboardService +{ + private TelemetryData _latestData; + private readonly Timer _updateTimer; + + public event Action OnDashboardUpdate; + + public DashboardService() + { + // Update dashboard at 10Hz (lower than telemetry rate) + _updateTimer = new Timer(SendDashboardUpdate, null, 0, 100); + } + + public async Task StartTelemetry(CancellationToken cancellationToken) + { + await using var client = TelemetryClient.Create(logger); + + // Start telemetry consumption task + var telemetryTask = Task.Run(async () => + { + await foreach (var data in client.TelemetryData.WithCancellation(cancellationToken)) + { + _latestData = data; // Just store latest, don't process here + } + }, cancellationToken); + + var monitorTask = client.Monitor(cancellationToken); + + await Task.WhenAny(monitorTask, telemetryTask); + } + + private void SendDashboardUpdate(object state) + { + var dashData = new DashboardData + { + SpeedMph = _latestData.Speed * 2.23694f, + RPM = _latestData.RPM, + Gear = _latestData.Gear, + ThrottlePercent = _latestData.Throttle * 100f, + BrakePercent = _latestData.Brake * 100f + }; + OnDashboardUpdate?.Invoke(dashData); + } +} +``` + + +## Source Generation Details & Constraints + +### How Source Generation Works +The SDK uses Roslyn source generators to create the `TelemetryData` struct at compile time: + +1. **Attribute Processing**: The `[RequiredTelemetryVars]` attribute is processed during compilation +2. **Code Generation**: A struct is generated with properties matching the specified variable names +3. **Type Safety**: The generated struct provides compile-time type checking and IntelliSense + +### Generated Code Structure +Given this attribute: +```csharp +[RequiredTelemetryVars([TelemetryVar.Speed, TelemetryVar.RPM, TelemetryVar.Gear, TelemetryVar.IsOnTrack])] +``` + +The source generator creates: +```csharp +// Generated automatically - DO NOT MODIFY +public readonly struct TelemetryData +{ + public readonly float Speed; + public readonly float RPM; + public readonly int Gear; + public readonly bool IsOnTrack; + + public TelemetryData(float speed, float rpm, int gear, bool isOnTrack) + { + Speed = speed; + RPM = rpm; + Gear = gear; + IsOnTrack = isOnTrack; + } +} +``` + +### Variable Name Resolution +- **Enum-Based**: Use `TelemetryVar` enum values instead of strings for type safety +- **Exact Matching**: Enum values correspond exactly to iRacing's internal variable names +- **Type Inference**: Types are determined from iRacing's variable definitions +- **Array Support**: Array variables like `TelemetryVar.CarIdxLapDistPct` become `float[]` properties + +### Compilation Requirements +- **Build Order**: Source generation happens during compilation, before your code is compiled +- **Clean Builds**: Sometimes required after changing `[RequiredTelemetryVars]` attributes +- **IDE Support**: Modern IDEs show generated code in "Dependencies > Analyzers" + +### Source Generator Constraints (v1.0+) +```csharp +// โœ… VALID: TelemetryVar enum array literals +[RequiredTelemetryVars([TelemetryVar.Speed, TelemetryVar.RPM, TelemetryVar.Gear])] + +// โœ… VALID: Constant enum arrays +private static readonly TelemetryVar[] REQUIRED_VARS = [TelemetryVar.Speed, TelemetryVar.RPM]; +[RequiredTelemetryVars(REQUIRED_VARS)] + +// โŒ INVALID: String arrays (pre-v1.0 syntax) +[RequiredTelemetryVars(["Speed", "RPM"])] // Compilation error in v1.0+ + +// โŒ INVALID: Runtime-determined arrays +[RequiredTelemetryVars(GetVariablesFromConfig())] // Compilation error + +// โŒ INVALID: Variables from other assemblies +[RequiredTelemetryVars(ExternalClass.Variables)] // May not work +``` + +### Multiple Attribute Support +```csharp +// Each class can have its own set of variables +[RequiredTelemetryVars([TelemetryVar.Speed, TelemetryVar.RPM])] +public class BasicMonitor { } + +[RequiredTelemetryVars([TelemetryVar.Speed, TelemetryVar.RPM, TelemetryVar.Gear, TelemetryVar.Throttle, TelemetryVar.Brake])] +public class DetailedMonitor { } + +// Different TelemetryData structs are generated for each +``` + + +### Debugging Generated Code +View generated code in your IDE: +1. **Visual Studio**: Solution Explorer > Dependencies > Analyzers > SVappsLAB.iRacingTelemetrySDK.CodeGen +2. **VS Code**: Use "Go to Definition" on `TelemetryData` +3. **Build Output**: Check `obj/Generated/` folder + +--- + +## ๐Ÿค– AI Agent v1.0 Quick Reference + +### Essential v1.0 Patterns for AI Code Generation + +**โœ… Correct v1.0 Client Creation:** +```csharp +// Live telemetry +await using var client = TelemetryClient.Create(logger); + +// IBT playback +var ibtOptions = new IBTOptions(@"C:\path\to\file.ibt", speedMultiplier: 10); +await using var client = TelemetryClient.Create(logger, ibtOptions); + +// With ClientOptions (requires BOTH ibtOptions and clientOptions) +var clientOptions = new ClientOptions { MeterFactory = meterFactory }; +await using var client = TelemetryClient.Create(logger, ibtOptions, clientOptions); +``` + +**โœ… Correct v1.0 Variable Declaration:** +```csharp +[RequiredTelemetryVars([TelemetryVar.Speed, TelemetryVar.RPM, TelemetryVar.Gear])] +public class Program { /* AI generates this */ } +``` + +**โœ… Correct v1.0 Data Consumption (Extension Method):** +```csharp +// All delegates are OPTIONAL - only provide what you need +var subscriptionTask = client.SubscribeToAllStreams( + onTelemetryUpdate: async data => { /* handle data - T type */ }, + onSessionInfoUpdate: async session => { /* handle session - TelemetrySessionInfo */ }, + onConnectStateChanged: async state => { /* handle connection - ConnectState enum */ }, + onError: async error => { /* handle errors - Exception */ }, + cancellationToken: cts.Token +); + +var monitorTask = client.Monitor(cts.Token); + +await Task.WhenAny(monitorTask, subscriptionTask); +``` + +**โœ… Correct v1.0 Data Consumption (Direct Channels):** +```csharp +var telemetryTask = Task.Run(async () => +{ + await foreach (var data in client.TelemetryData.WithCancellation(cts.Token)) + { + // Process telemetry + } +}, cts.Token); + +var monitorTask = client.Monitor(cts.Token); + +await Task.WhenAny(monitorTask, telemetryTask); +``` + +**โœ… Correct v1.0 Nullable Handling:** +```csharp +// Display with fallback +var display = $"Speed: {data.Speed?.ToString("F1") ?? "N/A"}"; + +// Boolean checks +if (data.IsOnTrackCar == true) { /* explicit true check */ } + +// Safe arithmetic +var speedMph = data.Speed * 2.23694f; // Preserves null semantics +``` + +**โœ… Correct v1.0 Synchronous Methods:** +```csharp +// v1.0: GetTelemetryVariables() is synchronous (no await), IsConnected is a property +var variables = client.GetTelemetryVariables(); +var isConnected = client.IsConnected; +``` + +### โŒ Anti-Patterns to NEVER Generate + +```csharp +// โŒ v0.x event-based patterns (compilation errors in v1.0) +client.OnTelemetryUpdate += (sender, data) => { }; + +// โŒ String-based variables (compilation errors in v1.0) +[RequiredTelemetryVars(["Speed", "RPM"])] + +// โŒ Direct .Value access (runtime errors) +var speed = data.Speed.Value; + +// โŒ Implicit bool conversion (compilation errors) +if (data.IsOnTrackCar) { } + +// โŒ Synchronous disposal (v1.0 requires async) +using var client = TelemetryClient.Create(logger); + +// โŒ Await on synchronous methods (unnecessary in v1.0) +var variables = await client.GetTelemetryVariables(); +``` + +### AI Decision Framework + +1. **Always use `TelemetryVar` enums** instead of strings for variable declaration +2. **Always use `await using`** for client disposal +3. **Always handle nullable properties** with null-conditional operators or explicit checks +4. **Choose Extension Method approach** unless maximum performance is explicitly required +5. **Always use `SubscribeToAllStreams`** method name (not `SubscribeToAllStreamsAsync`) +6. **Never await `GetTelemetryVariables()`** - it's synchronous in v1.0 + +### Variable Validation +The source generator validates variable names at compile time: +```csharp +// โœ… Valid iRacing variable +[RequiredTelemetryVars([TelemetryVar.Speed])] // Compiles successfully + +// โŒ Invalid enum value would not compile +// [RequiredTelemetryVars([TelemetryVar.InvalidVar])] // Compilation error +``` + +## Threading and Async Patterns + +The ITelemetryClient is designed for async/await usage with async data streaming: + +```csharp +// Proper async pattern with channels +var monitorTask = client.Monitor(cancellationToken); + +var telemetryConsumer = Task.Run(async () => +{ + await foreach (var data in client.TelemetryData.WithCancellation(cancellationToken)) + { + // Process telemetry + } +}); + +await Task.WhenAny(monitorTask, telemetryConsumer); +``` + +## Data Streaming Architecture Details + +### Stream Types and Behavior +- **TelemetryData**: High-frequency (60Hz) 60-sample ring buffer with drop-oldest behavior (1 second buffering) +- **SessionData**: Low-frequency 60-sample ring buffer with drop-oldest behavior +- **SessionDataYaml**: Low-frequency 60-sample ring buffer with drop-oldest behavior +- **ConnectStates**: 60-sample ring buffer for connection state changes +- **Errors**: 60-sample ring buffer for error notifications + +All channels use FIFO semantics with destructive reads. When buffer fills, oldest unread items are automatically dropped. + +### Stream Consumption Patterns +```csharp +// Pattern 1: Consume all items as they arrive +await foreach (var data in client.TelemetryData.WithCancellation(cancellationToken)) +{ + ProcessData(data); +} + +// Pattern 2: Consume with timeout and selective processing +var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); +timeout.CancelAfter(TimeSpan.FromSeconds(5)); + +await foreach (var data in client.TelemetryData.WithCancellation(timeout.Token)) +{ + if (ShouldProcess(data)) + ProcessData(data); +} + +// Pattern 3: Multiple stream coordination +var tasks = new[] +{ + ConsumeStream(client.TelemetryData, ProcessTelemetry), + ConsumeStream(client.SessionData, ProcessSession), + ConsumeStream(client.Errors, ProcessError) +}; + +await Task.WhenAll(tasks); +``` + +## License + +This SDK is licensed under the Apache License, Version 2.0. See the LICENSE file for details. diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/irSDK_defines.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK/irSDK_defines.cs index 30dd479..519e8a7 100644 --- a/Sdk/SVappsLAB.iRacingTelemetrySDK/irSDK_defines.cs +++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/irSDK_defines.cs @@ -11,7 +11,7 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; + * limitations under the License. **/ using System; @@ -29,13 +29,16 @@ internal static class Constants internal enum irsdk_VarType : Int32 { + // 1 byte irsdk_char = 0, irsdk_bool, + // 4 bytes irsdk_int, irsdk_bitField, irsdk_float, + // 8 bytes irsdk_double, irsdk_ETCount diff --git a/Sdk/tests/Directory.Build.props b/Sdk/tests/Directory.Build.props new file mode 100644 index 0000000..86bfc83 --- /dev/null +++ b/Sdk/tests/Directory.Build.props @@ -0,0 +1,6 @@ + + + true + true + + diff --git a/Sdk/tests/IBT_Tests/Files/ReadFile.cs b/Sdk/tests/IBT_Tests/Files/ReadFile.cs deleted file mode 100644 index 961f590..0000000 --- a/Sdk/tests/IBT_Tests/Files/ReadFile.cs +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Copyright (C) 2024-2025 Scott Velez - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; -**/ - -using Microsoft.Extensions.Logging.Abstractions; -using SVappsLAB.iRacingTelemetrySDK; - -namespace IBT_Tests.Files -{ - public class ReadFile - { - CancellationTokenSource cts = new CancellationTokenSource(); - - // TODO: convert these to use xunit raised event assertions - [Fact] - public async Task ValidFileSucceeds() - { - var ibtFile = @"../../../data/race_test/lamborghinievogt3_spa up.ibt"; - - using var tc = TelemetryClient.Create(NullLogger.Instance, new IBTOptions(ibtFile, int.MaxValue)); - - var gotConnected = false; - EventHandler handler = (_sender, e) => - { - if (e.State == ConnectState.Connected) - { - gotConnected = true; - } - }; - - tc.OnConnectStateChanged += handler; - await tc.Monitor(cts.Token); - tc.OnConnectStateChanged -= handler; - - Assert.True(gotConnected); - } - - [Fact] - public void InvalidFileThrows() - { - Assert.Throws(() => - { - var ibtFile = @"no-such-file-name"; - using var tc = TelemetryClient.Create(NullLogger.Instance, new IBTOptions(ibtFile)); - }); - } - } -} diff --git a/Sdk/tests/IBT_Tests/Fixture.cs b/Sdk/tests/IBT_Tests/Fixture.cs deleted file mode 100644 index 55fe9c8..0000000 --- a/Sdk/tests/IBT_Tests/Fixture.cs +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Copyright (C) 2024-2025 Scott Velez - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; -**/ - -using Microsoft.Extensions.Logging.Abstractions; -using SVappsLAB.iRacingTelemetrySDK; -using SVappsLAB.iRacingTelemetrySDK.Models; - -namespace IBT_Tests -{ - [RequiredTelemetryVars(["IsOnTrackCar", "SessionTick", "EngineWarnings", "rpm", "SessionTimeRemain"])] - public class Fixture : IAsyncLifetime - { - const int TIMEOUT_SECS = 5; - public TelemetrySessionInfo TelemetrySessionInfo { get; private set; } = default!; - public ITelemetryClient TelemetryClient = default!; - private string _ibtPath; - - public Fixture(string ibtPath) - { - _ibtPath = ibtPath; - } - - public async ValueTask InitializeAsync() - { - var delayTs = new CancellationTokenSource(TimeSpan.FromSeconds(TIMEOUT_SECS)); - - // cancel when either the test context is cancelled or after 5 seconds - var cts = CancellationTokenSource.CreateLinkedTokenSource( - TestContext.Current.CancellationToken, - delayTs.Token - ); - - TelemetryClient = TelemetryClient.Create(NullLogger.Instance, new IBTOptions(_ibtPath)); - TelemetryClient.OnSessionInfoUpdate += (object? sender, TelemetrySessionInfo si) => - { - TelemetrySessionInfo = si; - - // now that we have the sessionInfo, we can cancel monitoring - cts.Cancel(); - }; - - await TelemetryClient.Monitor(cts.Token); - - // check if the timeout cancellation was requested - if (delayTs.IsCancellationRequested) - { - throw new Exception("timeout. unable to read session. cancelling the monitoring"); - } - } - - public ValueTask DisposeAsync() - { - TelemetryClient.Dispose(); - - return ValueTask.CompletedTask; - } - } - -} diff --git a/Sdk/tests/IBT_Tests/IBT_Tests.csproj b/Sdk/tests/IBT_Tests/IBT_Tests.csproj deleted file mode 100644 index 5be43ef..0000000 --- a/Sdk/tests/IBT_Tests/IBT_Tests.csproj +++ /dev/null @@ -1,46 +0,0 @@ -๏ปฟ - - - Exe - net8.0 - enable - enable - - false - true - true - - - - - - - - - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - - diff --git a/Sdk/tests/IBT_Tests/SessionInfo/SessionInfoTests.cs b/Sdk/tests/IBT_Tests/SessionInfo/SessionInfoTests.cs deleted file mode 100644 index 33c2df5..0000000 --- a/Sdk/tests/IBT_Tests/SessionInfo/SessionInfoTests.cs +++ /dev/null @@ -1,161 +0,0 @@ -/** - * Copyright (C) 2024-2025 Scott Velez - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; -**/ - -namespace IBT_Tests.SessionInfo -{ - public class TestSession_Fixture : Fixture - { - public TestSession_Fixture() : base(Path.Combine(Directory.GetCurrentDirectory(), @"data/race_test/lamborghinievogt3_spa up.ibt")) - { - } - } - public class RoadRaceSession_Fixture : Fixture - { - public RoadRaceSession_Fixture() : base(Path.Combine(Directory.GetCurrentDirectory(), @"data/race_road/audir8lmsevo2gt3_spa up.ibt")) - { - } - } - public class OvalRaceSession_Fixture : Fixture - { - public OvalRaceSession_Fixture() : base(Path.Combine(Directory.GetCurrentDirectory(), @"data/race_oval/latemodel_southboston.ibt")) - { - } - } - - // tests common to all IBT fixtures - public abstract class SessionInfoTestBase - { - protected Fixture _fixture; - public SessionInfoTestBase(Fixture fixture) - { - _fixture = fixture; - } - public object? GetActualSessionValue(string key) - { - var si = _fixture.TelemetrySessionInfo; - - var val = key switch - { - // WeekendInfo - "WeekendInfo.TrackName" => si.WeekendInfo.TrackName, - "WeekendInfo.TrackID" => si.WeekendInfo.TrackID, - "WeekendInfo.EventType" => si.WeekendInfo.EventType, - "WeekendInfo.Category" => si.WeekendInfo.Category, - // WeekendInfo.WeekendOptions - "WeekendInfo.WeekendOptions.NumStarters" => si.WeekendInfo.WeekendOptions.NumStarters, - // WeekendInfo.TelemetryOptions - "WeekendInfo.TelemetryOptions.TelemetryDiskFile" => si.WeekendInfo.TelemetryOptions.TelemetryDiskFile, - // SessionInfo - "SessionInfo.SessionsCount" => si.SessionInfo.Sessions.Count, - "SessionInfo.SessionType" => si.SessionInfo.Sessions[0].SessionType, - // CameraInfo - "CameraInfo.GroupsCount" => si.CameraInfo.Groups.Count, - // RadioInfo - "RadioInfo.RadiosCount" => si.RadioInfo.Radios.Count, - "RadioInfo.NumFrequencies" => si.RadioInfo.Radios[0].NumFrequencies, - // DriverInfo - "DriverInfo.DriverCarIdx" => si.DriverInfo.DriverCarIdx, - "DriverInfo.DriverCarSLShiftRPM" => si.DriverInfo.DriverCarSLShiftRPM, - "DriverInfo.CarNumber" => si.DriverInfo.Drivers[0].CarNumber, - // SplitTimeInfo - "SplitTimeInfo.SectorStartPct" => si.SplitTimeInfo.Sectors[1].SectorStartPct, - // CarSetup - "CarSetup.UpdateCount" => si.CarSetup["UpdateCount"], - _ => throw new NotImplementedException() - }; - return val; - } - public virtual void VerifySessionValue(string key, object expected) - { - var actualVal = GetActualSessionValue(key); - - Assert.Equal(expected, actualVal); - } - - - - } - - public class SessionInfo_Test : SessionInfoTestBase, IClassFixture - { - public SessionInfo_Test(TestSession_Fixture fixture) : base(fixture) { } - - [Theory] - [InlineData("WeekendInfo.TrackName", "spa up")] - [InlineData("WeekendInfo.TrackID", 163)] - [InlineData("WeekendInfo.EventType", "Test")] - [InlineData("WeekendInfo.Category", "Road")] - [InlineData("WeekendInfo.WeekendOptions.NumStarters", 0)] - [InlineData("WeekendInfo.TelemetryOptions.TelemetryDiskFile", "")] - [InlineData("SessionInfo.SessionsCount", 1)] - [InlineData("SessionInfo.SessionType", "Offline Testing")] - [InlineData("CameraInfo.GroupsCount", 22)] - [InlineData("RadioInfo.RadiosCount", 1)] - [InlineData("RadioInfo.NumFrequencies", 7)] - [InlineData("DriverInfo.DriverCarIdx", 0)] - [InlineData("DriverInfo.DriverCarSLShiftRPM", 8050.000f)] - [InlineData("DriverInfo.CarNumber", "52")] - [InlineData("SplitTimeInfo.SectorStartPct", 0.310613f)] - [InlineData("CarSetup.UpdateCount", "1")] - public override void VerifySessionValue(string key, object expected) => base.VerifySessionValue(key, expected); - } - public class SessionInfo_Race_Road : SessionInfoTestBase, IClassFixture - { - public SessionInfo_Race_Road(RoadRaceSession_Fixture fixture) : base(fixture) { } - - [Theory] - [InlineData("WeekendInfo.TrackName", "spa up")] - [InlineData("WeekendInfo.TrackID", 163)] - [InlineData("WeekendInfo.EventType", "Race")] - [InlineData("WeekendInfo.Category", "Road")] - [InlineData("WeekendInfo.WeekendOptions.NumStarters", 60)] - [InlineData("WeekendInfo.TelemetryOptions.TelemetryDiskFile", "")] - [InlineData("SessionInfo.SessionsCount", 4)] - [InlineData("SessionInfo.SessionType", "Practice")] - [InlineData("CameraInfo.GroupsCount", 22)] - [InlineData("RadioInfo.RadiosCount", 1)] - [InlineData("RadioInfo.NumFrequencies", 6)] - [InlineData("DriverInfo.DriverCarIdx", 10)] - [InlineData("DriverInfo.DriverCarSLShiftRPM", 8050.000f)] - [InlineData("DriverInfo.CarNumber", "0")] - [InlineData("SplitTimeInfo.SectorStartPct", 0.310613f)] - [InlineData("CarSetup.UpdateCount", "10")] - public override void VerifySessionValue(string key, object expected) => base.VerifySessionValue(key, expected); - } - public class SessionInfo_Oval_Road : SessionInfoTestBase, IClassFixture - { - public SessionInfo_Oval_Road(OvalRaceSession_Fixture fixture) : base(fixture) { } - - [Theory] - [InlineData("WeekendInfo.TrackName", "southboston")] - [InlineData("WeekendInfo.TrackID", 14)] - [InlineData("WeekendInfo.EventType", "Race")] - [InlineData("WeekendInfo.Category", "Oval")] - [InlineData("WeekendInfo.WeekendOptions.NumStarters", 20)] - [InlineData("WeekendInfo.TelemetryOptions.TelemetryDiskFile", "")] - [InlineData("SessionInfo.SessionsCount", 3)] - [InlineData("SessionInfo.SessionType", "Practice")] - [InlineData("CameraInfo.GroupsCount", 20)] - [InlineData("RadioInfo.RadiosCount", 1)] - [InlineData("RadioInfo.NumFrequencies", 7)] - [InlineData("DriverInfo.DriverCarIdx", 1)] - [InlineData("DriverInfo.DriverCarSLShiftRPM", 7050.000f)] - [InlineData("DriverInfo.CarNumber", "0")] - [InlineData("SplitTimeInfo.SectorStartPct", 0.500000f)] - [InlineData("CarSetup.UpdateCount", "1")] - public override void VerifySessionValue(string key, object expected) => base.VerifySessionValue(key, expected); - } -} diff --git a/Sdk/tests/IBT_Tests/Variables/TelemetryVariables.cs b/Sdk/tests/IBT_Tests/Variables/TelemetryVariables.cs deleted file mode 100644 index 7700e60..0000000 --- a/Sdk/tests/IBT_Tests/Variables/TelemetryVariables.cs +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright (C) 2024-2025 Scott Velez - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; -**/ - -using IBT_Tests.SessionInfo; - -namespace IBT_Tests.Variables -{ - public class TelemetryVariables : IClassFixture - { - private readonly TestSession_Fixture _fixture; - - - public TelemetryVariables(TestSession_Fixture fixture) - { - _fixture = fixture; - } - - [Theory] - [InlineData(true, "IsOnTrackCar", "Car on track", typeof(bool), false)] - [InlineData(true, "SessionTick", "Current update number", typeof(int), false)] - [InlineData(true, "SessionTime", "Seconds since session start", typeof(double), false)] - [InlineData(true, "EngineWarnings", "Bitfield for warning lights", typeof(uint), false)] - [InlineData(true, "RPM", "Engine rpm", typeof(float), false)] - [InlineData(true, "Lat", "Latitude in decimal degrees", typeof(double), false)] - [InlineData(false, "no_such_var", "", typeof(object), false)] - public async Task Variables(bool validVar, string varName, string desc, Type type, bool isTimeValue) - { - var vars = await _fixture.TelemetryClient.GetTelemetryVariables(); - - var v = vars.FirstOrDefault(v => v.Name == varName); - if (validVar) - { - Assert.NotNull(v); - Assert.Contains(desc, v.Desc); - Assert.Equal(type, v.Type); - Assert.Equal(isTimeValue, v.IsTimeValue); - } - else - { - Assert.Null(v); - } - } - } -} diff --git a/Sdk/tests/IBT_Tests/data/race_oval/latemodel_southboston.ibt b/Sdk/tests/IBT_Tests/data/race_oval/latemodel_southboston.ibt deleted file mode 100644 index 24f13ef..0000000 --- a/Sdk/tests/IBT_Tests/data/race_oval/latemodel_southboston.ibt +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:031f8f315528f3966e9e3d2c07e4c41a42aa72b53d789fdde59be6575b982129 -size 7530020 diff --git a/Sdk/tests/IBT_Tests/data/race_oval/latemodel_southboston.yaml b/Sdk/tests/IBT_Tests/data/race_oval/latemodel_southboston.yaml deleted file mode 100644 index b80550b..0000000 --- a/Sdk/tests/IBT_Tests/data/race_oval/latemodel_southboston.yaml +++ /dev/null @@ -1,546 +0,0 @@ -WeekendInfo: - TrackName: southboston - TrackID: 14 - TrackLength: 0.59 km - TrackDisplayName: South Boston Speedway - TrackDisplayShortName: South Boston - TrackConfigName: - TrackCity: South Boston - TrackCountry: USA - TrackAltitude: 126.43 m - TrackLatitude: 36.709879 m - TrackLongitude: -78.869115 m - TrackNorthOffset: 4.8900 rad - TrackNumTurns: 4 - TrackPitSpeedLimit: 55.98 kph - TrackType: short oval - TrackDirection: left - TrackWeatherType: Generated / Dynamic Sky - TrackSkies: Clear - TrackSurfaceTemp: 29.37 C - TrackAirTemp: 29.37 C - TrackAirPressure: 29.47 Hg - TrackWindVel: 0.59 m/s - TrackWindDir: 4.41 rad - TrackRelativeHumidity: 50 % - TrackFogLevel: 0 % - TrackCleanup: 0 - TrackDynamicTrack: 1 - SeriesID: 0 - SeasonID: 0 - SessionID: 115222477 - SubSessionID: 27957442 - LeagueID: 1689 - Official: 0 - RaceWeek: 0 - EventType: Race - Category: Oval - SimMode: full - TeamRacing: 0 - MinDrivers: 0 - MaxDrivers: 1 - DCRuleSet: None - QualifierMustStartRace: 0 - NumCarClasses: 1 - NumCarTypes: 2 - HeatRacing: 0 - WeekendOptions: - NumStarters: 20 - StartingGrid: single file - QualifyScoring: best lap - CourseCautions: full - StandingStart: 0 - Restarts: double file lapped cars behind - WeatherType: Generated / Dynamic Sky - Skies: Partly Cloudy - WindDirection: N - WindSpeed: 3.22 km/h - WeatherTemp: 25.56 C - RelativeHumidity: 55 % - FogLevel: 0 % - TimeOfDay: 9:20 pm - Date: 2019-05-15 - EarthRotationSpeedupFactor: 1 - Unofficial: 1 - CommercialMode: consumer - NightMode: variable - IsFixedSetup: 1 - StrictLapsChecking: default - HasOpenRegistration: 1 - HardcoreLevel: 3 - NumJokerLaps: 0 - IncidentLimit: unlimited - TelemetryOptions: - TelemetryDiskFile: "" - -SessionInfo: - Sessions: - - SessionNum: 0 - SessionLaps: unlimited - SessionTime: 5400.0000 sec - SessionNumLapsToAvg: 0 - SessionType: Practice - SessionTrackRubberState: moderately high usage - SessionName: PRACTICE - SessionSubType: - SessionSkipped: 0 - SessionRunGroupsUsed: 0 - ResultsPositions: - ResultsFastestLap: - - CarIdx: 255 - FastestLap: 0 - FastestTime: -1.0000 - ResultsAverageLapTime: -1.0000 - ResultsNumCautionFlags: 0 - ResultsNumCautionLaps: 0 - ResultsNumLeadChanges: 0 - ResultsLapsComplete: -1 - ResultsOfficial: 0 - - SessionNum: 1 - SessionLaps: 1 - SessionTime: 300.0000 sec - SessionNumLapsToAvg: 0 - SessionType: Lone Qualify - SessionTrackRubberState: carry over - SessionName: QUALIFY - SessionSubType: - SessionSkipped: 0 - SessionRunGroupsUsed: 0 - ResultsPositions: - ResultsFastestLap: - - CarIdx: 255 - FastestLap: 0 - FastestTime: -1.0000 - ResultsAverageLapTime: -1.0000 - ResultsNumCautionFlags: 0 - ResultsNumCautionLaps: 0 - ResultsNumLeadChanges: 0 - ResultsLapsComplete: -1 - ResultsOfficial: 0 - - SessionNum: 2 - SessionLaps: unlimited - SessionTime: 1200.0000 sec - SessionNumLapsToAvg: 0 - SessionType: Race - SessionTrackRubberState: carry over - SessionName: RACE - SessionSubType: - SessionSkipped: 0 - SessionRunGroupsUsed: 0 - ResultsPositions: - ResultsFastestLap: - - CarIdx: 255 - FastestLap: 0 - FastestTime: -1.0000 - ResultsAverageLapTime: -1.0000 - ResultsNumCautionFlags: 0 - ResultsNumCautionLaps: 0 - ResultsNumLeadChanges: 0 - ResultsLapsComplete: -1 - ResultsOfficial: 0 - -CameraInfo: - Groups: - - GroupNum: 1 - GroupName: Nose - Cameras: - - CameraNum: 1 - CameraName: CamNose - - GroupNum: 2 - GroupName: Gearbox - Cameras: - - CameraNum: 1 - CameraName: CamGearbox - - GroupNum: 3 - GroupName: Roll Bar - Cameras: - - CameraNum: 1 - CameraName: CamRoll Bar - - GroupNum: 4 - GroupName: LF Susp - Cameras: - - CameraNum: 1 - CameraName: CamLF Susp - - GroupNum: 5 - GroupName: LR Susp - Cameras: - - CameraNum: 1 - CameraName: CamLR Susp - - GroupNum: 6 - GroupName: Gyro - Cameras: - - CameraNum: 1 - CameraName: CamGyro - - GroupNum: 7 - GroupName: RF Susp - Cameras: - - CameraNum: 1 - CameraName: CamRF Susp - - GroupNum: 8 - GroupName: RR Susp - Cameras: - - CameraNum: 1 - CameraName: CamRR Susp - - GroupNum: 9 - GroupName: Cockpit - Cameras: - - CameraNum: 1 - CameraName: CamCockpit - - GroupNum: 10 - GroupName: Scenic - IsScenic: true - Cameras: - - CameraNum: 1 - CameraName: Scenic_00 - - CameraNum: 2 - CameraName: Scenic_01 - - CameraNum: 3 - CameraName: Scenic_02 - - CameraNum: 4 - CameraName: Scenic_03 - - CameraNum: 5 - CameraName: Scenic_04 - - CameraNum: 6 - CameraName: Scenic_05 - - GroupNum: 11 - GroupName: TV1 - Cameras: - - CameraNum: 1 - CameraName: CamTV1_00 - - CameraNum: 2 - CameraName: CamTV1_01 - - CameraNum: 3 - CameraName: CamTV1_02 - - CameraNum: 4 - CameraName: CamTV1_03 - - GroupNum: 12 - GroupName: TV2 - Cameras: - - CameraNum: 1 - CameraName: CamTV2_00 - - CameraNum: 2 - CameraName: CamTV2_01 - - CameraNum: 3 - CameraName: CamTV2_02 - - CameraNum: 4 - CameraName: CamTV2_03 - - CameraNum: 5 - CameraName: CamTV2_04 - - GroupNum: 13 - GroupName: TV3 - Cameras: - - CameraNum: 1 - CameraName: CamTV3_00 - - CameraNum: 2 - CameraName: CamTV3_01 - - GroupNum: 14 - GroupName: Pit Lane - Cameras: - - CameraNum: 1 - CameraName: CamPit Lane - - GroupNum: 15 - GroupName: Pit Lane 2 - Cameras: - - CameraNum: 1 - CameraName: CamPit Lane 2 - - GroupNum: 16 - GroupName: Chopper - Cameras: - - CameraNum: 1 - CameraName: CamChopper - - GroupNum: 17 - GroupName: Blimp - Cameras: - - CameraNum: 1 - CameraName: CamBlimp - - GroupNum: 18 - GroupName: Chase - Cameras: - - CameraNum: 1 - CameraName: CamChase - - GroupNum: 19 - GroupName: Far Chase - Cameras: - - CameraNum: 1 - CameraName: CamFar Chase - - GroupNum: 20 - GroupName: Rear Chase - Cameras: - - CameraNum: 1 - CameraName: CamRear Chase - -RadioInfo: - SelectedRadioNum: 0 - Radios: - - RadioNum: 0 - HopCount: 2 - NumFrequencies: 7 - TunedToFrequencyNum: 0 - ScanningIsOn: 1 - Frequencies: - - FrequencyNum: 0 - FrequencyName: "@ALLTEAMS" - Priority: 12 - CarIdx: -1 - EntryIdx: -1 - ClubID: 0 - CanScan: 1 - CanSquawk: 1 - Muted: 0 - IsMutable: 1 - IsDeletable: 0 - - FrequencyNum: 1 - FrequencyName: "@DRIVERS" - Priority: 15 - CarIdx: -1 - EntryIdx: -1 - ClubID: 0 - CanScan: 1 - CanSquawk: 1 - Muted: 0 - IsMutable: 1 - IsDeletable: 0 - - FrequencyNum: 2 - FrequencyName: "@TEAM" - Priority: 60 - CarIdx: 1 - EntryIdx: -1 - ClubID: 0 - CanScan: 1 - CanSquawk: 1 - Muted: 0 - IsMutable: 0 - IsDeletable: 0 - - FrequencyNum: 3 - FrequencyName: "@CLUB" - Priority: 20 - CarIdx: -1 - EntryIdx: -1 - ClubID: 15 - CanScan: 1 - CanSquawk: 1 - Muted: 0 - IsMutable: 1 - IsDeletable: 0 - - FrequencyNum: 4 - FrequencyName: "@ADMIN" - Priority: 90 - CarIdx: -1 - EntryIdx: -1 - ClubID: 0 - CanScan: 1 - CanSquawk: 1 - Muted: 0 - IsMutable: 0 - IsDeletable: 0 - - FrequencyNum: 5 - FrequencyName: "@RACECONTROL" - Priority: 80 - CarIdx: -1 - EntryIdx: -1 - ClubID: 0 - CanScan: 1 - CanSquawk: 1 - Muted: 0 - IsMutable: 0 - IsDeletable: 0 - - FrequencyNum: 6 - FrequencyName: "@PRIVATE" - Priority: 70 - CarIdx: -1 - EntryIdx: 1 - ClubID: 0 - CanScan: 1 - CanSquawk: 1 - Muted: 0 - IsMutable: 0 - IsDeletable: 0 - -DriverInfo: - DriverCarIdx: 1 - DriverUserID: 22176 - PaceCarIdx: 0 - DriverHeadPosX: -0.059 - DriverHeadPosY: 0.480 - DriverHeadPosZ: 0.544 - DriverCarIdleRPM: 800.000 - DriverCarRedLine: 7300.000 - DriverCarEngCylinderCount: 8 - DriverCarFuelKgPerLtr: 0.750 - DriverCarFuelMaxLtr: 83.867 - DriverCarMaxFuelPct: 0.200 - DriverCarSLFirstRPM: 6150.000 - DriverCarSLShiftRPM: 7050.000 - DriverCarSLLastRPM: 7050.000 - DriverCarSLBlinkRPM: 7300.000 - DriverPitTrkPct: 0.136028 - DriverCarEstLapTime: 15.6967 - DriverSetupName: southboston_night.sto - DriverSetupIsModified: 0 - DriverSetupLoadTypeName: fixed - DriverSetupPassedTech: 1 - DriverIncidentCount: 0 - Drivers: - - CarIdx: 0 - UserName: Pace Car - AbbrevName: - Initials: - UserID: -1 - TeamID: 0 - TeamName: Pace Car - CarNumber: "0" - CarNumberRaw: 0 - CarPath: safety pcfr500s - CarClassID: 11 - CarID: 32 - CarIsPaceCar: 1 - CarIsAI: 0 - CarScreenName: safety pcfr500s - CarScreenNameShort: safety pcfr500s - CarClassShortName: - CarClassRelSpeed: 0 - CarClassLicenseLevel: 0 - CarClassMaxFuelPct: 1.000 % - CarClassWeightPenalty: 0.000 kg - CarClassColor: 0xffffff - IRating: 0 - LicLevel: 1 - LicSubLevel: 0 - LicString: R 0.00 - LicColor: 0xffffff - IsSpectator: 0 - CarDesignStr: 0,ffffff,ffffff,ffffff - HelmetDesignStr: 0,ffffff,ffffff,ffffff - SuitDesignStr: 0,ffffff,ffffff,ffffff - CarNumberDesignStr: 0,0,ffffff,ffffff,ffffff - CarSponsor_1: 0 - CarSponsor_2: 0 - ClubName: -none- - DivisionName: Division 1 - CurDriverIncidentCount: 0 - TeamIncidentCount: 0 - - CarIdx: 1 - UserName: Scott Velez - AbbrevName: Velez, S - Initials: SV - UserID: 22176 - TeamID: 0 - TeamName: Scott Velez - CarNumber: "222" - CarNumberRaw: 222 - CarPath: latemodel - CarClassID: 0 - CarID: 12 - CarIsPaceCar: 0 - CarIsAI: 0 - CarScreenName: Chevrolet Monte Carlo SS - CarScreenNameShort: Chevy SS - CarClassShortName: Hosted All Cars - CarClassRelSpeed: 100 - CarClassLicenseLevel: 1 - CarClassMaxFuelPct: 0.200 % - CarClassWeightPenalty: 0.000 kg - CarClassColor: 0xffffff - IRating: 1848 - LicLevel: 16 - LicSubLevel: 417 - LicString: B 4.17 - LicColor: 0x00c702 - IsSpectator: 0 - CarDesignStr: 0,000000,ffef26,00c702 - HelmetDesignStr: 4,000000,12bfca,fd35d5 - SuitDesignStr: 24,0c06fc,e704c6,00e007 - CarNumberDesignStr: 0,0,ffffff,777777,000000 - CarSponsor_1: 106 - CarSponsor_2: 94 - ClubName: Canada - DivisionName: Division 1 - CurDriverIncidentCount: 0 - TeamIncidentCount: 0 - -SplitTimeInfo: - Sectors: - - SectorNum: 0 - SectorStartPct: 0.000000 - - SectorNum: 1 - SectorStartPct: 0.500000 - -CarSetup: - UpdateCount: 1 - Tires: - LeftFront: - ColdPressure: 138 kPa - LastHotPressure: 138 kPa - LastTempsOMI: 30C, 30C, 30C - TreadRemaining: 100%, 100%, 100% - LeftRear: - ColdPressure: 131 kPa - LastHotPressure: 131 kPa - LastTempsOMI: 30C, 30C, 30C - TreadRemaining: 100%, 100%, 100% - RightFront: - ColdPressure: 145 kPa - LastHotPressure: 145 kPa - LastTempsIMO: 30C, 30C, 30C - TreadRemaining: 100%, 100%, 100% - Stagger: 29 mm - RightRear: - ColdPressure: 145 kPa - LastHotPressure: 145 kPa - LastTempsIMO: 30C, 30C, 30C - TreadRemaining: 100%, 100%, 100% - Stagger: 22 mm - Chassis: - Front: - BallastForward: 0 mm - NoseWeight: 47.3% - CrossWeight: 49.7% - ToeIn: +3 mm - SteeringRatio: 10:1 - SteeringOffset: +4 deg - BrakeBalanceBar: 51.6% - SwayBarSize: 51 mm - SwayBarArmLength: 330 mm - LeftBarEndClearance: 0 mm - AttachLeftSide: 1 - TapeConfiguration: 25% - LeftFront: - CornerWeight: 3714 N - RideHeight: 107 mm - SpringPerchOffset: 119 mm - SpringRate: 53 N/mm - BumpStiffness: +8 clicks - ReboundStiffness: +16 clicks - Camber: +5.5 deg - Caster: +3.6 deg - LeftRear: - CornerWeight: 4045 N - RideHeight: 108 mm - SpringPerchOffset: 131 mm - SpringRate: 57 N/mm - BumpStiffness: +12 clicks - ReboundStiffness: +16 clicks - TrackBarHeight: +279 mm - TruckArmMount: bottom - RightFront: - CornerWeight: 2919 N - RideHeight: 107 mm - SpringPerchOffset: 91 mm - SpringRate: 35 N/mm - BumpStiffness: +10 clicks - ReboundStiffness: +9 clicks - Camber: -3.0 deg - Caster: +3.4 deg - RightRear: - CornerWeight: 3339 N - RideHeight: 108 mm - SpringPerchOffset: 163 mm - SpringRate: 61 N/mm - BumpStiffness: +12 clicks - ReboundStiffness: +12 clicks - TrackBarHeight: +279 mm - TruckArmMount: middle - TruckArmPreload: -9.8 Nm - Rear: - FuelFillTo: 16.6 L - RearEndRatio: 4.98 diff --git a/Sdk/tests/IBT_Tests/data/race_test/lamborghinievogt3_spa up.ibt b/Sdk/tests/IBT_Tests/data/race_test/lamborghinievogt3_spa up.ibt deleted file mode 100644 index 7057c56..0000000 --- a/Sdk/tests/IBT_Tests/data/race_test/lamborghinievogt3_spa up.ibt +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7dc35b87c09efaed85705dd93fcf5490f1c7935744b8ccfef113a9781dcfb06d -size 55634108 diff --git a/Sdk/tests/IBT_Tests/data/race_test/lamborghinievogt3_spa up.yaml b/Sdk/tests/IBT_Tests/data/race_test/lamborghinievogt3_spa up.yaml deleted file mode 100644 index aa8f4e1..0000000 --- a/Sdk/tests/IBT_Tests/data/race_test/lamborghinievogt3_spa up.yaml +++ /dev/null @@ -1,860 +0,0 @@ -๏ปฟ--- -WeekendInfo: - TrackName: spa up - TrackID: 163 - TrackLength: 6.93 km - TrackDisplayName: Circuit de Spa-Francorchamps - TrackDisplayShortName: Spa - TrackConfigName: Grand Prix Pit - TrackCity: Francorchamps - TrackCountry: Belgium - TrackAltitude: 414.45 m - TrackLatitude: 50.444061 m - TrackLongitude: 5.965178 m - TrackNorthOffset: 5.8076 rad - TrackNumTurns: 21 - TrackPitSpeedLimit: 60.00 kph - TrackType: road course - TrackDirection: neutral - TrackWeatherType: Generated / Dynamic Sky - TrackSkies: Partly Cloudy - TrackSurfaceTemp: 32.53 C - TrackAirTemp: 18.28 C - TrackAirPressure: 28.48 Hg - TrackWindVel: 1.15 m/s - TrackWindDir: 4.10 rad - TrackRelativeHumidity: 53 % - TrackFogLevel: 0 % - TrackCleanup: 0 - TrackDynamicTrack: 1 - TrackVersion: 2020.11.28.01 - SeriesID: 0 - SeasonID: 0 - SessionID: 0 - SubSessionID: 0 - LeagueID: 0 - Official: 0 - RaceWeek: 0 - EventType: Test - Category: Road - SimMode: full - TeamRacing: 0 - MinDrivers: 0 - MaxDrivers: 0 - DCRuleSet: None - QualifierMustStartRace: 0 - NumCarClasses: 1 - NumCarTypes: 1 - HeatRacing: 0 - BuildType: Release - BuildTarget: Members - BuildVersion: 2020.12.08.06 - WeekendOptions: - NumStarters: 0 - StartingGrid: single file - QualifyScoring: best lap - CourseCautions: off - StandingStart: 0 - ShortParadeLap: 0 - Restarts: single file - WeatherType: Generated / Dynamic Sky - Skies: Partly Cloudy - WindDirection: N - WindSpeed: 3.22 km/h - WeatherTemp: 25.56 C - RelativeHumidity: 55 % - FogLevel: 0 % - TimeOfDay: 12:55 pm - Date: 2021-03-01 - EarthRotationSpeedupFactor: 1 - Unofficial: 0 - CommercialMode: consumer - NightMode: variable - IsFixedSetup: 0 - StrictLapsChecking: default - HasOpenRegistration: 0 - HardcoreLevel: 1 - NumJokerLaps: 0 - IncidentLimit: unlimited - FastRepairsLimit: unlimited - GreenWhiteCheckeredLimit: 0 - TelemetryOptions: - TelemetryDiskFile: "" - -SessionInfo: - Sessions: - - SessionNum: 0 - SessionLaps: unlimited - SessionTime: unlimited - SessionNumLapsToAvg: 0 - SessionType: Offline Testing - SessionTrackRubberState: moderate usage - SessionName: TESTING - SessionSubType: - SessionSkipped: 0 - SessionRunGroupsUsed: 0 - ResultsPositions: - ResultsFastestLap: - - CarIdx: 255 - FastestLap: 0 - FastestTime: -1.0000 - ResultsAverageLapTime: -1.0000 - ResultsNumCautionFlags: 0 - ResultsNumCautionLaps: 0 - ResultsNumLeadChanges: 0 - ResultsLapsComplete: -1 - ResultsOfficial: 0 - -CameraInfo: - Groups: - - GroupNum: 1 - GroupName: Nose - Cameras: - - CameraNum: 1 - CameraName: CamNose - - GroupNum: 2 - GroupName: Gearbox - Cameras: - - CameraNum: 1 - CameraName: CamGearbox - - GroupNum: 3 - GroupName: Roll Bar - Cameras: - - CameraNum: 1 - CameraName: CamRoll Bar - - GroupNum: 4 - GroupName: LF Susp - Cameras: - - CameraNum: 1 - CameraName: CamLF Susp - - GroupNum: 5 - GroupName: LR Susp - Cameras: - - CameraNum: 1 - CameraName: CamLR Susp - - GroupNum: 6 - GroupName: Gyro - Cameras: - - CameraNum: 1 - CameraName: CamGyro - - GroupNum: 7 - GroupName: RF Susp - Cameras: - - CameraNum: 1 - CameraName: CamRF Susp - - GroupNum: 8 - GroupName: RR Susp - Cameras: - - CameraNum: 1 - CameraName: CamRR Susp - - GroupNum: 9 - GroupName: Cockpit - Cameras: - - CameraNum: 1 - CameraName: CamCockpit - - GroupNum: 10 - GroupName: Scenic - IsScenic: true - Cameras: - - CameraNum: 1 - CameraName: Scenic_03 - - CameraNum: 2 - CameraName: Scenic_04 - - CameraNum: 3 - CameraName: Scenic_05 - - CameraNum: 4 - CameraName: Scenic_06 - - CameraNum: 5 - CameraName: Scenic_10 - - CameraNum: 6 - CameraName: Scenic_07 - - CameraNum: 7 - CameraName: Scenic_08 - - CameraNum: 8 - CameraName: Scenic_09 - - CameraNum: 9 - CameraName: Scenic_02 - - CameraNum: 10 - CameraName: Scenic_01 - - GroupNum: 11 - GroupName: TV1 - Cameras: - - CameraNum: 1 - CameraName: CamTV1_16 - - CameraNum: 2 - CameraName: CamTV1_16b - - CameraNum: 3 - CameraName: CamTV1_00 - - CameraNum: 4 - CameraName: CamTV1_01 - - CameraNum: 5 - CameraName: CamTV1_02 - - CameraNum: 6 - CameraName: CamTV1_05 - - CameraNum: 7 - CameraName: CamTV1_07b - - CameraNum: 8 - CameraName: CamTV1_08 - - CameraNum: 9 - CameraName: CamTV1_08b - - CameraNum: 10 - CameraName: CamTV1_09 - - CameraNum: 11 - CameraName: CamTV1_10 - - CameraNum: 12 - CameraName: CamTV1_11 - - CameraNum: 13 - CameraName: CamTV1_13 - - CameraNum: 14 - CameraName: CamTV1_15 - - CameraNum: 15 - CameraName: CamTV1_12 - - CameraNum: 16 - CameraName: CamTV1_10b - - CameraNum: 17 - CameraName: CamTV1_02b - - CameraNum: 18 - CameraName: CamTV1_03 - - CameraNum: 19 - CameraName: CamTV1_11b - - CameraNum: 20 - CameraName: CamTV1_06b - - CameraNum: 21 - CameraName: CamTV1_09b - - CameraNum: 22 - CameraName: CamTV1_04 - - CameraNum: 23 - CameraName: CamTV1_05b - - CameraNum: 24 - CameraName: CamTV1_07 - - CameraNum: 25 - CameraName: CamTV1_15b - - CameraNum: 26 - CameraName: CamTV1_00b - - CameraNum: 27 - CameraName: CamTV1_06 - - CameraNum: 28 - CameraName: CamTV1_12b - - CameraNum: 29 - CameraName: CamTV1_13b - - CameraNum: 30 - CameraName: CamTV1_14 - - GroupNum: 12 - GroupName: TV2 - Cameras: - - CameraNum: 1 - CameraName: CamTV2_06 - - CameraNum: 2 - CameraName: CamTV2_05 - - CameraNum: 3 - CameraName: CamTV2_06b - - CameraNum: 4 - CameraName: CamTV2_03 - - CameraNum: 5 - CameraName: CamTV2_19 - - CameraNum: 6 - CameraName: CamTV2_18 - - CameraNum: 7 - CameraName: CamTV2_18b - - CameraNum: 8 - CameraName: CamTV2_16 - - CameraNum: 9 - CameraName: CamTV2_17b - - CameraNum: 10 - CameraName: CamTV2_13b - - CameraNum: 11 - CameraName: CamTV2_13 - - CameraNum: 12 - CameraName: CamTV2_10 - - CameraNum: 13 - CameraName: CamTV2_10b - - CameraNum: 14 - CameraName: CamTV2_12 - - CameraNum: 15 - CameraName: CamTV2_12b - - CameraNum: 16 - CameraName: CamTV2_09 - - CameraNum: 17 - CameraName: CamTV2_09c - - CameraNum: 18 - CameraName: CamTV2_08 - - CameraNum: 19 - CameraName: CamTV2_04b - - CameraNum: 20 - CameraName: CamTV2_02 - - CameraNum: 21 - CameraName: CamTV2_00 - - CameraNum: 22 - CameraName: CamTV2_21 - - CameraNum: 23 - CameraName: CamTV2_01 - - CameraNum: 24 - CameraName: CamTV2_01b - - CameraNum: 25 - CameraName: CamTV2_14 - - CameraNum: 26 - CameraName: CamTV2_19b - - CameraNum: 27 - CameraName: CamTV2_15 - - CameraNum: 28 - CameraName: CamTV2_17 - - CameraNum: 29 - CameraName: CamTV2_07b - - CameraNum: 30 - CameraName: CamTV2_07 - - CameraNum: 31 - CameraName: CamTV2_15b - - CameraNum: 32 - CameraName: CamTV2_20 - - CameraNum: 33 - CameraName: CamTV2_21b - - CameraNum: 34 - CameraName: CamTV2_04 - - GroupNum: 13 - GroupName: TV3 - Cameras: - - CameraNum: 1 - CameraName: CamTV3_00 - - CameraNum: 2 - CameraName: CamTV3_01b - - CameraNum: 3 - CameraName: CamTV3_02b - - CameraNum: 4 - CameraName: CamTV3_02 - - CameraNum: 5 - CameraName: CamTV3_06 - - CameraNum: 6 - CameraName: CamTV3_07 - - CameraNum: 7 - CameraName: CamTV3_08 - - CameraNum: 8 - CameraName: CamTV3_09 - - CameraNum: 9 - CameraName: CamTV3_10 - - CameraNum: 10 - CameraName: CamTV3_11 - - CameraNum: 11 - CameraName: CamTV3_12 - - CameraNum: 12 - CameraName: CamTV3_13 - - CameraNum: 13 - CameraName: CamTV3_05 - - CameraNum: 14 - CameraName: CamTV3_04 - - CameraNum: 15 - CameraName: CamTV3_03 - - CameraNum: 16 - CameraName: CamTV3_15 - - CameraNum: 17 - CameraName: CamTV3_14 - - GroupNum: 14 - GroupName: TV Static - Cameras: - - CameraNum: 1 - CameraName: CamTV4_02 - - CameraNum: 2 - CameraName: CamTV4_01 - - CameraNum: 3 - CameraName: CamTV4_03 - - CameraNum: 4 - CameraName: CamTV4_04 - - CameraNum: 5 - CameraName: CamTV4_04b - - CameraNum: 6 - CameraName: CamTV4_05 - - CameraNum: 7 - CameraName: CamTV4_06b - - CameraNum: 8 - CameraName: CamTV4_06 - - CameraNum: 9 - CameraName: CamTV4_07 - - CameraNum: 10 - CameraName: CamTV4_08 - - CameraNum: 11 - CameraName: CamTV4_09b - - CameraNum: 12 - CameraName: CamTV4_09 - - CameraNum: 13 - CameraName: CamTV4_10 - - CameraNum: 14 - CameraName: CamTV4_11 - - CameraNum: 15 - CameraName: CamTV4_12 - - CameraNum: 16 - CameraName: CamTV4_13 - - CameraNum: 17 - CameraName: CamTV4_14 - - CameraNum: 18 - CameraName: CamTV4_15 - - CameraNum: 19 - CameraName: CamTV4_16 - - CameraNum: 20 - CameraName: CamTV4_17 - - CameraNum: 21 - CameraName: CamTV4_17b - - CameraNum: 22 - CameraName: CamTV4_18 - - CameraNum: 23 - CameraName: CamTV4_19 - - CameraNum: 24 - CameraName: CamTV4_20 - - CameraNum: 25 - CameraName: CamTV4_21 - - CameraNum: 26 - CameraName: CamTV4_00 - - GroupNum: 15 - GroupName: TV Mixed - Cameras: - - CameraNum: 1 - CameraName: CamTV3_13 - - CameraNum: 2 - CameraName: CamTV1_00 - - CameraNum: 3 - CameraName: CamTV1_01 - - CameraNum: 4 - CameraName: CamTV1_02b - - CameraNum: 5 - CameraName: CamTV1_02 - - CameraNum: 6 - CameraName: CamTV1_05 - - CameraNum: 7 - CameraName: CamTV1_06b - - CameraNum: 8 - CameraName: CamTV1_07b - - CameraNum: 9 - CameraName: CamTV1_08 - - CameraNum: 10 - CameraName: CamTV1_08b - - CameraNum: 11 - CameraName: CamTV1_09 - - CameraNum: 12 - CameraName: CamTV1_09b - - CameraNum: 13 - CameraName: CamTV1_10 - - CameraNum: 14 - CameraName: CamTV1_10b - - CameraNum: 15 - CameraName: CamTV1_11 - - CameraNum: 16 - CameraName: CamTV1_12 - - CameraNum: 17 - CameraName: CamTV1_13 - - CameraNum: 18 - CameraName: CamTV1_15 - - CameraNum: 19 - CameraName: CamTV2_21b - - CameraNum: 20 - CameraName: CamTV1_16 - - CameraNum: 21 - CameraName: CamTV2_00 - - CameraNum: 22 - CameraName: CamTV2_01 - - CameraNum: 23 - CameraName: CamTV2_01b - - CameraNum: 24 - CameraName: CamTV2_02 - - CameraNum: 25 - CameraName: CamTV2_04b - - CameraNum: 26 - CameraName: CamTV2_05 - - CameraNum: 27 - CameraName: CamTV2_06 - - CameraNum: 28 - CameraName: CamTV1_04 - - CameraNum: 29 - CameraName: CamTV2_06b - - CameraNum: 30 - CameraName: CamTV2_07b - - CameraNum: 31 - CameraName: CamTV2_08 - - CameraNum: 32 - CameraName: CamTV2_09 - - CameraNum: 33 - CameraName: CamTV2_10 - - CameraNum: 34 - CameraName: CamTV2_15b - - CameraNum: 35 - CameraName: CamTV2_12b - - CameraNum: 36 - CameraName: CamTV2_13b - - CameraNum: 37 - CameraName: CamTV2_13 - - CameraNum: 38 - CameraName: CamTV2_14 - - CameraNum: 39 - CameraName: CamTV2_16 - - CameraNum: 40 - CameraName: CamTV2_17b - - CameraNum: 41 - CameraName: CamTV2_18 - - CameraNum: 42 - CameraName: CamTV2_19 - - CameraNum: 43 - CameraName: CamTV2_19b - - CameraNum: 44 - CameraName: CamTV2_21 - - CameraNum: 45 - CameraName: CamTV2_20 - - CameraNum: 46 - CameraName: CamTV3_00 - - CameraNum: 47 - CameraName: CamTV3_02b - - CameraNum: 48 - CameraName: CamTV3_02 - - CameraNum: 49 - CameraName: CamTV3_04 - - CameraNum: 50 - CameraName: CamTV3_06 - - CameraNum: 51 - CameraName: CamTV3_07 - - CameraNum: 52 - CameraName: CamTV3_08 - - CameraNum: 53 - CameraName: CamTV3_09 - - CameraNum: 54 - CameraName: CamTV3_10 - - CameraNum: 55 - CameraName: CamTV3_11 - - CameraNum: 56 - CameraName: CamTV3_12 - - CameraNum: 57 - CameraName: CamRoll Bar - - CameraNum: 58 - CameraName: CamTV1_05b - - CameraNum: 59 - CameraName: CamTV1_06 - - CameraNum: 60 - CameraName: CamTV1_07 - - CameraNum: 61 - CameraName: CamTV1_12b - - CameraNum: 62 - CameraName: CamTV1_14 - - CameraNum: 63 - CameraName: CamTV1_15b - - CameraNum: 64 - CameraName: CamTV1_15b0 - - CameraNum: 65 - CameraName: CamTV1_16b - - CameraNum: 66 - CameraName: CamTV2_03 - - CameraNum: 67 - CameraName: CamTV2_04 - - CameraNum: 68 - CameraName: CamTV2_15 - - CameraNum: 69 - CameraName: CamTV2_17 - - CameraNum: 70 - CameraName: CamTV3_01b - - CameraNum: 71 - CameraName: CamTV3_05 - - CameraNum: 72 - CameraName: CamTV3_14 - - CameraNum: 73 - CameraName: CamTV3_15 - - CameraNum: 74 - CameraName: CamTV1_00b - - CameraNum: 75 - CameraName: CamTV2_09c - - CameraNum: 76 - CameraName: CamTV4_16 - - CameraNum: 77 - CameraName: CamTV3_03 - - GroupNum: 16 - GroupName: Pit Lane - Cameras: - - CameraNum: 1 - CameraName: CamPit Lane - - CameraNum: 2 - CameraName: CamPit Lane 3 - - CameraNum: 3 - CameraName: CamPit Lane 2 - - GroupNum: 17 - GroupName: Pit Lane 2 - Cameras: - - CameraNum: 1 - CameraName: CamPit Lane 4 - - GroupNum: 18 - GroupName: Chopper - Cameras: - - CameraNum: 1 - CameraName: CamChopper - - GroupNum: 19 - GroupName: Blimp - Cameras: - - CameraNum: 1 - CameraName: CamBlimp - - GroupNum: 20 - GroupName: Chase - Cameras: - - CameraNum: 1 - CameraName: CamChase - - GroupNum: 21 - GroupName: Far Chase - Cameras: - - CameraNum: 1 - CameraName: CamFar Chase - - GroupNum: 22 - GroupName: Rear Chase - Cameras: - - CameraNum: 1 - CameraName: CamRear Chase - -RadioInfo: - SelectedRadioNum: 0 - Radios: - - RadioNum: 0 - HopCount: 2 - NumFrequencies: 7 - TunedToFrequencyNum: 0 - ScanningIsOn: 1 - Frequencies: - - FrequencyNum: 0 - FrequencyName: "@ALLTEAMS" - Priority: 12 - CarIdx: -1 - EntryIdx: -1 - ClubID: 0 - CanScan: 1 - CanSquawk: 1 - Muted: 0 - IsMutable: 1 - IsDeletable: 0 - - FrequencyNum: 1 - FrequencyName: "@DRIVERS" - Priority: 15 - CarIdx: -1 - EntryIdx: -1 - ClubID: 0 - CanScan: 1 - CanSquawk: 1 - Muted: 0 - IsMutable: 1 - IsDeletable: 0 - - FrequencyNum: 2 - FrequencyName: "@TEAM" - Priority: 60 - CarIdx: 0 - EntryIdx: -1 - ClubID: 0 - CanScan: 1 - CanSquawk: 1 - Muted: 0 - IsMutable: 0 - IsDeletable: 0 - - FrequencyNum: 3 - FrequencyName: "@CLUB" - Priority: 20 - CarIdx: -1 - EntryIdx: -1 - ClubID: 15 - CanScan: 1 - CanSquawk: 1 - Muted: 0 - IsMutable: 1 - IsDeletable: 0 - - FrequencyNum: 4 - FrequencyName: "@ADMIN" - Priority: 90 - CarIdx: -1 - EntryIdx: -1 - ClubID: 0 - CanScan: 1 - CanSquawk: 1 - Muted: 0 - IsMutable: 0 - IsDeletable: 0 - - FrequencyNum: 5 - FrequencyName: "@RACECONTROL" - Priority: 80 - CarIdx: -1 - EntryIdx: -1 - ClubID: 0 - CanScan: 1 - CanSquawk: 1 - Muted: 0 - IsMutable: 0 - IsDeletable: 0 - - FrequencyNum: 6 - FrequencyName: "@PRIVATE" - Priority: 70 - CarIdx: -1 - EntryIdx: 0 - ClubID: 0 - CanScan: 1 - CanSquawk: 1 - Muted: 0 - IsMutable: 0 - IsDeletable: 0 - -DriverInfo: - DriverCarIdx: 0 - DriverUserID: 130468 - PaceCarIdx: -1 - DriverHeadPosX: 0.015 - DriverHeadPosY: 0.324 - DriverHeadPosZ: 0.530 - DriverCarIdleRPM: 1100.000 - DriverCarRedLine: 8500.000 - DriverCarEngCylinderCount: 10 - DriverCarFuelKgPerLtr: 0.750 - DriverCarFuelMaxLtr: 120.000 - DriverCarMaxFuelPct: 1.000 - DriverCarSLFirstRPM: 7300.000 - DriverCarSLShiftRPM: 8050.000 - DriverCarSLLastRPM: 8100.000 - DriverCarSLBlinkRPM: 8150.000 - DriverCarVersion: 2020.12.08.04 - DriverPitTrkPct: 0.038893 - DriverCarEstLapTime: 137.9766 - DriverSetupName: Reid Spa Med Downforce Endurance.sto - DriverSetupIsModified: 0 - DriverSetupLoadTypeName: user - DriverSetupPassedTech: 1 - DriverIncidentCount: 0 - Drivers: - - CarIdx: 0 - UserName: CR Reid - AbbrevName: - Initials: - UserID: 130468 - TeamID: 0 - TeamName: CR Reid - CarNumber: "52" - CarNumberRaw: 52 - CarPath: lamborghinievogt3 - CarClassID: 0 - CarID: 133 - CarIsPaceCar: 0 - CarIsAI: 0 - CarScreenName: Lamborghini Huracan GT3 EVO - CarScreenNameShort: Lamborghini GT3 - CarClassShortName: - CarClassRelSpeed: 0 - CarClassLicenseLevel: 0 - CarClassMaxFuelPct: 1.000 % - CarClassWeightPenalty: 0.000 kg - CarClassPowerAdjust: 0.000 % - CarClassDryTireSetLimit: 0 % - CarClassColor: 0xffffff - IRating: 1 - LicLevel: 1 - LicSubLevel: 1 - LicString: R 0.01 - LicColor: 0xundefined - IsSpectator: 0 - CarDesignStr: 1,000000,000000,000000,fffcfc - HelmetDesignStr: 0,000000,000000,000000 - SuitDesignStr: 2,000000,000000,000000 - CarNumberDesignStr: 0,0,000000,7f7b7b,000000 - CarSponsor_1: 0 - CarSponsor_2: 0 - CurDriverIncidentCount: 0 - TeamIncidentCount: 0 - -SplitTimeInfo: - Sectors: - - SectorNum: 0 - SectorStartPct: 0.000000 - - SectorNum: 1 - SectorStartPct: 0.310613 - - SectorNum: 2 - SectorStartPct: 0.522299 - - SectorNum: 3 - SectorStartPct: 0.692902 - - SectorNum: 4 - SectorStartPct: 0.931096 - -CarSetup: - UpdateCount: 1 - TiresAero: - LeftFront: - StartingPressure: 141 kPa - LastHotPressure: 141 kPa - LastTempsOMI: 44C, 44C, 44C - TreadRemaining: 100%, 100%, 100% - LeftRear: - StartingPressure: 141 kPa - LastHotPressure: 141 kPa - LastTempsOMI: 44C, 44C, 44C - TreadRemaining: 100%, 100%, 100% - RightFront: - StartingPressure: 141 kPa - LastHotPressure: 141 kPa - LastTempsIMO: 44C, 44C, 44C - TreadRemaining: 100%, 100%, 100% - RightRear: - StartingPressure: 141 kPa - LastHotPressure: 141 kPa - LastTempsIMO: 44C, 44C, 44C - TreadRemaining: 100%, 100%, 100% - AeroBalanceCalc: - FrontRhAtSpeed: 50 mm - RearRhAtSpeed: 75 mm - WingSetting: 6 degrees - FrontDownforce: 43.20% - Chassis: - Front: - ArbBlades: 3-3 - ArbOuterDiameter: Large - ToeIn: -1.2 mm - FrontMasterCyl: 20.6 mm - RearMasterCyl: 19.1 mm - BrakePads: Medium friction - CrossWeight: 50.0% - LeftFront: - CornerWeight: 3063 N - RideHeight: 51.0 mm - SpringPerchOffset: 35.5 mm - SpringSelected: 220 - SpringRate: 220 N/mm - CompDamping: -9 clicks - RbdDamping: -13 clicks - Camber: -4.5 deg - Caster: +12.5 deg - LeftRear: - CornerWeight: 3929 N - RideHeight: 81.8 mm - SpringPerchOffset: 2.5 mm - SpringSelected: 250 - SpringRate: 250 N/mm - CompDamping: -12 clicks - RbdDamping: -10 clicks - Camber: -3.4 deg - ToeIn: +0.4 mm - InCarDials: - BrakePressureBias: 50.2% - TractionControlSetting: 6 (TC) - ThrottleShapeSetting: 1 (THR) - EngineMapSetting: 1 (MAP) - AbsSetting: 1 (ABS) - DisplayPage: Night - EnduranceLightPackage: Fitted - LeftNightLedStrip: Red - RightNightLedStrip: Red - RightFront: - CornerWeight: 3063 N - RideHeight: 51.0 mm - SpringPerchOffset: 35.5 mm - SpringSelected: 220 - SpringRate: 220 N/mm - CompDamping: -9 clicks - RbdDamping: -13 clicks - Camber: -4.5 deg - Caster: +12.5 deg - RightRear: - CornerWeight: 3929 N - RideHeight: 81.8 mm - SpringPerchOffset: 2.5 mm - SpringSelected: 250 - SpringRate: 250 N/mm - CompDamping: -12 clicks - RbdDamping: -10 clicks - Camber: -3.4 deg - ToeIn: +0.4 mm - Rear: - FuelLevel: 79.0 L - ArbBlades: 1-1 - ArbOuterDiameter: Medium - SixthGear: FiA - DiffPreload: 200 Nm - WingSetting: 6 degrees diff --git a/Sdk/tests/Live_Tests/Fixture.cs b/Sdk/tests/Live_Tests/Fixture.cs deleted file mode 100644 index c7d72f3..0000000 --- a/Sdk/tests/Live_Tests/Fixture.cs +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Copyright (C) 2024-2025 Scott Velez - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; -**/ - -using Microsoft.Extensions.Logging.Abstractions; -using SVappsLAB.iRacingTelemetrySDK; -using SVappsLAB.iRacingTelemetrySDK.Models; - -namespace Live_Tests -{ - public class Fixture : IAsyncLifetime - { - const int TIMEOUT_SECS = 5; - public ITelemetryClient? TelemetryClient; - public TelemetrySessionInfo TelemetrySessionInfo { get; private set; } - - public Fixture() - { - } - - public async ValueTask InitializeAsync() - { - var delayTs = new CancellationTokenSource(TimeSpan.FromSeconds(TIMEOUT_SECS)); - - // cancel when either the test context is cancelled or after 5 seconds - var cts = CancellationTokenSource.CreateLinkedTokenSource( - TestContext.Current.CancellationToken, - delayTs.Token - ); - - TelemetryClient = TelemetryClient.Create(NullLogger.Instance); - TelemetryClient.OnSessionInfoUpdate += (object? sender, TelemetrySessionInfo si) => - { - TelemetrySessionInfo = si; - - // now that we have the sessionInfo, we can cancel monitoring - cts.Cancel(); - }; - - await TelemetryClient.Monitor(cts.Token); - - // check if the timeout cancellation was requested - if (delayTs.IsCancellationRequested) - { - throw new Exception("timeout. unable to read session. cancelling the monitoring"); - } - } - - public ValueTask DisposeAsync() - { - TelemetryClient?.Dispose(); - - return ValueTask.CompletedTask; - } - } - -} diff --git a/Sdk/tests/Live_Tests/Live_Tests.csproj b/Sdk/tests/Live_Tests/Live_Tests.csproj deleted file mode 100644 index da8b6e7..0000000 --- a/Sdk/tests/Live_Tests/Live_Tests.csproj +++ /dev/null @@ -1,33 +0,0 @@ - - - - Exe - net8.0 - enable - enable - - false - true - true - - - - $(NoWarn);CS8601,CS8618 - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - - diff --git a/Sdk/tests/Live_Tests/SessionInfo/ModelYamlMatch.cs b/Sdk/tests/Live_Tests/SessionInfo/ModelYamlMatch.cs deleted file mode 100644 index cfde81f..0000000 --- a/Sdk/tests/Live_Tests/SessionInfo/ModelYamlMatch.cs +++ /dev/null @@ -1,147 +0,0 @@ -/** - * Copyright (C) 2024-2025 Scott Velez - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; -**/ - -using System.Reflection; -using Microsoft.Extensions.Logging.Abstractions; -using SVappsLAB.iRacingTelemetrySDK; -using SVappsLAB.iRacingTelemetrySDK.Models; -using YamlDotNet.Serialization; - -namespace Live_Tests -{ - public class ModelYamlMatch - { - const int TIMEOUT_SECS = 5; - ITestOutputHelper _output; - - public ModelYamlMatch(ITestOutputHelper output) - { - _output = output; - } - - [Fact] - public async Task ModelShouldMatchYaml() - { - CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(TIMEOUT_SECS)); - - var client = TelemetryClient.Create(NullLogger.Instance); - - bool sessionInfoReceived = false; - - client.OnRawSessionInfoUpdate += (object? sender, string rawYaml) => - { - sessionInfoReceived = true; - - // save rawYaml to a file for debugging - //File.WriteAllText("rawyaml.yml", rawYaml); - - var allMissingProperties = ValidateModelAgainstYaml(rawYaml); - - // skip 'CarSetup' properties since they are dynamic and can vary widely - var missingProperties = allMissingProperties - .Where(prop => !prop.StartsWith("CarSetup")) - .ToList(); - - cts.Cancel(); - - Assert.True(missingProperties.Count == 0, $"missing properties in model: {string.Join(", ", missingProperties)}"); - }; - - await client.Monitor(cts.Token); - - Assert.True(sessionInfoReceived, "Session info was not received within the timeout period."); - } - - List ValidateModelAgainstYaml(string rawYaml) - { - var deserializer = new DeserializerBuilder().Build(); - var rawSessionInfo = deserializer.Deserialize>(rawYaml); - - // check if all YAML keys exist in the model - var missingProperties = new List(); - RecursiveMatcher(rawSessionInfo, typeof(T), "", missingProperties); - - return missingProperties; - } - - bool RecursiveMatcher(Dictionary yamlObject, Type modelType, string currentPath, List missingProperties) - { - bool allPropertiesFound = true; - var properties = modelType.GetProperties(BindingFlags.Public | BindingFlags.Instance); - var propertyNames = properties.Select(p => p.Name).ToList(); - - foreach (var entry in yamlObject) - { - string? key = entry.Key?.ToString(); - if (key == null) continue; - - string propertyPath = string.IsNullOrEmpty(currentPath) ? key : $"{currentPath}.{key}"; - - // check if the property exists in the model - var matchingProperty = properties.FirstOrDefault(p => - p.Name.Equals(key, StringComparison.OrdinalIgnoreCase)); -#if DEBUG - var debugMsg = $"checking property '{propertyPath}' against model type '{modelType.Name}', matched: {matchingProperty != null}"; - _output.WriteLine(debugMsg); -#endif - if (matchingProperty == null) - { - missingProperties.Add(propertyPath); - allPropertiesFound = false; - continue; - } - - // if this is a nested object, recurse into it - if (entry.Value is Dictionary nestedDict) - { - Type propertyType = matchingProperty.PropertyType; - - // if property is a collection or dictionary type, get the element type - if (propertyType.IsGenericType) - { - Type[] genericArgs = propertyType.GetGenericArguments(); - if (genericArgs.Length > 0) - { - // use the value type for dictionaries or the element type for collections - propertyType = genericArgs[genericArgs.Length - 1]; - } - } - - bool nestedResult = RecursiveMatcher(nestedDict, propertyType, propertyPath, missingProperties); - allPropertiesFound = allPropertiesFound && nestedResult; - } - else if (entry.Value is List list) - { - foreach (var item in list) - { - if (item is Dictionary itemDict) - { - Type elementType = matchingProperty.PropertyType.GetElementType() ?? - (matchingProperty.PropertyType.IsGenericType ? - matchingProperty.PropertyType.GetGenericArguments()[0] : - typeof(object)); - - bool listItemResult = RecursiveMatcher(itemDict, elementType, $"{propertyPath}[item]", missingProperties); - allPropertiesFound = allPropertiesFound && listItemResult; - } - } - } - } - - return allPropertiesFound; - } - } -} diff --git a/Sdk/tests/Live_Tests/SessionInfo/SessionInfoTests.cs b/Sdk/tests/Live_Tests/SessionInfo/SessionInfoTests.cs deleted file mode 100644 index b462d73..0000000 --- a/Sdk/tests/Live_Tests/SessionInfo/SessionInfoTests.cs +++ /dev/null @@ -1,83 +0,0 @@ - -/** - * Copyright (C) 2024-2025 Scott Velez - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; -**/ - -using SVappsLAB.iRacingTelemetrySDK; - -namespace Live_Tests -{ - public class SessionInfo : IClassFixture - - { - Fixture _fixture; - ITelemetryClient _telemetryClient; - public SessionInfo(Fixture fixture) - { - _fixture = fixture; - _telemetryClient = fixture.TelemetryClient; - } - - [Theory] - [InlineData("WeekendInfo", true)] - [InlineData("SessionInfo", true)] - [InlineData("CameraInfo", true)] - [InlineData("RadioInfo", true)] - [InlineData("DriverInfo", true)] - [InlineData("SplitTimeInfo", true)] - [InlineData("CarSetup", true)] - [InlineData("_NoSuchProperty_", false)] - public void EnsureRawYamlContainsSpecifiedStrings(string str, bool shouldExist) - { - var isValid = _telemetryClient.GetRawTelemetrySessionInfoYaml().Contains(str) == shouldExist; - Assert.True(isValid, $"{str} not found in TelemetrySessionInfo"); - } - - [Fact] - public void EnsureSessionInfoHasExpectedValues() - { - // to ensure this test has a chance of passing, lets pick values that are likely to find in ANY online session - - var si = _fixture.TelemetrySessionInfo; - - // WeekendInfo - Assert.Equal("full", si.WeekendInfo.SimMode); - Assert.Equal("Release", si.WeekendInfo.BuildType); - Assert.Equal("Members", si.WeekendInfo.BuildTarget); - - // SessionInfo - Assert.True(si.SessionInfo.Sessions.Count > 0); - - // CameraInfo - Assert.True(si.CameraInfo.Groups.Count > 0); - Assert.True(si.CameraInfo.Groups.Exists(g => g.GroupName == "Gearbox")); - - // RadioInfo - Assert.True(si.RadioInfo.Radios.Count > 0); - Assert.True(si.RadioInfo.Radios[0].NumFrequencies > 0); - Assert.True(si.RadioInfo.Radios[0].Frequencies.Exists(f => f.FrequencyName == "@DRIVERS")); - - // DriverInfo - Assert.True(si.DriverInfo.DriverCarIdleRPM > 0); - Assert.True(si.DriverInfo.DriverCarIdleRPM < si.DriverInfo.DriverCarRedLine); - - // CarSetup -#pragma warning disable CS8604 // Possible null reference - var updateCount = int.Parse(si.CarSetup["UpdateCount"] as string); - Assert.True(updateCount > 0); - } - - } -} diff --git a/Sdk/tests/Live_Tests/Telemetry.cs b/Sdk/tests/Live_Tests/Telemetry.cs deleted file mode 100644 index bf3b6aa..0000000 --- a/Sdk/tests/Live_Tests/Telemetry.cs +++ /dev/null @@ -1,142 +0,0 @@ -/** - * Copyright (C) 2024-2025 Scott Velez - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; -**/ - -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using SVappsLAB.iRacingTelemetrySDK; -using SVappsLAB.iRacingTelemetrySDK.Models; - -namespace Live_Tests -{ - // the variables we want to use - [RequiredTelemetryVars(["EngineWarnings", "IsOnTrackCar", "PlayerTrackSurface", "rpm", "SessionTick", "SessionTimeRemain"])] - public class Telemetry - { - [Fact] - public async Task EnsureEventsFired() - { - // ensure all these 'events' were triggered - bool test_OnConnectStateChangedCalled = false; - bool test_OnSessionInfoUpdateCalled = false; - bool test_OnTelemetryUpdateCalled = false; - - ILogger logger = NullLogger.Instance; - CancellationTokenSource cts = new CancellationTokenSource(); - - using var telemetryClient = TelemetryClient.Create(logger); - - // set up event handlers - telemetryClient.OnConnectStateChanged += - (object? _o, ConnectStateChangedEventArgs e) => test_OnConnectStateChangedCalled = true; - telemetryClient.OnTelemetryUpdate += - (object? _o, TelemetryData _e) => test_OnTelemetryUpdateCalled = true; - telemetryClient.OnSessionInfoUpdate += - (object? _o, TelemetrySessionInfo e) => test_OnSessionInfoUpdateCalled = true; - - // start monitoring - var monitorTask = telemetryClient.Monitor(cts.Token); - - // loop up to 5 seconds, checking if the events have been triggered - for (int i = 0; i < 5; i++) - { - var allEventsFired = test_OnConnectStateChangedCalled && test_OnTelemetryUpdateCalled && test_OnSessionInfoUpdateCalled; - if (allEventsFired) - { - break; - } - await Task.Delay(TimeSpan.FromSeconds(1)); - } - - Assert.True(test_OnConnectStateChangedCalled, "OnConnectStateChanged not called"); - Assert.True(test_OnTelemetryUpdateCalled, "OnTelemetryUpdate not called"); - Assert.True(test_OnSessionInfoUpdateCalled, "OnSessionInfoUpdate not called"); - - // cancel monitoring - cts.Cancel(); - await monitorTask; - } - - [Fact] - public async Task VerifyTelemetry() - { - ILogger logger = NullLogger.Instance; - CancellationTokenSource cts = new CancellationTokenSource(); - - // create client and set telemetry handler - using var telemetryClient = TelemetryClient.Create(logger); - telemetryClient.OnTelemetryUpdate += onNewTelemetryData; - - // telemetry data we expect to receive - EngineWarnings engineWarnings = 0; - var isOnTrackCar = false; - TrackLocation playerTrackSurface = TrackLocation.NotInWorld; - var rpm = 0.0f; - var sessionTick = 0.0d; - var sessionTimeRemain = 0.0d; - - // grab telemetry data when event is fired - void onNewTelemetryData(object? _o, TelemetryData e) - { - // test flags - engineWarnings = e.EngineWarnings; - // test bool - isOnTrackCar = e.IsOnTrackCar; - // test enums - playerTrackSurface = e.PlayerTrackSurface; - // test floats - rpm = e.RPM; - // test ints - sessionTick = e.SessionTick; - // test doubles - sessionTimeRemain = e.SessionTimeRemain; - - // we received data. cancel monitoring - cts.Cancel(); - } - - // start monitoring - var monitorTask = telemetryClient.Monitor(cts.Token); - - // loop up to 5 seconds, checking if we received telemetry data - for (int i = 0; i < 5; i++) - { - if (isOnTrackCar) - { - break; - } - await Task.Delay(TimeSpan.FromSeconds(1)); - } - - - // check if the telemetry data is as expected - Assert.True(engineWarnings == 0 || engineWarnings == EngineWarnings.PitSpeedLimiter, $"should be no EngineWarnings flags: {engineWarnings}"); - Assert.True(isOnTrackCar, "IsOnTrackCar is false"); - - var isValidTrackSurface = - playerTrackSurface == TrackLocation.InPitStall || - playerTrackSurface == TrackLocation.OnTrack; - Assert.True(isValidTrackSurface, "PlayerTrackSurface is not 0"); - - Assert.True(rpm > 0, "rpm should be greater than 0"); - Assert.True(sessionTick > 0, "SessionTick is not greater than 0"); - Assert.True(sessionTimeRemain > 0, "SessionTimeRemain is not greater than 0"); - - // cancel monitoring - cts.Cancel(); - await monitorTask; - } - } -} diff --git a/Sdk/tests/SmokeTests/Base/Base.Models.cs b/Sdk/tests/SmokeTests/Base/Base.Models.cs new file mode 100644 index 0000000..9bdde14 --- /dev/null +++ b/Sdk/tests/SmokeTests/Base/Base.Models.cs @@ -0,0 +1,145 @@ +/** + * Copyright (C) 2024-2025 Scott Velez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +**/ + +using System.Diagnostics; +using System.Reflection; +using SVappsLAB.iRacingTelemetrySDK; +using YamlDotNet.Serialization; + +namespace SmokeTests; + +public abstract partial class Base where T : class +{ + protected async Task BaseVerifyModelMatchesRawYaml(ITelemetryClient client, int timeoutSecs) + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSecs)); + + bool sessionInfoReceived = false; + List? missingProperties = null; + + // start raw session info consumption + var rawSessionTask = Task.Run(async () => + { + await foreach (var rawYaml in client.SessionDataYaml.WithCancellation(cts.Token)) + { + sessionInfoReceived = true; + + var allMissingProperties = ValidateModelAgainstYaml(rawYaml); + + // skip 'CarSetup' properties since they are dynamic and can vary widely + missingProperties = allMissingProperties + .Where(prop => !prop.StartsWith("CarSetup")) + .ToList(); + + cts.Cancel(); + break; // exit after first item + } + }, cts.Token); + + // Start monitoring + var monitorTask = client.Monitor(cts.Token); + + // Wait for either task to complete + await Task.WhenAny(rawSessionTask, monitorTask); + + Assert.True(sessionInfoReceived, "Session info was not received within the timeout period."); + + if (missingProperties != null) + { + Assert.True(missingProperties.Count == 0, $"missing properties in model: {string.Join(", ", missingProperties)}"); + } + } + + private List ValidateModelAgainstYaml(string rawYaml) + { + var deserializer = new DeserializerBuilder().Build(); + var rawSessionInfo = deserializer.Deserialize>(rawYaml); + + // check if all YAML keys exist in the model + var missingProperties = new List(); + RecursiveMatcher(rawSessionInfo, typeof(TModel), "", missingProperties); + + return missingProperties; + } + + private bool RecursiveMatcher(Dictionary yamlObject, Type modelType, string currentPath, List missingProperties) + { + bool allPropertiesFound = true; + var properties = modelType.GetProperties(BindingFlags.Public | BindingFlags.Instance); + var propertyNames = properties.Select(p => p.Name).ToList(); + + foreach (var entry in yamlObject) + { + string? key = entry.Key?.ToString(); + if (key == null) continue; + + string propertyPath = string.IsNullOrEmpty(currentPath) ? key : $"{currentPath}.{key}"; + + // check if the property exists in the model + var matchingProperty = properties.FirstOrDefault(p => + p.Name.Equals(key, StringComparison.OrdinalIgnoreCase)); + var matched = matchingProperty != null; + + var debugMsg = $"checking property '{propertyPath}' against model type '{modelType.Name}', matched: {matchingProperty != null}"; + Debug.WriteLine(debugMsg); + + if (matchingProperty == null) + { + missingProperties.Add(propertyPath); + allPropertiesFound = false; + continue; + } + + // if this is a nested object, recurse into it + if (entry.Value is Dictionary nestedDict) + { + Type propertyType = matchingProperty.PropertyType; + + // if property is a collection or dictionary type, get the element type + if (propertyType.IsGenericType) + { + Type[] genericArgs = propertyType.GetGenericArguments(); + if (genericArgs.Length > 0) + { + // use the value type for dictionaries or the element type for collections + propertyType = genericArgs[genericArgs.Length - 1]; + } + } + + bool nestedResult = RecursiveMatcher(nestedDict, propertyType, propertyPath, missingProperties); + allPropertiesFound = allPropertiesFound && nestedResult; + } + else if (entry.Value is List list) + { + foreach (var item in list) + { + if (item is Dictionary itemDict) + { + Type elementType = matchingProperty.PropertyType.GetElementType() ?? + (matchingProperty.PropertyType.IsGenericType ? + matchingProperty.PropertyType.GetGenericArguments()[0] : + typeof(object)); + + bool listItemResult = RecursiveMatcher(itemDict, elementType, $"{propertyPath}[item]", missingProperties); + allPropertiesFound = allPropertiesFound && listItemResult; + } + } + } + } + + return allPropertiesFound; + } +} diff --git a/Sdk/tests/SmokeTests/Base/Base.Variables.cs b/Sdk/tests/SmokeTests/Base/Base.Variables.cs new file mode 100644 index 0000000..39536fd --- /dev/null +++ b/Sdk/tests/SmokeTests/Base/Base.Variables.cs @@ -0,0 +1,71 @@ +/** + * Copyright (C) 2024-2025 Scott Velez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +**/ + +using System.Diagnostics; +using SVappsLAB.iRacingTelemetrySDK; + +namespace SmokeTests; + +public abstract partial class Base where T : class +{ + protected async Task BaseVerifyAllVariablesCovered(ITelemetryClient client, int timeoutSecs = 5) + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSecs)); + + bool variablesReceived = false; + List? allMissingVariables = null; + + // start telemetry data consumption + var telemetryTask = Task.Run(async () => + { + await foreach (var telemetryData in client.TelemetryData.WithCancellation(cts.Token)) + { + variablesReceived = true; + + // get all available variable definitions from iRacing + var availableVariables = client.GetTelemetryVariables(); + var availableVariableNames = availableVariables.Select(v => v.Name).ToHashSet(); + + // get all TelemetryVar enum values + var enumVariables = Enum.GetValues() + .Select(e => e.ToString()) + .ToHashSet(); + + // find variables that exist in iRacing but not in our enum + allMissingVariables = availableVariableNames + .Where(varName => !enumVariables.Contains(varName)) + .OrderBy(varName => varName) + .ToList(); + + cts.Cancel(); + break; // exit after first item + } + }, cts.Token); + + // start monitoring + var monitorTask = client.Monitor(cts.Token); + + // wait for either task to complete + await Task.WhenAny(telemetryTask, monitorTask); + + Assert.True(variablesReceived, "Telemetry data was not received within the timeout period."); + + if (allMissingVariables != null && allMissingVariables.Count > 0) + { + Assert.Fail($"Found {allMissingVariables.Count} variables in iRacing telemetry that are missing from TelemetryVar enum: {string.Join(", ", allMissingVariables)}"); + } + } +} diff --git a/Sdk/tests/SmokeTests/Base/Base.cs b/Sdk/tests/SmokeTests/Base/Base.cs new file mode 100644 index 0000000..f333ec7 --- /dev/null +++ b/Sdk/tests/SmokeTests/Base/Base.cs @@ -0,0 +1,133 @@ +/** + * Copyright (C) 2024-2025 Scott Velez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +using Microsoft.Extensions.Logging; +using SVappsLAB.iRacingTelemetrySDK; + +namespace SmokeTests; + +public abstract partial class Base where T : class +{ + private const int TIMEOUT_SECS = 5; + + protected readonly ILogger _logger; + protected readonly ITestOutputHelper _output; + + protected Base(ITestOutputHelper output) + { + _output = output; + _logger = new TestLogger(_output, LogLevel.Information); + } + + /// + /// performs basic monitoring of telemetry and session sessionInfo streams to validate client functionality. + /// + public virtual async Task BasicMonitoring(string _mode, Func> clientFactory) + { + + await using var client = clientFactory(_logger); + Assert.NotNull(client); + + VariableSummary? _variableSummary = null; + SessionSummary? _sessionInfoSummary = null; + var connectionStateReceived = false; + + // monitor the data streams, until cancelled + using var dataTasksCancellationSource = new CancellationTokenSource(); + + var dataTasks = new[] + { + MonitorData(client.TelemetryData, async telemetryData => + { + Assert.True(telemetryData.RPM.HasValue); + Assert.True(telemetryData.RPM > 200); + Assert.True(telemetryData.CarIdxTrackSurface == null || telemetryData.CarIdxTrackSurface.Length >= 64); + + // only need one sample + if (_variableSummary is null) { + var telemetryVars = client.GetTelemetryVariables(); + var variableSummary = VariableSummary.Create(telemetryVars); + _variableSummary = variableSummary; + } + }, dataTasksCancellationSource.Token), + + MonitorData(client.SessionData, async sessionInfo => + { + Assert.NotNull(sessionInfo); + Assert.NotEmpty(sessionInfo.WeekendInfo.TrackName); + + // only need one sample + if (_sessionInfoSummary is null) + { + var sessionSummary = SessionSummary.Create(sessionInfo); + _sessionInfoSummary = sessionSummary; + } + }, dataTasksCancellationSource.Token), + + MonitorData(client.ConnectStates, async connectState => + { + Assert.True(client.IsConnected); + + connectionStateReceived = true; + }, dataTasksCancellationSource.Token), + + MonitorData(client.Errors, async error => + { + Assert.NotNull(error); + _output.WriteLine($"Error received: {error.Message}"); + }, dataTasksCancellationSource.Token), + }; + + // monitor for a few seconds then cancel the operation + using var timeoutTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(TIMEOUT_SECS)); + await client.Monitor(timeoutTokenSource.Token); + + + // when the timeout occurs, we are done monitoring the data streams, so cancel them. + dataTasksCancellationSource.Cancel(); + await Task.WhenAll(dataTasks); // wait for them to complete + + Assert.NotNull(_variableSummary); + _output.WriteLine($"Variables Summary: {_variableSummary}"); + + Assert.NotNull(_sessionInfoSummary); + _output.WriteLine($"Session Summary: {_sessionInfoSummary}"); + + Assert.True(connectionStateReceived); + } + + private async Task MonitorData( + IAsyncEnumerable stream, + Func processItem, + CancellationToken cancellationToken) + { + try + { + await foreach (var data in stream.WithCancellation(cancellationToken)) + { + await processItem(data); + } + } + catch (OperationCanceledException e) when (e.CancellationToken.IsCancellationRequested) + { + // expected when the cancellation token is triggered + } + catch (Exception ex) + { + _output.WriteLine($"Error monitoring data streams: {ex.Message}"); + } + } +} diff --git a/Sdk/tests/SmokeTests/Base/Summaries.cs b/Sdk/tests/SmokeTests/Base/Summaries.cs new file mode 100644 index 0000000..cd035b3 --- /dev/null +++ b/Sdk/tests/SmokeTests/Base/Summaries.cs @@ -0,0 +1,73 @@ +using System.Collections; +using SVappsLAB.iRacingTelemetrySDK; + +namespace SmokeTests +{ + internal class SessionSummary() : SummaryBase + { + + public string? TrackName { get; init; } + public int NumberOfDrivers { get; init; } + public string? WeatherConditions { get; init; } + public string? SessionType { get; init; } + public int NumSessions { get; init; } + + + public static SessionSummary Create(TelemetrySessionInfo si) + { + return new SessionSummary + { + TrackName = si.WeekendInfo?.TrackDisplayShortName ?? UNKNOWN, + NumberOfDrivers = si.DriverInfo?.Drivers?.Count ?? -1, + WeatherConditions = $"{si.WeekendInfo?.TrackSkies ?? UNKNOWN}, {si.WeekendInfo?.TrackPrecipitation ?? UNKNOWN} Rain", + SessionType = si.WeekendInfo?.EventType ?? UNKNOWN, + NumSessions = (si.SessionInfo.Sessions?.Count ?? -1), + + // TODO: track number of drivers currently active in the session + }; + } + } + + internal class VariableSummary() : SummaryBase + { + public int NumVariables { get; init; } + public int VarTypes { get; init; } + public Dictionary LengthCounts { get; init; } = new(); + + public static VariableSummary Create(IEnumerable vars) + { + + return new VariableSummary + { + NumVariables = vars.Count(), + VarTypes = vars.Select(v => v.Type).Distinct().Count(), + LengthCounts = vars.GroupBy(v => v.Length) + .ToDictionary(g => g.Key, g => g.Count()), + }; + } + } + + internal class SummaryBase + { + protected const string UNKNOWN = ""; + public override string ToString() + { + var properties = GetType().GetProperties() + .Where(p => p.Name != nameof(ToString)) + .Select(p => + { + var value = p.GetValue(this); + if (value is IDictionary dict) + { + var pairs = dict.Cast() + .Select(kvp => $"{kvp.Key}={kvp.Value}") + .ToArray(); + return $"{p.Name}: {{{string.Join(", ", pairs)}}}"; + } + return $"{p.Name}: {value ?? UNKNOWN}"; + }); + return string.Join(", ", properties); + } + } +} + diff --git a/Sdk/tests/SmokeTests/Base/TestLogger.cs b/Sdk/tests/SmokeTests/Base/TestLogger.cs new file mode 100644 index 0000000..bdcdd16 --- /dev/null +++ b/Sdk/tests/SmokeTests/Base/TestLogger.cs @@ -0,0 +1,48 @@ +/** + * Copyright (C) 2024-2025 Scott Velez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +using Microsoft.Extensions.Logging; + +namespace SmokeTests; + +public class TestLogger : ILogger +{ + private readonly ITestOutputHelper _output; + private readonly LogLevel _logLevel; + + public TestLogger(ITestOutputHelper output, LogLevel logLevel = LogLevel.Information) + { + _output = output; + _logLevel = logLevel; + } + + public IDisposable? BeginScope(TState state) where TState : notnull => null; + + public bool IsEnabled(LogLevel logLevel) => logLevel >= _logLevel; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (!IsEnabled(logLevel)) + return; + + var message = formatter(state, exception); + if (exception != null) + { + message += $" Exception: {exception}"; + } + _output.WriteLine($"[{logLevel}] {message}"); + } +} diff --git a/Sdk/tests/SmokeTests/IBT/IBT.cs b/Sdk/tests/SmokeTests/IBT/IBT.cs new file mode 100644 index 0000000..d64277f --- /dev/null +++ b/Sdk/tests/SmokeTests/IBT/IBT.cs @@ -0,0 +1,103 @@ +/** + * Copyright (C) 2024-2025 Scott Velez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +**/ + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using SVappsLAB.iRacingTelemetrySDK; + + +namespace SmokeTests; + + +public class IBT : Base +{ + const int TIMEOUT_SECS = 5; + + public IBT(ITestOutputHelper output) : base(output) + { + } + + /// + /// find all the IBT files and use them for testing + /// + /// + /// a collection of test cases where each case contains: + /// - test name based on the IBT file name + /// - factory function that creates a TelemetryClient configured for IBT file playback + /// + public static TheoryData>> TestModes + { + get + { + var testData = new TheoryData>>(); + + var ibtDirectory = @"data\ibt"; + var ibtFiles = Directory.GetFiles(ibtDirectory, "*.ibt"); + + foreach (var ibtFile in ibtFiles) + { + var fileName = Path.GetFileNameWithoutExtension(ibtFile); + testData.Add( + $"IBT - {fileName}", + logger => TelemetryClient.Create(logger, new IBTOptions(ibtFile)) + ); + } + + return testData; + } + } + + [Theory] + [MemberData(nameof(TestModes))] + public override async Task BasicMonitoring(string _mode, Func> clientFactory) + { + await base.BasicMonitoring(_mode, clientFactory); + } + + [Fact] + public async Task InvalidFileThrows() + { + await Assert.ThrowsAsync(async () => + { + var ibtFile = @"no-such-file-name"; + await using var tc = TelemetryClient.Create(NullLogger.Instance, new IBTOptions(ibtFile)); + }); + } + + [Theory] + [MemberData(nameof(TestModes))] + public async Task VerifyModelMatchesRawYaml(string mode, Func> clientFactory) + { + await using var client = clientFactory(_logger); + await BaseVerifyModelMatchesRawYaml(client, TIMEOUT_SECS); + } + + [Theory] + [MemberData(nameof(TestModes))] + public async Task VerifyAllVariablesAreCovered(string mode, Func> clientFactory) + { + await using var client = clientFactory(_logger); + await BaseVerifyAllVariablesCovered(client, TIMEOUT_SECS); + } + + public static TheoryData IBTPlaybackSpeeds => + new() + { + { "Normal Speed", 1 }, + { "Fast Playback", 10 }, + { "Maximum Speed", int.MaxValue } + }; +} diff --git a/Sdk/tests/SmokeTests/Live/Live.cs b/Sdk/tests/SmokeTests/Live/Live.cs new file mode 100644 index 0000000..66a40c4 --- /dev/null +++ b/Sdk/tests/SmokeTests/Live/Live.cs @@ -0,0 +1,60 @@ +/** + * Copyright (C) 2024-2025 Scott Velez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +**/ + +using Microsoft.Extensions.Logging; +using SVappsLAB.iRacingTelemetrySDK; + +namespace SmokeTests; + +public class Live : Base +{ + const int TIMEOUT_SECS = 5; + + public Live(ITestOutputHelper output) : base(output) + { + } + + public static TheoryData>> TestModes => + new() + { + { + "Live", + logger => TelemetryClient.Create(logger) + }, + }; + [Theory] + [MemberData(nameof(TestModes))] + public override async Task BasicMonitoring(string _mode, Func> clientFactory) + { + await base.BasicMonitoring(_mode, clientFactory); + } + + [Theory] + [MemberData(nameof(TestModes))] + public async Task VerifyAllVariablesAreCovered(string _mode, Func> clientFactory) + { + await using var client = clientFactory(_logger); + await BaseVerifyAllVariablesCovered(client, TIMEOUT_SECS); + } + + [Theory] + [MemberData(nameof(TestModes))] + public async Task VerifyModelMatchesRawYaml(string _mode, Func> clientFactory) + { + await using var client = clientFactory(_logger); + await BaseVerifyModelMatchesRawYaml(client, TIMEOUT_SECS); + } +} diff --git a/Sdk/tests/SmokeTests/Properties/launchSettings.json b/Sdk/tests/SmokeTests/Properties/launchSettings.json new file mode 100644 index 0000000..996f025 --- /dev/null +++ b/Sdk/tests/SmokeTests/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "SmokeTests": { + "commandName": "Project", + "commandLineArgs": "--filter-class \"SmokeTests.Live\"" + } + } +} \ No newline at end of file diff --git a/Sdk/tests/SmokeTests/SmokeTests.csproj b/Sdk/tests/SmokeTests/SmokeTests.csproj new file mode 100644 index 0000000..be44df5 --- /dev/null +++ b/Sdk/tests/SmokeTests/SmokeTests.csproj @@ -0,0 +1,67 @@ +๏ปฟ + + + Exe + enable + enable + SmokeTests + net8.0 + + + + false + true + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + diff --git a/Sdk/tests/SmokeTests/VarsToTest.cs b/Sdk/tests/SmokeTests/VarsToTest.cs new file mode 100644 index 0000000..9ef00be --- /dev/null +++ b/Sdk/tests/SmokeTests/VarsToTest.cs @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2024-2025 Scott Velez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +**/ + +using SVappsLAB.iRacingTelemetrySDK; + +namespace SmokeTests +{ + // this class is only used as a convenient place to define a + // set of shared variables that the live and ibt test classes can use + [RequiredTelemetryVars([ + TelemetryVar.dcPushToPass, // type 1 - boolean + TelemetryVar.SessionNum, // type 2 - int + TelemetryVar.CarIdxTrackSurface, // type 2 - int (ARRAY) + TelemetryVar.EngineWarnings, // type 3 - bitfield (flags) + TelemetryVar.RPM, // type 4 - float + TelemetryVar.SessionTime, // type 5 - double + TelemetryVar.CarDistAhead, // new var, doesn't exist in old ibt files + ])] + public class VarsToTest + { + } +} diff --git a/Sdk/tests/IBT_Tests/data/race_road/audir8lmsevo2gt3_spa up.ibt b/Sdk/tests/SmokeTests/data/ibt/audir8lmsevo2gt3_spa up.ibt similarity index 100% rename from Sdk/tests/IBT_Tests/data/race_road/audir8lmsevo2gt3_spa up.ibt rename to Sdk/tests/SmokeTests/data/ibt/audir8lmsevo2gt3_spa up.ibt diff --git a/Sdk/tests/SmokeTests/data/ibt/ministock_mtwashington climb 2025-03-28.ibt b/Sdk/tests/SmokeTests/data/ibt/ministock_mtwashington climb 2025-03-28.ibt new file mode 100644 index 0000000..4560c7f --- /dev/null +++ b/Sdk/tests/SmokeTests/data/ibt/ministock_mtwashington climb 2025-03-28.ibt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6003cb90fa788cc3fba2c7cbd53c842bb586622546dbe846b3dd56ce0384e86 +size 21048183 diff --git a/Sdk/tests/SmokeTests/data/ibt/mx5 cup_okayama full 2011-05-13.ibt b/Sdk/tests/SmokeTests/data/ibt/mx5 cup_okayama full 2011-05-13.ibt new file mode 100644 index 0000000..aaba128 --- /dev/null +++ b/Sdk/tests/SmokeTests/data/ibt/mx5 cup_okayama full 2011-05-13.ibt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:286fbb494316db72fce7fd1076e92eb8980095a984c549db818f3968ceb018a6 +size 7944545 diff --git a/Sdk/tests/SmokeTests/data/ibt/raygr22_roadatlanta full 2025-07-15.ibt b/Sdk/tests/SmokeTests/data/ibt/raygr22_roadatlanta full 2025-07-15.ibt new file mode 100644 index 0000000..d030ae3 --- /dev/null +++ b/Sdk/tests/SmokeTests/data/ibt/raygr22_roadatlanta full 2025-07-15.ibt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3cf478eac64b7bb9b79a71dbb31b9133708b22ea41bea5fa591c48dcab2a96d2 +size 453675 diff --git a/Sdk/tests/IBT_Tests/data/race_road/audir8lmsevo2gt3_spa up.yaml b/Sdk/tests/SmokeTests/data/yaml/audir8lmsevo2gt3_spa up.yaml similarity index 100% rename from Sdk/tests/IBT_Tests/data/race_road/audir8lmsevo2gt3_spa up.yaml rename to Sdk/tests/SmokeTests/data/yaml/audir8lmsevo2gt3_spa up.yaml diff --git a/Sdk/tests/IBT_Tests/data/yaml/invalid_data.yaml b/Sdk/tests/SmokeTests/data/yaml/invalid_data.yaml similarity index 100% rename from Sdk/tests/IBT_Tests/data/yaml/invalid_data.yaml rename to Sdk/tests/SmokeTests/data/yaml/invalid_data.yaml diff --git a/Sdk/tests/UnitTests/IBTPlaybackGovernor.cs b/Sdk/tests/UnitTests/IBTPlaybackGovernor.cs index 7e7f238..ee2dd5d 100644 --- a/Sdk/tests/UnitTests/IBTPlaybackGovernor.cs +++ b/Sdk/tests/UnitTests/IBTPlaybackGovernor.cs @@ -11,10 +11,12 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; + * limitations under the License. **/ +using System; using System.Diagnostics; +using System.Threading.Tasks; #if DEBUG using Microsoft.Extensions.Logging; diff --git a/Sdk/tests/UnitTests/UnitTests.csproj b/Sdk/tests/UnitTests/UnitTests.csproj index f7cf45d..a2b19aa 100644 --- a/Sdk/tests/UnitTests/UnitTests.csproj +++ b/Sdk/tests/UnitTests/UnitTests.csproj @@ -1,36 +1,47 @@ -๏ปฟ - - - Exe - net8.0 - enable - enable - true + + + + Exe + enable + enable + net8.0 + + + + false + true - - - - - - - - PreserveNewest - - - PreserveNewest - - - - - - - - - - - - - - - + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/Sdk/tests/UnitTests/YamlParsing.cs b/Sdk/tests/UnitTests/YamlParsing.cs index 0f88d53..507db52 100644 --- a/Sdk/tests/UnitTests/YamlParsing.cs +++ b/Sdk/tests/UnitTests/YamlParsing.cs @@ -11,14 +11,16 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and - * limitations under the License.using Microsoft.CodeAnalysis; + * limitations under the License. **/ // disable for file #pragma warning disable CS8602 +using System; +using System.IO; +using System.Reflection; using SVappsLAB.iRacingTelemetrySDK; -using SVappsLAB.iRacingTelemetrySDK.Models; using Xunit; namespace UnitTests