17. Sending Microsoft Flight Simulator data to ForeFlight

17. Sending Microsoft Flight Simulator data to ForeFlight
Photo by Kristina Delp / Unsplash

So let's do something different today. Now that we know how to read data from the sim and send it to our Arduino devices, let's use that same idea to send data to Electronic Flight Bags (EFB) apps like ForeFlight, Garmin Pilot and Avare.

But what is an EFB and what can I we use it for? An EFB is a digital device or mobile device application that pilots use to manage flight information, replacing the need of paper based materials they usually carry in their flight bags, like VFR Sectional and IFR charts, with the addition that they can be connected to the aircraft's GPS to read navigation and positional data and display it on the screen for flight planning and tracking in real time, with the additional benefit that you can record your flights and play them back later.

ForeFlight's Track Logs

We want to be able to use an EFB together with our simulator the same way you would use it while flying on a real aircraft and connecting it to the aircraft's GPS, via Wi-Fi or Bluetooth, depending on the equipment your aircraft has. Although there are already a few Windows apps that do this, let's look at how it's done so you can add it to the app we built earlier since we already have some of the data we need and we can figure out the rest.

I will show an example code for sending the data to ForeFlight, since it's pretty straight forward. The code for supporting Avare (on Android) is similar, with the difference that it uses NMEA sentences instead of ForeFlight's proprietary ones. I haven't looked into how this would be implemented for other EFB's like Garmin Pilot, but I'm guessing it might be a similar idea.

ForeFlight's Protocol

We will be using the UDP protocol to send data to devices running ForeFlight that are connected to the same network our PC. Unlike TCP, UDP is connectionless so the packets are just broadcasted over the network so any device on the network can receive the traffic. We can have multiple devices connected on the network and as long as they are listening to the same port, all of them will be able to read it.

If we look at the ForeFlight GPS Integration documentation, it listens on UDP port 49002 and supports 3 types of sentences, two for your own aircraft's information and the other for other traffic. They are all text based and the fields are comma separated.

XGPS

This is used for your own aircraft's position, altitude, track and ground speed. It expects one sentence per second. The format is:

XGPSname,longitude,latitude,altitude,track,groundspeed

Property Description SimVar
name your C# App name This will show in ForeFlight under Devices
longitude Aircraft's longitude coordinate PLANE LONGITUDE
latitude Aircraft's latitude coordinate PLANE LATITUDE
altitude Aircraft's MSL altitude in meters INDICATED ALTITUDE
track Aircraft's true north based track GPS GROUND TRUE TRACK
groundspeed Aircraft's ground speed in meters per second GROUND VELOCITY

XATT

This is used by ForeFlight's built-in Attitude Indicator. The sentences need to be sent between 4 to 10 times per second. The format is:

XATTname,heading,pitch,roll

Property Description SimVar
name your C# App name This will show in ForeFlight under Devices
heading true heading GPS GROUND TRUE HEADING
pitch in degrees, up is positive PLANE PITCH DEGREES
roll in degrees, right is positive PLANE BANK DEGREES

XTRAFFIC

This is used for traffic aircraft's properties surrounding your own aircraft. It expects one sentence per second. The format is:

XTRAFFICname,id,lat,long,alt,vspeed,airborne,heading,speed,callsign

Property Description SimVar
name your C# App name This will show in ForeFlight under Devices
id an integer ID See Note
lat aircraft's current location. PLANE LATITUDE
ong aircraft's current location. PLANE LONGITUDE
alt aircraft's altitude in feet. PLANE ALTITUDE
vspeed aircraft's vertical speed in feet per minute VERTICAL SPEED
airborne flag: 0 for surface, 1 for airborne SIM ON GROUND
heading aircraft's true heading in degrees PLANE HEADING DEGREES MAGNETIC
speed aircraft's speed in knots AIRSPEED TRUE
callsign aircraft's callsign as a string ATC ID

Note: The id will be given to us in the OnRecvSimobjectDataBytype(SimConnect sender, SIMCONNECT_RECV_SIMOBJECT_DATA_BYTYPE data) as data.dwObjectId. Normally your own aircraft's ID = 1, and the rest start around 300.

