17. Sending Microsoft Flight Simulator data to ForeFlight
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.
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!