SimConnect + WASM combined project using VS2019

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.

Hi HBilliet,

Thanks a lot for the very detailed explanation and sharing your information. I just looking into this but I have to digg in to understand all of it. I have not much background in C but build in the past an ipad app for x-plane because I was fed up with the golflight panels. Hopefully i can build the same for msfs with the explanation you gave!

Thanks again much appreciated!

1 Like

In my case, I will make a choice for one particular airplane, and stick to it. Currently it is the FBW A32NX, but I’m also looking forward to the release of the Fenix A320. In that case, my Standalone WASM Module should check if that type of plane is loaded, and if it is, I should start using the variables. Although, I also read somewhere that some variables might not be available immediately when the plane is loaded, so regular checking should indeed be a good practice.

1 Like

I like “figures and facts”, so I did a simple timing experiment. I wrote some code in my WASM Module to get the value of the variable “A32NX_EFIS_L_OPTION” several times each second. I compared 3 methods, to see what the difference is.

  1. MEASUREMENT 1
    This uses the execute_calculator_code for “(L:A32NX_EFIS_L_OPTION)”. This is expected to be the slowest way.
  2. MEASUREMENT 2
    This first pre-compiles the code using gauge_calculator_code_precompile. This is expected to be faster than the previous method.
  3. MEASUREMENT 3
    This uses the native Gauge API like get_named_variable_value. This is expected to be the fastest method.

The code I used for this (for people who want to check if I didn’t miss anything) is this:

I used a timing function that I found on stackoverflow. Don’t blame me if this is a complex method, because I didn’t put too much attention on it. I simply googled, found it and used it (lazy Sunday evenings :laughing:).

// file WASM_HABI.cpp

...

#include <chrono>

...

// Get time stamp in microseconds.
uint64_t micros()
{
	uint64_t us = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::high_resolution_clock::
		now().time_since_epoch()).count();
	return us;
}

Next, in module_init, I subscribe for the “1sec” event.

// file WASM_HABI.cpp

enum eEvents
{
	EVENT_FLIGHT_LOADED,
	EVENT_FRAME,
	EVENT_1SEC,
	EVENT_FLIGHTLOADED,
	EVENT_AIRCRAFTLOADED
};

...

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

SubscribeToSystemEvent lets you subscribe to several SystemEvents. We used this before to subscribe to the “Frame” Event. If you look at the SimConnect SDK Documentation, you will see that there are about 21 SystemEvents we can subscribe to. Once subscribed, your MyDispatchProc will be called when the SystemEvent occurs.

Although, the way to identify which SystemEvent occured can be different depending on the SystemEvent itself:

SystemEvents having their own SIMCONNECT_RECV_ID

This is for example the case with the “Frame” SystemEvent, which has the SIMCONNECT_RECV_ID = SIMCONNECT_RECV_ID_EVENT_FRAME. In this case we simple include an additional case in our main switch statement in MyDispatchProc.

SystemEvents that are captured as SIMCONNECT_RECV_ID_EVENT

This is for example the case with the “1sec” SystemEvent. When the SystemEvent occurs, our MyDispatchProc will be triggered with a SIMCONNECT_RECV_ID = SIMCONNECT_RECV_ID_EVENT, which we need to add as an additional case in our main switch statement. There we cast the SIMCONNECT_RECV data structure in a SIMCONNECT_RECV_EVENT structure:

SIMCONNECT_RECV_EVENT* evt = (SIMCONNECT_RECV_EVENT*)pData;

And now, evt->uEventID will contain the value that was used when subscribing to the SystemEvent. For “1sec” we used the value “EVENT_1SEC”.

Remark: I didn’t find any documentation where it is clearly described which SystemEvents have their own SIMCONNECT_RECV_ID, and which SystemEvents use SIMCONNECT_RECV_ID_EVENT. If somebody has this info, I would be more than interested to know!

In the code below you see that every second I am getting the value of the variable “A32NX_EFIS_L_OPTION” 500 times using the 3 methods described above, and each time I’m showing the time difference in micro-seconds. I use the presence of items in the vector LVars as a way to start measuring (if not, it could crash because my FBW A32NX is not loaded yet), and if I register more variables I even loop the test multiple times (I’m misusing my LVars count for this purpose - it was the easiest way to get some control).