The Coding Part

Let's write a simple Console Application to explain some concepts:

using System.Net;
using System.Net.Sockets;
using System.Text;

namespace HelloForeFlight
{
    internal class Program
    {
        private const string APP_NAME = "HelloForeFlight";

        private const float FEET_TO_METERS = 3.281f;
        private const float KNOTS_TO_METERS_PER_SECOND = 0.51444f;

        private const string BROADCAST_ADDRESS = "255.255.255.255";
        private const int BROADCAST_PORT = 49002;

        private static void Main(string[] args)
        {
            float altitude = 12000;         // in feet
            float groundspeed = 110;        // in knots
            float groundtrack = 45;
            float latitude = 42.4630f;
            float longitude = -71.2872f;

            // Define the broadcast endpoint (IP address and port)
            var endPoint = new IPEndPoint(IPAddress.Parse(BROADCAST_ADDRESS), BROADCAST_PORT);

            using (UdpClient udpClient = new UdpClient() { EnableBroadcast = true })
            {
                while (true)
                {
                    try
                    {
                        // Write the XGPS sentence.
                        // Since we are using altitude in feet and ground speed in knots, we need to convert them to meters and to meters per second respectively.
                        // If you pull them from the Sim as Meters or Meters per second (check MSFS SDK documentation), you don't have to convert them
                        var xgps = $"XGPS{APP_NAME},{longitude:F4},{latitude:F4},{altitude / FEET_TO_METERS:F2},{groundtrack:F2},{groundspeed * KNOTS_TO_METERS_PER_SECOND:F2}\n";

                        // Convert the message to bytes
                        byte[] data = Encoding.UTF8.GetBytes(xgps);

                        // Send the UDP message to the broadcast address
                        udpClient.Send(data, data.Length, endPoint);
                        Console.WriteLine($"Sent: {xgps}");

                        // Finally, let's modify the latitude and longited a little to see the plane move in ForeFlight
                        longitude += 0.005f;
                        latitude += 0.005f;
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine($"Error: {ex.Message}");
                    }

                    Thread.Sleep(1000);
                }
            }
        }
    }
}

Code Breakdown

First, we'll create an IPEndpoint to specify the target broadcast address and port, and an UdpClient to send the data over UDP. Make sure you set EnableBroadcast to true after you create the client. We'll be using 255.255.255.255 as the broadcast address for simplicity.

var endPoint = new IPEndPoint(IPAddress.Parse(BROADCAST_ADDRESS), BROADCAST_PORT);

using (UdpClient udpClient = new UdpClient() { EnableBroadcast = true })
If you want to only broadcast to a specific network, you could do so by either specifying the broadcast IP address of the that network, i.e. 192.168.0.255, and provide some sort of mechanism like a config file, a text box, etc. to specify, or even programmatically query the network devices on your PC and let the user select. A query could look something like this, which is what I use in my plugin:
var nics = NetworkInterface.GetAllNetworkInterfaces().Where(n => n.NetworkInterfaceType == NetworkInterfaceType.Ethernet && n.OperationalStatus == OperationalStatus.Up).ToList();
var addrinfos = nics.SelectMany(x => x.GetIPProperties().UnicastAddresses.Where(y => y.Address.AddressFamily == AddressFamily.InterNetwork)).ToList();

Then, we will loop once per second and build our XGPS sentence based on the data we get from the Sim (or some hardcoded values for demo purposes). We have to remember that ForeFlight expects the altitude in meters and the ground speed in meters per second, so if you subscribed to receive the SimVar in feet as we did in previous posts, you'll have to convert the value to meters or subscribe to it in meters, but convert it to feet when we send it to the Arduino.

 simconnect.AddToDataDefinition(RequestType.PerFrameData, "INDICATED ALTITUDE", "feet", SIMCONNECT_DATATYPE.FLOAT32, 0, SimConnect.SIMCONNECT_UNUSED);

We set the precision based on ForeFlight's documentation. I chose 4 decimals for latitude/longitude and 2 for the rest.

