Communication between X-Plane and an Arduino Uno microcontroller

A few months ago, I bought an Arduino Uno R3. It is a single-board microcontroller, intended to make the application of interactive objects or environments more accessible.

The hardware consists of an open-source hardware board designed around an 8-bit Atmel AVR microcontroller, or a 32-bit Atmel ARM. Current models feature a USB interface, 6 analog input pins, as well as 14 digital I/O pins which allows the user to attach various extension boards.

After playing a while with this small device, it was obvious that it could be a very useful platform to build instruments for an homemade cockpit. For more information about the LiquidCrystal library, please take a look at the following URL: http://arduino.cc/en/Tutorial/LiquidCrystal

Generalities

The first step is figuring out how X-Plane communicates with external programs. The first way, would be by writing a plug-in. The second option, is communicating with X-Plane using UDP, or User Datagram Protocol.



Data sent using UDP are sent in the form of a string of bytes, called datagrams. X-Plane has many options for the sending of this data, such as what data it sends and how often it is sent.



For each data element, you can select to have it sent via UDP, you can output the data to a text file (Data.txt), which will be very helpful for the development of the C# software. Also, you may have it display on-screen while the simulation is running.

How it works

The architecture of my prototype will be very simple. On a Windows machine running X-Plane, a C# console application reads the UDP packets sent by the simulator, parses and interprets those datagrams then sends the message to be displayed on the serial port where the Arduino is connected.




X-Plane binds to port 49000 for receiving data, 49001 for sending, and 49002 for iPad devices. For receiving any data in my C# application, I will need to use a port other than those, such as 49003.

Understanding the format of X-Planes UDP sentences is very important, obviously.

All data is sent as bytes:

  • There are 41 bytes per sentence
  • The first 5 bytes are the message header, or "prolouge"
  • First 4 of the 5 prolouge bytes are the message type, like "DATA"
  • Fifth byte of prolouge is an "internal-use" byte
  • The next 36 bytes are the message
  • First 4 bytes of message indicates the index number of a data element, as shown in the Data Output screen in X-Plane
  • Last 32 bytes is the data, up to 8 single-precision floating point numbers


Here is a raw data string sent from X-Plane:

68 65 84 65 60 18 0 0 0 171 103 81 191 187 243 46 190 103 246 45 67 156 246 26 67 47 231 26 67 0 192 121 196 0 192 121 196 85 254 151 193

C# code

This is the code of the console application which reads the UDP packets and sends the message to the Arduino. Any version of the .Net Framework greater than 3.5 works.

//// This code works well so far for reading data from X-Plane

using System;
using System.IO.Ports;
using System.Net;
using System.Net.Sockets;
using System.Threading;


namespace XP2Arduino
{
    class Program
    {
        static void Main(string[] args)
        {
        SerialPort serialPortToArduino = new SerialPort("COM7");
        serialPortToArduino.BaudRate=9600;
       
            byte[] data = new byte[1024];
            IPEndPoint ipep = new IPEndPoint(IPAddress.Any, 49003);
            UdpClient newsock = new UdpClient(ipep);

            Console.WriteLine("Waiting for x-plane 9.70 data...");

            IPEndPoint sender = new IPEndPoint(IPAddress.Any, 0);

            while (!Console.KeyAvailable) {
                data = newsock.Receive(ref sender);
                int numDatasets = (data.Length-5)/36;
                
                string buffer = string.Empty;
                
                for(int i=0; i < numDatasets; i++) {
                string msg = processMessage(data, (i*36)+5);
                if (msg.Length>0) {
                buffer+=msg;
                }
                }
                
                serialPortToArduino.Open();
                Thread.Sleep(320);                
                serialPortToArduino.Write(buffer);
                Thread.Sleep(320);
                serialPortToArduino.Close();
            }

            newsock.Close();
            Console.ReadKey();
            Console.ReadKey();
        }
        
        private static string processMessage(byte[] data, int startIndex) {
        string retValue = string.Empty;

          // this is an index number that correspond to a specifix dataset in x-plane        
        int datasetIndex = Convert.ToInt32(data[startIndex]);

        if (datasetIndex==3) { // speed
            // these 32 bytes make up the 8 single-precision floating point numbers
            // sent in the message
        byte[] tmp = { data[startIndex+4], data[startIndex+5], data[startIndex+6], data[startIndex+7] };
        float ias = BitConverter.ToSingle(tmp,0);
        retValue=string.Format("{0,3:##0}",ias) + "KIAS ";
        }

        if (datasetIndex==18) { // magnetic heading
        byte[] tmp = { data[startIndex+16], data[startIndex+17], data[startIndex+18], data[startIndex+19] };
        float hdg = BitConverter.ToSingle(tmp,0);
        retValue=string.Format("{0,3:##0}",hdg) + "DEG ";
        }

        if (datasetIndex==20) { // altitude
        byte[] tmp = { data[startIndex+24], data[startIndex+25], data[startIndex+26], data[startIndex+27] };
        float alt = BitConverter.ToSingle(tmp,0);
        retValue=string.Format("{0,5:####0}",alt) + "FT ";
        }
       
       
        if (datasetIndex==4) { // vvi
        byte[] tmp = { data[startIndex+12], data[startIndex+13], data[startIndex+14], data[startIndex+15] };
        float vvi = BitConverter.ToSingle(tmp,0);
        retValue= string.Format("{0,5:#####}",vvi) + "FPM ";
        }
       
        return retValue;
        }
    }
}

C++ code

The following code must be compiled and uploaded to the Arduino.

#include <LiquidCrystal.h>

//Setup message bytes

byte inputByte1_0 = '';
byte inputByte1_1 = '';
byte inputByte1_2 = '';
...

// initialize the library with the numbers of the interface pins
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);

void setup() {
  // Listen on serial port
  Serial.begin(9600);
  
  // set up the LCD's number of columns and rows: 
  lcd.begin(16, 2);
}

void printCharAt(byte value, byte col, byte row) {
  char b = value;
  lcd.setCursor(col, row);
  lcd.print(b);
}

void loop() {
  // If 16 bytes are available on the serial port
  if (Serial.available() == 16) 
  {
    //Read buffer
    inputByte1_0 = Serial.read(); delay(50);    
    inputByte1_1 = Serial.read(); delay(50);      
    inputByte1_2 = Serial.read(); delay(50);   
...

// set the cursor to column 0, line 1
// (note: line 1 is the second row, since counting begins with 0):
printCharAt(inputByte1_0,0,0);
printCharAt(inputByte1_1,1,0);
printCharAt(inputByte1_2,2,0);
...
}

Commentaires