void CALLBACK MyDispatchProc(SIMCONNECT_RECV* pData, DWORD cbData, void* pContext)
{
	switch (pData->dwID)
	{
		case SIMCONNECT_RECV_ID_EVENT:
		{
			SIMCONNECT_RECV_EVENT* evt = (SIMCONNECT_RECV_EVENT*)pData;

			switch (evt->uEventID)
			{
				case EVENT_1SEC:
				{
					uint64_t tStart;
					uint64_t tEnd;
					FLOAT64 val = 0;
					ID varID;
					PCSTRINGZ sCompiled;
					UINT32 uCompiledSize;

					if (LVars.size() != 0)
					{

						// MEASUREMENT 1 - using execute_calculator_code

						// start of measurement
						tStart = micros();

						// perform action
						for (auto& lv : LVars)
						{
							for (int i = 0; i < 500; i++)
							{
								execute_calculator_code("(L:A32NX_EFIS_L_OPTION)", &val, (SINT32*)0, (PCSTRINGZ*)0);
							}
						}

						// end of measurment
						tEnd = micros();

						fprintf(stderr, "%s: MEAS1: 500 times %u variables - Time: %llu - Last val: %f\n", WASM_Name, LVars.size(), tEnd - tStart, val);

						// MEASUREMENT 2 - using pre-compiled calculator code

						// start of measurement
						tStart = micros();

						// perform action
						for (auto& lv : LVars)
						{
							gauge_calculator_code_precompile(&sCompiled, &uCompiledSize, "(L:A32NX_EFIS_L_OPTION)");

							for (int i = 0; i < 500; i++)
							{
								execute_calculator_code(sCompiled, &val, (SINT32*)0, (PCSTRINGZ*)0);
							}
						}

						// end of measurment
						tEnd = micros();

						fprintf(stderr, "%s: MEAS2: 500 times %u size %u - Time: %llu - Last val: %f\n", WASM_Name, LVars.size(), uCompiledSize, tEnd - tStart, val);

						// MEASUREMENT 3 - using Gauge API

						// start of measurement
						tStart = micros();

						// perform action
						for (auto& lv : LVars)
						{
							varID = check_named_variable("A32NX_EFIS_L_OPTION");

							for (int i = 0; i < 500; i++)
							{
								val = get_named_variable_value(varID);
							}
						}

						// end of measurment
						tEnd = micros();

						fprintf(stderr, "%s: MEAS3: 500 times %u varID %i - Time: %llu - Last val: %f\n", WASM_Name, LVars.size(), varID, tEnd - tStart, val);
					}

					break;
				}
				default:
					fprintf(stderr, "%s: SIMCONNECT_RECV_ID_EVENT - Event: UNKNOWN %u\n", WASM_Name, evt->uEventID);
					break; // end case default
			}

			break; // end case SIMCONNECT_RECV_ID_EVENT
		}

... 

	}
}

Now the most exiting moment… the results!!!

When having 1 variable registered in LVars (means 500 iterations):

image

When having 2 variables registred in LVars (means 1000 iterations):

image

It proofs for sure that @Umberto67 was right. If we look at the last results (divide by 1000), then we see that:

  • Using execute_calculator_code it takes 591 nanoseconds per variable
  • Using pre-compilation takes 170 nanoseconds per variable
  • Using native Gauge API takes 10 nanoseconds per variable

This means that get_named_variable_value is about 60 times faster than execute_calculator_code. Still, I must say that I was positively suprised to see that 1000 variables using execute_calculator_code only took 591 microseconds. But if we do that every frame, which in my case is 60 times per second or each 16666 microseconds, it means that 591 microseconds are lost which is 3.5%. If you compare that with the native Gauge API, this is only 0.06%.

Anyway, I wouldn’t look too much at the absolute values, because these will also depend on the hardware you are using. But the relative values are definitely convincing!

1 Like

Amazing work! I have another use for this application in addition to interfacing with your hardware. I have been working on a freeware aircraft for the community that will be pretty comprehensive, and this tool of yours might prove to be extremely useful in testing and debugging. I only glossed over this for right now and if I understand this correctly, in addition to manipulating variables and events (which this alone would be valuable enough) I would also have the ability to enter and test my XML RPN code as well. Either way, I am going to give this a hard look tomorrow, and… Well, this might very well be a game changer. I can’t thank you enough. I’m excited now.

2 Likes

Thanks a lot for your test, I must say I never tried testing this, but I must say I fully expected that result: it’s reasonable to expect a marked improvement by “just” precompiling the XML expression ( there wouldn’t be a separate call if it wasn’t useful ) and of course the native call to read L: variables is WAY faster, because it doesn’t have to deal with any kind of expression evaluation.

Let’s hope other developers will read this post, and would try use these optimizations, when possible.

Hello @MtnFlyCO,

Thanks a lot for the nice words. My implementation is a kind of a framework that explains the concepts. You could indeed expand its functionality to allow sending pure RPN strings, omitting the parser (which is pretty irrelevant in this case), and let these strings then be processed with execute_calculator_code in the WASM Module. I have already some ideas in mind, even allowing to “dictate” the system whether it needs to use execute_calculator_code, precompilation or Gauge API. The main goal of the tool is indeed to allow experimenting and testing.

Hello, I am enjoying your post about Simconnect en WASM. As a C# learner, I’m working on my own throttle controller application. I have a question that I cannot find a satisfactory answer for online:

The simconnect interface uses an enum for the events to transmit (TransmitClientEvent function). As far as I understand now, the EVENTS enum needs to be defined up front, before compiling. Is there no way to create this enum in runtime, so that a user can select the events he wants to use (e.g. AXIS_THROTTLE1_SET, AXIS_THROTTLE2_SET, PROP_PITCH1_AXIS_SET_EX1, etc etc etc) through a WPF app (a drop down list), and I add them at that moment to the enum? Or should I include all potentially usable events then in the EVENTS enum already? But then my enum definition code becomes very very long…

Hope this question makes sense…

1 Like