SimConnect + WASM combined project using VS2019

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.

5 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

6 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.

1 Like

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

Hello @TFEV1909,

@Dragonlaird mentioned this also in one of his posts.

That is also what I have been doing several times in my code (you find my source code on GitHub). Below I show some extract of the code.

First I create a few empty enums to be used for type conversion (I could have done it with one single enum, but I think that is in favor of the readability of the code to use different ones). And then I use this to typecast a variable.

// file: SimConnectHUB.cs

// Some enums for type conversions
private enum SIMCONNECT_DEFINITION_ID { }
private enum SIMCONNECT_REQUEST_ID { }
private enum EVENT_ID { }

...

// usage later somewhere in the code

_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);

...

I’m not the expert on Events (yet :slight_smile:), but I guess you want to use the below function:

HRESULT SimConnect_MapClientEventToSimEvent(
    HANDLE  hSimConnect,
    SIMCONNECT_CLIENT_EVENT_ID  EventID,
    const char*  EventName
    );

If we look at the signature in C#:

public void MapClientEventToSimEvent(Enum EventID, string EventName);

Below is an example of how you could use this.

// you need some variable
UInt16 uDefineID;

// at runtime you can define the value
uDefineID = [next available DefineID, or whatever way to define a value...]

// and somewhere in your code you will "link" that DefineID with the Event - here we use the typecasting
_oSimConnect.MapClientEventToSimEvent((SIMCONNECT_DEFINITION_ID)uDefineID, "AXIS_THROTTLE1_SET");

I hope that this is the answer you needed?

1 Like

Just one question: the Event ID is actually a string (like KEY_AUTOPILOT_ON). Should I be able to typecast this into an enum as well?

Why? I don’t see any reason to do that.

The name of the Event is a “human readable format” of the Event, and you convert it in an ID, unique for your client, with the function MapClientEventToSimEvent. From then onwards, you keep using that ID.

Great Project @HBilliet

Any chance of including a copy of the Built app (exe) on your GitHub ?

The issue I’m struggling with is in the TransmitClientEvent function.

I was looking at this post: (C#) I can read! Can I write? | FSDeveloper, and did not understand the value of the MapClientEventToSimEvent, as he isn’t using the mapped event as far as I understand in the TransmitClientEvent function, but the direct event.

But now I understand what you are explaining. I could just map AUTOPILOT_ON to whatever numeric ID, and then map that using MapClientEventToSimEvent, and then should be able to recall that numeric ID in the TransmitClientEvent function (casted to the EVENTS enum).

Am going to test!

EDIT: This works indeed as you described! I now use (with 123 as numeric ID just to test, and events is an empty ENUM as defined by @HBilliet above):

simconnect.MapClientEventToSimEvent((EVENTS)123, "AUTOPILOT_ON");
simconnect.TransmitClientEvent(SimConnect.SIMCONNECT_OBJECT_ID_USER, (EVENTS)123, 0, GROUP.ID_PRIORITY_STANDARD, SIMCONNECT_EVENT_FLAG.GROUPID_IS_PRIORITY);

This solution indeed allows me to set up any event during runtime.

1 Like

Hello @N6722C,

Sure. I have put a Release build of SimConnectHUB and the WASM Module including the json files here on GitHub.

  • Do not forget to copy the 2 dll files included in the Release folder - they should be in the same folder as the executable “SimConnectWasmHUB.exe”
  • Copy the directory WASM_HABI with all files in the MSFS Community folder (see Chapter 7: Making our own WASM module, section Use our first WASM module, how to find this folder)

There is still an issue!

I just discovered one issue with my implementation, and have no clue why this is happening. Below is how you can reproduce the issue. Would be good to see if you have the same issue:

  • Start MSFS2020 with HABI_WASM installed
  • Use FBW A32NX with engines running (select start of runway)
  • Start “SimConnectWasmHUB.exe” and connect
  • Register “K:FUELSYSTEM_PUMP_TOGGLE”
  • Use “Set Value” with values 1 up to 6 to toggle each Fuel Pump

You should see the LED’s on the fuel pumps toggle, but nothing happens. Instead, you get an SimConnect Exception message “ERROR”.

  • Stop “SimConnectWasmHUB.exe”
  • Restart “SimConnectWasmHUB.exe”
  • Connect and register again for “K:FUELSYSTEM_PUMP_TOGGLE”
  • Use “Set Value” with values 1 up to 6 to toggle each Fuel Pump

Now you will see that all works fine. You can now stop and restart “SimConnectWasmHUB.exe” as much as you want, it should work every time. But once you stop MSFS2020, and start all over again, the first attempt to control the Fuel Pumps will fail.

All other variables will work without issue.

I hope that somebody has a clue what could be going on!