Skip to content

Add SimVar subscription API and refactor struct handling#10

Merged
AussieScorcher merged 13 commits intostopbars:mainfrom
bstudtma:add-simvar-subscription
Sep 23, 2025
Merged

Add SimVar subscription API and refactor struct handling#10
AussieScorcher merged 13 commits intostopbars:mainfrom
bstudtma:add-simvar-subscription

Conversation

@bstudtma
Copy link
Contributor

Summary

This pull request introduces several enhancements and refactors to the SimVar infrastructure, focusing on extensibility, error handling, and internal API clarity. The most significant changes are the addition of new internal interfaces and classes to support SimVar subscriptions and field reading, as well as improvements to the SimConnectAttribute constructor overloads and property types for greater flexibility.

Changes Made

SimVar infrastructure and extensibility

  • Added ISimVarSubscription interface to represent an active SimVar subscription, supporting disposal and completion tracking.
  • Introduced the ISimVarRequest interface for handling SimVar requests in a non-generic, hot-path manner, with support for result setting, cancellation, and completion.
  • Added IFieldReader<T> interface (renamed from IFieldAccessor) to clarify its responsibility for reading values from native memory into managed structs.
  • Implemented SimVarFieldReader<T, TDest> class to read and convert SimVar field values from native memory, supporting multiple SimConnect data types.

SimConnectAttribute flexibility and error handling

  • Relaxed error handling in SimConnectAttribute constructors: removed exceptions when a SimVar is not found, allowing more flexible attribute creation. [1] [2]
  • Made unit, dataType, and order parameters optional and nullable in relevant SimConnectAttribute constructors, and updated their property types to nullable for improved flexibility. Also introduced a new constructor overload for specifying only name and data type. [1] [2] [3]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.

Subscribe API Example

using SimConnect.NET;

using (var client = new SimConnectClient())
{
    await client.ConnectAsync();

    client.SimVars.Subscribe<AircraftData>(SimConnectPeriod.Second, data =>
    {
        Console.WriteLine($"[Subscription] Altitude: {data.Altitude:F0} ft, Airspeed: {data.Airspeed:F0} kts");
    });

    client.SimVars.Subscribe<double>("PLANE ALTITUDE", "feet", SimConnectPeriod.Second, value =>
    {
        Console.WriteLine($"Altitude: {value:F0} ft");
    });

    await Task.Delay(TimeSpan.FromSeconds(5));
}

SimVar Subscription API

The SimVarManager exposes two Subscribe overloads to stream recurring simulator data. Both return an ISimVarSubscription handle you dispose (or cancel) to stop updates.

Overload 1: Typed Struct Subscription

ISimVarSubscription Subscribe<T>(
    SimConnectPeriod period,
    Action<T> onValue,
    uint objectId = 0,
    CancellationToken cancellationToken = default)
    where T : struct

For aggregated / model-style data (T is a struct whose fields map to multiple SimVars). A single data definition is auto-built and cached per T.

Example Struct

public struct AircraftData
{
    [SimConnect("PLANE ALTITUDE")]
    public long Altitude;

    [SimConnect("AIRSPEED INDICATED", "knots")]
    public double Airspeed;

    [SimConnect("STRUCT LATLONALT")]
    public SimConnectDataLatLonAlt Position;

    [SimConnect("TITLE", SimConnectDataType.String64)]
    public string Title;
}

Overload 2: Scalar SimVar Subscription

ISimVarSubscription Subscribe<T>(
    string simVarName,
    string unit,
    SimConnectPeriod period,
    Action<T> onValue,
    uint objectId = 0,
    CancellationToken cancellationToken = default)

For a single named SimVar (e.g. "PLANE ALTITUDE" / "feet"). Definition is cached per (Name, Unit, DataType) tuple.

Type Parameter (T)

Scenario Constraints Notes
Struct model overload where T : struct Each field read by generated readers (SimVarFieldReaderFactory).
Scalar name/unit overload Supported primitive (int, long, float, double, bool, string, SimConnectDataLatLonAlt, SimConnectDataXyz) Type drives SimConnectDataType inference.

Booleans transported as Int32 (0/1). Some implicit conversion fallback paths exist (e.g. float satisfied by double).

Parameters

Name Overload(s) Required Description
simVarName Scalar only Yes SimConnect variable name.
unit Scalar only Yes Unit string (empty allowed if SimVar has none).
period Both Yes Must be recurring (not Once or Never).
onValue Both Yes Callback per update (keep lightweight).
objectId Both No Target object; defaults to user aircraft (0).
cancellationToken Both No Cancels & disposes subscription.

Return Value

ISimVarSubscription — disposable handle. Disposing issues an internal stop (SimConnectPeriod.Never) and removes tracking.

Lifecycle & Cancellation

var sub = simVarManager.Subscribe<double>(
    "PLANE ALTITUDE",
    "feet",
    SimConnectPeriod.Second,
    v => Console.WriteLine($"Alt: {v:F0} ft"));

// Later
sub.Dispose();

With cancellation:

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var sub2 = simVarManager.Subscribe<AircraftData>(
    SimConnectPeriod.VisualFrame,
    d => Console.WriteLine($"{d.Airspeed:F0} kts"),
    cancellationToken: cts.Token); // auto-disposes after 10s

Error Conditions

Condition Exception
Manager disposed ObjectDisposedException
Null/empty simVarName or unit (scalar) ArgumentException
Null onValue ArgumentNullException
period is Once or Never ArgumentException
Unsupported type ArgumentException
Native request/definition failure SimConnectException

Performance & Caching

  • Struct definitions: created once per type and cached.
  • Scalar definitions: cached by (Name, Unit, DataType).
  • Parsers pre-registered per definition id for fast hot-path.
  • Disposal unsubscribes via a single native call.

