14. Sending events - Part 1
So up to this point we were only pulling data from Microsoft Flight Simulator 2020
and sending it down to the devices. But what about interacting back with the sim? There are two things that you can do with the SimConnect SDK
: setting some SimVars
, and sending EventIds
. I clarify that we can only set someSimVars
since a lot of them are read only, but you can check the Simulation Variables section in the SDK's documentation to see which ones are read/write. Here's an example:
We'll skip setting SimVars
for now. Right now we just want to focus on sending EventIds
.
The SimConnect SDK
offers a way of communicating back with the sim via EventIds
. A list of all the existing ones and their documentation can be found here: https://docs.flightsimulator.com/html/Programming_Tools/Event_IDs/Event_IDs.htm
Continuing to build our Altimeter instrument, let's create some events to change the barometer setting, the same way we would by turning a knob in an airplane's Altimeter. If we look at the documentation, we can see there are 3 events that we can use to increase, decrease and reset the barometer setting:
Event Name | Parameters | Description |
---|---|---|
KOHLSMAN_INC | N/A | Increments altimeter setting. |
KOHLSMAN_DEC | N/A | Decrements altimeter setting. |
KOHLSMAN_SET | [0]: Value to set, [1]: Altimeter index | Sets altimeter setting (Millibars * 16). |
Note: we really only need the INC and DEC to mimic the behavior of the Altimeter's knob in an airplane, but I'll use SET to reset to standard barometric pressure of 29.92 to explain the two different ways of sending the events based on the EventId
properties.
Code Changes
Setting the events is very similar to how we setup the Simvars
. We first need to create an enum
with our events:
public enum SimEventType
{
KOHLSMAN_INC, // increments the altimeter setting
KOHLSMAN_DEC, // decrements the altimeter setting
KOHLSMAN_SET, // sets the altimeter setting to a specified value
}
Next, we need to register our EventIds
in a similar way to when we registered our SimVars
. Let's do this in the same Simconnect_OnRecvOpen()
method we used before:
private void Simconnect_OnRecvOpen(SimConnect sender, SIMCONNECT_RECV_OPEN data)
{
// original simconnect.AddToDataDefinition(...)'s here
...
// register events
simconnect.MapClientEventToSimEvent(SimEventType.KOHLSMAN_SET, "KOHLSMAN_SET");
simconnect.MapClientEventToSimEvent(SimEventType.KOHLSMAN_INC, "KOHLSMAN_INC");
simconnect.MapClientEventToSimEvent(SimEventType.KOHLSMAN_DEC, "KOHLSMAN_DEC");
}
Next let's modify the XAML UI a bit so we can add some buttons to send the events. Also, I added a text field for specifying the COM port so I can test with different devices without having to rebuild the code.
<Window x:Class="HelloMSFS.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:HelloMSFS"
mc:Ignorable="d"
Title="Hello MSFS" SizeToContent="WidthAndHeight" Topmost="True">
<Grid Margin="20">
<Grid.ColumnDefinitions>
<ColumnDefinition MinWidth="100"/>
<ColumnDefinition MinWidth="100"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition MinHeight="25"/>
<RowDefinition MinHeight="25"/>
<RowDefinition MinHeight="25"/>
<RowDefinition MinHeight="25"/>
<RowDefinition MinHeight="25"/>
<RowDefinition MinHeight="25"/>
</Grid.RowDefinitions>
<Label x:Name="label0" Content="Serial Port:" Grid.Row="0" VerticalContentAlignment="Center" Margin="5"/>
<Label x:Name="label1" Content="Altimeter:" Grid.Row="1" VerticalContentAlignment="Center" Margin="5" />
<Label x:Name="label2" Content="Barometer:" Grid.Row="2" VerticalContentAlignment="Center" Margin="5"/>
<TextBox x:Name="serialPortTextBox" TextWrapping="Wrap" Text="COM5" Grid.Row="0" Grid.Column="1" VerticalContentAlignment="Center" Margin="5" IsReadOnly="True"/>
<TextBox x:Name="altimeterTextBox" TextWrapping="Wrap" Text="1000" Grid.Row="1" Grid.Column="1" VerticalContentAlignment="Center" Margin="5" IsReadOnly="True"/>
<TextBox x:Name="barometerTextBox" TextWrapping="Wrap" Text="29.92" Grid.Row="2" Grid.Column="1" VerticalContentAlignment="Center" Margin="5" IsReadOnly="True"/>
<Grid Grid.Row="3" Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Button x:Name="decreaseBaroButton" Content="-" Grid.Column="0" Click="decreaseBaroButton_Click" IsEnabled="False"/>
<Button x:Name="increaseBaroButton" Content="+" Grid.Column="1" Click="increaseBaroButton_Click" IsEnabled="False"/>
</Grid>
<Button x:Name="resetBaroButton" Content="STD Baro" Grid.Row="3" Grid.Column="0" Click="resetBaroButton_Click" IsEnabled="False"/>
<Button x:Name="connectButton" Content="Connect" Grid.Row="5" Click="connectButton_Click" />
<Button x:Name="disconnectButton" Content="Disconnect" Grid.Row="5" Grid.Column="1" Click="disconnectButton_Click" IsEnabled="False"/>
</Grid>
</Window>
To use the new serialPortTextBox, change the private void InitializeSerialPort()
to this:
private void InitializeSerialPort()
{
serialPort = new SerialPort(serialPortTextBox.Text, 115200)
{
ReadTimeout = 1000,
WriteTimeout = 1000,
DtrEnable = true,
RtsEnable = true,
NewLine = Environment.NewLine,
};
// attach an event handler
serialPort.DataReceived += SerialPort_DataReceived;
serialPort.Open();
}
For the button event handlers, this is the code behind:
private void decreaseBaroButton_Click(object sender, RoutedEventArgs e)
{
simconnect.TransmitClientEvent(SimConnect.SIMCONNECT_OBJECT_ID_USER, SimEventType.KOHLSMAN_DEC, 0, GroupId.FLAG, SIMCONNECT_EVENT_FLAG.GROUPID_IS_PRIORITY);
}
private void increaseBaroButton_Click(object sender, RoutedEventArgs e)
{
simconnect.TransmitClientEvent(SimConnect.SIMCONNECT_OBJECT_ID_USER, SimEventType.KOHLSMAN_INC, 0, GroupId.FLAG, SIMCONNECT_EVENT_FLAG.GROUPID_IS_PRIORITY);
}
private void resetBaroButton_Click(object sender, RoutedEventArgs e)
{
simconnect.TransmitClientEvent_EX1(SimConnect.SIMCONNECT_OBJECT_ID_USER, SimEventType.KOHLSMAN_SET, GroupId.FLAG, SIMCONNECT_EVENT_FLAG.GROUPID_IS_PRIORITY, (uint)(29.92 * 33.864 * 16), 0, 0, 0, 0);
}
Code Break down
Notice we have two different methods that send an EventId
:
TransmitClientEvent(uint ObjectID, Enum EventID, uint dwData, Enum GroupID, SIMCONNECT_EVENT_FLAG Flags)
TransmitClientEvent_EX1(uint ObjectID, Enum EventID, Enum GroupID, SIMCONNECT_EVENT_FLAG Flags, uint dwData0, uint dwData1, uint dwData2, uint dwData3, uint dwData4)
The difference between the two is that TransmitClientEvent
has a single dwData argument, while TransmitClientEvent_EX1
is used for when the data is expected in an array, and dwData0 through dwData4 are the elements of that array.
If we look back at the documentation for KOHLSMAN_SET
, it expects the first parameter [0]: Value to set
and the second [1]: Altimeter index
, which in this case would be 0 for the first altimeter. This would be used in cockpits that have multiple altimeters, like for pilot and copilot, or backup instruments, where you would send 0,1,2, etc. as dwData1 in this case.
Note: If we tried to useTransmitClientEvent
forKOHLSMAN_SET
, we would see that nothing happens, since we are not sending the data in the format it expects.
In the resetBaroButton_Click
we are using the standard aviation barometric pressure of 29.92 inHg, then we multiply by 33.864 to convert to Millibars and then we multiply by 16 as the sim expects it.
Of course if we want to be efficient in our coding, we could some constants for this or write a method to convert from a given pressure in inHg to Millibars, but got lazy, no judging! 😄
The rest of the parameters are:
- The SimConnect "User ID" const used to represent the user's own aircraft in the sim. We also used this to subscribe to the
SimVars
earlier. - The
enum
for the event you want to send, that you previously registered withMapClientEventToSimEvent(...)
. - The group id, set to 1 which maps to
SIMCONNECT_GROUP_PRIORITY_HIGHEST
- A flag that provides additional options. Common ones are
SIMCONNECT_EVENT_FLAG.DEFAULT
, andSIMCONNECT_EVENT_FLAG.GROUPID_IS_PRIORITY
Running and testing
If we build and run our code, we get the updated UI:
We connect and again verify that the numbers match what the sim is showing. Pressing on the +/- buttons should increase and decrease the barometer setting both in game and in the C# app, along an altitude value corresponding to the new barometer setting. This is because we send the event to the game, and then the game sends the new SimVars
values back to us so we update the text field with the newest values.
Now press on the STD Baro button, and you should see that it changed to 29.92 and the altitude was updated accordingly as well.
Conclusion
Sending events to the sim is pretty straight forward. One of the biggest mistakes I've done is calling TransmitClientEvent
instead of TransmitClientEvent_EX1
by missing that it was expecting an array in the documentation. Another challenge I've encountered is that sometimes it expects the data in a very specific data format I have never worked with before, so I had to spend some time transforming something like a float
value into some weird byte array of some obscure standard I have never heard of before. 🤣
In Part 2 and Part 3 of this series I will introduce a rotary encoder so we can send decrease, increase and button push events from the Teensy to the C# app, and repurpose the DataReceived
handler method to interpret what I'm sending it and use that to trigger these same events.