SimConnect + WASM combined project using VS2019

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!

May thanks Hans.

At the moment, my MSFS is down for service, but I hope to have it back up this weekend, and will certainly do the tests you outlined, and report back.

Hopefully, some others here in the MSFS Forum Community, can also test it for you, it would appear to be a Must have tool for any hardware cockpit interfacing design work :+1:

1 Like

Hello @N6722C,

I have found the (stupid) mistake. In SimConnectHUB.cs, in the method SetVariable, I made a mistake. I was using (EVENT_ID)v.uDefineID, but that should of course be (EVENT_ID).vInList.uDefineID.

// file SimConnectHUB.cs - method SetVariable - case 'K':

_oSimConnect.TransmitClientEvent(
    0, 
    (EVENT_ID)vInList.uDefineID,  // <-- here was the mistake
    (uint)dValue, 
    (EVENT_ID)SimConnect.SIMCONNECT_GROUP_PRIORITY_HIGHEST, 
    SIMCONNECT_EVENT_FLAG.GROUPID_IS_PRIORITY);

Problem solved!

OK… Good – but it is also worth noting, a lot of the Good Quality P3D documentation, also applies to MSFS, and is a source for info, when it is missing from MSFS’s sketchy Documentation.

oops-- seem to have my replies to post mixed up !!!

ref What is format_calculator_string in Gauge API, and how does it work? - #3 by HBilliet

1 Like

Addendum 1: Some more on Events

I’m planning to add some addendum’s in this post, if I discover some new things, or have some improvements or major changes. You can always find the latest files on HansBilliet/SimConnectWasmHUB: SimConnect and WASM tryout (github.com).

1. Improved Event implementation

I did a few more investigations on Events. I now realize that my implementation was not wrong (it works), but not really good practice.

Triggering an Event is done in the switch (vInList.cType) / Case 'K' in SimConnectHUB.SetVariable with the below code:

case 'K':
    if (v.sName.IndexOf('.') == -1)
        try
        {
            _oSimConnect.TransmitClientEvent(
                0, 
                (EVENT_ID)vInList.uDefineID, 
                (uint)dValue, 
                (EVENT_ID)SimConnect.SIMCONNECT_GROUP_PRIORITY_HIGHEST, 
                SIMCONNECT_EVENT_FLAG.GROUPID_IS_PRIORITY);
            LogResult?.Invoke(this, $"{vInList} SimConnect Event triggered with {(sValue == "" ? "no" : "")} value {sValue}");
        }
        catch (Exception ex)
        {
            LogResult?.Invoke(this, $"SetValue SimConnect Event Error: {ex.Message}");
        }
    else
    {
        if (sValue == "")
            SendWASMCmd($"HW.Set.(>K:{v.sName})");
        else
            SendWASMCmd($"HW.Set.{sValue} (>K:{v.sName})");
        LogResult?.Invoke(this, $"{vInList} Custom Event triggered with {(sValue == "" ? "no" : "")} value {sValue}");
    }
    break;

My implementation is kind of making a detour to the WASM Module for Custom Events (sName contains a “.”). But that is definitely not needed, and even a dumb thing to do! Honestly, I did that because I misinterpreted some answer I got on the forum.

I can simplify the code as below:

case 'K':
    try
    {
        _oSimConnect.TransmitClientEvent(
            0, 
            (EVENT_ID)vInList.uDefineID, 
            (uint)dValue, 
            (EVENT_ID)SimConnect.SIMCONNECT_GROUP_PRIORITY_HIGHEST, 
            SIMCONNECT_EVENT_FLAG.GROUPID_IS_PRIORITY);
        LogResult?.Invoke(this, $"{vInList} SimConnect Event triggered with {(sValue == "" ? "no" : "")} value {sValue}");
    }
    catch (Exception ex)
    {
        LogResult?.Invoke(this, $"SetValue SimConnect Event Error: {ex.Message}");
    }
    break;

But if we also want to use TransmitClientEvent with our Custom Events, then we will need to give them an ID as well using MapClientEventToSimEvent. That was not needed if we sent them to our WASM Module, where the Event was triggered using the versatile execute_calculator_code.

That means that we will also register our Custom Events (before we didn’t do that).

