diff --git a/Samples/DumpVariables_DumpSessionInfo/DumpVariables_DumpSessionInfo.csproj b/Samples/DumpVariables_DumpSessionInfo/DumpVariables_DumpSessionInfo.csproj
index 727d1a7..b60d884 100644
--- a/Samples/DumpVariables_DumpSessionInfo/DumpVariables_DumpSessionInfo.csproj
+++ b/Samples/DumpVariables_DumpSessionInfo/DumpVariables_DumpSessionInfo.csproj
@@ -15,7 +15,7 @@
-
+
diff --git a/Samples/LocationAndWarnings/LocationAndWarnings.csproj b/Samples/LocationAndWarnings/LocationAndWarnings.csproj
index 727d1a7..b60d884 100644
--- a/Samples/LocationAndWarnings/LocationAndWarnings.csproj
+++ b/Samples/LocationAndWarnings/LocationAndWarnings.csproj
@@ -15,7 +15,7 @@
-
+
diff --git a/Samples/MinimalExample/MinimalExample.csproj b/Samples/MinimalExample/MinimalExample.csproj
index 727d1a7..b60d884 100644
--- a/Samples/MinimalExample/MinimalExample.csproj
+++ b/Samples/MinimalExample/MinimalExample.csproj
@@ -15,7 +15,7 @@
-
+
diff --git a/Samples/SpeedRPMGear/SpeedRPMGear.csproj b/Samples/SpeedRPMGear/SpeedRPMGear.csproj
index 3c9f825..772f0f3 100644
--- a/Samples/SpeedRPMGear/SpeedRPMGear.csproj
+++ b/Samples/SpeedRPMGear/SpeedRPMGear.csproj
@@ -17,7 +17,7 @@
-
+
diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK.CodeGen/SVappsLAB.iRacingTelemetrySDK.CodeGen.csproj b/Sdk/SVappsLAB.iRacingTelemetrySDK.CodeGen/SVappsLAB.iRacingTelemetrySDK.CodeGen.csproj
index 8e7e76c..63180c1 100644
--- a/Sdk/SVappsLAB.iRacingTelemetrySDK.CodeGen/SVappsLAB.iRacingTelemetrySDK.CodeGen.csproj
+++ b/Sdk/SVappsLAB.iRacingTelemetrySDK.CodeGen/SVappsLAB.iRacingTelemetrySDK.CodeGen.csproj
@@ -9,7 +9,7 @@
-
+
diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK.EnumsAndFlags/TelemetryClient_Flags.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK.EnumsAndFlags/TelemetryClient_Flags.cs
index 63fea5c..91a07d4 100644
--- a/Sdk/SVappsLAB.iRacingTelemetrySDK.EnumsAndFlags/TelemetryClient_Flags.cs
+++ b/Sdk/SVappsLAB.iRacingTelemetrySDK.EnumsAndFlags/TelemetryClient_Flags.cs
@@ -59,6 +59,7 @@ public enum SessionFlags
Serviceable = 0x00040000, // car is allowed service (not a flag)
Furled = 0x00080000,
Repair = 0x00100000,
+ DqScoringInvalid = 0x00200000, // car is disqualified and scoring is disabled
// start lights
StartHidden = 0x10000000,
diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/DataProviderBase.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/DataProviderBase.cs
index 52a9e80..e31c003 100644
--- a/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/DataProviderBase.cs
+++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/DataProviderBase.cs
@@ -1,12 +1,12 @@
/**
* Copyright (C) 2024-2026 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.
@@ -120,9 +120,8 @@ public string GetSessionInfoYaml()
var offSet = header.sessionInfoOffset;
var maxLen = header.sessionInfoLen;
- var span = new Span(_dataPtr + offSet, maxLen);
- var sessInfo = ExtractNullTerminatedString(span, maxLen);
- return sessInfo;
+ var span = new ReadOnlySpan(_dataPtr + offSet, maxLen);
+ return SessionInfoDecoder.Decode(span);
}
public object? GetVarValue(string varName)
@@ -198,6 +197,42 @@ protected void CopyNewTelemetryDataToBuffer(int recNum = 0)
ros.CopyTo(_telemetryDataBuffer);
}
+ // live telemetry can be overwritten by the sim while we read it. mirror the
+ // official sdk's torn-read detection: 'tickCountBegin' is updated before a
+ // write starts and 'tickCount' after it completes. if the tickCount read
+ // before the copy matches tickCountBegin after, no write was in progress
+ protected bool TryCopyLiveTelemetryDataToBuffer(out int validTickCount)
+ {
+ const int MAX_ATTEMPTS = 2;
+
+ // try a few times to get the data out
+ for (var attempt = 0; attempt < MAX_ATTEMPTS; attempt++)
+ {
+ var header = GetHeader();
+ var bufIndex = header.GetMostRecentBufferIndex();
+ var varBuf = header.GetVarBuf(bufIndex);
+
+ var curTickCount = varBuf.tickCount;
+ Thread.MemoryBarrier();
+
+ var ros = new ReadOnlySpan(_dataPtr + varBuf.bufOffset, header.bufLen);
+ ros.CopyTo(_telemetryDataBuffer);
+
+ Thread.MemoryBarrier();
+
+ // re-read from shared memory to see if a write was in progress
+ if (curTickCount == GetHeader().GetVarBuf(bufIndex).tickCountBegin)
+ {
+ validTickCount = curTickCount;
+ return true;
+ }
+ }
+
+ // the data changed out from under us
+ validTickCount = 0;
+ return false;
+ }
+
VarHeaderDictionary ReadVarHeaders()
{
var ros = new ReadOnlySpan(_dataPtr + _header.varHeaderOffset, _header.numVars);
diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/LiveDataProvider.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/LiveDataProvider.cs
index 93214ba..a032a8e 100644
--- a/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/LiveDataProvider.cs
+++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/LiveDataProvider.cs
@@ -19,7 +19,6 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
-using SVappsLAB.iRacingTelemetrySDK.irSDKDefines;
namespace SVappsLAB.iRacingTelemetrySDK.DataProviders
{
@@ -58,7 +57,13 @@ public override Task WaitForDataReady(TimeSpan timeSpan, CancellationToken
internal bool ProcessNewData()
{
- var latestTickCount = GetLatestVarBuff().tickCount;
+ // copy new data to the access buffer,
+ // validating data is good and no write was in progress
+ if (!TryCopyLiveTelemetryDataToBuffer(out var latestTickCount))
+ {
+ _logger.LogWarning("data changed while we were reading it. skipping this sample");
+ return false;
+ }
// if we missed any telemetry data, log that it happened
if (latestTickCount > _lastTickCount)
@@ -67,7 +72,7 @@ internal bool ProcessNewData()
if (_lastTickCount != 0 && tickDiff > 0)
{
_dataDropCount += tickDiff;
- _logger.LogWarning("dropped {count} data records. {total} total. last tick: {lastTick}, current tick: {currentTick}", tickDiff, _dataDropCount, _lastTickCount, latestTickCount);
+ _logger.LogWarning("dropped {count} data records. a total of {total} missed so far. last tick: {lastTick}, current tick: {currentTick}", tickDiff, _dataDropCount, _lastTickCount, latestTickCount);
}
}
@@ -78,28 +83,11 @@ internal bool ProcessNewData()
_logger.LogDebug("new data is older than our last sample. lost connection? will resync");
}
- // copy new data to the access buffer for later reading
- CopyNewTelemetryDataToBuffer();
// resync - update our last tick count
_lastTickCount = latestTickCount;
return true;
}
-
- irsdk_varBuf GetLatestVarBuff()
- {
- var header = GetHeader();
-
- var vb = header.varBuf1;
- if (header.varBuf2.tickCount > vb.tickCount)
- vb = header.varBuf2;
- if (header.varBuf3.tickCount > vb.tickCount)
- vb = header.varBuf3;
- if (header.varBuf4.tickCount > vb.tickCount)
- vb = header.varBuf4;
- return vb;
-
- }
public override ValueTask DisposeAsync()
{
if (_dataReadyEvent != null)
diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/SessionInfoDecoder.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/SessionInfoDecoder.cs
new file mode 100644
index 0000000..57825b7
--- /dev/null
+++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/DataProviders/SessionInfoDecoder.cs
@@ -0,0 +1,61 @@
+/**
+ * Copyright (C) 2024-2026 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.Text;
+
+namespace SVappsLAB.iRacingTelemetrySDK.DataProviders
+{
+ // Decodes the raw session-info bytes from the memory-mapped file / IBT file
+ // into a string, depending on the WeekendInfo:Encoding: tag (UTF8).
+ internal static class SessionInfoDecoder
+ {
+ private static readonly byte[] EncodingKey = Encoding.ASCII.GetBytes("Encoding:");
+ private static readonly byte[] Utf8Value = Encoding.ASCII.GetBytes("UTF8");
+
+ ///
+ /// Decode using the encoding declared by the WeekendInfo:Encoding: tag
+ /// Older session info without "UTF8" use ISO-8859-1
+ ///
+ public static string Decode(ReadOnlySpan raw)
+ {
+ // trim at the first null terminator
+ var nullIdx = raw.IndexOf((byte)0);
+ if (nullIdx >= 0)
+ raw = raw.Slice(0, nullIdx);
+
+ if (IsUtf8(raw))
+ return Encoding.UTF8.GetString(raw);
+
+ return Encoding.Latin1.GetString(raw);
+ }
+
+ public static bool IsUtf8(ReadOnlySpan raw)
+ {
+ var keyIdx = raw.IndexOf(EncodingKey);
+ if (keyIdx < 0)
+ return false;
+
+ // the value is on the same line, immediately after the key
+ var value = raw.Slice(keyIdx + EncodingKey.Length);
+ var eol = value.IndexOf((byte)'\n');
+ if (eol >= 0)
+ value = value.Slice(0, eol);
+
+ return value.IndexOf(Utf8Value) >= 0;
+ }
+ }
+}
diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/DriverInfo.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/DriverInfo.cs
index ed81a4b..b0bb07d 100644
--- a/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/DriverInfo.cs
+++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/DriverInfo.cs
@@ -24,6 +24,7 @@ public class DriverInfo
public int DriverCarIdx { get; set; } // 0
public int DriverUserID { get; set; } // 22176
public int PaceCarIdx { get; set; } // -1
+ public int DriverIsAdmin { get; set; } // 0/1 - the player is an admin and can have access to the admin chat commands
public float DriverHeadPosX { get; set; } // in units of length (mm, m, etc.)
public float DriverHeadPosY { get; set; } // in units of length (mm, m, etc.)
public float DriverHeadPosZ { get; set; } // in units of length (mm, m, etc.)
@@ -38,6 +39,9 @@ public class DriverInfo
public int DriverCarGearNumForward { get; set; } // 6
public int DriverCarGearNeutral { get; set; } // 1
public int DriverCarGearReverse { get; set; } // 1
+ public string DriverGearboxType { get; set; } // Sequential/H-Pattern/Automatic/Unknown - transmission type in virtual car
+ public string DriverGearboxControlType { get; set; } // Sequential/H-Pattern/H-Pattern Direct/Automatic/Unknown - physical shifter type player is using
+ public string DriverCarShiftAid { get; set; } // Manual/Antistall/Antistall_Clutch/Antistall_Clutch_Throttle/Automatic - type of shift aids the driver has turned on
public float DriverCarSLFirstRPM { get; set; } // 5600.000
public float DriverCarSLShiftRPM { get; set; } // 7200.000
public float DriverCarSLLastRPM { get; set; } // 7200.000
@@ -51,12 +55,8 @@ public class DriverInfo
public int DriverSetupPassedTech { get; set; } // 1
public int DriverIncidentCount { get; set; } // 0
public float DriverBrakeCurvingFactor { get; set; } // 0.001
- public int DriverIsAdmin { get; set; } // 0/1 - the player is an admin and can have access to the admin chat commands
- public string DriverGearboxType { get; set; } // Sequential/H-Pattern/Automatic/Unknown - transmission type in virtual car
- public string DriverGearboxControlType { get; set; } // Sequential/H-Pattern/H-Pattern Direct/Automatic/Unknown - physical shifter type player is using
- public string DriverCarShiftAid { get; set; } // Manual/Antistall/Antistall_Clutch/Antistall_Clutch_Throttle/Automatic - type of shift aids the driver has turned on
- public List Drivers { get; set; }
public List DriverTires { get; set; }
+ public List Drivers { get; set; }
}
public class Driver
diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/WeekendInfo.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/WeekendInfo.cs
index c59b20a..80ffd2f 100644
--- a/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/WeekendInfo.cs
+++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/Models/WeekendInfo.cs
@@ -1,12 +1,12 @@
/**
* Copyright (C) 2024-2026 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.
@@ -21,6 +21,7 @@ namespace SVappsLAB.iRacingTelemetrySDK
{
public class WeekendInfo
{
+ public string Encoding { get; set; } // "UTF8"
public string TrackName { get; set; } // spa up
public int TrackID { get; set; } // 143
public string TrackLength { get; set; } // 6.93 km (10cm accuracy)
@@ -116,7 +117,7 @@ public class WeekendOptions
public string IncidentWarningInitialLimit { get; set; } // 'unlimited' or '0'..'n
public object IncidentWarningSubsequentLimit { get; set; } // 'unlimited' or '0'..'n'
public string FastRepairsLimit { get; set; } // 'unlimited' or '0'..'n'
- public string GreenWhiteCheckeredLimit { get; set; } //'unlimited' or '0'..'n'
+ public string GreenWhiteCheckeredLimit { get; set; } //'unlimited' or '0'..'n'
}
diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/SVappsLAB.iRacingTelemetrySDK.csproj b/Sdk/SVappsLAB.iRacingTelemetrySDK/SVappsLAB.iRacingTelemetrySDK.csproj
index c5be267..5e3248e 100644
--- a/Sdk/SVappsLAB.iRacingTelemetrySDK/SVappsLAB.iRacingTelemetrySDK.csproj
+++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/SVappsLAB.iRacingTelemetrySDK.csproj
@@ -27,9 +27,9 @@
-
-
-
+
+
+
@@ -47,7 +47,7 @@
iRacing iRacingSDK irsdk IBT Telemetry SDK API
NUGET.md
LICENSE
- 2.0.0
+ 2.1.0
Scott Velez
SVappsLAB
Copyright (c) 2026 SVappsLAB
diff --git a/Sdk/SVappsLAB.iRacingTelemetrySDK/irSDK_defines.cs b/Sdk/SVappsLAB.iRacingTelemetrySDK/irSDK_defines.cs
index ab9614f..d682667 100644
--- a/Sdk/SVappsLAB.iRacingTelemetrySDK/irSDK_defines.cs
+++ b/Sdk/SVappsLAB.iRacingTelemetrySDK/irSDK_defines.cs
@@ -1,12 +1,12 @@
/**
* Copyright (C) 2024-2026 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.
@@ -24,7 +24,15 @@ internal static class Constants
{
public const int IRSDK_MAX_BUFS = 4;
public const int IRSDK_MAX_STRING = 32;
+ // descriptions can be longer than max_string!
public const int IRSDK_MAX_DESC = 64;
+
+ // define markers for unlimited session lap and time
+ public const int IRSDK_UNLIMITED_LAPS = 32767;
+ public const float IRSDK_UNLIMITED_TIME = 604800.0f;
+
+ // latest version of our telemetry headers
+ public const int IRSDK_VER = 2;
}
internal enum irsdk_VarType : Int32
@@ -47,28 +55,33 @@ internal enum irsdk_VarType : Int32
[StructLayout(LayoutKind.Sequential, Pack = 4)]
internal struct irsdk_varBuf
{
- public int tickCount;
- public int bufOffset;
- public int pad1;
- public int pad2;
+ public int tickCount; // used to detect changes in data (updated AFTER write completes)
+ public int bufOffset; // offset from header
+ public int tickCountBegin; // updated BEFORE write starts (for torn read detection)
+ public int pad; // (16 byte align)
}
[StructLayout(LayoutKind.Sequential, Pack = 4)]
- internal struct irsdk_header
+ internal unsafe struct irsdk_header
{
- public int ver;
- public irsdk_StatusField status;
- public int tickRate;
- public int sessionInfoUpdate;
- public int sessionInfoLen;
- public int sessionInfoOffset;
- public int numVars;
- public int varHeaderOffset;
- public int numBuf;
- public int bufLen;
- // padding
- public int pad1;
- public int pad2;
+ public int ver; // this api header version, see IRSDK_VER
+ public irsdk_StatusField status;// bitfield using irsdk_StatusField
+ public int tickRate; // ticks per second (60 or 360 etc)
+
+ // session information, updated periodically
+ public int sessionInfoUpdate; // Incremented when session info changes
+ public int sessionInfoLen; // Length in bytes of session info string
+ public int sessionInfoOffset; // Session info, encoded in YAML format
+
+ // State data, output at tickRate
+ public int numVars; // length of array pointed to by varHeaderOffset
+ public int varHeaderOffset; // offset to irsdk_varHeader[numVars] array
+
+ public int numBuf; // <= IRSDK_MAX_BUFS (3 for now)
+ public int bufLen; // length in bytes for one line
+ public int curBufTickCount; // stashed copy of the current tickCount, can read this to see if new data is available
+ public byte curBuf; // index of the most recently written buffer (0 to IRSDK_MAX_BUFS-1)
+ public fixed byte pad1[3]; // 16 byte align
// if we don't use an array here. allows us to read this structure directly from unmanaged memory
public irsdk_varBuf varBuf1;
@@ -77,17 +90,27 @@ internal struct irsdk_header
public irsdk_varBuf varBuf4;
#region methods
- public irsdk_varBuf GetMostRecentBuffer()
+ public irsdk_varBuf GetMostRecentBuffer() => GetVarBuf(GetMostRecentBufferIndex());
+
+ public int GetMostRecentBufferIndex()
{
- var vb = varBuf1;
- if (varBuf2.tickCount > vb.tickCount)
- vb = varBuf2;
- if (varBuf3.tickCount > vb.tickCount)
- vb = varBuf3;
- if (varBuf4.tickCount > vb.tickCount)
- vb = varBuf4;
- return vb;
+ // use curBuf to find the most recently written buffer
+ int index = curBuf;
+ if (index >= Math.Min(numBuf, Constants.IRSDK_MAX_BUFS))
+ index = 0;
+ return index;
}
+
+ // varBuf is exposed as discrete fields (so the header can be read directly
+ // from unmanaged memory). this helper provides access
+ public irsdk_varBuf GetVarBuf(int index) => index switch
+ {
+ 0 => varBuf1,
+ 1 => varBuf2,
+ 2 => varBuf3,
+ 3 => varBuf4,
+ _ => throw new ArgumentOutOfRangeException(nameof(index)),
+ };
#endregion
}
diff --git a/Sdk/tests/SmokeTests/Base/Base.Models.cs b/Sdk/tests/SmokeTests/Base/Base.Models.cs
index bcbd6a7..cf09815 100644
--- a/Sdk/tests/SmokeTests/Base/Base.Models.cs
+++ b/Sdk/tests/SmokeTests/Base/Base.Models.cs
@@ -30,28 +30,29 @@ protected async Task BaseVerifyModelMatchesRawYaml(ITelemetryClient? missingProperties = null;
- var rawSessionTask = Task.Run(async () =>
- {
- await foreach (var rawYaml in client.SessionDataYaml)
+ await client.Monitor(
+ new TelemetryHandlers
{
- sessionInfoReceived = true;
-
- var allMissingProperties = ValidateModelAgainstYaml(rawYaml);
+ OnRawSessionInfoUpdate = rawYaml =>
+ {
+ // only need one sample
+ if (sessionInfoReceived)
+ return Task.CompletedTask;
- // skip 'CarSetup' properties since they are dynamic and can vary widely
- missingProperties = allMissingProperties
- .Where(prop => !prop.StartsWith("CarSetup"))
- .ToList();
+ sessionInfoReceived = true;
- cts.Cancel();
- break; // exit after first item
- }
- });
+ var allMissingProperties = ValidateModelAgainstYaml(rawYaml);
- // Start monitoring
- var monitorTask = client.Monitor(cts.Token);
+ // skip 'CarSetup' properties since they are dynamic and can vary widely
+ missingProperties = allMissingProperties
+ .Where(prop => !prop.StartsWith("CarSetup"))
+ .ToList();
- await Task.WhenAll(rawSessionTask, monitorTask);
+ cts.Cancel();
+ return Task.CompletedTask;
+ },
+ },
+ cts.Token);
Assert.True(sessionInfoReceived, "Session info was not received within the timeout period.");
diff --git a/Sdk/tests/SmokeTests/Base/Base.Variables.cs b/Sdk/tests/SmokeTests/Base/Base.Variables.cs
index c348dd5..26d2370 100644
--- a/Sdk/tests/SmokeTests/Base/Base.Variables.cs
+++ b/Sdk/tests/SmokeTests/Base/Base.Variables.cs
@@ -27,36 +27,36 @@ protected async Task BaseVerifyAllVariablesCovered(ITelemetryClient? allMissingVariables = null;
- var telemetryTask = Task.Run(async () =>
- {
- await foreach (var telemetryData in client.TelemetryData)
+ await client.Monitor(
+ new TelemetryHandlers
{
- variablesReceived = true;
-
- // get all available variable definitions from iRacing
- var availableVariables = client.GetTelemetryVariables();
- var availableVariableNames = availableVariables.Select(v => v.Name).ToHashSet();
+ OnTelemetryUpdate = _ =>
+ {
+ // only need one sample
+ if (variablesReceived)
+ return Task.CompletedTask;
- // get all TelemetryVar enum values
- var enumVariables = Enum.GetValues()
- .Select(e => e.ToString())
- .ToHashSet();
+ variablesReceived = true;
- // find variables that exist in iRacing but not in our enum (keep full variable info)
- allMissingVariables = availableVariables
- .Where(v => !enumVariables.Contains(v.Name))
- .OrderBy(v => v.Name)
- .ToList();
+ // get all available variable definitions from iRacing
+ var availableVariables = client.GetTelemetryVariables();
- cts.Cancel();
- break; // exit after first item
- }
- });
+ // get all TelemetryVar enum values
+ var enumVariables = Enum.GetValues()
+ .Select(e => e.ToString())
+ .ToHashSet();
- // start monitoring
- var monitorTask = client.Monitor(cts.Token);
+ // find variables that exist in iRacing but not in our enum (keep full variable info)
+ allMissingVariables = availableVariables
+ .Where(v => !enumVariables.Contains(v.Name))
+ .OrderBy(v => v.Name)
+ .ToList();
- await Task.WhenAll(telemetryTask, monitorTask);
+ cts.Cancel();
+ return Task.CompletedTask;
+ },
+ },
+ cts.Token);
Assert.True(variablesReceived, "Telemetry data was not received within the timeout period.");
diff --git a/Sdk/tests/SmokeTests/SmokeTests.csproj b/Sdk/tests/SmokeTests/SmokeTests.csproj
index 69b594d..37e1f71 100644
--- a/Sdk/tests/SmokeTests/SmokeTests.csproj
+++ b/Sdk/tests/SmokeTests/SmokeTests.csproj
@@ -1,4 +1,4 @@
-
+
Exe
@@ -11,6 +11,7 @@
false
true
+ $(NoWarn);CS0436
@@ -54,8 +55,8 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
diff --git a/Sdk/tests/UnitTests/DataProviders/SessionInfoDecoderTests.cs b/Sdk/tests/UnitTests/DataProviders/SessionInfoDecoderTests.cs
new file mode 100644
index 0000000..94d32ce
--- /dev/null
+++ b/Sdk/tests/UnitTests/DataProviders/SessionInfoDecoderTests.cs
@@ -0,0 +1,66 @@
+/**
+ * Copyright (C) 2024-2026 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.Collections.Generic;
+using System.Text;
+using SVappsLAB.iRacingTelemetrySDK.DataProviders;
+
+namespace UnitTests.DataProviders
+{
+ public class SessionInfoDecoderTests
+ {
+ // "José Muñoz" differs between UTF-8 and Latin1; "René" round-trips through Latin1
+ const string MultiByteName = "José Muñoz";
+ const string Latin1Name = "René";
+
+ static byte[] BuildSessionInfo(string? encodingTagValue, string driverName, Encoding payloadEncoding)
+ {
+ var sb = new StringBuilder();
+ sb.Append("---\n");
+ sb.Append("WeekendInfo:\n");
+ if (encodingTagValue != null)
+ sb.Append($" Encoding: {encodingTagValue}\n");
+ sb.Append("DriverInfo:\n");
+ sb.Append(" Drivers:\n");
+ sb.Append($" - UserName: {driverName}\n");
+ sb.Append("...\n");
+
+ return payloadEncoding.GetBytes(sb.ToString());
+ }
+
+ [Fact]
+ public void Utf8Tag_DecodesMultibyteCorrectly()
+ {
+ var bytes = BuildSessionInfo("UTF8", MultiByteName, Encoding.UTF8);
+
+ Assert.True(SessionInfoDecoder.IsUtf8(bytes));
+
+ var result = SessionInfoDecoder.Decode(bytes);
+ Assert.Contains($"UserName: {MultiByteName}", result);
+ }
+
+ [Fact]
+ public void NoTag_DefaultsToIso8859()
+ {
+ var bytes = BuildSessionInfo(encodingTagValue: null, Latin1Name, Encoding.Latin1);
+
+ Assert.False(SessionInfoDecoder.IsUtf8(bytes));
+
+ var result = SessionInfoDecoder.Decode(bytes);
+ Assert.Contains($"UserName: {Latin1Name}", result);
+ }
+ }
+}
diff --git a/Sdk/tests/UnitTests/TelemetryVarDictionaryTests.cs b/Sdk/tests/UnitTests/TelemetryVarDictionaryTests.cs
new file mode 100644
index 0000000..fb68286
--- /dev/null
+++ b/Sdk/tests/UnitTests/TelemetryVarDictionaryTests.cs
@@ -0,0 +1,57 @@
+/**
+ * Copyright (C) 2024-2026 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 UnitTests
+{
+ // TelemetryVar enum and the iRacingVars dictionary are maintained by hand.
+ // the compiler guarantees every dictionary entry has an enum key, but not the
+ // reverse - an enum member without a dictionary entry has no codegen metadata.
+ // these tests keep the two in sync
+ public class TelemetryVarDictionaryTests
+ {
+ [Fact]
+ public void EveryEnumValueHasDictionaryEntry()
+ {
+ var vars = new iRacingVars().Vars;
+
+ var missing = Enum.GetValues()
+ .Where(v => !vars.ContainsKey(v))
+ .Select(v => v.ToString())
+ .ToList();
+
+ Assert.True(missing.Count == 0,
+ $"found {missing.Count} TelemetryVar enum values missing from the iRacingVars dictionary: {string.Join(", ", missing)}");
+ }
+
+ [Fact]
+ public void EveryDictionaryEntryNameMatchesItsEnumKey()
+ {
+ var vars = new iRacingVars().Vars;
+
+ // the VarItem name string is what's used at runtime to look up the
+ // variable in the iRacing data, so a typo would silently break the lookup
+ var mismatched = vars
+ .Where(kvp => kvp.Key.ToString() != kvp.Value.Name)
+ .Select(kvp => $"key={kvp.Key}, name=\"{kvp.Value.Name}\"")
+ .ToList();
+
+ Assert.True(mismatched.Count == 0,
+ $"found {mismatched.Count} iRacingVars entries whose Name doesn't match their enum key: {string.Join("; ", mismatched)}");
+ }
+ }
+}
diff --git a/Sdk/tests/UnitTests/UnitTests.csproj b/Sdk/tests/UnitTests/UnitTests.csproj
index 9207687..8343414 100644
--- a/Sdk/tests/UnitTests/UnitTests.csproj
+++ b/Sdk/tests/UnitTests/UnitTests.csproj
@@ -30,7 +30,7 @@
-
+
@@ -42,6 +42,9 @@
+
+
+