var xgps = $"XGPS{APP_NAME},{longitude:F4},{latitude:F4},{altitude / FEET_TO_METERS:F2},{groundtrack:F2},{groundspeed * KNOTS_TO_METERS_PER_SECOND:F2}\n";

We then convert our sentence to a byte array and send it using the UdpClient and IPEndpoint we previously created.

byte[] data = Encoding.UTF8.GetBytes(xgps);

udpClient.Send(data, data.Length, endPoint);

And finally just to see some stuff happen in ForeFlight, we increase the latitude and longitude to see the plane moving in the northeast direction.

The XATT sentence is pretty much built the same as the XGPS one is, so not going to cover it here.

Traffic

Traffic is a bit different. You have to register an additional SimObject event handler and request traffic in a different way.

First, you want to create a struct for the Traffic data and an enum to register it:

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
public struct TrafficSimVarData
{
    public bool IsGrounded;                         // SIM ON GROUND (bool)

    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
    public string CallSign;                         // ATC ID (string)

    public float AirSpeed;                          // AIRSPEED TRUE (knots)
    public float Altitude;                          // PLANE ALTITUDE (feet)
    public float Heading;                           // PLANE HEADING DEGREES TRUE (degrees)
    public float Latitude;                          // PLANE LATITUDE (degree)
    public float Longitude;                         // PLANE LONGITUDE (degree)
    public float VerticalSpeed;                     // VERTICAL SPEED (feet/minute)
}

internal enum RequestType
{
    PerFrameData,
    TrafficData,
}

Then, you want to register a new event handler by type:

  // add this below where you registered your other handlers
  simConnect.OnRecvSimobjectDataBytype += new SimConnect.RecvSimobjectDataBytypeEventHandler(SimConnect_OnRecvSimobjectDataBytype);

...

// define your handler
public void RecvSimobjectDataBytypeEventHandler(SimConnect sender, SIMCONNECT_RECV_SIMOBJECT_DATA_BYTYPE data)
{
    try
    {
        var objectId = data.dwObjectID;
        var requestType = (RequestType)data.dwRequestID;
        // your aircraft's objectId is 1, and it seems that the rest start above 300 
        if (requestType == RequestType.TrafficData && objectId > 300)
        {
            var traffic = (TrafficSimVarData)data.dwData[0];
            // call your method to build and send the XTRAFFIC package here
            // SendTraffic(objectId, traffic);
        }
    }
}

Last but not least, you might want to have a background worker thread to run once per second, to request the traffic data from the sim and sent it to ForeFlight like the documentation suggests.

simConnect?.RequestDataOnSimObjectType(RequestType.TrafficData, RequestType.TrafficData, radius, SIMCONNECT_SIMOBJECT_TYPE.AIRCRAFT);

Note that when you register this, you'll specify the radius in meters around your aircraft and the RecvSimobjectDataBytypeEventHandler() method will be called once for any object within that radius, and it's unique object Id will be included in the response data as data.dwObjectId, which you will send as the id in the XTRAFFIC sentence that ForeFlight uses to keep track of each unique traffic aircraft.

Running and testing

That's it, build and run the Console Application and open ForeFlight. First thing you want to verify is that ForeFlight is receiving data from your "plugin". In ForeFlight, go to Devices and you should see your "HelloForeFlight" app connected. Now open the Maps tab and watch your plane move around.

Wrapping up

That's it. With everything here you should be able to add register some new SimVars on your C# app and add the UdpClient broadcast logic along the other minor modifications to request traffic data and implement the other two sentences.

If you are on Android and cannot use ForeFlight, the code is very similar for Avare and the only difference is the sentences used are NMEA ones. Since Avare is open source, you can find the sentences they expect in their github repo here: https://github.com/apps4av/avare/tree/master/app/src/main/java/com/ds/avare/nmea

One thing I discovered is that you can actually send the sentences to ForeFlight faster than one per second, but be aware that if you broadcast too often, you can literally kill your network with all the traffic being generated... speaking from experience 😅

In the next post I'll show you how to use 3rd party tools for Windows, and Linux so you can see the broadcast traffic live in case you need to do some debugging, or even connect your mobile device to the aircraft's device and see the actual traffic being sent. See you on the next one!