case 'K':
    // Register a K-event
    try
    {
        v.SetID(VarData.AUTO_ID, VarData.NOTUSED_ID);
        // Registration of a SimConnect Event
        _oSimConnect.MapClientEventToSimEvent((EVENT_ID)v.uDefineID, v.sName);
        //_oSimConnect.TransmitClientEvent(0, EVENT_ID.MY_EVENT, 4, (EVENT_ID)SimConnect.SIMCONNECT_GROUP_PRIORITY_HIGHEST, SIMCONNECT_EVENT_FLAG.GROUPID_IS_PRIORITY);
        LogResult?.Invoke(this, $"{v} SimConnect Event added and registered");
    }
    catch (Exception ex)
    {
        LogResult?.Invoke(this, $"Register K-event Error: {ex.Message}");
    }
    break;

The above changes result in simpler code. All Events are handled the same way. SimConnect will take care of the dispatching between SimEvents or Custom Events.

2. My own Custom Event

In my opinion, Events are very handy to reduce communication. Reason is that you call TransmitClientEvent using a simple ID, but this could trigger several commands. To demonstrate this, I added a Custom Event in my WASM Module.

Below addition in the module_init will add the Custom Event “HABI_WASM.EVENT_TEST”, adds it in the group HABI_WASM_GROUP::GROUP, and finally sets the highest priority on this group.

// file WASM_HABI.cpp

enum eEvents
{
	EVENT_FLIGHT_LOADED,
	EVENT_FRAME,
	EVENT_TEST,   // <-- this has been added


enum HABI_WASM_GROUP
{
	GROUP
};

...

extern "C" MSFS_CALLBACK void module_init(void)
{
        ...

	// Add EVENT_TEST and let it do something
	hr = SimConnect_MapClientEventToSimEvent(g_hSimConnect, EVENT_TEST, "HABI_WASM.EVENT_TEST");
	if (hr != S_OK)
	{
		fprintf(stderr, "%s: SimConnect_MapClientEventToSimEvent failed.\n", WASM_Name);
		return;
	}
	hr = SimConnect_AddClientEventToNotificationGroup(g_hSimConnect, HABI_WASM_GROUP::GROUP, EVENT_TEST, false);
	if (hr != S_OK)
	{
		fprintf(stderr, "%s: SimConnect_AddClientEventToNotificationGroup failed.\n", WASM_Name);
		return;
	}
	hr = SimConnect_SetNotificationGroupPriority(g_hSimConnect, HABI_WASM_GROUP::GROUP, SIMCONNECT_GROUP_PRIORITY_HIGHEST);
	if (hr != S_OK)
	{
		fprintf(stderr, "%s: SimConnect_SetNotificationGroupPriority failed.\n", WASM_Name);
		return;
	}

        ...
}

In a SimConnect Client, we can now use MapClientEventToSimEvents to map the Event “HABI_WASM.EVENT_TEST” to an EventID, and then trigger it using TransmitClientEvent with this EventID. Next to the eventID, TransmitClientEvent has also a parameter dwData. We can use this as extra information to send with the Event (in my implementation, this is entered in the Value field).

In the WASM Module, Events are captured in the MyDispatchProc with SIMCONNECT_RECV_ID_EVENT.

case SIMCONNECT_RECV_ID_EVENT:
{
	SIMCONNECT_RECV_EVENT* evt = (SIMCONNECT_RECV_EVENT*)pData;

	if (evt->uEventID == EVENT_TEST)
	{
		DWORD d = evt->dwData;
		char sCmd[1024];

		sprintf(sCmd, "%u (>L:A32NX_EFIS_L_OPTION, enum) %u (>L:A32NX_EFIS_R_OPTION, enum)", d, d);
		execute_calculator_code(sCmd, nullptr, nullptr, nullptr);
		fprintf(stderr, "%s: execute_calculator_code(\"%s\"))\n", WASM_Name, sCmd);
	}
	break; // end case SIMCONNECT_RECV_ID_EVENT
}

Here you see the power of Events. In my WASM Module, it executes 2 commands using dwData as a parameter.

In the MobiFlight WASM Module that you can find here, Events are loaded during intialization of the WASM Module. They are defined in a file “modules/events.txt” and the user can add its own events in the file “modules/events.user.txt”.

I’m thinking of using a decentralized way of working (not carved in stone yet). The principle would be that my individual cockpit hardware modules will know the commands it needs. When connecting, each hardware module will register this via the SimConnect Client, which will add the Event Name and the Event Command dynamically in the WASM Module using some registration command (we will need some addition in the protocol for that).

Bottom line is that in both cases, once the Event ID’s are defined, they can be used as a much more efficient way then sending the complete commands using “HW.Set.”.

Goto Addendum 2: Real “execute_calculator_code” tool

Addendum 2: Real “execute_calculator_code” tool

I added another feature in my tool. Now you can enter whatever execute_calculator_code string, send it to the WASM Module, and see the results in FLOAT64, SINT32 or String format. As usual, you can find the latest version on GitHub. The updated executables (release version) can also be found here.

The UI of the tool looks like this:

You enter an RPN formatted string, press the Send button, and see the results in the Float, Int and String fields. This is very powerful, because execute_calculator_code accepts all types of variables (SimVars, LVars, Events, etc…), which means that you can now experiment and see how they work.

The implementation is as follows:

