Skip to content

Commit 1fc1d7f

Browse files
bstudtmaCopilotAussieScorcher
authored
Add SimVar subscription API and refactor struct handling (#10)
* Add SimVar subscription API and refactor struct handling Replaces SimVarStructBinder with a new reflection-based SimVarFieldReaderFactory and related internal readers for efficient struct marshalling. Introduces ISimVarRequest and ISimVarSubscription interfaces, refactors SimVarManager to use these for hot-path request/response handling, and adds a strongly-typed subscription API for recurring SimVar updates. Updates SimConnectAttribute to support more flexible constructor overloads and nullable properties. * Update src/SimConnect.NET/SimVar/SimVarManager.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/SimConnect.NET/SimVar/Internal/SimVarRequest.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/SimConnect.NET/SimVar/Internal/SimVarRequest.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/SimConnect.NET/SimVar/SimVarManager.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update tests/SimConnect.NET.Tests.Net8/Tests/SimVarTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Remove extra blank line in SimVarRequest.cs Deleted an unnecessary blank line for improved code formatting and readability. * Refactor input event handling to improve payload extraction and update struct documentation to prevent test crash * Update version to 0.1.16-beta in Directory.Build.props and enhance CHANGELOG with new features, fixes, and notes * Removed inline comments per Copilot suggestion: SimVarRequest.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix version number in CHANGELOG.md * Downgrade version from 0.1.16-beta to 0.1.15-beta --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: AussieScorcher <edward@stopbars.com>
1 parent 0417e30 commit 1fc1d7f

18 files changed

Lines changed: 1453 additions & 589 deletions

CHANGELOG.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.1.15-beta] - 2025-09-15
11+
12+
### Added
13+
14+
- SimVar subscription API for streaming recurring simulator data.
15+
- Subscribe to a struct model (single cached definition) or a single SimVar by name/unit.
16+
- Both overloads return an `ISimVarSubscription` you can dispose to stop updates.
17+
18+
### Fixed
19+
20+
- Resolved a crash observed in tests by refactoring subscribed input event processing:
21+
- Treat `SimConnectRecvSubscribeInputEvent` as a header; the value payload follows immediately in memory.
22+
- `ProcessSubscribeInputEvent` now computes the payload pointer and size from the header, then extracts the value robustly for doubles and strings.
23+
- Added `[StructLayout(LayoutKind.Sequential)]` to `SimConnectRecvSubscribeInputEvent` to guarantee interop layout.
24+
- Removed the `Value` field from `SimConnectRecvSubscribeInputEvent` to clarify it is header-only.
25+
26+
### Changed
27+
28+
- `SimConnectAttribute` constructors are more flexible: optional/nullable `unit`, `dataType`, and `order`; avoids throwing when a SimVar isn't found in the registry.
29+
- Internal refactor for performance and clarity: new field readers and request/subscription types to optimize hot-path handling.
30+
31+
### Notes
32+
33+
- Thanks to @bstudtma for the contribution in PR #10.
34+
1035
## [0.1.14-beta] - 2025-08-25
1136

1237
### Added

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project>
22
<PropertyGroup>
3-
<Version>0.1.14-beta</Version>
3+
<Version>0.1.15-beta</Version>
44
<Authors>BARS</Authors>
55
<Company>BARS</Company>
66
<Product>SimConnect.NET</Product>

