5. Reading SimVar data with SimConnect SDK

5. Reading SimVar data with SimConnect SDK
Photo by Mika Baumeister / Unsplash

Prerequisites for Developing with the MSFS SDK

Before we start developing with the MSFS SDK, there are a few things you’ll need to have installed:

  1. Microsoft Flight Simulator 2020: Ensure you have a copy of MSFS 2020 installed on your machine.
  2. Enable Developer Mode: While not strictly necessary for using the SDK, enabling Developer Mode within the simulator is recommended. This mode allows you to download the SDK directly from within the sim and provides access to additional tools that will be useful throughout the development process, which I’ll cover below.
Note: when you install the SDK it goes under your C:\MSFS SDK folder, so keep that in mind if you install it elsewhere.
  1. Microsoft Visual Studio 2022. You can download the free Community version here. During installation, make sure to select the .NET desktop development workload, as this will include everything you need to develop WPF applications. Once installed, you’re ready to move on to the next step.

Setting up your project

Lets proceed by creating a new project. A .NET WPF Application is ideal for what I want to achieve. However, if you prefer, you can write a Console Application and most of the steps this tutorial still applies.

After completing the "Create New Project" wizard in Visual Studio, the first thing you should do is configure your project file. To do this:

  1. In the Solution Explorer, right-click on your project’s name.
  2. Select Edit Project File.

This allows you to manually adjust the project settings, add dependencies, and make other configurations necessary for integrating the MSFS SDK.

  1. To properly integrate the MSFS SDK, you need to reference the SimConnect library in your project. Add the following two <ItemGroup> entries in lines 12 through 24 to your project file to include SimConnect.dll for both build and runtime:
<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <OutputType>WinExe</OutputType>
        <TargetFramework>net6.0-windows7.0</TargetFramework>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
        <UseWPF>true</UseWPF>
    </PropertyGroup>
    
    <!-- Add this reference for build and intellisense -->
    <ItemGroup>
        <Reference Include="Microsoft.FlightSimulator.SimConnect">
            <HintPath>C:\MSFS SDK\SimConnect SDK\lib\managed\Microsoft.FlightSimulator.SimConnect.dll</HintPath>
        </Reference>
    </ItemGroup>
    
    <!-- Add this so the dll gets copied to the project output directory for runtime -->
    <ItemGroup>
        <Content Include="C:\MSFS SDK\SimConnect SDK\lib\SimConnect.dll">
            <Link>SimConnect.dll</Link>
            <CopyToOutputDirectory>Always</CopyToOutputDirectory>
        </Content>
    </ItemGroup>
</Project>

HelloMSFS.csproj

Note: If you installed the SDK in a different location, update these paths accordingly.

Build an Altimeter... kinda...

Let’s start by building a simple instrument: the Altimeter. For this, we need to read two values from the simulator: the indicated altitude and the barometric pressure setting. By consulting the Simulation Variables documentation, we can identify the specific SimVars that will provide these values:

SimVar Descritpion Units
INDICATED ALTITUDE The indicated altitude. feet
KOHLSMAN SETTING HG The value for the given altimeter index in inches of mercury. Inches of Mercury, inHg

Now, let’s build a simple UI for our Altimeter. Open MainWindow.xaml and add the following XAML code to create a basic layout for displaying the altitude and barometric pressure:

<Window x:Class="HelloMSFS.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:HelloMSFS"
        mc:Ignorable="d"
        Title="Hello MSFS" SizeToContent="WidthAndHeight" Topmost="True">
    <Grid Margin="20">
        <Grid.ColumnDefinitions>
            <ColumnDefinition MinWidth="100"/>
            <ColumnDefinition MinWidth="100"/>
        </Grid.ColumnDefinitions>

        <Grid.RowDefinitions>
            <RowDefinition MinHeight="25"/>
            <RowDefinition MinHeight="25"/>
            <RowDefinition MinHeight="25"/>
        </Grid.RowDefinitions>

        <Label x:Name="label" Content="Altimeter:" VerticalContentAlignment="Center" Margin="5" />
        <Label x:Name="label1" Content="Barometer:" Grid.Row="1" VerticalContentAlignment="Center" Margin="5"/>
        
        <TextBox x:Name="altimeterTextBox" TextWrapping="Wrap" Text="1000" Grid.Column="1" VerticalContentAlignment="Center" Margin="5" IsReadOnly="True"/>
        <TextBox x:Name="barometerTextBox" TextWrapping="Wrap" Text="29.92" Grid.Row="1" Grid.Column="1" VerticalContentAlignment="Center" Margin="5" IsReadOnly="True"/>
        
        <Button x:Name="connectButton" Content="Connect" Grid.Row="2" Click="connectButton_Click" />
        <Button x:Name="disconnectButton" Content="Disconnect" Grid.Row="2" Grid.Column="1" Click="disconnectButton_Click" IsEnabled="False"/>
    </Grid>
</Window>

MainWindow.xaml

This should create a simple text based altimeter like this:

Implementing the SimConnect Code

With the UI in place, it’s time to add the functionality. Open MainWindow.xaml.cs and include the following C# code to connect to the simulator and update the UI with the Altimeter data:

using Microsoft.FlightSimulator.SimConnect;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Threading;

namespace HelloMSFS
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private const uint WM_USER_SIMCONNECT = 0x0402;                 //
        private const string MSFS_PROCESS_NAME = "FlightSimulator";     // this is MSFS's process name
        private const string PLUGIN_NAME = "Hello MSFS";                // your plugin's name

        private const int SIMCONNECT_TIMER_INTERVAL_MS = 50;            // how often to poll data from the game
        private DispatcherTimer simConnectDispatcherTimer;              // the DispatcherTimer object used to poll data from the game

        private SimConnect simconnect;                                  // the SimConnect SDK object used to subscribe and interact with the game
        private SimVars simvars;                                        // the object we'll store our new data from the game

        public MainWindow()
        {
            InitializeComponent();

            InitializeTimers();
        }

        // this is the structure that will store the data received from the game. note that the number of properties here have to be exactly the same number
        // of AddToDataDefinition(...) calls we are registering, and the type has to match the SIMCONNECT_DATATYPE type.
        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
        private struct SimVars
        {
            public float Altitude;                         // INDICATED ALTITUDE (feet)
            public float KohlsmanSettingHg;                // KOHLSMAN SETTING HG (inHG)
        }

        // we need an enum to register the data structure above. note that if you want to store them in separate structures, you'll need to register with a separate enum value here.
        private enum RequestType
        {
            PerFrameData,
        }

        // initializing our DispatcherTimers
        private void InitializeTimers()
        {
            simConnectDispatcherTimer = new DispatcherTimer() { Interval = new TimeSpan(0, 0, 0, 0, SIMCONNECT_TIMER_INTERVAL_MS) };
            simConnectDispatcherTimer.Tick += SimConnectTimer_Tick;
        }

        // function that will be called by the DispatcherTimer to receive new data from the game
        private void SimConnectTimer_Tick(object? sender, EventArgs e)
        {
            try
            {
                simconnect?.ReceiveMessage();
            }
            catch (Exception ex)
            {
                Debug.WriteLine($"Exception in {nameof(SimConnectTimer_Tick)}: {ex.Message}");
            }
        }

        // called whenever we receive a new set of SimVar data
        private void Simconnect_OnRecvSimobjectData(SimConnect sender, SIMCONNECT_RECV_SIMOBJECT_DATA data)
        {
            switch ((RequestType)data.dwRequestID)
            {
                case RequestType.PerFrameData:
                    simvars = (SimVars)data.dwData[0];

                    Debug.WriteLine($"Received: Altitude: {simvars.Altitude}, Barometer: {simvars.KohlsmanSettingHg}");

                    altimeterTextBox.Text = $"{simvars.Altitude:0.00}";
                    barometerTextBox.Text = $"{simvars.KohlsmanSettingHg:0.00}";

                    break;

                default:
                    Debug.WriteLine($"Unsupported Request Type: {data.dwRequestID}");
                    break;
            }
        }

        // called when the connection throws an exception so we can log it
        private void Simconnect_OnRecvException(SimConnect sender, SIMCONNECT_RECV_EXCEPTION data)
        {
            Debug.WriteLine($"Exception received: {data}");
        }

        // called when the game exits
        private void Simconnect_OnRecvQuit(SimConnect sender, SIMCONNECT_RECV data)
        {
            simConnectDispatcherTimer.Stop();
        }

        // called when we connect to the game for the first time
        private void Simconnect_OnRecvOpen(SimConnect sender, SIMCONNECT_RECV_OPEN data)
        {
            // the framework is flexible enough to let you specify the units. check the documentation, for example for altitude you could set this to "feet", "meters", etc.
            simconnect.AddToDataDefinition(RequestType.PerFrameData, "INDICATED ALTITUDE", "feet", SIMCONNECT_DATATYPE.FLOAT32, 0, SimConnect.SIMCONNECT_UNUSED);
            simconnect.AddToDataDefinition(RequestType.PerFrameData, "KOHLSMAN SETTING HG", "inHG", SIMCONNECT_DATATYPE.FLOAT32, 0, SimConnect.SIMCONNECT_UNUSED);

            // register our SimVar structure with the PerFrameData enum
            simconnect.RegisterDataDefineStruct<SimVars>(RequestType.PerFrameData);

            // request data from sim
            simconnect.RequestDataOnSimObject(RequestType.PerFrameData, RequestType.PerFrameData, SimConnect.SIMCONNECT_OBJECT_ID_USER, SIMCONNECT_PERIOD.SIM_FRAME, SIMCONNECT_DATA_REQUEST_FLAG.DEFAULT, 0, 0, 0);
        }

        // Connect button event handler
        private void connectButton_Click(object sender, RoutedEventArgs e)
        {
            if (Process.GetProcessesByName(MSFS_PROCESS_NAME).Any())
            {
                try
                {
                    var handle = Process.GetCurrentProcess().MainWindowHandle;

                    simconnect = new SimConnect(PLUGIN_NAME, handle, WM_USER_SIMCONNECT, null, 0);

                    // register a callback for each handler:
                    // called when you connect to the sim for the first time
                    simconnect.OnRecvOpen += new SimConnect.RecvOpenEventHandler(Simconnect_OnRecvOpen);
                    // called when the sim exits
                    simconnect.OnRecvQuit += new SimConnect.RecvQuitEventHandler(Simconnect_OnRecvQuit);
                    // called when there's an exception when sending us the data
                    simconnect.OnRecvException += new SimConnect.RecvExceptionEventHandler(Simconnect_OnRecvException);
                    // called every time we are sent new simvar data
                    simconnect.OnRecvSimobjectData += new SimConnect.RecvSimobjectDataEventHandler(Simconnect_OnRecvSimobjectData);

                    // start out DispatcherTimer to start polling from the game
                    simConnectDispatcherTimer.Start();

                    // toggle the connect/disconnect buttons
                    connectButton.IsEnabled = false;
                    disconnectButton.IsEnabled = true;
                }
                catch (Exception ex)
                {
                    Debug.WriteLine($"{ex.Message}");
                }
            }
        }

        // Disconnect button event handler
        private void disconnectButton_Click(object sender, RoutedEventArgs e)
        {
            // stop our DispatcherTimer
            simConnectDispatcherTimer.Stop();

            // toggle the connect/disconnect buttons
            connectButton.IsEnabled = true;
            disconnectButton.IsEnabled = false;
        }
    }
}

