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