SimConnect + WASM combined project using VS2019

Chapter 3: Connection with SimConnect

Now we have the framework of our VS2019 Solution ready, we can start with the real work. First we start with some basics on how to connect and disconnect with SimConnect. We will discuss the WASM module in a later chapter.

I am not going to dig into the C++ approach, because I am using the C# version (managed code) which simplifies a lot. But just be aware the all the documentation is written with C++ in mind. You will see that C# uses a slightly different syntax.

The below shows the difference between a C++ and C# implementation for opening a SimConnect client session and using some function.

C++ implementation:

// SimConnect HANDLE
HANDLE hSimConnect = NULL;  

// SimConnect result
HRESULT hr;

// Open the SimConnect client session
hr = SimConnect_Open(&hSimConnect, "Your Application Name", hWnd, WM_USER_SIMCONNECT, 0, 0);
if (hr != S_OK)
    ... error processing

// Use some SimConnect function
hr = SimConnect_AddToDataDefinition(
    hSimConnect,
    DEFINITION_1,
    "Kohlsman setting hg",
    "inHg");
if (hr != S_OK)
    ... error processing where "hr" can be used to identify the error

C# implementation:

// SimConnect object
private SimConnect _oSimConnect = null;

try
{
    // Open the SimConnect client session
    _oSimConnect = new SimConnect("SimConnectHub", hWnd, WM_USER_SIMCONNECT, null, 0);

    // Use some SimConnect function
    _oSimConnect.AddToDataDefinition(
        DEFINITION_1,
        "Kohlsman setting hg",
        "inHg",
        SIMCONNECT_DATATYPE.FLOAT64,
        0.0f,
        SimConnect.SIMCONNECT_UNUSED);
}
catch (COMException ex)
{
    ... error processing where "ex.Message" can be used to identify the error
}