  1. A new command “HW.Exe.[RPN based string]” is added in the WASM Module
  2. A new Client Area Data “HABI_WASM.Result” is added to send back the results to the SimConnect client

If we look at the signature of execute_calculator_code, we see that it takes a string as parameter, and it returns it values as FLOAT64, SINT32 and STRING.

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

In the WASM Module, the values are combined in a structure that is sent back through the Client Data Area.

Let’s look at some code:

Changes in WASM Module

First the Client Data Area is initialized:

// file WASM_HABI.cpp

struct Result {
	FLOAT64 exeF;
	SINT32 exeI;
	char exeS[256];
};

...

void RegisterClientDataArea()
{
	...

	// Create Client Data Area for result of execute_calculator_code
	hr = SimConnect_MapClientDataNameToID(g_hSimConnect, CLIENT_DATA_NAME_RESULT, CLIENT_DATA_ID_RESULT);
	if (hr != S_OK) {
		fprintf(stderr, "%s: Error creating Client Data Area %s. %u", WASM_Name, CLIENT_DATA_NAME_RESULT, hr);
		return;
	}
	SimConnect_CreateClientData(g_hSimConnect, CLIENT_DATA_ID_RESULT, sizeof(Result), SIMCONNECT_CREATE_CLIENT_DATA_FLAG_DEFAULT);

	...

	// This Data Definition will be used with the RESULT Client Area Data
	hr = SimConnect_AddToClientDataDefinition(
		g_hSimConnect,
		DATA_DEFINITION_ID_RESULT,
		0,				// Offset
		sizeof(Result)	// Size
	);

	...
}

In MyDisptachProc, the below code is added in the SIMCONNECT_RECV_ID_CLIENT_DATA case to receive the “HW.Exe” command.

// file WASM_HABI.cpp

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

And in the function ExecuteCalculatorCode, the command is executed, and the results are sent back in the Client Data Area.

void ExecuteCalculatorCode(char* sExe)
{
	Result exeRes;
	exeRes.exeS[0] = '\0';
	PCSTRINGZ ps;

	execute_calculator_code(sExe, &exeRes.exeF, &exeRes.exeI, &ps);
	strncat(exeRes.exeS, ps, 255);

	fprintf(stderr, "%s: ExecuteCalulculatorCode: float %f, int %i, string %s", WASM_Name, exeRes.exeF, exeRes.exeI, exeRes.exeS);

	HRESULT hr = SimConnect_SetClientData(
		g_hSimConnect,
		CLIENT_DATA_ID_RESULT,
		DATA_DEFINITION_ID_RESULT,
		SIMCONNECT_CLIENT_DATA_SET_FLAG_DEFAULT,
		0,
		sizeof(exeRes),
		&exeRes
	);

	if (hr != S_OK)
	{
		fprintf(stderr, "%s: Error on Setting Client Data RESULT", WASM_Name);
	}
}

Be aware that PCSTRINGZ is a const CHAR *, which is a pointer to a string. We pass the address of this pointer to execute_calculator_code. execute_calculator_code creates the necessary memory depending on the length of the string result, and puts the address of that string in my variable. After that, I copy the string in the exeS member of the exeRes structure. Using strncat guarantees that the max length of 256 characters (including ‘\0’) is not exceeded.

With SetClientData, the result is put in the Client Data Area and will trigger the SimConnect Client on the receiving side.

Changes in the SimConnect Client

First the Client Data Area is initialized:

// file SimConnectHUB.cs

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

