diff --git a/src/SimConnect.NET/SimConnectAttribute.cs b/src/SimConnect.NET/SimConnectAttribute.cs new file mode 100644 index 0000000..22c1e87 --- /dev/null +++ b/src/SimConnect.NET/SimConnectAttribute.cs @@ -0,0 +1,103 @@ +// +// Copyright (c) BARS. All rights reserved. +// + +using System; +using SimConnect.NET.SimVar; + +namespace SimConnect.NET +{ + /// Annotates a struct field with the SimVar you want marshalled into it. + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] + public sealed class SimConnectAttribute : Attribute + { + /// + /// Initializes a new instance of the class with name and unit. + /// The data type is inferred from the SimVar registry if available. + /// + /// The SimVar name to marshal. + /// The unit of the SimVar. + public SimConnectAttribute(string name, string unit) + { + this.Name = name; + this.Unit = unit; + var simVar = SimVarRegistry.Get(name); + if (simVar != null) + { + this.DataType = simVar.DataType; + } + else + { + throw new ArgumentException($"SimVar '{name}' not found in registry. Please specify unit and dataType explicitly.", nameof(name)); + } + } + + /// + /// Initializes a new instance of the class using the SimVar name. + /// The unit and data type are inferred from the SimVar registry if available. + /// + /// The SimVar name to marshal. + public SimConnectAttribute(string name) + { + this.Name = name; + var simVar = SimVarRegistry.Get(name); + if (simVar != null) + { + 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)); + } + } + + /// + /// Initializes a new instance of the class. + /// + /// The SimVar name to marshal. + /// The unit of the SimVar. + /// The SimConnect data type for marshaling. + public SimConnectAttribute(string name, string unit, SimConnectDataType dataType) + { + this.Name = name; + this.Unit = unit; + this.DataType = dataType; + } + + /// + /// Initializes a new instance of the class. + /// + /// The SimVar name to marshal. + /// The unit of the SimVar. + /// The SimConnect data type for marshaling. + /// The order in which the SimVar should be marshaled. + public SimConnectAttribute(string name, string? unit, SimConnectDataType dataType, int order) + { + this.Name = name; + this.Unit = unit; + this.DataType = dataType; + this.Order = order; + } + + /// + /// Gets the SimVar name to marshal. + /// + public string Name { get; } + + /// + /// Gets the unit of the SimVar. + /// + public string? Unit { get; } + + /// + /// Gets the SimConnect data type for marshaling. + /// + public SimConnectDataType DataType { get; } + + /// + /// Gets the order in which the SimVar should be marshaled. + /// + public int Order { get; } + } +} diff --git a/src/SimConnect.NET/SimVar/Internal/SimVarStructBinder.cs b/src/SimConnect.NET/SimVar/Internal/SimVarStructBinder.cs new file mode 100644 index 0000000..2b150f3 --- /dev/null +++ b/src/SimConnect.NET/SimVar/Internal/SimVarStructBinder.cs @@ -0,0 +1,139 @@ +// +// Copyright (c) BARS. All rights reserved. +// + +using System; +using System.Linq; +using System.Reflection; + +namespace SimConnect.NET.SimVar.Internal +{ + internal static class SimVarStructBinder + { + /// + /// Returns the ordered [SimVar]-annotated fields for T, validating .NET types vs SimConnect types. + /// + internal static (System.Reflection.FieldInfo Field, SimConnectAttribute Attr)[] GetOrderedFields() + { + var t = typeof(T); + if (!t.IsLayoutSequential) + { + throw new InvalidOperationException($"{t.Name} must be annotated with [StructLayout(LayoutKind.Sequential)]."); + } + + var fields = t.GetFields(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public) + .Select(f => (Field: f, Attr: f.GetCustomAttribute())) + .Where(x => x.Attr != null) + .OrderBy(x => x!.Attr!.Order) + .ThenBy(x => x.Field.MetadataToken) + .ToArray(); + + if (fields.Length == 0) + { + throw new InvalidOperationException($"{t.Name} has no fields annotated with [SimVar]."); + } + + foreach (var (field, attr) in fields) + { + var ft = field.FieldType; + switch (attr!.DataType) + { + case SimConnectDataType.FloatDouble: + if (ft != typeof(double)) + { + throw Fail(field, "double"); + } + + break; + case SimConnectDataType.FloatSingle: + if (ft != typeof(float)) + { + throw Fail(field, "float"); + } + + break; + case SimConnectDataType.Integer32: + if (ft != typeof(int) && ft != typeof(uint)) + { + throw Fail(field, "int/uint"); + } + + break; + case SimConnectDataType.Integer64: + if (ft != typeof(long) && ft != typeof(ulong)) + { + throw Fail(field, "long/ulong"); + } + + break; + case SimConnectDataType.String8: + case SimConnectDataType.String32: + case SimConnectDataType.String64: + case SimConnectDataType.String128: + case SimConnectDataType.String256: + case SimConnectDataType.String260: + if (ft != typeof(string)) + { + throw Fail(field, "string"); + } + + break; + } + } + + return fields!; + + static InvalidOperationException Fail(FieldInfo f, string expected) + => new($"Field {f.DeclaringType!.Name}.{f.Name} must be {expected} to match its [SimVar] attribute."); + } + + /// + /// Builds a single SimConnect data definition for T (using [SimVar] attributes), + /// registers T for marshalling, and returns the definition ID. + /// + /// Native SimConnect handle. + internal static (uint DefId, (System.Reflection.FieldInfo Field, SimConnectAttribute Attr)[] Fields) BuildAndRegisterFromStruct(IntPtr handle) + { + var t = typeof(T); + if (!t.IsLayoutSequential) + { + throw new InvalidOperationException($"{t.Name} must be annotated with [StructLayout(LayoutKind.Sequential)]."); + } + + var fields = GetOrderedFields(); + + uint defId = unchecked((uint)Guid.NewGuid().GetHashCode()); + + var result = SimConnectNative.SimConnect_ClearDataDefinition(handle, defId); + if (result != 0) + { + throw new InvalidOperationException($"Failed to clear data definition for {t.Name}: {result}"); + } + + foreach (var (field, attr) in fields) + { + // Add each SimVar field to the SimConnect data definition using the native layer + if (attr == null) + { + throw new InvalidOperationException($"Field {field.Name} is missing [SimVar] attribute."); + } + + result = SimConnectNative.SimConnect_AddToDataDefinition( + handle, + defId, + attr.Name, + attr.Unit ?? string.Empty, + (uint)attr.DataType); + + if (result != 0) + { + throw new InvalidOperationException($"Failed to add data definition for {field.Name}: {result}"); + } + } + + var size = SimVarDataTypeSizing.GetPayloadSizeBytes(fields.Select(f => f.Attr!.DataType)); + var offsets = SimVarDataTypeSizing.ComputeOffsets(fields.Select(f => f.Attr!.DataType)); + return (defId, fields); + } + } +} diff --git a/src/SimConnect.NET/SimVar/SimVarDataTypeSizing.cs b/src/SimConnect.NET/SimVar/SimVarDataTypeSizing.cs new file mode 100644 index 0000000..17cb7d4 --- /dev/null +++ b/src/SimConnect.NET/SimVar/SimVarDataTypeSizing.cs @@ -0,0 +1,77 @@ +// +// Copyright (c) BARS. All rights reserved. +// +using SimConnect.NET; + +namespace SimConnect.NET.SimVar +{ + /// + /// Provides utilities for determining the size and offsets of SimConnect data types in unmanaged payloads. + /// + public static class SimVarDataTypeSizing + { + /// + /// Raw bytes for one datum of the given SimConnect type in the untagged payload. + /// + /// The SimConnect data type to evaluate. + /// The size in bytes of a single datum of the specified type. + public static int GetDatumSizeBytes(SimConnectDataType type) => type switch + { + SimConnectDataType.Invalid => 0, + + // Scalars (raw sizes) + SimConnectDataType.Integer32 => 4, // uint (SimConnect uses unsigned 32-bit for Integer32) + SimConnectDataType.Integer64 => 8, // long + SimConnectDataType.FloatSingle => 4, // float + SimConnectDataType.FloatDouble => 8, // double + + // Fixed-length strings (ANSI, fixed buffer including NUL) + SimConnectDataType.String8 => 8, // string + SimConnectDataType.String32 => 32, // string + SimConnectDataType.String64 => 64, // string + SimConnectDataType.String128 => 128, // string + SimConnectDataType.String256 => 256, // string + SimConnectDataType.String260 => 260, // string + + // Not supported in this marshaller + SimConnectDataType.StringV => throw new NotSupportedException( + "StringV is not supported. Use fixed-length String8..String260."), + + // Composite structs (per SDK) + SimConnectDataType.LatLonAlt => 24, // 3 x double + SimConnectDataType.Xyz => 24, // 3 x double + SimConnectDataType.InitPosition => 56, // 6 x double + 2 x DWORD (pack=1) + + // These depend on your interop definition; use Marshal.SizeOf in your code. + SimConnectDataType.MarkerState => throw new NotSupportedException("Use Marshal.SizeOf()."), + SimConnectDataType.Waypoint => throw new NotSupportedException("Use Marshal.SizeOf()."), + _ => throw new ArgumentOutOfRangeException(nameof(type)), + }; + + /// + /// Total payload size (bytes) for a sequence of datums in untagged SIMOBJECT_DATA. + /// + /// The sequence of SimConnect data types to calculate the total payload size for. + /// The total size in bytes of the payload for the provided sequence of data types. + public static int GetPayloadSizeBytes(IEnumerable types) + => types.Sum(GetDatumSizeBytes); + + /// + /// Compute byte offsets for each datum in order (untagged). + /// + /// The list of SimConnect data types to compute offsets for. + /// An array of byte offsets for each datum in the provided list. + public static int[] ComputeOffsets(IEnumerable types) + { + var offsets = new List(); + int cursor = 0; + foreach (var type in types) + { + offsets.Add(cursor); + cursor += GetDatumSizeBytes(type); + } + + return offsets.ToArray(); + } + } +} diff --git a/src/SimConnect.NET/SimVar/SimVarManager.cs b/src/SimConnect.NET/SimVar/SimVarManager.cs index 3526f73..cf42489 100644 --- a/src/SimConnect.NET/SimVar/SimVarManager.cs +++ b/src/SimConnect.NET/SimVar/SimVarManager.cs @@ -7,6 +7,7 @@ using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; +using SimConnect.NET.SimVar.Internal; namespace SimConnect.NET.SimVar { @@ -24,6 +25,9 @@ public sealed class SimVarManager : IDisposable private readonly IntPtr simConnectHandle; private readonly ConcurrentDictionary pendingRequests; private readonly ConcurrentDictionary<(string Name, string Unit), uint> dataDefinitions; + private readonly ConcurrentDictionary defIndex = new(); + private readonly ConcurrentDictionary typeToDefIndex = new(); + private uint nextDefinitionId; private uint nextRequestId; private bool disposed; @@ -141,6 +145,70 @@ public async Task SetAsync(string simVarName, string unit, T value, uint obje await this.SetWithDefinitionAsync(dynamicDefinition, value, objectId, cancellationToken).ConfigureAwait(false); } + /// + /// Gets a full struct from SimConnect as a strongly-typed object using a dynamically built data definition. + /// + /// The struct type to request. Must be blittable/marshalable. + /// The SimConnect object ID (defaults to user aircraft). + /// Cancellation token for the operation. + /// A task that represents the asynchronous operation and returns the requested struct. + public async Task GetAsync( + uint objectId = SimConnectObjectIdUser, + CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(this.disposed, nameof(SimVarManager)); + cancellationToken.ThrowIfCancellationRequested(); + + if (this.simConnectHandle == IntPtr.Zero) + { + throw new InvalidOperationException("SimConnect handle is not initialized."); + } + + // Build definition for the struct using the native handle directly to avoid client coupling. + if (!this.typeToDefIndex.TryGetValue(typeof(T), out var defId)) + { + var (newDefId, fields) = SimVarStructBinder.BuildAndRegisterFromStruct(this.simConnectHandle); + this.defIndex[newDefId] = fields; + this.typeToDefIndex[typeof(T)] = newDefId; + defId = newDefId; + } + + var requestId = Interlocked.Increment(ref this.nextRequestId); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + this.pendingRequests[requestId] = tcs; + + // Request data once for the specified object. + var hr = SimConnectNative.SimConnect_RequestDataOnSimObject( + this.simConnectHandle, + requestId, + defId, + objectId, + (uint)SimConnectPeriod.Once); + + if (hr != (int)SimConnectError.None) + { + this.pendingRequests.TryRemove(requestId, out _); + throw new SimConnectException($"Failed to request struct {typeof(T).Name}: {(SimConnectError)hr}", (SimConnectError)hr); + } + + using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken))) + { + // Apply optional manager timeout consistent with SimVar value requests + if (this.requestTimeout != Timeout.InfiniteTimeSpan) + { + var timeoutTask = Task.Delay(this.requestTimeout, CancellationToken.None); + var completed = await Task.WhenAny(tcs.Task, timeoutTask).ConfigureAwait(false); + if (completed == timeoutTask) + { + this.pendingRequests.TryRemove(requestId, out _); + throw new TimeoutException($"Struct request '{typeof(T).Name}' timed out after {this.requestTimeout} (RequestId={requestId})"); + } + } + + return await tcs.Task.ConfigureAwait(false); + } + } + /// /// Processes a received SimConnect message and completes any pending requests. /// @@ -164,12 +232,61 @@ public void ProcessReceivedData(IntPtr data, uint dataSize) var objectData = Marshal.PtrToStructure(data); var requestId = objectData.RequestId; - SimConnectLogger.Debug($"SimVar response received: RequestId={requestId}, DefineId={objectData.DefineId}, Size={recv.Size}"); - if (this.pendingRequests.TryRemove(requestId, out var request)) { - // Complete the request with the received data - CompleteRequest(request, data, dataSize); + // If this is a simple SimVarRequest<>, use existing completion logic + if (request.GetType().IsGenericType && request.GetType().GetGenericTypeDefinition() == typeof(SimVarRequest<>)) + { + CompleteRequest(request, data, dataSize); + } + else + { + // Otherwise, treat as a struct T TaskCompletionSource stored by GetAsync() struct overload + try + { + var definitionId = objectData.DefineId; + if (!this.defIndex.TryGetValue(definitionId, out var fields)) + { + throw new InvalidOperationException($"No struct definition found for DefineId={definitionId}"); + } + else + { + var headerSize = Marshal.SizeOf() - sizeof(ulong); + var dataPtr = IntPtr.Add(data, headerSize); + var currentPtr = dataPtr; + var tcsType = request.GetType(); + if (tcsType.IsGenericType && tcsType.GetGenericTypeDefinition() == typeof(TaskCompletionSource<>)) + { + var structType = tcsType.GetGenericArguments()[0]; + var boxed = Activator.CreateInstance(structType)!; + foreach (var (field, attr) in fields) + { + var simConnectType = attr.DataType; + var fieldType = field.FieldType; + var simConnectSize = SimVarDataTypeSizing.GetDatumSizeBytes(simConnectType); + var fieldName = field.Name; + object? value = ParseType(currentPtr, simConnectType, fieldType); + field.SetValue(boxed, value); + currentPtr = IntPtr.Add(currentPtr, simConnectSize); + } + + var trySetResultMethod = tcsType.GetMethod("TrySetResult"); + trySetResultMethod?.Invoke(request, new[] { boxed }); + } + else + { + SimConnectLogger.Warning("Unexpected TCS type for struct response."); + } + } + } + catch (Exception ex) + { + SimConnectLogger.Error("Error completing struct request", ex); + var tcsType = request.GetType(); + var setEx = tcsType.GetMethod("TrySetException", new[] { typeof(Exception) }); + setEx?.Invoke(request, new object[] { ex }); + } + } } else { @@ -207,42 +324,17 @@ private static SimConnectDataType InferDataType() { var type = typeof(T); - if (type == typeof(int) || type == typeof(bool)) - { - return SimConnectDataType.Integer32; - } - - if (type == typeof(long)) - { - return SimConnectDataType.Integer64; - } - - if (type == typeof(float)) - { - return SimConnectDataType.FloatSingle; - } - - if (type == typeof(double)) - { - return SimConnectDataType.FloatDouble; - } - - if (type == typeof(string)) + return type switch { - return SimConnectDataType.String256; // Default string size - } - - if (type == typeof(SimConnectDataLatLonAlt)) - { - return SimConnectDataType.LatLonAlt; - } - - if (type == typeof(SimConnectDataXyz)) - { - return SimConnectDataType.Xyz; - } - - throw new ArgumentException($"Unsupported type for SimVar: {type.Name}"); + Type t when t == typeof(int) || t == typeof(bool) => SimConnectDataType.Integer32, + Type t when t == typeof(long) => SimConnectDataType.Integer64, + Type t when t == typeof(float) => SimConnectDataType.FloatSingle, + Type t when t == typeof(double) => SimConnectDataType.FloatDouble, + Type t when t == typeof(string) => SimConnectDataType.String256, // Default string size + Type t when t == typeof(SimConnectDataLatLonAlt) => SimConnectDataType.LatLonAlt, + Type t when t == typeof(SimConnectDataXyz) => SimConnectDataType.Xyz, + _ => throw new ArgumentException($"Unsupported type for SimVar: {type.Name}"), + }; } private static bool IsTypeCompatible(Type requestedType, Type definitionType) @@ -368,6 +460,7 @@ private static void CompleteRequest(object request, IntPtr data, uint dataSize) // Parse the data based on the definition's data type var parsedValue = ParseDataByType(dataPtr, definition.DataType, valueType); + // Use reflection to call SetResult directly, avoiding dynamic var setResultMethod = requestType.GetMethod("SetResult"); setResultMethod?.Invoke(request, new[] { parsedValue }); } @@ -447,6 +540,20 @@ private static object ParseFloat64Value(IntPtr dataPtr, Type expectedType) return value; } + private static object? ParseType(IntPtr dataPtr, SimConnectDataType simConnectType, Type fieldType) + { + return fieldType switch + { + Type t when t == typeof(int) => ParseInteger32(dataPtr), + Type t when t == typeof(long) => ParseInteger64(dataPtr), + Type t when t == typeof(float) => ParseFloat32(dataPtr), + Type t when t == typeof(double) => ParseFloat64(dataPtr), + Type t when t == typeof(string) => ParseString(dataPtr, simConnectType), + Type t when t == typeof(bool) => ParseInteger32(dataPtr) != 0, + _ => Marshal.PtrToStructure(dataPtr, fieldType), + }; + } + private static double ParseFloat64(IntPtr dataPtr) { var bytes = Float64Bytes.Value!; diff --git a/tests/SimConnect.NET.Tests.Net8/Tests/SimVarTests.cs b/tests/SimConnect.NET.Tests.Net8/Tests/SimVarTests.cs index 7882910..66e6b49 100644 --- a/tests/SimConnect.NET.Tests.Net8/Tests/SimVarTests.cs +++ b/tests/SimConnect.NET.Tests.Net8/Tests/SimVarTests.cs @@ -2,6 +2,7 @@ // Copyright (c) BARS. All rights reserved. // +using System.Runtime.InteropServices; using SimConnect.NET; namespace SimConnect.NET.Tests.Net8.Tests @@ -90,6 +91,26 @@ private static async Task TestPositionSimVars(SimConnectClient client, Can return false; } + var position = await client.SimVars.GetAsync(cancellationToken: cancellationToken); + Console.WriteLine($" 🗺️ Position struct: {position.Latitude:F6}°, {position.Longitude:F6}°, {position.Altitude:F0}ft"); + if (position.Latitude < -90 || position.Latitude > 90) + { + Console.WriteLine(" ❌ Invalid latitude value"); + return false; + } + + if (position.Longitude < -180 || position.Longitude > 180) + { + Console.WriteLine(" ❌ Invalid longitude value"); + return false; + } + + if (position.Altitude < 0 || position.Altitude > 60000) + { + Console.WriteLine(" ❌ Invalid altitude value"); + return false; + } + return true; } @@ -158,3 +179,27 @@ private static async Task TestRapidRequests(SimConnectClient client, Cance } } } + +/// +/// Represents the aircraft position using SimVars. +/// +public struct Position +{ + /// + /// Gets or sets the latitude of the plane in degrees. + /// + [SimConnect("PLANE LATITUDE", "degrees")] + public double Latitude; + + /// + /// Gets or sets the longitude of the plane in degrees. + /// + [SimConnect("PLANE LONGITUDE", "degrees")] + public double Longitude; + + /// + /// Gets or sets the altitude of the plane in feet. + /// + [SimConnect("PLANE ALTITUDE", "feet")] + public double Altitude; +}