src/SimConnect.NET/InputEvents/InputEventManager.cs

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -725,18 +725,38 @@ private void ProcessSubscribeInputEvent(IntPtr ppData)
725725
{
726726
var recvSubscribe = Marshal.PtrToStructure<SimConnectRecvSubscribeInputEvent>(ppData);
727727

728-
// Extract value based on the type, similar to how it's done in ProcessGetInputEvent
728+
// Compute pointer to the payload (value) directly after the header
729+
int headerSize = Marshal.SizeOf<SimConnectRecvSubscribeInputEvent>();
730+
int totalSize = checked((int)recvSubscribe.Size);
731+
int payloadSize = totalSize - headerSize;
732+
IntPtr pValue = IntPtr.Add(ppData, headerSize);
733+
734+
// Extract value based on the type, mirroring GetInputEvent
729735
object value;
730736
switch (recvSubscribe.Type)
731737
{
732738
case SimConnectInputEventType.DoubleValue:
733-
value = Marshal.PtrToStructure<double>(recvSubscribe.Value);
739+
value = payloadSize >= sizeof(double)
740+
? Marshal.PtrToStructure<double>(pValue)
741+
: 0d;
734742
break;
735743
case SimConnectInputEventType.StringValue:
736-
value = Marshal.PtrToStringAnsi(recvSubscribe.Value) ?? string.Empty;
744+
if (payloadSize > 0)
745+
{
746+
byte[] buf = new byte[payloadSize];
747+
Marshal.Copy(pValue, buf, 0, buf.Length);
748+
int nul = Array.IndexOf(buf, (byte)0);
749+
int len = nul >= 0 ? nul : buf.Length;
750+
value = Encoding.ASCII.GetString(buf, 0, len);
751+
}
752+
else
753+
{
754+
value = string.Empty;
755+
}
756+
737757
break;
738758
default:
739-
value = recvSubscribe.Value.ToInt64();
759+
value = null!;
740760
break;
741761
}
742762

src/SimConnect.NET/SimConnectAttribute.cs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public SimConnectAttribute(string name, string unit)
2828
}
2929
else
3030
{
31-
throw new ArgumentException($"SimVar '{name}' not found in registry. Please specify unit and dataType explicitly.", nameof(name));
31+
// throw new ArgumentException($"SimVar '{name}' not found in registry. Please specify unit and dataType explicitly.", nameof(name));
3232
}
3333
}
3434

@@ -46,10 +46,6 @@ public SimConnectAttribute(string name)
4646
this.Unit = simVar.Unit;
4747
this.DataType = simVar.DataType;
4848
}
49-
else
50-
{
51-
throw new ArgumentException($"SimVar '{name}' not found in registry. Please specify unit and dataType explicitly.", nameof(name));
52-
}
5349
}
5450

