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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.1.15-beta] - 2025-09-15

### Added

- SimVar subscription API for streaming recurring simulator data.
- Subscribe to a struct model (single cached definition) or a single SimVar by name/unit.
- Both overloads return an `ISimVarSubscription` you can dispose to stop updates.

### Fixed

- Resolved a crash observed in tests by refactoring subscribed input event processing:
- Treat `SimConnectRecvSubscribeInputEvent` as a header; the value payload follows immediately in memory.
- `ProcessSubscribeInputEvent` now computes the payload pointer and size from the header, then extracts the value robustly for doubles and strings.
- Added `[StructLayout(LayoutKind.Sequential)]` to `SimConnectRecvSubscribeInputEvent` to guarantee interop layout.
- Removed the `Value` field from `SimConnectRecvSubscribeInputEvent` to clarify it is header-only.

### Changed

- `SimConnectAttribute` constructors are more flexible: optional/nullable `unit`, `dataType`, and `order`; avoids throwing when a SimVar isn't found in the registry.
- Internal refactor for performance and clarity: new field readers and request/subscription types to optimize hot-path handling.

### Notes

- Thanks to @bstudtma for the contribution in PR #10.

## [0.1.14-beta] - 2025-08-25

### Added
Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>0.1.14-beta</Version>
<Version>0.1.15-beta</Version>
<Authors>BARS</Authors>
<Company>BARS</Company>
<Product>SimConnect.NET</Product>
Expand Down
28 changes: 24 additions & 4 deletions src/SimConnect.NET/InputEvents/InputEventManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -725,18 +725,38 @@ private void ProcessSubscribeInputEvent(IntPtr ppData)
{
var recvSubscribe = Marshal.PtrToStructure<SimConnectRecvSubscribeInputEvent>(ppData);

// Extract value based on the type, similar to how it's done in ProcessGetInputEvent
// Compute pointer to the payload (value) directly after the header
int headerSize = Marshal.SizeOf<SimConnectRecvSubscribeInputEvent>();
int totalSize = checked((int)recvSubscribe.Size);
int payloadSize = totalSize - headerSize;
IntPtr pValue = IntPtr.Add(ppData, headerSize);

// Extract value based on the type, mirroring GetInputEvent
object value;
switch (recvSubscribe.Type)
{
case SimConnectInputEventType.DoubleValue:
value = Marshal.PtrToStructure<double>(recvSubscribe.Value);
value = payloadSize >= sizeof(double)
? Marshal.PtrToStructure<double>(pValue)
: 0d;
break;
case SimConnectInputEventType.StringValue:
value = Marshal.PtrToStringAnsi(recvSubscribe.Value) ?? string.Empty;
if (payloadSize > 0)
{
byte[] buf = new byte[payloadSize];
Marshal.Copy(pValue, buf, 0, buf.Length);
int nul = Array.IndexOf(buf, (byte)0);
int len = nul >= 0 ? nul : buf.Length;
value = Encoding.ASCII.GetString(buf, 0, len);
}
else
{
value = string.Empty;
}

break;
default:
value = recvSubscribe.Value.ToInt64();
value = null!;
break;
}

Expand Down
27 changes: 18 additions & 9 deletions src/SimConnect.NET/SimConnectAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public SimConnectAttribute(string name, string unit)
}
else
{
throw new ArgumentException($"SimVar '{name}' not found in registry. Please specify unit and dataType explicitly.", nameof(name));
// throw new ArgumentException($"SimVar '{name}' not found in registry. Please specify unit and dataType explicitly.", nameof(name));
}
}

Expand All @@ -46,10 +46,6 @@ public SimConnectAttribute(string name)
this.Unit = simVar.Unit;
this.DataType = simVar.DataType;
}
else
{
throw new ArgumentException($"SimVar '{name}' not found in registry. Please specify unit and dataType explicitly.", nameof(name));
}
}