    try
    {
        ...

        // register Client Data (for RESULT)
        _oSimConnect.MapClientDataNameToID(CLIENT_DATA_NAME_RESULT, CLIENT_DATA_ID.RESULT);
        _oSimConnect.CreateClientData(CLIENT_DATA_ID.RESULT, (uint)Marshal.SizeOf<Result>(), SIMCONNECT_CREATE_CLIENT_DATA_FLAG.DEFAULT);
        _oSimConnect.AddToClientDataDefinition(CLIENTDATA_DEFINITION_ID.RESULT, 0, (uint)Marshal.SizeOf<Result>(), 0, 0);
        _oSimConnect.RegisterStruct<SIMCONNECT_RECV_CLIENT_DATA, Result>(CLIENTDATA_DEFINITION_ID.RESULT);
        _oSimConnect.RequestClientData(
            CLIENT_DATA_ID.RESULT,
            CLIENTDATA_REQUEST_ID.RESULT,
            CLIENTDATA_DEFINITION_ID.RESULT,
            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}");
    }

}

In SimConnect_OnRecvClientData, an extra branch is added to catch the CLIENTDATA_REQUEST_ID.RESULT.

// file SimConnectHUB.cs

...

else if (data.dwRequestID == (uint)CLIENTDATA_REQUEST_ID.RESULT)
{
    try
    {
        var exeResult = (Result)(data.dwData[0]);
        Debug.WriteLine($"----> Result: float: {exeResult.exeF}, int: {exeResult.exeI}, string: {exeResult.exeS}");

        ExeResult?.Invoke(this, exeResult);
    }
    catch (Exception ex)
    {
        LogResult?.Invoke(this, $"SimConnect_OnRecvClientData Error: {ex.Message}");
    }
}

...

An extra EventHandler ExeResult is used to report the results back to the UI (same way as LogResult). This EventHandler is initialized in the constructor of the form.

// file FormHUB.cs

public FormHUB()
{
    InitializeComponent();

    _SimConnectHUB.LogResult += OnAddResult;
    _SimConnectHUB.ExeResult += OnExeResult;
}

...

private void OnExeResult(object sender, SimConnectHUB.Result ExeResult)
{
    textBoxExecCalcCodeFloat.Text = ExeResult.exeF.ToString("0.000");
    textBoxExecCalcCodeInt.Text = ExeResult.exeI.ToString();
    textBoxExecCalcCodeString.Text = ExeResult.exeS;
}

Everything is now ready to press the Send button. This is just taking the string from the TextBox, and sends it to the WASM Module using the method SendWASMCmd, now using the “HW.Exe” command.

// file SimConnectHUB.cs

public void ExecuteCalculatorCode(string sExe)
{
    SendWASMCmd($"HW.Exe.{sExe}");
}

We can now use this execute_calculator_code addition to experiment with all kinds of variables.

Get and set values of A-Vars:

Get and set values of L-Vars:

Trigger Events:

Getting string values:


(in this case, the Float and Int values are apprantly 0)

Enjoy experimenting!

Goto Addendum 3: A more responsive Connect method

3 Likes

Hi all, my Simconnect application is getting there! I am now just struggling with the issue that when you try to connect to SimConnect without the simulator running, the application freezes until the connect call reports an error. This can take quite a while. It is also happening with the SimVarWatcher application, so it is not just my code.

Any suggestions how to prevent this? I basically want the application to check if SimConnect connection can be established without freezing the application of course.

Hello @TFEV1909,

I just tried with my application, and within a fraction of a second I get an Exception, which is the result of my Try/Catch block.

Have you shared your code somewhere?

I have the same try / catch block as you have. I now realised I was using a remote connection, will check if it works better on the same pc.