Threading & Callback Dispatch

SimConnect.NET deliberately offloads user callbacks to the .NET ThreadPool.

Relevant implementation excerpt (SimVarRequest.SetResult lines 85–121):

ThreadPool.UnsafeQueueUserWorkItem<(Action<T> Callback, T Value)>(
    static state =>
    {
        try
        {
            state.Callback(state.Value);
        }
        catch
        {
            // Swallow user exceptions.
        }
    },
    (cb, result),
    preferLocal: true);

Why this design?

  • Keeps the SimConnect processing loop responsive (no user code stalls).
  • Prevents a slow or exception-throwing handler from blocking subsequent simulator messages.
  • UnsafeQueueUserWorkItem avoids extra allocation vs Task.Run and uses a static lambda (no captures) for minimal overhead.
  • preferLocal: true biases work to a local thread, improving cache locality under burst loads.

Practical Implications

  • Your callback is NOT executed synchronously with data arrival; there is a small (typically micro‑ to low millisecond) scheduling delay.
  • Ordering per subscription is generally preserved under light load, but multiple callbacks may execute concurrently if they run long.
  • Exceptions inside your handler are swallowed (logged only if you add logging). Add your own try/catch if you need custom error handling.
  • Heavy processing should still be batched or posted to dedicated workers (e.g., Channels, ActionBlock) to avoid saturating the shared pool.

If You Need Synchronous Semantics

Use one‑shot GetAsync for atomic reads, or build an internal queue in your callback and signal a consumer thread.

Writing Thread-Safe Handlers

When updating shared state:

var latestAltitude = 0.0;
var gate = new object();
var sub = client.SimVars.Subscribe<double>(
    "PLANE ALTITUDE", "feet", SimConnectPeriod.Second,
    v => { lock (gate) latestAltitude = v; });

Or use Volatile.Write/Volatile.Read for primitive snapshots.

Best Practice: Unsubscribe

Always dispose (or rely on a cancellation token) when done to stop native update traffic and free tracking entries.

Mixed Example

// Aggregated struct updates
var aircraftSub = client.SimVars.Subscribe<AircraftData>(
    SimConnectPeriod.Second,
    a => Console.WriteLine($"ALT {a.Altitude:F0}  IAS {a.Airspeed:F0}"));

// Single SimVar
var altSub = client.SimVars.Subscribe<double>(
    "PLANE ALTITUDE",
    "feet",
    SimConnectPeriod.Second,
    v => Console.WriteLine($"Raw Alt: {v:F0}"));

// Cleanup
aircraftSub.Dispose();
altSub.Dispose();

Summary

Use the struct overload for cohesive snapshots; use the scalar overload for single values. Both yield a disposable ISimVarSubscription for explicit lifetime control.

Author Information

Discord Username: bstudtma


Checklist:

  • [ x] Have you followed the guidelines in our Contributing document?
  • [ x] Have you checked to ensure there aren't other open Pull Requests for the same update/change?

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.
Copilot AI review requested due to automatic review settings September 15, 2025 05:31
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This pull request introduces a comprehensive SimVar subscription API and refactors struct handling infrastructure to support recurring data updates from Microsoft Flight Simulator. The changes focus on enabling real-time data streaming through subscription patterns while improving internal architecture for better performance and maintainability.

Key changes include:

  • Addition of subscription API for both typed structs and scalar SimVars with recurring update periods
  • Replacement of reflection-heavy struct binding with a new field reader factory system for efficient marshalling
  • Introduction of internal interfaces (ISimVarRequest, ISimVarSubscription) to support hot-path request/response handling

Reviewed Changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
tests/SimConnect.NET.Tests.Net8/Tests/SimVarTests.cs Adds comprehensive subscription testing with Position struct validation
src/SimConnect.NET/SimVar/SimVarManager.cs Major refactor adding subscription methods and replacing reflection-based parsing with interface-driven approach
src/SimConnect.NET/SimVar/SimVarRequest.cs Removed old generic request class
src/SimConnect.NET/SimVar/Internal/SimVarRequest.cs New internal request implementation with subscription support and thread-pool callback dispatch
src/SimConnect.NET/SimVar/Internal/SimVarFieldReaderFactory.cs New reflection-based factory for generating efficient struct field readers
src/SimConnect.NET/SimVar/Internal/SimVarFieldReader.cs Concrete field reader implementation for typed struct marshalling
src/SimConnect.NET/SimVar/Internal/ISimVarRequest.cs Internal interface for hot-path request handling without reflection
src/SimConnect.NET/SimVar/Internal/IFieldReader.cs Interface for reading values from native memory into managed structs
src/SimConnect.NET/SimVar/ISimVarSubscription.cs Public interface for subscription lifecycle management
src/SimConnect.NET/SimVar/SimVarSubscription.cs Default subscription implementation with disposal and cancellation support
src/SimConnect.NET/SimConnectAttribute.cs Enhanced with nullable parameters and additional constructor overloads

bstudtma and others added 12 commits September 15, 2025 00:42
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Deleted an unnecessary blank line for improved code formatting and readability.
…e struct documentation to prevent test crash
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Refactor input event handling to improve payload extraction and update struct documentation to prevent test crash
Copy link
Member

@AussieScorcher AussieScorcher left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing work!

@AussieScorcher AussieScorcher merged commit 1fc1d7f into stopbars:main Sep 23, 2025
2 checks passed
@llamavert
Copy link
Contributor

llamavert commented Sep 23, 2025

Thanks for your work, good job!

@bstudtma bstudtma deleted the add-simvar-subscription branch September 24, 2025 01:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants