14. Sending events - Part 1

14. Sending events - Part 1
Photo by Kevin Bluer / Unsplash

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 use TransmitClientEvent for KOHLSMAN_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:

  1. 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.
  2. The enum for the event you want to send, that you previously registered with MapClientEventToSimEvent(...).
  3. The group id, set to 1 which maps to SIMCONNECT_GROUP_PRIORITY_HIGHEST
  4. A flag that provides additional options. Common ones are SIMCONNECT_EVENT_FLAG.DEFAULT, and SIMCONNECT_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.