Monday, April 1, 2024

Learning to use SimConnect: Part Three - System Events

In my first post about SimConnect, I showed how to create a simple C# console app that connected to the SimConnect server running within Microsoft Flight Simulator 2020, using the managed SimConnect library. In the second post, I showed how to receive messages from the simulator using events defined by the SimConnect object, namely OnRecvOpen and OnRecvQuit.

In this post, we'll get into another way of getting information from the simulator, system events. In later posts, I'll hopefully be able to dive into two-way communication, using client events and simulator variables.

I have a GitHub repository with all the code from this series in it, tagged at each stage. If you want to start out with the code as it was at the end of the second post, use the tag "06-quit-event-handler".

Before jumping into today's discussion, however, I'd like to clean up the ugly while (!isSimConnectOpen).../while (isSimConnectOpen)... logic that I kind of handwaved away last time. On further reflection, the logic can be revised to more clearly articulate what's happening. First, add a second flag variable, isSimConnectInitialized.

    	
static bool isSimConnectInitialized = false;
static bool isSimConnectOpen = false;

static void Main()
{
  SimConnect? simConnect = null;
	    

Add a new line to the OnSimConnectOpen() event handler as follows:

private static void OnSimConnectOpen(SimConnect sender, SIMCONNECT_RECV_OPEN data)
{
  Console.WriteLine($"SimConnect connection to application '{data.szApplicationName}' opened.");
        
  isSimConnectInitialized = true;
        
  isSimConnectOpen = true;
}
    

Finally, replace the two ReceiveMessage() conditionals with a single one, as follows:

simConnect.OnRecvQuit += OnSimConnectQuit;

simConnect.OnRecvEvent += OnSimConnectEvent;

while (!isSimConnectInitialized || isSimConnectOpen) simConnect.ReceiveMessage();

Console.WriteLine("Press any key to exit the program.");
  

The logic is now "while SimConnect is initializing, or is still open, receive any messages in the queue."

System events

System events are the simplest part of SimConnect, but they also establish a foundation for working with the more complex parts. System events represent things that happen outside of the simulation engine, such as when the user pauses the simulator, or starts a new flight.

To interact with system events, the client program has to tell the SimConnect server to associate an enum value with a particular system event, represented by a specific name. Technically, you can use just an integer (specifically, an unsigned integer, or uint), but since you usually have multiple events that you want to receive (including simulation events, which we will discuss next), it makes sense to group them together in an enum, for better code readability, if nothing else.

To start with, we'll use the "4sec" system event, which the SimConnect server sends out every four seconds. It's not that useful, but it has the advantage for demonstration purposes of not requiring us to do anything in the simulator in order for it to fire. Start off by creating a global enum with a single entry, having a value of 1.

static bool isSimConnectInitialized = false;

static bool isSimConnectOpen = false;

enum EventId
{
  FourSeconds = 1
}

static void Main()
{
  SimConnect? simConnect = null;
	

Next, add a call to SimConnect.SubscribeToSystemEvent() to map that enum value to the event name.

simConnect.OnRecvOpen += OnSimConnectOpen;

simConnect.OnRecvQuit += OnSimConnectQuit;

simConnect.SubscribeToSystemEvent(EventId.FourSeconds, "4sec");
	

Next, we'll need a new event handler for the SimConnect.OnRecvEvent() event. This event fires for all events, not just system events, so we'll be expanding it later, but for now we can just code a handler as follows:

private static void OnSimConnectEvent(SimConnect sender, SIMCONNECT_RECV_EVENT data)
{
  if (data.uEventID == (uint)EventId.FourSeconds)
  {
    Console.WriteLine("System event: '4sec'");
  }
}
    

While we could have coded the enum as a uint, and set the value to 1u as a uint literal, in the end it doesn't matter, because C# would still treat data.uEventID == EventId.FourSeconds as an error, so for simplicity I just left it as the default (int).

Finally, we need to wire our event handler to the event:

simConnect.SubscribeToSystemEvent(EventId.FourSeconds, "4sec");

simConnect.OnRecvEvent += OnSimConnectEvent;

Start MSFS, then build/run our client.

Well, that's something, I guess.

In addition to the "4sec" event, the SimConnect server provides "1sec" and "6Hz" (six times per second) events. We can easily add these events if we want, just by adding enum values to EventId,

enum EventId

{

    FourSeconds = 1,

    OneSecond,

    SixHertz

}

adding SubscribeToSystemEvent() calls for them,

simConnect.SubscribeToSystemEvent(EventId.FourSeconds, "4sec");

simConnect.SubscribeToSystemEvent(EventId.OneSecond, "1sec");

simConnect.SubscribeToSystemEvent(EventId.SixHertz, "6Hz");

and modifying OnSimConnectEvent() to display different messages depending on the value of data.uEventID.

private static void OnSimConnectEvent(SimConnect sender, SIMCONNECT_RECV_EVENT data)

{

    string message = "";

    switch (data.uEventID)

    {

        case (uint)EventId.FourSeconds:

            {

                message = "System event: '4sec'";

                break;

            }

        case (uint)EventId.OneSecond:

            {

                message = "System event: '1sec'";

                break;

            }

        case (uint)EventId.SixHertz:

            {

                message = "System event: '6Hz'";

                break;

            }

    }

    Console.WriteLine(message);

}


To stop the stream of events, you can either exit the simulator or close the client.

You may have noticed in that screenshot that there were only three "6Hz" events before the first "1sec" event, and only one "1Sec" event before the first "4sec" event. That's because the events start when the simulator starts, not the client application. This can be useful for coordinating actions between multiple clients, since they will all receive these timing events at approximately the same time. The "6Hz" event in particular represents how often the simulator polls for control inputs, so if you're building your own controls, you can use this to match the simulator's polling rate, if you like.

There is a corresponding SimConnect.UnsubscribeFromSystemEvent() function to stop receiving the event notifications, but it should only be used if the client is truly done with that event. To temporarily pause notifications, you can call SimConnect.SetSystemEventState(), with the enum value and either SIMCONNECT_STATE.OFF to stop notifications, or SIMCONNECT_STATE.ON to resume them. According to the documentation, this is more efficient than unsubscribing and then re-subscribing. Just for demonstration purposes, let's turn off the "6Hz" event when "1sec" fires, but turn it back on when "4sec" fires. We'll use the sender argument in the event handler as our SimConnect object reference, since the simConnect variable is not global. We should see up to six "6Hz" notifications, then up to four "1Sec" notifications, then a "4Sec" notification, then a repeating pattern of six "6Hz", four "1sec", and one "4sec", until we either quit MSFS or stop the client.

case (uint)EventId.FourSeconds:

    {

        message = "System event: '4sec'";

        sender.SetSystemEventState(EventId.SixHertz, SIMCONNECT_STATE.ON);

        break;

    }

case (uint)EventId.OneSecond:

    {

        message = "System event: '1sec'";

        sender.SetSystemEventState(EventId.SixHertz, SIMCONNECT_STATE.OFF);

        break;

    }

case (uint)EventId.SixHertz:

    {

        message = "System event: '6Hz'";

        break;

    }


As I said, somewhat useful, but not terribly exciting. In future posts I plan to get into the more interactive methods of using SimConnect.