There are 2 main differences:

  1. C++ makes use of a HANDLE that identifies the SimConnect client session, and is obtained from the SimConnect_Open call. It is used as first parameter in all further calls to SimConnect functions.
    C# makes use of a SimConnect object, in which the constructor takes care of the HANDLE internally (probably some private property). The constructor acts as a wrapper of the SimConnect_Open function. The SimConnect functions are members of the SimConnect object, omitting the prefix “SimConnect_” from the function name.
    Example: “hr = SimConnect_AddToDataDefinition(hSimConnect, DEFINITION_1, …” becomese “_oSimConnect.AddToDataDefinition(DEFINITION_1, …”.
  2. The error handling in C++ is using the return value HRESULT. If this value is not equal to S_OK, there is an issue. In C# this is translated in the raising of a COMException, that can be used within a try/catch block.

It is definitely worth reading the section Programming SimConnect Clients using Managed Code. It contains a lot more specifics on the C# wrapper. I’m not going to repeat them all in this post.

The SimConnect constructor has the following signature:

public SimConnect(string szName, IntPtr hWnd, uint UserEventWin32, WaitHandle hEventHandle, uint ConfigIndex);

An easy way to find these signatures is right clicking on the method (in this case “SimConnect”, as that is the constructor name), and choose “Peek Definition”.

image

The problem with SimConnect is the lack of documentation :frowning_face:. So sometimes you will have to do some “guesswork”. For example, I have no clue what the “WaitHandle” is doing as the SimConnect documentation doesn’t talk about this in the context of C#. But we seem not to need it for now, so we will use null which seems to work just fine. (if anybody has a clue, don’t hesitate to comment).

Two important parameters are:

  1. IntPtr hWnd
  2. uint UserEventWin32

It means that when opening a SimConnect session, we will need to provide a window handle of a form that has a WndProc (if I’m not mistaken, all forms have one). SimConnect will then interact with this form by sending messages with the Msg defined in UserEventWin32. You simple need to hook into the WinProc, and dispatch all SimConnect messages to your application.

If you are planning to build a SimConnect client as a DLL, then you probably don’t want to have a visible form. In that case, you will have to use a “MessagePump” (which is actually some kind of “hidden” form). I suggest you look at this post written by @dragonlaird who is exactly doing this.

But as my implementation is based on a Windows Form, I can simply use that handle and pass it to my SimConnect constructor. A good moment to do that is when the Form is “shown”. In VS2019 this is even extremely simple by including the “Shown Event” - just double click on the empty space next to “Shown” below, and it will automatically add “FormHUB_Shown” for you.

image

Let’s have a look in the “FormHUB.cs” file included in my project (I only explain the parts that are relevant - the order of the methods might also differ):

// file FormHUB.cs

public partial class FormHUB : Form
{
    SimConnectHUB _SimConnectHUB = new SimConnectHUB();

    private void FormHUB_Shown(object sender, EventArgs e)
    {
        _SimConnectHUB.SetHandle(this.Handle);
    }
   
    protected override void WndProc(ref Message m)
    {
        _SimConnectHUB?.HandleWndProc(ref m);

        base.WndProc(ref m);
    }

    ....
}

The above is pretty selfexplanatory:

  1. A new “SimConnectHUB” object is created, which is the SimConnect client implementation (file SimConnectHUB.cs)
  2. When the Form is shown, I pass its windows handle to SimConnectHUB
  3. In the Form, WndProc is intercepted, and calls the SimConnectHUB.HandleWndProc before calling its base.WndProc (remark: I could have checked for the specific SimConnect message here, but I do that in HandleWndProc - doesn’t make a lot of difference really).

In the SimConnectHUB object, you will find the implemenation to store the Form handle and deal with the WndProc call:

// file SimConnectHUB.cs

// User-defined win32 event
public const int WM_USER_SIMCONNECT = 0x0402;
// Window handle
private IntPtr _handle = new IntPtr(0);

// Keep Window Handle for connecting with SimConnect
public void SetHandle(IntPtr handle)
{
    _handle = handle;
}

// WndProc hook
public void HandleWndProc(ref Message m)
{
    if (m.Msg == WM_USER_SIMCONNECT)
    {
        ReceiveSimConnectMessage();
    }
}

public void ReceiveSimConnectMessage()
{
    try
    {
        _oSimConnect?.ReceiveMessage();
    }
    catch (Exception ex)
    {
        LogResult?.Invoke(this, $"ReceiveSimConnectMessage Error: {ex.Message}");
        Disconnect();
    }
}

In HandleWndProc, we check if SimConnect has something to say. If it does, then we call “ReceiveSimConnectMessage()” that will call the SimConnect “ReceiveMessage()” method. This is the method that triggers all the SimConnect magic.

I could have put the call to “ReceiveMessage()” directly in “HandleWndProc”, but decided not to do so in case I want to call “ReceiveMessage()” myself somewhere in the code (although, so far this was not needed).

Now that the preparation is done, we are ready to use the “Connect” and “Disconnect” methods that are called by the Connect and Disconnect buttons on the form.

The Connect method looks like this:

// file SimConnectHUB.cs

public void Connect()
{
    if (_bConnected)
    {
        LogResult?.Invoke(this, "Already connected");
        return;
    }

    try
    {
        _oSimConnect = new SimConnect("SimConnectHub", _handle, WM_USER_SIMCONNECT, null, 0);

        // Listen for connect and quit msgs
        _oSimConnect.OnRecvOpen += SimConnect_OnRecvOpen;
        _oSimConnect.OnRecvQuit += SimConnect_OnRecvQuit;

        // Listen for Exceptions
        _oSimConnect.OnRecvException += SimConnect_OnRecvException;

        // Listen for SimVar Data
        _oSimConnect.OnRecvSimobjectData += SimConnect_OnRecvSimobjectData;

        // Listen for ClientData
        _oSimConnect.OnRecvClientData += SimConnect_OnRecvClientData;

        LogResult?.Invoke(this, "Connected");
    }
    catch (COMException ex)
    {
        LogResult?.Invoke(this, $"Connect Error: {ex.Message}");
    }
}

The C# wrapper of SimConnect provides an easy way to intercept interactions by adding EventHandlers that are called when some specific SimConnect events are happening. In my implementation, I only provided the ones that are relevant, but if you use “Peek Definition” you will find 27 of them. Some event names are self-explanatory, but for other ones you might have to dig in to the documentation or google, or simply experiment.

The ones that I added are:

  • OnRecvOpen: This will be called by SimConnect when the session successfully opened, and will be used to initialize some additional items.
  • OnRecvQuit: If we close MSFS2020, then SimConnect will notify our client through this event.
  • OnRecvException: If SimConnect discovers some error, it will notify our client through this event.
  • OnRecvSimobjectData: This is to receive data from Simobjects (SimConnect variables)
  • OnRecvClientData: This is to receive data from Client Area Data which is going to be used to communicate with our WASM module.

The Disconnect method looks like this:

// file SimConnectHUB.cs

public void Disconnect()
{
    if (!_bConnected)
    {
        LogResult?.Invoke(this, "Already disconnected");
        return;
    }

    // Raise event to notify client we've disconnected and get rid of client
    _oSimConnect?.Dispose(); // May have already been disposed or not even been created, e.g. Disconnect called before Connect
    _oSimConnect = null;
    _bConnected = false;
    LogResult?.Invoke(this, "Disconnected");
}

“LogResult” is my own EventHandler that is used to send information back to a multiline TextBox in the form. Because my SimConnectHUB object has no clue at compile time where to send its result to, we deal with that using an EventHandler.

// file SimConnectHUB.cs

public event EventHandler<string> LogResult = null;

When the constructor of the form is called, this EventHandler gets initialized like this:

// file FormHUB.cs

public FormHUB()
{
    InitializeComponent();

    _SimConnectHUB.LogResult += OnAddResult;
}

private void OnAddResult(object sender, string sResult)
{
    if (textResult.Text != "")
        textResult.AppendText(Environment.NewLine);
    textResult.AppendText(sResult);
}

Now SimConnectHUB can Invoke the EventHandler whenever it has something to tell. I’m using the “Null-conditional operator” (the ‘?’-character) in the call, which guarantees that LogResult will only be used when not null (see here).

// file SimConnectHUB.cs

LogResult?.Invoke(this, "some text");

Et voila, we can connect and disconnect our SimConnect Client, and get some results in a multiline TextBox. Ready to start using SimConnect in the next chapter.

Goto Chapter 4: Variables

6 Likes

Without wanting to give compliments for the pleasure of giving them, I think that you are a very good teacher and that you know how to “popularize” your subject without drowning the reader in details.
I read DragonLaird’s posts on FSDeveloper since many years and and I must admit that he is as good as you.
Continue, I follow.

2 Likes

Probably OK in your code, but I assume the _bConnected is set to True upon connection?

You are right @TFEV1909, but I control the flag only in the EventHandlers “OnRecvOpen” and “OnRecvQuit”, because then you are sure that the connection or disconnection really succeeded. Especially for then “OnRecvQuit”, that can even happen “unexpectedly” when MFSF is shut down.

// file SimConnectHUB.cs

private void SimConnect_OnRecvOpen(SimConnect sender, SIMCONNECT_RECV_OPEN data)
{
    Debug.WriteLine("SimConnect_OnRecvOpen"); 
            
    _bConnected = true;

    LogResult?.Invoke(this, $"- Application name: {data.szApplicationName}");
    LogResult?.Invoke(this, $"- Application Version {data.dwApplicationVersionMajor}.{data.dwApplicationVersionMinor} - build {data.dwApplicationBuildMajor}.{data.dwApplicationBuildMinor}");
    LogResult?.Invoke(this, $"- SimConnect  Version {data.dwSimConnectVersionMajor}.{data.dwSimConnectVersionMinor} - build {data.dwSimConnectBuildMajor}.{data.dwSimConnectBuildMinor}");

    InitializeClientDataAreas();
}

private void SimConnect_OnRecvQuit(SimConnect sender, SIMCONNECT_RECV data)
{
    Debug.WriteLine("SimConnect_OnRecvQuit");
    Disconnect();

    _bConnected = false;
}
1 Like

That makes sense. Your steps convinced me to start working on my on C# Simconnect application (potentially moving away from my current Python implementation). Thx a lot!

Chapter 4: Variables

One of the things that confused me most when I was starting with SimConnect are all the different types of variables. This is probably the most difficult chapter for me to explain.

@Umberto67 posted a nice summary of several variables in this post. At the time that I read this (about a month ago), I only understood half of it. But the good thing is that you can bookmark posts in this forum, and re-read them when you have acquired more knowledge. Thanks to a lot of reading, I think I start getting it. But I haven’t experimented with all the possible variable types yet, and might even never need to do so within the scope of my implementation. This means that there are definitely some bits and pieces that I might still be missing, so be a bit cautious with the explanation I’m giving below.

Bottom line is that we have “real variables” which you can give a value and/or you can read the value from (example: “KOHLSMAN SETTING HG”), and some are called “events” which you only trigger and do not necessarily need a value (example: “A32NX.FCU_HDG_INC”).

In the below explanation, I will call them all “variables”, which is including “real variables” and “events”.

We can make a distinction between 2 types of variables:

  1. Variables directly controlled via SimConnect
    I call these variables “native MSFS2020 variables”, because they are found within the simulator. You can find a detailed explanation in the SDK Documentation, where these are called “Simulation Variables” and Events.
    Example of such variable: AUTOPILOT ALTITUDE LOCK VAR.
    The main characteristic of these variables is that we can control them directly with SimConnect.

  2. Variables controlled through a WASM module
    I call these variables “custom variables”, because they are in a lot of (all of the?) cases very specific for a certain object in MSFS2020, example an add-on airplane.
    Example of such variable: A32NX_EFIS_L_OPTION. This is only available for the FlyByWire A32NX, which is a free A320 add-on of very high quality that you can find here. I’m going to use this add-on regularly in this tutorial.
    The main characteristic of these variables is that we can only control them through a WASM module.

If we are going to deal with WASM (see in one of the next chapters), we can use a function called execute_calculator_code. The nice thing is that with this function, you can access all the types of variables, whether they are “native MSFS2020 variables” or “custom variables”. But for this function to know which type of variable we are dealing with, the variables are prefixed with a character and a ‘:’. You can find all these prefixes in the section Variable Types in this part of the SimConnect SDK Documentation. This immediately shows how many different variables do exists (and I only know a few of them so far).

I will use the same type of prefixes in my implementation. At the time of writing this tutorial, the following variable types are used:

  • A: - These are the “native MSFS2020 variables”, which in the FBW documentation are also called “SIMCONNECT VAR”.
  • L: - These are the “custom variables” or also called LVars, which in the FBW documentation are also called “Custom LVAR”.
  • K: - These are EVENTS, and could be “native” or “custom”. With SimConnect, the rule is that if the variable name doesn’t contain a dot ‘.’, it is a “native” one and can be controlled directly via SimConnect. If the name contains a “.”, then it is a custom event and needs to be controlled through WASM. In the FBW documentation these are also called “SIMCONNECT EVENT” or “Custom EVENT”.

Remark: In the FBW documentation, SIMCONNECT VAR or SIMCONNECT EVENT is sometimes written as MSFS VAR or MSFS EVENT - at least, I think that these are the same. If my interpretation is wrong, please don’t hesitate to comment.

The examples we are going to use in my implementation are the following:

  • A:AUTOPILOT ALTITUDE LOCK VAR (native MSFS variable)
  • A:KOHLSMAN SETTING HG (native MSFS variable)
  • A:LIGHT POTENTIOMETER (native MSFS variable)
  • L:A32NX_EFIS_L_OPTION (custom FBW A32NX variable)
  • L:A32NX_EFIS_R_OPTION (custom FBW A32NX variable)
  • K:FUELSYSTEM_PUMP_TOGGLE (native MSFS event - there is no “.” in the name)
  • K:A32NX.FCU_HDG_INC (custom FBW A32NX event - there is a “.” in the name)

One of the challenging things is finding the variable you need to control a certain switch, or read a certain indication in an airplane. Because that’s what you need if you want to interface with your cockpit hardware. I have no common method for this, because it will heavily depend on the airplane you are dealing with. But I will explain some methods that I used with FBW A32NX.

Because FBW A32NX is a custom add on, you won’t find any information in MSFS2020 or SimConnect SDK. You need to look into the documentation of the creators of the add-on. Luckily, FBW A32NX is free and open source, which means that all information is available on the web.

Let’s give a few examples.

Example 1:

I want to read the value of altitude on the FCU (Flight Control Unit) of the A32NX.
image
In the FBW A32NX documentation there is a section for the FCU panel. If you scroll down to the “ALT knob”, you find that you can use the variable “AUTOPILOT ALTITUDE LOCK VAR:3”. This is an “MSFS VAR”, which in my terminology means a “native variable”. You will indeed also find this variable in the SDK Documentation here, where you can read that the value is given in “feet”, and that you can both read from and write to it.

In my implementation, this variable will be entered as “A:AUTOPILOT ALTITUDE LOCK VAR:3,feet,FLOAT64”. Don’t worry too much about the syntax I’m using, and the word FLOAT64. We will see that in some later chapters.

Example 2:

I want to control the 5 buttons on the EFIS (Electronic Flight Instrument System) panel of the A32NX.
image
In the FBW A32NX documentation there is a section for the EFIS control panel. If you scroll down to the “ND Filter”, you find that you can use the variable “A32NX_EFIS_L_OPTION” and “A32NX_EFIS_R_OPTION”. These are “Custom LVAR”, which in my terminology means “custom variables”. You won’t find these in the SDK Documentation. The variable is “R/W” and can take 6 values: 0=none, 1=CSTR, 2=VOR, 3=WPT, 4=NDB, 5=ARPT.

In my implementation, this variable will be entered as “L:A32NX_EFIS_L_OPTION,enum” and “L:A32NX_EFIS_R_OPTION,enum”.

Some more ways to find variables

1. The behaviors window

If you have enabled “Developer Mode” in MSFS2020 (which you probably have to install the SimConnect SDK), then you can also use the “Behaviors” window:

image

This is (at least for me :laughing:) a very complex tool, but it can help revealing some last pieces of the puzzle in finding the correct variables. Hover over a button until it gets the blue focus:

image

Now press “CTRL-G” while the behaviors window is open. The behaviors window will now show a lot of information for the control you selected. I have no clue about most of the information presented here, but if you open a few of the entries, you might find some hints on what variables to use.

image

The above shows again the usage of variable “L:A32NX_EFIS_L_OPTION, enum”, which is the one we showed before. The code is written using RPN (Reverse Polish Notation) that is explained in the SDK Documentation. I’m not going in much more detail here, because it’s not really in the scope of this tutorial.

2. Some interesting websites

We are obviously not the only ones that are looking for variables. In an attempt to avoid to reinvent the wheel, some flightsim enthousiasts have created a website Hubhop that is consolidating the research of many people in finding the right variables. The information found on this website might be shown as “presets” which can be used by other tools like MobiFlight. But it does reveal some useful information as you can see in the below screenshot, where we again see the usage of the variable “L:A32NX_EFIS_L_OPTION” (and if you understand the RPN code, you will see that the preset is toggling the WPT button).

Just be careful with the information found on this website. Everybody can enter information in it, so it is good practice to double check the information. But it is still a great source!

Conclusion on variables

As you can see, it is not easy to find the correct variables. There is no simple method that will work in all cases, but will depend on what you are looking for, and whether you are dealing with an add-on or native MSFS2020. But with the tool that we are making in this tutorial, you will at least have the possibility to experiment by trying out several variables and see the results, until you find the correct ones that you can then use for your own project.

Goto Chapter 5: Variable housekeeping using object VarData

4 Likes

So with help of this topic I managed to build my first C# SimConnect application. I also included a basic event setter, using this site: (C#) I can read! Can I write? | FSDeveloper.

Great stuff!

Translating the data received from Simconnect back into the Struct was the most difficult step, as I’m not an experienced C# developer yet (and probably never :rofl: ).

1 Like

Chapter 5: Variable housekeeping using object VarData

This chapter is a bit “off topic”, but to avoid that people get completely lost in my implementation, I will take some time to explain.

In my object SimConnectHUB, variables are maintained in a “List” with objects of type “VarData”.

// List of registered variables
private List<VarData> Vars = new List<VarData>();

The details of object VarData can be found in the file “VarData.cs”. This object takes care of a few things:

  • Storage of all kinds of values that are needed for the variable (ID’s, name, type, etc…)
  • A helper method “SetID” to assign ID’s, and eventually create them automatically
  • A set of methods to implement the “IEquatable” interface, which allows comparing 2 VarData objects (needed to check if a VarData item already exists in the List, to avoid duplicates)
  • A method “ParseVarString” that parses a string provided by the user, extracting all the values and report if that was successful

The VarData method public ParseResult ParseVarString(string sVar) takes a string with a specific format, which depends on the variable type. The method validates the string sVar, and extracts all the neccessary values. If successful, it will return the enum “ParseResult.Ok”. If not, it will return an error that gives a hint on what is wrong.

There are currently 3 variable types supported:

  1. Type "A"
    These are “native MSFS2020 variables”
    Format: “A:[Variable name],[unit],[DataType]”
    Example: “A:AUTOPILOT ALTITUDE LOCK VAR:3,feet,FLOAT64”

  2. Type "L"
    These are “custom variables”
    Format: "L:[Variable name],[unit]
    Example: “L:A32NX_EFIS_L_OPTION,enum”

  3. Type "K"
    These are events.
    Format: "K:[Event name] (if it contains a “.”, it is a custom event)
    Example of native event: “K:FUELSYSTEM_PUMP_TOGGLE”
    Example of custom event: “K:A32NX.FCU_HDG_INC”

The object SimConnectHUB implements 3 methods that make use of the VarData object and the List.

  • public bool AddVariable(string sVar) - called when pressing the button “Add Variable”
  • public bool RemoveVariable(string sVar) - called when pressing the button “Remove Variable”
  • public bool SetVariable(string sVar, string sValue) - called when pressing the button “Set Value”

Each method will start with doing some “housekeeping” like checking if we are connected, checking if the user input is valid (not empty and parsing is successful) and checking if the variable does or does not exist already. If it passes all the tests, the real SimConnect work can start which will be explained in the next chapters.

I will not spend more time in explaining the implementation of VarData, because this tutorial is about SimConnect. But I think that the implementation is pretty self-explanatory. If you would have any questions, don’t hesitate to ask of course.

Goto Chapter 6: Controlling the “A:”-type variables

2 Likes

Chapter 6: Controlling the “A:”-type variables

This is a very long chapter, but there is a “6. Summary” at the end.

A-type variables are the “native MSFS2020 variables” which can be controlled directly by SimConnect (without the need of an extra WASM module). So let’s start with those.

1. Adding a data definition

Whether we want to set a variable with a certain value, or we want to get updated when the variable changes in MSFS2020, the first thing we ALWAYS need to do is to give an ID to the variable. This ID will be called the “DefineID”.

You will see this a lot in SimConnect. Variables or data structures are being identified with an ID in your client. We all know that computers prefer to work with ID’s, because they are a lot faster and easier to use than long strings.

In public bool AddVariable(string sVar) in the object SimConnectHUB, when dealing with an “A:-variable”, the following method is called (“v” is an object of type VarData):

_oSimConnect.AddToDataDefinition(
    (SIMCONNECT_DEFINITION_ID)v.uDefineID,
    v.sName,
    v.sUnit,
    v.scDataType,
    0.0f,
    SimConnect.SIMCONNECT_UNUSED);

In the SimConnect SDK Documentation you can find the explanation for SimConnect_AddToDataDefinition.

The AddToDataDefinition method is doing nothing more than linking the “v.sName, v.sUnit, v.scDataType” with an ID “v.uDefineID” that is unique in my client.

The “v.uDefineID” has been defined in the line of code just preceeding the AddToDataDefinition call:

v.SetID(VarData.AUTO_ID, VarData.AUTO_ID);

This method, when using the parameter “VarData.AUTO_ID” is taking the next available ID and stores it in “v.uDefineID”. The method also gives a value to “v.uRequestID”, which is another unique value that we will use later.

Let’s have a closer look at the signature of AddToDataDefinition:

public void AddToDataDefinition(Enum DefineID, string DatumName, string UnitsName, SIMCONNECT_DATATYPE DatumType, float fEpsilon, uint DatumID);
  • We see that the first parameter has type “Enum”. That is typical in SimConnect functions - rather then using “int” or other variable types, they use “Enum”. Because my ID’s are of type “UInt16”, I have created some enum types to use for typeconversion:
// Some enums for type conversions
private enum SIMCONNECT_DEFINITION_ID { }
private enum SIMCONNECT_REQUEST_ID { }
private enum EVENT_ID { }
  • “v.scDataType” has the type SIMCONNECT_DATATYPE. This is a type defined by SimConnect. You will see in the VarData.ParseVarString that the DataType string is converted in a SIMCONNECT_DATATYPE. Example: “FLOAT64” will be converted to “SIMCONNECT_DATATYPE.FLOAT64”, which is the value used as parameter in AddToDataDefinition.

  • The last 2 parameters are not used in my implementation. Although fEpsilon might be useful in some cases. I suggest you read the SimConnect SDK Documentation if you need more info.

If we call AddToDataDefinition with a variable that does not exist in MSFS2020, then the EventHandler “SimConnect_OnRecvException” would have been called with the exception “UNRECOGNIZED_ID”.

Remark: I’m using a static DefineID and RequestID counter in the object VarData that increases every time I’m calling “SetID”. If a call to AddToDataDefinition fails, I don’t reuse the ID. That means that you can call AddToDataDefinition a maximum of 65536 times before running out of ID’s. But I’m pretty sure that this limit will never be reached.

2. Define your data structure

This is something very specific for Managed Code, but it is extremely important not to forget this step. Although this is a very important step, you don’t find a lot of documentation or more details about it. You have to use the below method to link your DefineID with a data type.

_oSimConnect.RegisterDataDefineStruct<[your data type]>(DefineID);

In my implementation, this method is called immediately after AddToDataDefinition. It uses a switch statement to register the correct data type.

switch (v.scDataType)
{
    case SIMCONNECT_DATATYPE.INT32:
        _oSimConnect.RegisterDataDefineStruct<Int32>((SIMCONNECT_DEFINITION_ID)v.uDefineID);
        break;
    case SIMCONNECT_DATATYPE.INT64:
        _oSimConnect.RegisterDataDefineStruct<Int64>((SIMCONNECT_DEFINITION_ID)v.uDefineID);
        break;
    case SIMCONNECT_DATATYPE.FLOAT32:
        _oSimConnect.RegisterDataDefineStruct<float>((SIMCONNECT_DEFINITION_ID)v.uDefineID);
        break;
    case SIMCONNECT_DATATYPE.FLOAT64:
        _oSimConnect.RegisterDataDefineStruct<Double>((SIMCONNECT_DEFINITION_ID)v.uDefineID);
        break;
    case SIMCONNECT_DATATYPE.STRING256:
        _oSimConnect.RegisterDataDefineStruct<String256>((SIMCONNECT_DEFINITION_ID)v.uDefineID);
        break;
}

You can also use structures containing multiple variables. In that case, you will have to add all the members (variables) of the structure one by one with a call to AddToDataDefinition using the same DefineID. After that, you have to use RegisterDataDefineStruct to register your structure. In my implementation, I use a different DefineID for each variable, because that makes most sense.

When you want to use structures, you have to define them in a specific way as shown below. For strings, you have to use Type Marshaling. Using variable string also requires some extra efforts, which I haven’t used yet. My implementation only uses strings of 256 characters.

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
struct Struct1
{
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
    public String title;
    public double latitude;
    public double longitude;
    public double altitude;
};

3. Setting a variable value

Once we have successfully called AddToDataDefinition and RegisterDataDefineStruct, we can set the value of the variable with the following method:

_oSimConnect.SetDataOnSimObject(
    (SIMCONNECT_DEFINITION_ID)vInList.uDefineID,
    0,
    SIMCONNECT_DATA_SET_FLAG.DEFAULT,
    dValue);

In the SimConnect SDK Documentation you can find the explanation for SimConnect_SetDataOnSimObject.

The signature of SetDataOnSimObject looks like this:

public void SetDataOnSimObject(Enum DefineID, uint ObjectID, SIMCONNECT_DATA_SET_FLAG Flags, object pDataSet);

If you compare this signature with the one in the SimConnect SDK Documentation, you will see that the parameters ArrayCount and cbUnitSize are not found. I am not 100% sure, but I think that this has to do with the fact that in the Managed Code version, TAGGED data is not supported. At least, you can read this somewhere in the section Notes On .NET Client Programming.

Partial data return (SIMCONNECT_DATA_REQUEST_FLAG_TAGGED) is unsupported. For a description of this flag see the SimConnect_AddToDataDefinition function.

Let’s have a closer look at the parameters for SetDataOnSimObject:

  • DefineID: This is our earlier ID that we linked with our variable.
  • ObjectID: This is always 0 in my case. This tells SimConnect which “object” I’m controlling. 0 means that I’m controlling the plane I’m flying, and this will always be the case when I want to connect with my cockpit hardware.
  • Flags: We can choose between SIMCONNECT_DATA_SET_FLAG.DEFAULT and SIMCONNECT_DATA_SET_FLAG.TAGGED. As I don’t think TAGGED is supported (see above comment), I have not done any efforts to study this option. Hence I always use DEFAULT.
  • pDataSet: This is the data to be sent. Be careful that you use the same data type as the one you registered with the call to RegisterDataDefineStruct (in my implementation I’m using some dynamic casting).

Be aware that not all variables can be set. If you try to set a variable that is not settable, you will get a call to SimConnect_OnRecvException with some error.

  1. Getting a variable value

Finally we want to get the value of a variable. To do that, we will tell SimConnect that we are interested in the value of a variable. We will formalize this interest in a “Request” and give it a “RequestID”. SimConnect will then send back the value when it is available, and will identify this with our earlier provided RequestID. This means that there is no synchronous call to SimConnect to get the value of a variable, but only an asynchronous way of working.

Telling SimConnect that you are interested in a value is done by a call to the method RequestDataOnSimObject.

_oSimConnect.RequestDataOnSimObject(
    (SIMCONNECT_REQUEST_ID)v.uRequestID,
    (SIMCONNECT_DEFINITION_ID)v.uDefineID,
    0,
    SIMCONNECT_PERIOD.SIM_FRAME,
    SIMCONNECT_DATA_REQUEST_FLAG.CHANGED,
    0, 0, 0);

In the SimConnect SDK Documentation you can find the explanation for SimConnect_RequestDataOnSimObject.

The signature of RequestDataOnSimObject looks like this:

public void RequestDataOnSimObject(Enum RequestID, Enum DefineID, uint ObjectID, SIMCONNECT_PERIOD Period, SIMCONNECT_DATA_REQUEST_FLAG Flags, uint origin, uint interval, uint limit);

The principle is pretty simple. You tell SimConnect that you request the value referenced by DefineID, and you give this request a number RequestID. Once SimConnect has a value, it will call your EventHandler SimConnect_OnRecvSimobjectData where you can identify the request with the earlier provided RequestID.

RequestDataOnSimObject takes some extra parameters:

  • ObjectID: This is always 0 in my case. This tells SimConnect which “object” I’m controlling. 0 means that I’m controlling the plane I’m flying, and this will always be the case when I want to connect with my cockpit hardware.
  • Period: This tells SimConnect how many times we want the value. Possibilities are NEVER, ONCE, VISUAL_FRAME, SIM_FRAME and SECOND.
  • Flags: This can be DEFAULT, CHANGED and TAGGED.
  • origin, interval, limit: I suggest you look at the SimConnect SDK Documentation for this. In my implementation these don’t make much sense.

I’m using Period = SIM_FRAME and Flags = CHANGED. This means that SimConnect sends me the variable value every frame (for example 60 times per second). That is a lot of data to digest, especially if you are doing that with a few 100 variables. But here the Flags = CHANGED come to the rescue, because this means that I will only get an update when the value has changed. But if the value changes, it will be sent within one frame, which is pretty fast (fast enough for cockpit hardware).

The EventHandler that receives the value is shown below.

private void SimConnect_OnRecvSimobjectData(SimConnect sender, SIMCONNECT_RECV_SIMOBJECT_DATA data)
{
    if (_oSimConnect != null)
    {
        VarData vInList = Vars.Find(x => x.cType == 'A' && x.uRequestID == data.dwRequestID);
        if (vInList != null)
        {
            vInList.oValue = data.dwData[0];
            LogResult?.Invoke(this, $"{vInList} value = {vInList.oValue}");
        }
    }
}

The Managed Code wrapper will make sure that the OnRecvSimobjectData EventHandler is provided with a structure SIMCONNECT_RECV_SIMOBJECT_DATA. There are a lot of members in that structure, but the first one that interests me is the RequestID. With this RequestID, I’m looking for the A-type variable in my List. If that variable is found, I update the value in the VarData object and send the result to my GUI. The fact that the value is stored in the VarData allows other processes to read from it in a synchronous way, but just be careful with locking in multithreaded approaches (not in scope to go further in detail here).

Remark: In my implementation, OnRecvSimobjectData will only be called for A-Type variables, so I don’t have to make the check x.cType == 'A', but this is to guarantee that my code is future proof.

If we are not interested anymore in the value of the variable, we can tell SimConnect to stop sending us updates. The safest way (to avoid an error) is to first stop the request with a call to RequestDataOnSimObject, but with the Period = NEVER. After that, we can call ClearDataDefinition, which simply removes the definition (and probably clears some memory as well).

_oSimConnect.RequestDataOnSimObject(
    (SIMCONNECT_REQUEST_ID)vInList.uRequestID,
    (SIMCONNECT_DEFINITION_ID)vInList.uDefineID,
    0,
    SIMCONNECT_PERIOD.NEVER,
    SIMCONNECT_DATA_REQUEST_FLAG.DEFAULT,
    0, 0, 0);
_oSimConnect.ClearDataDefinition((SIMCONNECT_DEFINITION_ID)vInList.uDefineID);

6. Summary

Let’s summarize the long explanation above.

In all cases, we need to call AddToDataDefinition providing a unique ID, and then we need to use RegisterDataDefineStruct to register the type of the variable (or struct).

v.SetID(VarData.AUTO_ID, VarData.AUTO_ID);
_oSimConnect.AddToDataDefinition(
    (SIMCONNECT_DEFINITION_ID)v.uDefineID,
    v.sName,
    v.sUnit,
    v.scDataType,
    0.0f,
    SimConnect.SIMCONNECT_UNUSED);

switch (v.scDataType)
{
    case SIMCONNECT_DATATYPE.INT32:
        _oSimConnect.RegisterDataDefineStruct<Int32>((SIMCONNECT_DEFINITION_ID)v.uDefineID);
        break;
    case SIMCONNECT_DATATYPE.INT64:
        _oSimConnect.RegisterDataDefineStruct<Int64>((SIMCONNECT_DEFINITION_ID)v.uDefineID);
        break;
    case SIMCONNECT_DATATYPE.FLOAT32:
        _oSimConnect.RegisterDataDefineStruct<float>((SIMCONNECT_DEFINITION_ID)v.uDefineID);
        break;
    case SIMCONNECT_DATATYPE.FLOAT64:
        _oSimConnect.RegisterDataDefineStruct<Double>((SIMCONNECT_DEFINITION_ID)v.uDefineID);
        break;
    case SIMCONNECT_DATATYPE.STRING256:
        _oSimConnect.RegisterDataDefineStruct<String256>((SIMCONNECT_DEFINITION_ID)v.uDefineID);
        break;
}

Then it depends on what we want to do.

  • If we want to write to a variable, we call SetDataOnSimObject
_oSimConnect.SetDataOnSimObject(
    (SIMCONNECT_DEFINITION_ID)vInList.uDefineID,
    0,
    SIMCONNECT_DATA_SET_FLAG.DEFAULT,
    vInList.oValue);
  • If we want to read from a variable, we first call RequestDataOnSimObject to tell SimConnect we want to get the value, and we use the SimConnect_OnRecvSimobjectData EventHandler to receive the data.
_oSimConnect.RequestDataOnSimObject(
    (SIMCONNECT_REQUEST_ID)v.uRequestID,
    (SIMCONNECT_DEFINITION_ID)v.uDefineID,
    0,
    SIMCONNECT_PERIOD.SIM_FRAME,
    SIMCONNECT_DATA_REQUEST_FLAG.CHANGED,
    0, 0, 0);

...

private void SimConnect_OnRecvSimobjectData(SimConnect sender, SIMCONNECT_RECV_SIMOBJECT_DATA data)
{
    if (_oSimConnect != null)
    {
        VarData vInList = Vars.Find(x => x.cType == 'A' && x.uRequestID == data.dwRequestID);
        if (vInList != null)
        {
            vInList.oValue = data.dwData[0];
            LogResult?.Invoke(this, $"{vInList} value = {vInList.oValue}");
        }
    }
}

Finally, when we want to get rid of being sent values of a variable, we can get rid of it

_oSimConnect.RequestDataOnSimObject(
    (SIMCONNECT_REQUEST_ID)vInList.uRequestID,
    (SIMCONNECT_DEFINITION_ID)vInList.uDefineID,
    0,
    SIMCONNECT_PERIOD.NEVER,
    SIMCONNECT_DATA_REQUEST_FLAG.DEFAULT,
    0, 0, 0);
_oSimConnect.ClearDataDefinition((SIMCONNECT_DEFINITION_ID)vInList.uDefineID);

Goto Chapter 7: Making our own WASM module

2 Likes

Hello Hans,

Apologies for the noob question. I am trying to send commands to MSFS through SimConnect. What is the difference between the Simulation Event ID and the Simulation Variables? Which should I use? Thank you.

Hello @AthenaGrey1,

I will control Events in a later chapter, but in Chapter4: Variables I also referred to a good explanation given by @Umberto67 that you can find in this post.

Sometimes you can use a variable or an event to achieve the same goal. Let’s take an example.

If we look at the FCU Panel documentation of FlyByWire, scrolling down to the “ALT knob”, we see the following:

The first one is an “MSFS Variable” and can be controlled directly with SimConnect. In my explanation above, I call this “native MSFS variables”, or “A-Type”. If you use my application, you can add the variable as “A:AUTOPILOT ALTITUDE LOCK VAR:3,feet,FLOAT64”, and then you can read from it, and even write to it. Makes sense, because if we look in the SimConnect SDK Documentation and search for the variable “AUTOPILOT ALTITUDE LOCK VAR” here, we see the green tickmark on the right indicating that this is a writeable variable (in fact, ALL variables are readable, some of them are writeable).

But the FlyByWire documentation also gives us 2 "Custom EVENT"s that can be used to Increase and Decrease the ALT. In my explanation above, I call this “custom variables”, which in case of Events are “K-Type”. If you use my application, you can add them as “K:A32NX.FCU_ALT_INC” and “K:A32NX.FCU_ALT_DEC”. If you then use the button “Set Value” in my application (the value is even irrelevant) you will see that the ALT is increasing and decreasing.

So when do you use the variable, and when do you use the event? That really depends on the implementation. In case of ALT, using the events has some advantages. In the A320, you can only increase or decrease with steps of 100 or 1000 (depending on the selection you made on the FCU). But if you use the variable, you seem even to be able to give a value like “12345”, which is normally not possible in the A320. I think there is even no guarantee that this couldn’t cause any unpredictable effects. So if you use the variable directly, you might need to build some logic yourself to avoid giving wrong values. So in my opinion, it’s safer to use the event in this case.

Probably a bit off-topic here, but I do have a reason to use the variable directly. If you use the INC/DEC Events with a rotary encoder in your cockpit hardware, there might be some “lag” when you turn the knob fast. Reason is that for each tick, you have to send an INC/DEC event. In my cockpit hardware (in the past, using JeeHell A320 FMGS with FSX), I used a different approach in which my encoder is driving the 7-Segment display directly, which means that it goes very fast and shows all the ticks. This gives a very reactive feel. And I only send the value to the simulator every 200 msec controlling the variable directly, which reduces the communication a lot. Although, you still have to build some extra “synchronization logic” to make sure that, if the simulation changes the ALT value itself, that this is also displayed correctly (without turning the encoder). I might explain this method later in the Cockpit Builder section on this forum.

3 Likes

Thank you very much for your helpful and detailed explanation.

Chapter 7: Making our own WASM module

If we want to control “custom variables”, we will need to do that through a WASM module. In Chapter 1: SimConnect and WASM - what is this? I already briefly touched on what a WASM module is, and in Chapter 2: Setting up the environment I showed how to prepare your VS2019 solution if you want to develop your own WASM module from scratch.

You can not use C# for WASM. You have to use C/C++ instead. So it might be a bit confusing if you have been dealing with the above chapters, like you have been speaking Italian, and now you have to switch to Spanish :laughing:.

Build our first WASM module

Unfortunately, the few lines of code that Microsoft includes in the WASM template are not very helpful.

// Test.cpp

#include <stdio.h>
#include "Test.h"

extern "C" MODULE_EXPORT void test(void)
{
	// TODO
}

I would at least have expected that the template would include something more useful code such as a framework for the initialization and de-initialization.

The most basic WASM module could look like this - let’s call the project “Test_WASM”:

// Test_WASM.cpp

#include <MSFS/MSFS.h>
#include <MSFS/MSFS_WindowsTypes.h>

#include <SimConnect.h>

#include <MSFS/Legacy/gauges.h>

#include "Test_WASM.h"

const char* WASM_Name = "Test_WASM";
const char* WASM_Version = "00.01";

extern "C" MSFS_CALLBACK void module_init(void)
{
	HRESULT hr;

	fprintf(stderr, "%s: Initializing WASM version [%s]", WASM_Name, WASM_Version);

	// ... do some initialization

	fprintf(stderr, "%s: Initialization completed", WASM_Name);
}

extern "C" MSFS_CALLBACK void module_deinit(void)
{
	fprintf(stderr, "%s: De-initializing WASM version [%s]", WASM_Name, WASM_Version);

	// ... do some de-initialization

	fprintf(stderr, "%s: De-initialization completed", WASM_Name);
}

Let’s first look at the includes:

  • MSFS/MSFS.h and MSFS/MSFS_WindowsTypes.h are always required.
  • MSFS/Legacy/gauges.h is required if you want to use the Gauge API. In my opinion, it doesn’t make much sense to create a WASM Module without using the Gauge API, because without that your WASM Module can’t do anything useful.
  • SimConnect.h is needed when you want your WASM module to use SimConnect. Also this seems pretty obvious, because as explained in earlier chapters you need SimConnect to be able to interact with the WASM Module.
  • Test_WASM.h is added by the template automatically

Next to that we have 2 important functions:

  • module_init which is called when the WASM module is starting. Here we can do all kinds of initializations to bring our WASM module to life.
  • module_deinit which is called when the WASM module stops. Here we can do some de-initialization and cleanup.

Remark: To get some information on what our WASM Module is doing, I’m using the fprintf command and write to “stderr”. This will be shown in the MSFS2020 Console window as error messages. I could also write to “stdout”, which would show it as warning messages. But if I use “stdout”, it seems that after a newline ("\n") the next lines aren’t shown anymore. It’s like I can only print one line to “stdout”, and that’s it. I might be missing something, but it’s maybe not a coincidence that @MobiFlight in their WASM Module also uses “stderr” as output. I already posted a question on this in this forum, but no answers yet. If somebody has a clue, please let me know in this post.

We can build the above WASM module in a separate project. If you have used the instructions in Chapter 2: Setting up the environment, then you should find your WASM module in [Project folder]\Test_WASM\MSFS\Debug\Test_WASM.wasm (or in the Release folder if you build a release version).

Use our first WASM module

Our WASM Module can be installed as a Gauge or as a Stand-Alone Module. I have no clue how to install it as a Gauge, but I know how to install it as a Stand-Alone Module. For this, you need to copy it to the MSFS2020 Community folder. You have to use a specific folder structure and also add 2 additional files “layout.json” and “manifest.json”.

Let’s first find the Community folder. It all depends whether you have installed MSFS2020 from the store, or via Steam. But there is a very easy way to find this. Launch MSFS2020, and if Developer Mode is enabled, you can use the menu on top and go to “Tools/Virtual File System”.

image

From the next window you can directly “Open Community Folder”.

image

Now create a folder for you WASM module, and in that folder create a subfolder “modules”.

image

Copy the earlier built file “Test_WASM.wasm” in the “modules” folder.

Next to that, we need 2 json files in the “Test_WASM” folder (not in the “modules” subfolder!).

image

The content of the json files is below:

layout.json

{
  "content": [
    {
      "path": "modules/Test_WASM.wasm",
      "size": 28171,
      "date": 132857136167661584
    }
  ]
}

manifest.json

{
  "dependencies": [],
  "content_type": "MISC",
  "title": "Test_WASM",
  "manufacturer": "",
  "creator": "HABI",
  "package_version": "0.1.00",
  "minimum_game_version": "1.19.8",
  "release_notes": {
    "neutral": {
      "LastUpdate": "",
      "OlderHistory": ""
    }
  }
}

Make sure that the “path” and “size” in the layout.json and the “title” in the manifest.json is using the correct info about your WASM Module. I have no more knowledge about the “manifest.json”, but the one shown here works fine. For the “layout.json”, make sure that the size of your WASM module is correctly set each time you update the WASM Module. You can easily find this size by right clicking on the Test_WASM.wasm file, and select properties. There you will find the size that you have to type over in the “layout.json” file.

image

There is also a handy tool MSFSLayoutGenerator which allows you to drag the “layout.json” file on it, and it will automatically update the size and even the date. When writing this tutorial, I could download the executable from here.

When all files are copied and json files are updated, we can start MSFS2020 (time to cross some fingers :crossed_fingers:). If you have done everything correctly, then you should see the below messages in the Console Window in MSFS2020 (in Developper mode, menu “Windows/Console”).

Now we are able to develop our own WASM module, we can start with writing its body to deal with our “custom variables” and communicate with our client through SimConnect.

Help needed! How can we restart a WASM Module without restarting MSFS2020?

The main issue with developing a WASM Module if you put them in the Community folder (Stand-Alone Module) is that you have to restart MFSF2020 completely after each change. Knowing that restarting the sim takes several minutes already makes it clear that developing a WASM Module is not an easy and smooth process. There is a post on this forum from @Voss1917 to ask if there is a way to restart a WASM Module without restarting MSFS2020, but so far none of the offered solutions seems to work.

It seems that installing the WASM Module as a Gauge should do the job, because then you are not using it as a Stand-Alone Module, but as part of an airplane. You can then restart your airplane, which should also restart the WASM Module. But I have no clue how to do that.

I hope that some clever people can come up with some answer.

EDIT: @RoystonSidhe came with the solution!!! You find his explanation here. This is a real time-saver! Reloading the WASM Module only takes about 15 seconds, which is a huge improvement compared to having to restart the sim every time. THANKS A MILLION RoystonSidhe!

Goto Chapter 8: Communicating with the WASM Module through Client Data Areas

3 Likes

Chapter 8: Communicating with the WASM Module through Client Data Areas

I warn you, but this is a very long chapter!!!

We already know that it is not possible to interact with a WASM Module directly from an External Application. The only way to do that is using ClientDataAreas within SimConnect. These ClientDataAreas can be used as shareable memory areas between our External Application and the WASM Module.

In my implementation, I use 3 Client Data Areas and built a protocol between my External Application and the WASM Module. This protocol will act as an interface between my SimConnect Client (External Application) and the Gauge API (WASM Module).

  • Send commands to the WASM Module
  • Get answers from the WASM Module
  • Get values of variables from the WASM Module

Schematically, this looks like the below:

Functions/Methods needed to work with ClientDataAreas

Setting up the ClientDataArea

There is some similarity between the functions for ClientDataAreas and the SimConnect methods we used for A-Vars in Chapter 6: Controlling the “A:”-type variables.

  • MapClientDataNameToID
    We can give each ClientDataArea a “human readable name”. This name allows the different SimConnect Clients to identify the ClientDataArea. But internally in the SimConnect Client, the name is mapped to an ID, that is a lot easier to work with. If the ClientDataArea already exists, then SimConnect will send an Exception with the message “ALREADY_CREATED”. In my implementation I just ignore this error.
  • CreateClientData
    This creates the ClientDataArea for this client.
  • AddToClientDataDefinition
    This is the same as AddToDataDefinition used in Chapter 6: Controlling the “A:”-type variables. Although, this time we are not dealing with variable names, but we use an offset and a size in the Client Data Area.

The below schematic shows how these 3 functions work together (using 2 ClientDataAreas):

It’s important that the above 3 functions are called in all SimConnect Clients that will interact with the same ClientDataAreas, and that the Offset/Size mappings do match. If not you will obviously get unpredictable results.

ClientDataArea ID’s and Definition ID’s are local for each SimConnect Client, which means that you don’t have to use the same ID’s in both SimConnect Clients. The SimObject Definition ID’s and ClientDataArea Definition ID’s are even completely separate, which means that you can use the same ID’s without conflicts.

In the Managed Code (C#), you also need to register the data structure of each definition, as we had to do for the A-Vars. This time, we don’t use RegisterDataDefineStruct, but RegisterStruct. The signature looks as below:

public void RegisterStruct<RECV, T>(Enum dwID) where RECV : SIMCONNECT_RECV;

We’ll see further below how we use this in our code.

Setting or getting data via ClientDataArea

This works exactly the same as explained in Chapter 6: Controlling the “A:”-type variables. Once we have defined our ClientDataAreas and the Definitions of the items, we can start using them.

  • SetClientData
    This function takes the ClientDataArea ID and the Definition ID together with the data that you want to set. Once the data is set, it can be read by the SimConnect Client on the receiving side.
  • RequestClientData
    This function takes the ClientDataArea ID, the Definition ID and a Request ID that will identify the data that we are going to receive (again, this works asynchronously). The requested data will be sent sent to the EventHandler OnRecvClientData. (Be aware that for A-Type variables, we used RequestDataOnSimObject and results were sent to the EventHandler OnRecvSimobjectData, so it is slightly different). We can use the same concepts using Period (example: FRAME) and Flags (example: CHANGED).

Protocol between External Application and WASM Module

Before we dive into the code, let me first explain the protocol that I use between the External Application and the WASM Module. The protocol takes care of:

  • Setting values of variables or trigger Events
  • Getting values of variables

The Gauge API function that we will use to set and get values is execute_calculator_code:

BOOL execute_calculator_code(
    PCSTRINGZ  code,
    FLOAT64*  fvalue,
    SINT32*  ivalue,
    PCSTRINGZ*  svalue
    );

This is a very versatile function to send data or get data using a “code”-string based on RPN notation (see Reverse Polish Notation). In the current implementation, I limit the return values to FLOAT64 only. Some examples:

This will set the value 4 in the variable “L:A32NX_EFIS_L_OPTION”:

FLOAT64 val = 0;
execute_calculator_code("4 (>L:A32NX_EFIS_L_OPTION)", &val, (SINT32 *)0, (PCSTRINGZ *)0);

This gets the value of the variable “L:A32NX_EFIS_R_OPTION” and stores it in the variable “val”:

FLOAT64 val = 0;
execute_calculator_code("(L:A32NX_EFIS_R_OPTION)", &val, (SINT32 *)0, (PCSTRINGZ *)0);

Set values of variables or trigger Events

This is achieved by sending a command “HW.Set.[command to be executed]” from the External Application to the WASM Module through the ClientDataArea “HABI_WASM.Command” (example: “HW.Set.4 (>L:A32NX_EFIS_L_OPTION)”. The WASM Module will get this command, and will use execute_calculator_code with [command to be executed]. This can be used to set values but also to trigger Events (example: “HW.Set.K:(>A32NX.FCU_HDG_INC)”)

Get values of variables

This is done with a few steps:

  1. First a variable is registered in the WASM Module by adding it to a list vector<LVar> LVars;. That is achieved by sending a command “HW.Reg.[command to be registered]” from the External Application to the WASM Module through the ClientDataArea “HABI_WASM.Command” (example: “HW.Reg.(L:A32NX_EFIS_R_OPTION)”. During that registration process, the WASM Module will take the next available Offset in the ClientDataArea “HABI_WASM.LVars”, and add the variable with size FLOAT64 using AddToClientDataDefinition.
  2. After that, the WASM Module will send back that Offset and Size data through the ClientDataArea “HABI_WASM.Acknowledge” using a data structure LVar. That structure also has a member to immediately send back the current value of that variable.
  3. The External Application will react to this acknowledge to start listening for that element (offset/size) in the ClientDataArea “HABI_WASM.LVars” using RequestClientData.
  4. The WASM Module will check all registered variables every FRAME. If a variable has a different value compared to the one stored in the list, the data element in the ClientDataArea “HABI_WASM.LVars” is updated using SetClientData.
  5. The External Application will get notified of this new value through its call to the EventHandler OnRecvClientData.

Although execute_calculator_code is very straight forward and easy to use, it might not be the fastest way as already mentioned by @Umberto67 in this post. I haven’t tested my code with several 100 registered variables that need to be checked every frame. But the code could be optimized by using the gauge API functions check_named_variable, get_named_variable_value and set_named_variable_value.

There seems also to exists something like gauge_calculator_code_precompile. I have no clue what this does, and I don’t know if the returned compiled string can then be used with execute_calculator_code, and if this results also in a performance gain. There is (as usual) not a lot of documentation available. If somebody has a clue, please don’t hesitate to reply in this post.

Let’s finally look at some code

In both the External Application and the WASM Module, I have added a function InitializeClientDataAreas() that does the neccessary initialization of the 3 ClientDataArea’s that we will use.

  1. ClientDataArea “HABI_WASM.Command” for commands sent from the External Application to the WASM Module
  2. ClientDataArea “HABI_WASM.Acknowledge” to get the acknowledge from the WASM Module when registering “custom variables” we want to listen for
  3. ClientDataArea “HABI_WASM.LVars” for receiving changed values from “custom variables”

Let’s have a look at the C# code of the External Application.

// file SimConnectHUB.cs

// Client Area Data names
private const string CLIENT_DATA_NAME_LVARS = "HABI_WASM.LVars";
private const string CLIENT_DATA_NAME_COMMAND = "HABI_WASM.Command";
private const string CLIENT_DATA_NAME_ACKNOWLEDGE = "HABI_WASM.Acknowledge";

// Client Area Data ID's
private enum CLIENT_DATA_ID
{
    LVARS = 0,
    CMD = 1,
    ACK = 2
}

...

private void InitializeClientDataAreas()
{
    Debug.WriteLine("InitializeClientDataAreas()");

    try
    {
        // register Client Data (for LVars)
        _oSimConnect.MapClientDataNameToID(CLIENT_DATA_NAME_LVARS, CLIENT_DATA_ID.LVARS);
        _oSimConnect.CreateClientData(CLIENT_DATA_ID.LVARS, SimConnect.SIMCONNECT_CLIENTDATA_MAX_SIZE, SIMCONNECT_CREATE_CLIENT_DATA_FLAG.DEFAULT);

        // register Client Data (for WASM Module Commands)
        _oSimConnect.MapClientDataNameToID(CLIENT_DATA_NAME_COMMAND, CLIENT_DATA_ID.CMD);
        _oSimConnect.CreateClientData(CLIENT_DATA_ID.CMD, MESSAGE_SIZE, SIMCONNECT_CREATE_CLIENT_DATA_FLAG.DEFAULT);
        _oSimConnect.AddToClientDataDefinition(CLIENTDATA_DEFINITION_ID.CMD, 0, MESSAGE_SIZE, 0, 0);

        // register Client Data (for LVar acknowledge)
        _oSimConnect.MapClientDataNameToID(CLIENT_DATA_NAME_ACKNOWLEDGE, CLIENT_DATA_ID.ACK);
        _oSimConnect.CreateClientData(CLIENT_DATA_ID.ACK, (uint)Marshal.SizeOf<LVarAck>(), SIMCONNECT_CREATE_CLIENT_DATA_FLAG.DEFAULT);
        _oSimConnect.AddToClientDataDefinition(CLIENTDATA_DEFINITION_ID.ACK, 0, (uint)Marshal.SizeOf<LVarAck>(), 0, 0);
        _oSimConnect.RegisterStruct<SIMCONNECT_RECV_CLIENT_DATA, LVarAck>(CLIENTDATA_DEFINITION_ID.ACK);
        _oSimConnect.RequestClientData(
            CLIENT_DATA_ID.ACK,
            CLIENTDATA_REQUEST_ID.ACK,
            CLIENTDATA_DEFINITION_ID.ACK,
            SIMCONNECT_CLIENT_DATA_PERIOD.ON_SET,
            SIMCONNECT_CLIENT_DATA_REQUEST_FLAG.DEFAULT,
            0, 0, 0);
    }
    catch (Exception ex)
    {
        LogResult?.Invoke(this, $"InitializeClientDataAreas Error: {ex.Message}");
    }
}

We can clearly see the 3 ClientDataArea initializations.

  • For the LVars area, we only create the ClientDataArea. We will add the definitions and requests later when we receive the acknowledge from the WASM Module.
  • For the Command area, we create the ClientDataArea and add the Definition of a 256 character string. I haven’t used RegisterStruct (honestly, I forgot), but the code seems to work perfectly. It might be that RegisterStruct is only needed when we want to receive data, and because we will only send data through the ClientDataArea “HABI_WASM.Command”, it might be that it can be omitted. If somebody knows for sure, please don’t hesitate to answer in this post.
  • For the Acknowledge area, we do all the calls. We create the ClientDataArea, we add the definition of the LVarAck structure (= same structure as the LVar structure in the WASM Module) register it, and finally start listening for it.

In the WASM Module, let’s first have a look at the module_init, which is called when the WASM Module is started.

// file WASM_HABI.cpp

extern "C" MSFS_CALLBACK void module_init(void)
{
	HRESULT hr;

	g_hSimConnect = 0;

	fprintf(stderr, "%s: Initializing WASM version [%s]", WASM_Name, WASM_Version);

	hr = SimConnect_Open(&g_hSimConnect, WASM_Name, (HWND)NULL, 0, 0, 0);
	if (hr != S_OK)
	{
		fprintf(stderr, "%s: SimConnect_Open failed.\n", WASM_Name);
		return;
	}

	hr = SimConnect_SubscribeToSystemEvent(g_hSimConnect, EVENT_FRAME, "Frame");
	if (hr != S_OK)
	{
		fprintf(stderr, "%s: SimConnect_SubsribeToSystemEvent \"Frame\" failed.\n", WASM_Name);
		return;
	}

	hr = SimConnect_CallDispatch(g_hSimConnect, MyDispatchProc, NULL);
	if (hr != S_OK)
	{
		fprintf(stderr, "%s: SimConnect_CallDispatch failed.\n", WASM_Name);
		return;
	}

	RegisterClientDataArea();

	fprintf(stderr, "%s: Initialization completed", WASM_Name);
}

In this call, we do 4 things:

  1. Open a SimConnect connection
  2. Subscribe to the SimConnect SystemEvent “Frame”. You can register for several SystemEvents. The Frame event will allow us to check all the values of the registered variables on each frame.
  3. Register our DispatchProc to SimConnect
  4. Register our ClientDataAreas

The registration of the ClientDataAreas is almost the same as in our External Application.

// file WASM_HABI.cpp

const char* CLIENT_DATA_NAME_LVARS = "HABI_WASM.LVars";
const SIMCONNECT_CLIENT_DATA_ID CLIENT_DATA_ID_LVARS = 0;

const char* CLIENT_DATA_NAME_COMMAND = "HABI_WASM.Command";
const SIMCONNECT_CLIENT_DATA_ID CLIENT_DATA_ID_COMMAND = 1;

const char* CLIENT_DATA_NAME_ACKNOWLEDGE = "HABI_WASM.Acknowledge";
const SIMCONNECT_CLIENT_DATA_ID CLIENT_DATA_ID_ACKNOWLEDGE = 2;

...

void RegisterClientDataArea()
{
	HRESULT hr;

	// Create Client Data Area for the LVars
	// Max size of Client Area Data can be 8192 which is 2048 LVars of type float
	// If more LVars are required, then multiple Client Data Areas would need to be foreseen
	hr = SimConnect_MapClientDataNameToID(g_hSimConnect, CLIENT_DATA_NAME_LVARS, CLIENT_DATA_ID_LVARS);
	if (hr != S_OK) {
		fprintf(stderr, "%s: Error creating Client Data Area %s. %u", WASM_Name, CLIENT_DATA_NAME_LVARS, hr);
		return;
	}
	SimConnect_CreateClientData(g_hSimConnect, CLIENT_DATA_ID_LVARS, SIMCONNECT_CLIENTDATA_MAX_SIZE, SIMCONNECT_CREATE_CLIENT_DATA_FLAG_DEFAULT);

	// Create Client Data Area for a command ("WH.Reg.[LVar]" or "WH.Set.[LVar]")
	// Max size of string in SimConnect is 256 characters (including the '/0')
	// If longer commands are required, then they would need to be split in several strings
	hr = SimConnect_MapClientDataNameToID(g_hSimConnect, CLIENT_DATA_NAME_COMMAND, CLIENT_DATA_ID_COMMAND);
	if (hr != S_OK) {
		fprintf(stderr, "%s: Error creating Client Data Area %s. %u", WASM_Name, CLIENT_DATA_NAME_COMMAND, hr);
		return;
	}
	SimConnect_CreateClientData(g_hSimConnect, CLIENT_DATA_ID_COMMAND, MESSAGE_SIZE, SIMCONNECT_CREATE_CLIENT_DATA_FLAG_DEFAULT);

	// Create Client Data Area to acknowledge a registration back to the client
	hr = SimConnect_MapClientDataNameToID(g_hSimConnect, CLIENT_DATA_NAME_ACKNOWLEDGE, CLIENT_DATA_ID_ACKNOWLEDGE);
	if (hr != S_OK) {
		fprintf(stderr, "%s: Error creating Client Data Area %s. %u", WASM_Name, CLIENT_DATA_NAME_ACKNOWLEDGE, hr);
		return;
	}
	SimConnect_CreateClientData(g_hSimConnect, CLIENT_DATA_ID_ACKNOWLEDGE, sizeof(LVar), SIMCONNECT_CREATE_CLIENT_DATA_FLAG_DEFAULT);

	// This Data Definition will be used with the COMMAND Client Area Data
	hr = SimConnect_AddToClientDataDefinition(
		g_hSimConnect,
		DATA_DEFINITION_ID_STRING_COMMAND,
		0,				// Offset
		MESSAGE_SIZE				// Size
		);

	// This Data Definition will be used with the ACKNOWLEDGE Client Area Data
	hr = SimConnect_AddToClientDataDefinition(
		g_hSimConnect,
		DATA_DEFINITION_ID_ACKNOWLEDGE,
		0,				// Offset
		sizeof(LVar)	// Size
		);

	// We immediately start to listen to commands coming from the client
	SimConnect_RequestClientData(
		g_hSimConnect,
		CLIENT_DATA_ID_COMMAND,						// ClientDataID
		CMD,										// RequestID
		DATA_DEFINITION_ID_STRING_COMMAND,			// DefineID
		SIMCONNECT_CLIENT_DATA_PERIOD_ON_SET,		// Trigger when data is set
		SIMCONNECT_CLIENT_DATA_REQUEST_FLAG_DEFAULT	// Always receive data
		);
}

The difference with the External Application is that here we only listen to the ClientDataArea “HABI_WASM.Command” for commands coming in.

The MyDispatchProc is dealing with 2 events:

  • When we receive data from the ClientDataArea “HABI_WASM.Command”.
  • When we are triggered for a new FRAME
void CALLBACK MyDispatchProc(SIMCONNECT_RECV* pData, DWORD cbData, void* pContext)
{
	switch (pData->dwID)
	{
		case SIMCONNECT_RECV_ID_CLIENT_DATA:
		{
			SIMCONNECT_RECV_CLIENT_DATA* recv_data = (SIMCONNECT_RECV_CLIENT_DATA*)pData;

			switch (recv_data->dwRequestID)
			{
				case CMD: // "HW.Set." or "HW.Reg." received
				{
					char* sCmd = (char*)&recv_data->dwData;
					fprintf(stderr, "%s: MyDispatchProc > SIMCONNECT_RECV_ID_CLIENT_DATA - CMD \"%s\"\n", WASM_Name, sCmd);

					// "HW.Set."
					if (strncmp(sCmd, CMD_Set, strlen(CMD_Set)) == 0)
					{
						execute_calculator_code(&sCmd[strlen(CMD_Set)], nullptr, nullptr, nullptr);
						break;
					}

					// "HW.Reg."
					if (strncmp(sCmd, CMD_Reg, strlen(CMD_Reg)) == 0)
					{
						RegisterLVar(&sCmd[strlen(CMD_Reg)]);
						break;
					}

					break; // end case CMD
				}

				default:
					fprintf(stderr, "%s: MyDispatchProc > SIMCONNECT_RECV_ID_CLIENT_DATA - UNKNOWN request %u\n", WASM_Name, recv_data->dwRequestID);
					break; // end case default
			}

			break; // end case SIMCONNECT_RECV_ID_CLIENT_DATA
		}

		case SIMCONNECT_RECV_ID_EVENT_FRAME:
		{
			ReadLVars();
			break; // end case SIMCONNECT_RECV_ID_EVENT_FRAME
		}

		default:
			break; // end case default
	}
}

The flow is pretty straight forward.

  • When we receive the command “HW.Set.”, we use execute_calculator_code to execute the command
  • When we receive the command “HW.Reg.”, we register a new variable with a call to RegisterLVar
  • When we receive a new FRAME trigger, we read all registered variables using a call to ReadLVars

I’m not going to explain the code in more detail, because the concept is already explained earlier in this chapter, and the code is pretty self-explanatory.

The last code I want to explain is the EventHandler OnRecvClientData in the External Application. This EventHandler is called in 2 cases:

  • When the WASM Module sends back an acknowledge through the ClientDataArea “HABI_WASM.Acknowledge”, the variable (that has already been added to the List Vars in AddVariable) is updated with the returned Offset and Size, after which the methods AddToClientDataDefinition, RegisterStruct and RequestClientData are being called. From now on, the External Application will be listening to changed values for these.
  • When a registered variable in the WASM Module has a new value through the ClientDataArea “HABI_WASM.LVars”, it’s value gets updated.
// file SimConnectHUB.cs

private void SimConnect_OnRecvClientData(SimConnect sender, SIMCONNECT_RECV_CLIENT_DATA data)
{
    Debug.WriteLine($"SimConnect_OnRecvClientData() - RequestID = {data.dwRequestID}");

    if (data.dwRequestID == (uint)CLIENTDATA_REQUEST_ID.ACK)
    {
        try
        {
            var ackData = (LVarAck)(data.dwData[0]);
            Debug.WriteLine($"----> Acknowledge: ID: {ackData.DefineID}, Offset: {ackData.Offset}, String: {ackData.str}, value: {ackData.value}");

            // if LVar DefineID already exists, ignore it, otherwise we will get "DUPLICATE_ID" exception
            if (Vars.Exists(x => x.cType == 'L' && x.uDefineID == ackData.DefineID))
                return;

            // find the LVar, and update it with ackData
            VarData vInList = Vars.Find(x => x.cType == 'L' && $"(L:{x.sName})" == ackData.str);
            if (vInList == null)
                return;
            vInList.SetID(ackData.DefineID, VarData.AUTO_ID); // use ackData.DefineID and next available RequestID
            vInList.uOffset = ackData.Offset;
            vInList.oValue = ackData.value;

            _oSimConnect?.AddToClientDataDefinition(
                (SIMCONNECT_DEFINITION_ID)ackData.DefineID,
                (uint)ackData.Offset,
                sizeof(float),
                0,
                0);
            Debug.WriteLine($"----> AddToClientDataDefinition ID:{ackData.DefineID} Offset:{ackData.Offset} Size:{sizeof(float)}");

            _oSimConnect?.RegisterStruct<SIMCONNECT_RECV_CLIENT_DATA, float>((SIMCONNECT_DEFINITION_ID)vInList.uDefineID);

            _oSimConnect?.RequestClientData(
                CLIENT_DATA_ID.LVARS,
                (SIMCONNECT_REQUEST_ID)vInList.uRequestID,
                (SIMCONNECT_DEFINITION_ID)vInList.uDefineID,
                SIMCONNECT_CLIENT_DATA_PERIOD.ON_SET, // data will be sent whenever SetClientData is used on this client area (even if this defineID doesn't change)
                SIMCONNECT_CLIENT_DATA_REQUEST_FLAG.CHANGED, // if this is used, this defineID only is sent when its value has changed
                0, 0, 0);

            LogResult?.Invoke(this, $"{vInList} value = {vInList.oValue}");
        }
        catch (Exception ex)
        {
            LogResult?.Invoke(this, $"SimConnect_OnRecvClientData Error: {ex.Message}");
        }
    }
    else if (data.dwRequestID >= (uint)CLIENTDATA_REQUEST_ID.START_LVAR)
    {
        VarData vInList = Vars.Find(x => x.cType == 'L' && x.uDefineID == data.dwDefineID);

        if (vInList != null)
        {
            vInList.oValue = data.dwData[0];
            LogResult?.Invoke(this, $"{vInList} value = {vInList.oValue}");
        }
        else
            LogResult?.Invoke(this, $"LVar data received: unknown LVar DefineID {data.dwDefineID}");
    }
}

That’s it folks! Now we can read and write from both “native MSFS variables” and “custom variables” including triggering native and custom Events. The code includes both the External Application and the WASM Module.

Remark: The method of registration/acknowledge is different as the way of working used in the MobiFlight WASM Module. There the Offset/Size is immediately defined when adding the variable. This means that both the External Application and the WASM Module increase the Offset every time a new variable is added, and use that same value in their registration process. That works fine for MobiFlight, because (if I am not mistaken) you can only have one instance of MobiFlight connected with MSFS2020. But in my concept, I want to run my SimConnectHUB application on several PC’s (you can use SimConnect over the network). This means that I might register the same variables through the WASM Module. If I use the MobiFlight method, the variable would be registered a second time in the WASM Module, which is reducing efficiency. The Register/Acknowledge protocol guarantees that if you register the same variable, that you also get the Offset/Size of the first registration.

3 Likes

Epilog: What next…

My frustration of not finding good documentation on SimConnect and WASM for MSFS2020 made me write this post. I hope that it can help people with their first steps in this matter. Although, I want to repeat that I’m not an expert and not a professional programmer. I just like to analyze things, and share my knowledge with other people.

My main goal is to build my own A320 cockpit, which I consider as a 10 year project. I’m still in study phase (this SimConnect/WASM implementation is part of it). This means that I will continue expanding this code and update the changes on GitHub. Next challenges will be to add my USB modules to connect with my PIC-Controlled hardware (I have this already working for the JeeHell A320 FMGS on FSX), and to expand the implementation to handle the interactions with my hardware. Still a few months work ahead! I might write some other posts in the Cockpit Builder section to report on my progress made.

If there is anything in this post that seems to be not correct, then please let me know. After all, this forum is to share knowledge and ideas, and we can all learn from it. Don’t hesitate to reach out to me with a PM, but if you have questions about SimConnect/WASM, I prefer you do that in this or a seperate post. In this way, a lot more people can learn from the answers given.

Greetz,

HBilliet

Goto Addendum 1: Some more on Events

4 Likes

Many thanks for your post.
I have it in a PDF file on my Desltop :wink:

1 Like

How many pages is it? :rofl: Because I honestly have no clue!

You guessed it right, it does exactly as it looks like: it parses an XML expression and return it in a more compact format, which can be used as input for the execute_calculator_code call. This should be used to optimize an XML expression that rarely changes so, you precompile it once, and store the compressed version and always use that one, until the XML expression changes for some reason, so you precompile it again.

Although execute_calculator_code is very straight forward and easy to use, it might not be the fastest way as already mentioned by @Umberto67 in this post. I haven’t tested my code with several 100 registered variables that need to be checked every frame. But the code could be optimized by using the gauge API functions check_named_variable, get_named_variable_value and set_named_variable_value.

Yes, you should try to use the Gauge API function when possible. Using execute_calculator_code JUST to read or write an L: variable is a waste of performance, the calculator code parser should only be used for complex XML expressions, possibly together with the precompile.

Here’s a link to an 8-year old post of mine on fsdeveloper, in which I explained how to create a C++ class to easily handle L: variables, mostly as an excuse to demonstrate some OOP techniques:

The attached code is C++, so I guess it should be fairly easy to adapt it to a WASM module.

Hello @Umberto67,

I’m glad to read that my assumptions were correct :smiley:. Just a small question. Is my assumption also correct that register_named_variable is used to register a complete new variable, that is then exposed to other WASM Modules? In my case, because I only want to control existing variables, I should use check_named_variable in stead. Is that correct?

Yes, you should probably always use check_named_variable first, to see if that variable has already being registered by somebody else, and call register_named_variable only if it hasn’t.

Be aware of another thing: If you are writing a Stand-Alone WASM module, DO NOT assume the variables IDs will always stay the same for the whole duration of the session.

If the airplane changes, it’s very likely the IDs will change, and it also depends whether the airplane code unregistered them on exit and even how the airplane code that registered the variables first is made ( C++/WASM or just XML or JS ).

So, it might be worth subscribing to the airplane loaded event, and re-check all the IDs again when the airplane changes. Of course, nothing theoretically prevents you to always check the id each time you access the variable, which will prevent your code from being confused by a changed ID but, this is clearly worse for performance so, better store the IDs after you use them for the first time, and re-check them when you have reasons to believe they might have changed, most notably when a new airplane is loaded.