/// <summary>
Expand All @@ -58,7 +54,7 @@ public SimConnectAttribute(string name)
/// <param name="name">The SimVar name to marshal.</param>
/// <param name="unit">The unit of the SimVar.</param>
/// <param name="dataType">The SimConnect data type for marshaling.</param>
public SimConnectAttribute(string name, string unit, SimConnectDataType dataType)
public SimConnectAttribute(string name, string? unit, SimConnectDataType dataType)
{
this.Name = name;
this.Unit = unit;
Expand All @@ -72,14 +68,27 @@ public SimConnectAttribute(string name, string unit, SimConnectDataType dataType
/// <param name="unit">The unit of the SimVar.</param>
/// <param name="dataType">The SimConnect data type for marshaling.</param>
/// <param name="order">The order in which the SimVar should be marshaled.</param>
public SimConnectAttribute(string name, string? unit, SimConnectDataType dataType, int order)
public SimConnectAttribute(string name, string? unit = null, SimConnectDataType? dataType = null, int? order = null)
{
this.Name = name;
this.Unit = unit;
this.DataType = dataType;
this.Order = order;
}

/// <summary>
/// Initializes a new instance of the <see cref="SimConnectAttribute"/> class with name and data type.
/// The unit is left unspecified.
/// </summary>
/// <param name="name">The SimVar name to marshal.</param>
/// <param name="dataType">The SimConnect data type for marshaling.</param>
public SimConnectAttribute(string name, SimConnectDataType dataType)
{
this.Name = name;
this.Unit = null;
this.DataType = dataType;
}

/// <summary>
/// Gets the SimVar name to marshal.
/// </summary>
Expand All @@ -93,11 +102,11 @@ public SimConnectAttribute(string name, string? unit, SimConnectDataType dataTyp
/// <summary>
/// Gets the SimConnect data type for marshaling.
/// </summary>
public SimConnectDataType DataType { get; }
public SimConnectDataType? DataType { get; }

/// <summary>
/// Gets the order in which the SimVar should be marshaled.
/// </summary>
public int Order { get; }
public int? Order { get; }
}
}
20 changes: 20 additions & 0 deletions src/SimConnect.NET/SimVar/ISimVarSubscription.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// <copyright file="ISimVarSubscription.cs" company="BARS">
// Copyright (c) BARS. All rights reserved.
// </copyright>

using System;
using System.Threading.Tasks;

namespace SimConnect.NET.SimVar
{
/// <summary>
/// Represents an active SimVar subscription that can be disposed to stop receiving data.
/// </summary>
public interface ISimVarSubscription : IDisposable
{
/// <summary>
/// Gets a task that completes when the subscription terminates (via dispose, cancellation, or error).
/// </summary>
Task Completion { get; }
}
}
28 changes: 28 additions & 0 deletions src/SimConnect.NET/SimVar/Internal/IFieldReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// <copyright file="IFieldReader.cs" company="BARS">
// Copyright (c) BARS. All rights reserved.
// </copyright>

using System;

namespace SimConnect.NET.SimVar.Internal
{
/// <summary>
/// Reads and assigns a value of type <typeparamref name="T"/> from native memory into a managed struct.
/// </summary>
/// <typeparam name="T">The value type being populated.</typeparam>
/// <remarks>
/// Renamed from IFieldAccessor to IFieldReader to better reflect its one-way responsibility (read/copy only).
/// Design note: generic only on <typeparamref name="T"/> so callers can hold a heterogeneous collection of
/// per-field readers without knowing each destination field's exact type parameter.
/// </remarks>
internal interface IFieldReader<T>
where T : struct
{
/// <summary>
/// Reads from the specified native base pointer plus the reader's offset and writes the value into the target.
/// </summary>
/// <param name="target">The struct instance being populated.</param>
/// <param name="basePtr">The base pointer to the native buffer.</param>
void ReadInto(ref T target, IntPtr basePtr);
}
}
66 changes: 66 additions & 0 deletions src/SimConnect.NET/SimVar/Internal/ISimVarRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// <copyright file="ISimVarRequest.cs" company="BARS">
// Copyright (c) BARS. All rights reserved.
// </copyright>

using System;
using System.Threading.Tasks;

namespace SimConnect.NET.SimVar.Internal
{
/// <summary>
/// Non-generic SimVar request contract for hot-path handling without reflection.
/// </summary>
internal interface ISimVarRequest
{
/// <summary>
/// Gets the unique request identifier.
/// </summary>
uint RequestId { get; }

/// <summary>
/// Gets the SimConnect object identifier targeted by this request.
/// </summary>
uint ObjectId { get; }

/// <summary>
/// Gets the data definition identifier associated with this request.
/// </summary>
uint DefinitionId { get; }

/// <summary>
/// Gets a value indicating whether this is a recurring subscription.
/// </summary>
bool IsRecurring { get; }

/// <summary>
/// Gets a task that completes when the request finishes (result, cancel, or error). For recurring requests this completes only on cancel/error.
/// </summary>
Task Completion { get; }

/// <summary>
/// Sets the result using a boxed value; the implementation converts to the expected T.
/// </summary>
/// <param name="value">The boxed value parsed from SimConnect.</param>
void SetResultBoxed(object? value);

/// <summary>
/// Completes the request with an exception.
/// </summary>
/// <param name="exception">The exception that occurred.</param>
void SetException(Exception exception);

/// <summary>
/// Cancels the request.
/// </summary>
void SetCanceled();

/// <summary>
/// Attempts to complete the request with the provided typed value without boxing.
/// Returns true only if the request's expected type matches <typeparamref name="TValue"/>.
/// </summary>
/// <typeparam name="TValue">The type of the provided value.</typeparam>
/// <param name="value">The value to set.</param>
/// <returns>True if the value type matches and was accepted; otherwise false.</returns>
bool TrySetResult<TValue>(TValue value);
}
}
102 changes: 102 additions & 0 deletions src/SimConnect.NET/SimVar/Internal/SimVarFieldReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// <copyright file="SimVarFieldReader.cs" company="BARS">
// Copyright (c) BARS. All rights reserved.
// </copyright>

using System;
using System.Runtime.InteropServices;

namespace SimConnect.NET.SimVar.Internal
{
internal delegate void Setter<TStruct, TValue>(ref TStruct target, TValue value)
where TStruct : struct; // necessary for TStruct parameter by ref, Action cannot express by ref

internal sealed class SimVarFieldReader<T, TDest> : IFieldReader<T>
where T : struct
{
public int OffsetBytes { get; set; }

public int Size { get; set; }

public SimConnectDataType DataType { get; set; }

public Setter<T, TDest> Setter { get; set; } = default!;

// Holds a typed converter matching the raw type for the chosen Kind, e.g. Func<double, TDest>.
public Delegate Converter { get; set; } = default!;

public void ReadInto(ref T target, IntPtr basePtr)
{
var addr = IntPtr.Add(basePtr, this.OffsetBytes);
switch (this.DataType)
{
case SimConnectDataType.FloatDouble:
double rawDouble = SimVarMemoryReader.ReadDouble(addr);
var convDouble = (Func<double, TDest>)this.Converter;
this.Setter(ref target, convDouble(rawDouble));
break;

case SimConnectDataType.FloatSingle:
float rawFloat = SimVarMemoryReader.ReadFloat(addr);
var convFloat = (Func<float, TDest>)this.Converter;
this.Setter(ref target, convFloat(rawFloat));
break;

case SimConnectDataType.Integer64:
long rawInt64 = SimVarMemoryReader.ReadInt64(addr);
var convInt64 = (Func<long, TDest>)this.Converter;
this.Setter(ref target, convInt64(rawInt64));
break;

case SimConnectDataType.Integer32:
int rawInt32 = SimVarMemoryReader.ReadInt32(addr);
var convInt32 = (Func<int, TDest>)this.Converter;
this.Setter(ref target, convInt32(rawInt32));
break;

case SimConnectDataType.String8:
case SimConnectDataType.String32:
case SimConnectDataType.String64:
case SimConnectDataType.String128:
case SimConnectDataType.String256:
case SimConnectDataType.String260:
string rawString = SimVarMemoryReader.ReadFixedString(addr, this.Size);
var convString = (Func<string, TDest>)this.Converter;
this.Setter(ref target, convString(rawString));
break;

case SimConnectDataType.InitPosition:
var rawInit = Marshal.PtrToStructure<SimConnectDataInitPosition>(addr);
var convInit = (Func<SimConnectDataInitPosition, TDest>)this.Converter;
this.Setter(ref target, convInit(rawInit));
break;

case SimConnectDataType.MarkerState:
var rawMarker = Marshal.PtrToStructure<SimConnectDataMarkerState>(addr);
var convMarker = (Func<SimConnectDataMarkerState, TDest>)this.Converter;
this.Setter(ref target, convMarker(rawMarker));
break;

case SimConnectDataType.Waypoint:
var rawWp = Marshal.PtrToStructure<SimConnectDataWaypoint>(addr);
var convWp = (Func<SimConnectDataWaypoint, TDest>)this.Converter;
this.Setter(ref target, convWp(rawWp));
break;

case SimConnectDataType.LatLonAlt:
var rawLatLonAlt = Marshal.PtrToStructure<SimConnectDataLatLonAlt>(addr);
var convLatLonAlt = (Func<SimConnectDataLatLonAlt, TDest>)this.Converter;
this.Setter(ref target, convLatLonAlt(rawLatLonAlt));
break;

case SimConnectDataType.Xyz:
var rawXyz = Marshal.PtrToStructure<SimConnectDataXyz>(addr);
var convXyz = (Func<SimConnectDataXyz, TDest>)this.Converter;
this.Setter(ref target, convXyz(rawXyz));
break;

default:
throw new NotSupportedException($"Unsupported SimConnectDataType {this.DataType}");
}
}
}
}
Loading