EDIT: On the same PC it works indeed much faster, so it’s related to Simconnect looking over the network.

I still wonder though why it is hanging at all if I start the simconnect in a separate thread. That isn’t supposed to freeze the application then is it?

EDIT: Brainwave: that’s probably because it is using the window handle of course…

If you use the window handle of you main window, then it might indeed be that you are blocking this messagepump. That’s why people like @Dragonlaird use an approach with a separate (hidden) window and pass this handle to SimConnect instead (see this post).

My application is intended as the basis for a kind of a Hardware hub, which is normally going to run in the background anyway, just offering some features for monitoring and testing during development. That means that I’m not so afraid of the blocking of the window for a while. But if it would really get annoying, then I would go back to the “separate message pump approach”.

1 Like

Great suggestion, thank you!

Addendum 3: A more responsive Connect method

As indicated by @TFEV1909 above, the Connect method can be blocking the main UI thread if connecting over the network. Especially when trying to connect over the network while MSFS2020 is not running. I never tried myself to connect over the network, but I have experience with WinSockets that have the same kind of behavior. The connection will retry several times until a timeout occurs, which blocks the call for several seconds.

@TFEV1909 came with a solution that uses async methods. We simply launch the connection in a different thread using Task.Run(), and await for this thread to finish. The await keyword will make sure that the call to Task.Run() immediately returns to the UI thread, which avoids the blocking. Once the connection is finished, whether it is successful or not, the code after the await continuous to execute. All this hocus pocus happens in the Task Parallel Library (TPL).

When you want to await for a Task, then you must precede your calling method with async. Visual Studio will make it very easy and even suggest these changes automatically. Below you will see my revised Connect method which is now preceded by async. The name of the method is also changed to ConnectAsync(). This is only a naming convention, and not mandatory. But it helps if you would provide your code in a DLL to the outside world. By using the Async suffix, you let consumers of your DLL know that this method is Async and uses await statements.

The revision of the Connect method is below. All credits to @TFEV1909.