MainWindow.xaml.cs

Ok that looks like a lot, I swear I tried to keep it as simple as possible, but here is what the code is doing:

  1. Creating a SimConnect object that will be used to read data from the sim
  2. Register the event handlers for when the SimConnect object sends the events we are interested on:
    1. OnRecvOpen - handler for when we connect successfully to the sim
    2. OnRecvQuit - handler for when the sim is exiting
    3. OnRecvSimobjectData - handler for when the sim sends us a new data object with SimVars
    4. OnRecvException - handler for when there is an exception
  3. Create one or more structures that will hold our SimVars, which the sim will return with the current values
  4. Create an enum with values representing our different structures
  5. Register our two SimVars so they can be returned as our structure object.
  6. Use a DispatcherTimer to run a background task to check for new data
  7. Connect/Disconnect to/from a local MSFS 2020 instance
  8. On new data received, update the UI with the latest altimeter values

About Data Types

An important thing we need to keep in mind when designing your struct is what data types to use. Let's look at our struct:

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
private struct SimVars
{
  public float Altitude;
  public float KohlsmanSettingHg;
}

Notice that in the Simconnect_OnRecvSimobjectData handler, we are doing an explicit casting of an object to our SimVars structure,

simvars = (SimVars)data.dwData[0]

and by doing so, the data gets copied from the object heap into our structure sequentially. This works because of the StructLayout(LayoutKind.Sequential) attribute we added to our struct, since the structure is stored in memory sequentially as well.

We told SimConnect the order and how big that data is when we registered the SimVars via the simconnect.AddToDataDefinition(...) calls, and specifying the SIMCONENCT_DATATYPE.

simconnect.AddToDataDefinition(RequestType.PerFrameData, "INDICATED ALTITUDE", "feet", SIMCONNECT_DATATYPE.FLOAT32, 0, SimConnect.SIMCONNECT_UNUSED);

simconnect.AddToDataDefinition(RequestType.PerFrameData, "KOHLSMAN SETTING HG", "inHG", SIMCONNECT_DATATYPE.FLOAT32, 0, SimConnect.SIMCONNECT_UNUSED);

That is, we are expecting to receive 4 bytes for the altitude and 4 bytes for the barometer in that order. This exactly matches our SimVars structure.

This sounds tedious and prone for errors, especially if you add/remove or move the properties in your structure and forget to do the corresponding update where you are registering your SimVars, so be mindful when updating your struct.

Here are some of the mappings I've used in my projects:

C# type SIMCONNECT_DATATYPE
bool, int INT32
long INT64
float FLOAT32
double FLOAT64
string STRING8, STRING32, STRING64, STRING128, STRING256
Note: when you are working with strings, you must add an extra annotation specifying the size of the string matching the SIMCONNECT_DATATYPE you registered with
public struct GPS {
  [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
  public string GpsWpNextId;                      // GPS WP NEXT ID
}


public void RegisterMySimVars()
{
  simconnect.AddToDataDefinition(RequestType.PerFrameData, "GPS WP NEXT ID", "", SIMCONNECT_DATATYPE.STRING256, 0, SimConnect.SIMCONNECT_UNUSED);
  ...
}

Running and Connecting to the Sim

  • Start MSFS 2020:Open Microsoft Flight Simulator 2020.Navigate to the World Map.Select your favorite airport and click Fly.Wait for the simulator to finish loading.
  • Debugging the App:In Visual Studio, start debugging your application.Once the app is running, click Connect.
  • Verify Functionality:If everything is set up correctly, the app should connect to the simulator.You’ll see the Altimeter and Barometer values updating in real-time on your UI.You can interact with the simulator: fly around, adjust the barometer knob, and observe the changes reflected in your application.Since we added some Debug.WriteLine(...) statements, we can open the Output View in Visual Studio to see some output. Should look something like this:

Developer Mode

If you haven't already done so, go to General Options-> Developers and turn on DEVELOPER MODE. Save and go back to the sim.

If we go to the top of the screen, you'll see the Developer menu shown. If it's not shown, move your mouse to the top of the screen and it should be visible. It has an autohide option, which you can turn off if you want. Click on the Tools tab and, on the SimConnect Inspector menu item.

Once the SimConnect Inspector window opens, click on the drop down menu and you'll see our new Hello MSFS application connected to the game.

Fun fact: if you are using other plugins or peripherals that use a plugin like Logitech's Instrument Panel, you'll see them here too.

If we click on the Frames tab, we can see the frame where we registered our two SimVars and requested data sent to us.

We can also click on the Data Definitions tab and see our two SimVars:

Fun fact: if you select the other plugins, you can see what SimVars they have registered, which helps you figure out what to use for yours.

Wrapping things up

This tutorial has covered how to read data from Microsoft Flight Simulator 2020 using the SimConnect SDK. To summarize:

  1. Reading Data:
    • Consult the documentation to find the SimVars you need.
    • Add the SimVars to your struct and register them using AddToDataDefinition(...).
  2. Data Order and Type Matching:
    • The order in which you register SimVars with AddToDataDefinition(...) must match the order of the properties in your data structure. This is crucial for accurate data retrieval.
    • Ensure that the data types of your structure properties align with the types specified in the SimConnect documentation to avoid mismatches.