5551
/// <summary>
@@ -58,7 +54,7 @@ public SimConnectAttribute(string name)
5854
/// <param name="name">The SimVar name to marshal.</param>
5955
/// <param name="unit">The unit of the SimVar.</param>
6056
/// <param name="dataType">The SimConnect data type for marshaling.</param>
61-
public SimConnectAttribute(string name, string unit, SimConnectDataType dataType)
57+
public SimConnectAttribute(string name, string? unit, SimConnectDataType dataType)
6258
{
6359
this.Name = name;
6460
this.Unit = unit;
@@ -72,14 +68,27 @@ public SimConnectAttribute(string name, string unit, SimConnectDataType dataType
7268
/// <param name="unit">The unit of the SimVar.</param>
7369
/// <param name="dataType">The SimConnect data type for marshaling.</param>
7470
/// <param name="order">The order in which the SimVar should be marshaled.</param>
75-
public SimConnectAttribute(string name, string? unit, SimConnectDataType dataType, int order)
71+
public SimConnectAttribute(string name, string? unit = null, SimConnectDataType? dataType = null, int? order = null)
7672
{
7773
this.Name = name;
7874
this.Unit = unit;
7975
this.DataType = dataType;
8076
this.Order = order;
8177
}
8278

79+
/// <summary>
80+
/// Initializes a new instance of the <see cref="SimConnectAttribute"/> class with name and data type.
81+
/// The unit is left unspecified.
82+
/// </summary>
83+
/// <param name="name">The SimVar name to marshal.</param>
84+
/// <param name="dataType">The SimConnect data type for marshaling.</param>
85+
public SimConnectAttribute(string name, SimConnectDataType dataType)
86+
{
87+
this.Name = name;
88+
this.Unit = null;
89+
this.DataType = dataType;
90+
}
91+
8392
/// <summary>
8493
/// Gets the SimVar name to marshal.
8594
/// </summary>
@@ -93,11 +102,11 @@ public SimConnectAttribute(string name, string? unit, SimConnectDataType dataTyp
93102
/// <summary>
94103
/// Gets the SimConnect data type for marshaling.
95104
/// </summary>
96-
public SimConnectDataType DataType { get; }
105+
public SimConnectDataType? DataType { get; }
97106

98107
/// <summary>
99108
/// Gets the order in which the SimVar should be marshaled.
100109
/// </summary>
101-
public int Order { get; }
110+
public int? Order { get; }
102111
}
103112
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// <copyright file="ISimVarSubscription.cs" company="BARS">
2+
// Copyright (c) BARS. All rights reserved.
3+
// </copyright>
4+
5+
using System;
6+
using System.Threading.Tasks;
7+
8+
namespace SimConnect.NET.SimVar
9+
{
10+
/// <summary>
11+
/// Represents an active SimVar subscription that can be disposed to stop receiving data.
12+
/// </summary>
13+
public interface ISimVarSubscription : IDisposable
14+
{
15+
/// <summary>
16+
/// Gets a task that completes when the subscription terminates (via dispose, cancellation, or error).
17+
/// </summary>
18+
Task Completion { get; }
19+
}
20+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// <copyright file="IFieldReader.cs" company="BARS">
2+
// Copyright (c) BARS. All rights reserved.
3+
// </copyright>
4+
5+
using System;
6+
7+
namespace SimConnect.NET.SimVar.Internal
8+
{
9+
/// <summary>
10+
/// Reads and assigns a value of type <typeparamref name="T"/> from native memory into a managed struct.
11+
/// </summary>
12+
/// <typeparam name="T">The value type being populated.</typeparam>
13+
/// <remarks>
14+
/// Renamed from IFieldAccessor to IFieldReader to better reflect its one-way responsibility (read/copy only).
15+
/// Design note: generic only on <typeparamref name="T"/> so callers can hold a heterogeneous collection of
16+
/// per-field readers without knowing each destination field's exact type parameter.
17+
/// </remarks>
18+
internal interface IFieldReader<T>
19+
where T : struct
20+
{
21+
/// <summary>
22+
/// Reads from the specified native base pointer plus the reader's offset and writes the value into the target.
23+
/// </summary>
24+
/// <param name="target">The struct instance being populated.</param>
25+
/// <param name="basePtr">The base pointer to the native buffer.</param>
26+
void ReadInto(ref T target, IntPtr basePtr);
27+
}
28+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// <copyright file="ISimVarRequest.cs" company="BARS">
2+
// Copyright (c) BARS. All rights reserved.
3+
// </copyright>
4+
5+
using System;
6+
using System.Threading.Tasks;
7+
8+
namespace SimConnect.NET.SimVar.Internal
9+
{
10+
/// <summary>
11+
/// Non-generic SimVar request contract for hot-path handling without reflection.
12+
/// </summary>
13+
internal interface ISimVarRequest
14+
{
15+
/// <summary>
16+
/// Gets the unique request identifier.
17+
/// </summary>
18+
uint RequestId { get; }
19+
20+
/// <summary>
21+
/// Gets the SimConnect object identifier targeted by this request.
22+
/// </summary>
23+
uint ObjectId { get; }
24+
25+
/// <summary>
26+
/// Gets the data definition identifier associated with this request.
27+
/// </summary>
28+
uint DefinitionId { get; }
29+
30+
/// <summary>
31+
/// Gets a value indicating whether this is a recurring subscription.
32+
/// </summary>
33+
bool IsRecurring { get; }
34+
35+
/// <summary>
36+
/// Gets a task that completes when the request finishes (result, cancel, or error). For recurring requests this completes only on cancel/error.
37+
/// </summary>
38+
Task Completion { get; }
39+
40+
/// <summary>
41+
/// Sets the result using a boxed value; the implementation converts to the expected T.
42+
/// </summary>
43+
/// <param name="value">The boxed value parsed from SimConnect.</param>
44+
void SetResultBoxed(object? value);
45+
46+
/// <summary>
47+
/// Completes the request with an exception.
48+
/// </summary>
49+
/// <param name="exception">The exception that occurred.</param>
50+
void SetException(Exception exception);
51+
52+
/// <summary>
53+
/// Cancels the request.
54+
/// </summary>
55+
void SetCanceled();
56+
57+
/// <summary>
58+
/// Attempts to complete the request with the provided typed value without boxing.
59+
/// Returns true only if the request's expected type matches <typeparamref name="TValue"/>.
60+
/// </summary>
61+
/// <typeparam name="TValue">The type of the provided value.</typeparam>
62+
/// <param name="value">The value to set.</param>
63+
/// <returns>True if the value type matches and was accepted; otherwise false.</returns>
64+
bool TrySetResult<TValue>(TValue value);
65+
}
66+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// <copyright file="SimVarFieldReader.cs" company="BARS">
2+
// Copyright (c) BARS. All rights reserved.
3+
// </copyright>
4+
5+
using System;
6+
using System.Runtime.InteropServices;
7+
8+
namespace SimConnect.NET.SimVar.Internal
9+
{
10+
internal delegate void Setter<TStruct, TValue>(ref TStruct target, TValue value)
11+
where TStruct : struct; // necessary for TStruct parameter by ref, Action cannot express by ref
12+
13+
internal sealed class SimVarFieldReader<T, TDest> : IFieldReader<T>
14+
where T : struct
15+
{
16+
public int OffsetBytes { get; set; }
17+
18+
public int Size { get; set; }
19+
20+
public SimConnectDataType DataType { get; set; }
21+
22+
public Setter<T, TDest> Setter { get; set; } = default!;
23+
24+
// Holds a typed converter matching the raw type for the chosen Kind, e.g. Func<double, TDest>.
25+
public Delegate Converter { get; set; } = default!;
26+
27+
public void ReadInto(ref T target, IntPtr basePtr)
28+
{
29+
var addr = IntPtr.Add(basePtr, this.OffsetBytes);
30+
switch (this.DataType)
31+
{
32+
case SimConnectDataType.FloatDouble:
33+
double rawDouble = SimVarMemoryReader.ReadDouble(addr);
34+
var convDouble = (Func<double, TDest>)this.Converter;
35+
this.Setter(ref target, convDouble(rawDouble));
36+
break;
37+
38+
case SimConnectDataType.FloatSingle:
39+
float rawFloat = SimVarMemoryReader.ReadFloat(addr);
40+
var convFloat = (Func<float, TDest>)this.Converter;
41+
this.Setter(ref target, convFloat(rawFloat));
42+
break;
43+
44+
case SimConnectDataType.Integer64:
45+
long rawInt64 = SimVarMemoryReader.ReadInt64(addr);
46+
var convInt64 = (Func<long, TDest>)this.Converter;
47+
this.Setter(ref target, convInt64(rawInt64));
48+
break;
49+
50+
case SimConnectDataType.Integer32:
51+
int rawInt32 = SimVarMemoryReader.ReadInt32(addr);
52+
var convInt32 = (Func<int, TDest>)this.Converter;
53+
this.Setter(ref target, convInt32(rawInt32));
54+
break;
55+
56+
case SimConnectDataType.String8:
57+
case SimConnectDataType.String32:
58+
case SimConnectDataType.String64:
59+
case SimConnectDataType.String128:
60+
case SimConnectDataType.String256:
61+
case SimConnectDataType.String260:
62+
string rawString = SimVarMemoryReader.ReadFixedString(addr, this.Size);
63+
var convString = (Func<string, TDest>)this.Converter;
64+
this.Setter(ref target, convString(rawString));
65+
break;
66+
67+
case SimConnectDataType.InitPosition:
68+
var rawInit = Marshal.PtrToStructure<SimConnectDataInitPosition>(addr);
69+
var convInit = (Func<SimConnectDataInitPosition, TDest>)this.Converter;
70+
this.Setter(ref target, convInit(rawInit));
71+
break;
72+
73+
case SimConnectDataType.MarkerState:
74+
var rawMarker = Marshal.PtrToStructure<SimConnectDataMarkerState>(addr);
75+
var convMarker = (Func<SimConnectDataMarkerState, TDest>)this.Converter;
76+
this.Setter(ref target, convMarker(rawMarker));
77+
break;
78+
79+
case SimConnectDataType.Waypoint:
80+
var rawWp = Marshal.PtrToStructure<SimConnectDataWaypoint>(addr);
81+
var convWp = (Func<SimConnectDataWaypoint, TDest>)this.Converter;
82+
this.Setter(ref target, convWp(rawWp));
83+
break;
84+
85+
case SimConnectDataType.LatLonAlt:
86+
var rawLatLonAlt = Marshal.PtrToStructure<SimConnectDataLatLonAlt>(addr);
87+
var convLatLonAlt = (Func<SimConnectDataLatLonAlt, TDest>)this.Converter;
88+
this.Setter(ref target, convLatLonAlt(rawLatLonAlt));
89+
break;
90+
91+
case SimConnectDataType.Xyz:
92+
var rawXyz = Marshal.PtrToStructure<SimConnectDataXyz>(addr);
93+
var convXyz = (Func<SimConnectDataXyz, TDest>)this.Converter;
94+
this.Setter(ref target, convXyz(rawXyz));
95+
break;
96+
97+
default:
98+
throw new NotSupportedException($"Unsupported SimConnectDataType {this.DataType}");
99+
}
100+
}
101+
}
102+
}

0 commit comments

Comments
 (0)