public async Task ConnectAsync()
{
    if (_bConnected)
    {
        LogResult?.Invoke(this, "Already connected");
        return;
    }

    try
    {
        await Task.Run(() => _oSimConnect = new SimConnect("SimConnectHub", _handle, WM_USER_SIMCONNECT, null, 0));

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

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

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

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

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

If you want to read a very good article about this asynchroneous concept, then I can definitly recommend Async Programming With the Task Parallel Library written by Nate Cook. It looks like this is a paid site, but I could click through the whole series for free.

Goto Addendum 3: Packing structures

3 Likes

@HBilliet @Dragonlaird I just wanted to let you both know what I created with help of your tutorials, see https://forums.flightsimulator.com/t/easycontrols-for-msfs-1-click-controllers-adjustment-per-aircraft

Thanks a lot for my questions answered by both of you. My project is still work in progress, but wouldn’t have been so far right now without your assistance!

Hello @TFEV1909,

Very impressive! I’m really glad to read that our tutorials have been a bit of help.

In the meantime, I’m rebuilding a complete application adding a USB server that allows connecting Hardware modules based on (for example) Arduino. I might write a new tutorial in the Third Party Addon section for this. So stay tuned!

Great job, glad we helped in some small way, that’s what the tutorial is for. Hope it really takes off (pun intended) :grinning_face_with_smiling_eyes:

Addendum 3: Packing structures

I had an issue with a structure I was using between my WASM Module and my SimConnect Client. When I was changing one of the datatypes from UInt16 to double, then suddenly my implementation stopped working and even completely crashed.

Below is the originally structure in both my WASM Module and SimConnect Client where all worked fine (this structure is not used in the implementation mentioned in this tutorial, but for a newer version for which I will soon create a new post).

WASM Module:

struct LVar {
	UINT16 lvID;
	UINT16 DefineID;
	UINT16 Offset;
	UINT16 Value;
	char Name[MESSAGE_SIZE];
	char Event[EVENT_NAME_SIZE];
};

SimConnect Client:

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
public struct LVarAck
{
    public UInt16 lvID;
    public UInt16 DefineID;
    public UInt16 Offset;
    public UInt16 Value;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
    public String str;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
    public String sEvent;
};

But when I changed the field “Value” into a FLOAT64 (double), things got wrong.

WASM Module:

struct LVar {
	UINT16 lvID;
	UINT16 DefineID;
	UINT16 Offset;
	FLOAT64 Value;
	char Name[MESSAGE_SIZE];
	char Event[EVENT_NAME_SIZE];
};

SimConnect Client:

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
public struct LVarAck
{
    public UInt16 lvID;
    public UInt16 DefineID;
    public UInt16 Offset;
    public double Value;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
    public String str;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
    public String sEvent;
};

It took a while for me to realize that the issue was caused by the setting “Pack = 1”. Here you can read the full story what this pack exactly does.

Bottom line is that by default, the fields in a structure are using an alignment that is defined by the settings of the compiler. In my compilers in VS2019 this is by default set to “Pack = 8”. You can read the details in the above mentioned link, but the result is that memory mapping of the LVar structure in my WASM Module looks like this:

Address 0: UINT16 lvID
Address 2: UINT16 DefineID
Address 4: UINT16 Offset
Address 6: padding             < this padding is automatically added to align at 8 bytes
Address 8: FLOAT64 Value
Address 16: ...

But in the SimConnect Client, the packing was forced to use “pack = 1”, which means that the below structure is expected:

Address 0: UInt16 lvID
Address 2: UInt16 DefineID
Address 4: UInt16 Offset
Address 6: double Value < HERE THE MISALINGMENT STARTS
Address 14: ...

Because “Pack = 1”, there are no gaps or padding used. You can clearly see the misalignment of 2 bytes, because no padding is added. If I used a UInt16 in both structures for the field Value, all worked well, but this is pure coincidence!

A way to solve this is to instruct the C++ compiler also to use “pack = 1”, but you need to use #pragma directives for this.

#pragma pack(push, 1) // packing is now 1
struct LVar {
	UINT16 lvID;
	UINT16 DefineID;
	UINT16 Offset;
	FLOAT64 Value;
	char Name[MESSAGE_SIZE];
	char Event[EVENT_NAME_SIZE];
};
#pragma pack(pop) // packing is 8 again

You could also remove the “pack = 1” in C#, in which case the #pragma directive in C++ isn’t necessary, because both compilers then use the (same) default packing. But in my opinion, it is safer to explicitly control the packing of the structures, because you never know what the default is.

Don’t hesitate to comment if you think I’m wrong :nerd_face:

2 Likes

@HBilliet very good write up! Thanks for mentioning https://mobiflight.com and https://hubhop.mobiflight.com - I can clearly see that a lot of your work is based on the MobiFlight WASM module - so i am happy it was useful.

Please feel invited to contribute to the MobiFlight WASM Module in case you’re seeing areas for improvement or if it is missing anything important. Your documentation would be a great addition to the repository too.

Just these days we added multi-client support to be able to let one instance be used by many clients at the same time.

I also added an improvement to make sure the client detects the WASM module once it is loaded.

I’m honored to receive these compliments from MobiFlight! Thank you very much. The MobiFlight sources have indeed been a great starting point. Without them, it would have taken me a lot more time to reach the point of writing this tutorial. I still have the printout of some modules next to me, full of notes and writeups of ideas :slight_smile: This together with the posts of @Dragonlaird got me started, combined with the feedback and tips of several other people on this forum. That’s why this is such a great forum!

MobiFlight is a great tool for people that are not so familiar with programming themselves. I have used it myself, and in most cases it did exactly what I wanted. It’s a great balance between being usable without the need of having to write one single line of code, but still flexible enough to configure and tweak the behavior.

I’m now working on a version of my software that connects with USB devices (even hot swappable!), as long as the devices support the “serial over USB” protocol (like Arduino). This implementation will be intended for people that like to write their own software in the USB device, which will provide greater flexibility, but at the cost of having to write your own code. I will again share my findings in a new post within a couple of weeks. So stay tuned!

Thanks for the invitation to contribute to MobiFlight. But I currently have not even enough time to finish my own projects. But don’t hesitate to connect with me if you want to discuss any topic. As I said before, I’m definitely not a professional programmer. I’m just doing this as a hobby, and like to analyze complex stuff. Just call me a nerd :nerd_face: