Contents
Recently, I've been exposed to the whole world there is to the ever smaller and lighter electronic-equipments. I've always been interested in cutting-edge technology and somehow I got involved in an aeromodelism group where it was decided that the best (and simplest) solution for measuring flight data was to use a GPS device. No doubt we chose the world leading brand Garmin, of all the features available in their devices, the one that was important to us was track recording. As the sole Windows application programmer in the group, I was requested to create software that would transfer data between the GPS unit and a Pocket PC so that adjustments could be made instantly based on the data collected which would be analyzed in graphs.
Garmin uses a proprietary format that is not complicated, but let's cover its basics here as there aren't many thorough resources on the internet.
Information transferred to and from the GPS is divided into packets, which we'll refer to as messages. Each message starts with a hexadecimal 0x10 code and ends with two bytes 0x10 and 0x03. The information itself is found between these starting and finishing bytes. The second byte refers to the kind of information that is being transmitted, i.e. the message ID. Next, we have a byte which represents the number of bytes that are to be sent starting from the following byte up to the last one, just before a checksum byte and the trailing 0x10 0x03. But if it were as simple as that, the protocol would not be totally robust. Let's imagine for example that the first information-byte, which is the 4th, has the bits set to 0x10 and the following one 0x03. A simple interpreter would get it as a message-finalizer, thus wrongly recognizing a message-escape sequence and causing an error. In order to remove such a possibility, Garmin chose to repeat the corresponding byte whenever 0x10 should be sent, as long as it's an information-byte, so that in the above case we would receive 0x10 0x10 0x03 accounting for only two bytes, thus removing any possible errors. Repeated 0x10 accounts for only one byte as far as the third-byte (count byte) is concerned. Let's see some examples of messages. Move your mouse over the bytes to see their descriptions:
Pocket PC: 10 0A 02 06 00 EE 10 03 - Ask for tracks, the first message sent.
GPS: 10 06 02 0A 00 EE 10 03 - Reply to the previous message, means OK.
GPS: 10 1B 02 05 00 DE 10 03 - Number of records to be sent next: 5.
Pocket PC: 10 06 02 22 00 D6 10 03 - Ask for the next record.
GPS: 10 63 0D 01 FF 41 43 54 49 56 45 20 4C 4F 47 00 D2 10 03 - Track name: ACTIVE LOG.
Pocket PC: 10 06 02 22 00 D6 10 03 - Ask for the next record.
GPS: 10 22 18 01 02 03 04 05 06 07 08 09 10 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 DE 10 03 - Point in track.
Pocket PC: 10 06 02 22 00 D6 10 03 - Ask for the next record.
GPS: 10 0C 02 06 22 CA 10 03 - Track EOF.
In order to calculate the checksum of the message we first sum up all the information-bytes and get the least significant byte of the result by applying an AND (&) operation. Then, we invert the bits by calling XOR (^) 0xff and then we just add 1. The code for doing this is shown below:
private bool CheckSum(System.Collections.ArrayList command,
bool errorDetails)
{
int res=0;
int orig=(byte)command[command.Count-3];
for(int i= 1;i<command.Count-3;i++)
{
res+=(byte)command[i];
}
res &= 0xff;
res ^= 0xff;
res+=1;
bool retval=(byte)(res) == (byte)orig;
if(!retval && errorDetails)
{
System.Windows.Forms.MessageBox.Show(
"Received message:\n" +
ToHEXstring(command) + "\n\n" +
"Received checksum: " + orig.ToString() +
"\nCalculated checksum:" + res.ToString(),
"Error details",
System.Windows.Forms.MessageBoxButtons.OK,
System.Windows.Forms.MessageBoxIcon.Asterisk,
System.Windows.Forms.MessageBoxDefaultButton.Button1);
}
return ( retval );
}
For each point data sent by the GPS, there is the latitude, longitude, time, altitude and whether, it is a new segment within the same track. Each of the first four fields is composed of 4 bytes, in a way that the most significant one is the one to the right. Next we'll discuss how to obtain data values from the bytes.
Coordinates
Both the latitude and longitude are represented by an int32
ranging from -2147483648 to +2147483647 that should be translated into numbers from -90 to +90 for latitude and from -180 to +180 for longitude. The mathematical formulae for both are the same and are shown in the extract below which comprises of all the parsing of a track point message.
Time
The time is the simplest to retrieve from the message: we just read the four corresponding bytes and store them.
Altitude
The altitude is not easy to calculate from within the .NET Compact Framework. This field is transmitted as a float32
and we must retrieve a variable of that type based on the 4 bytes. This complex routine, which I'll not go into detail here, is also shown in the extract below.
New segment flag
Once I discovered which byte to check for such information, getting its data was fairly trivial.
Retrieving data from a track point message
latitude=( ((byte)command[6]<<24 |
(byte)command[5]<<16 |
(byte)command[4]<<8 |
(byte)command[3])
* ( 180.0 / 2147483648.0) );
longitude=( ((byte)command[10]<<24 |
(byte)command[9]<<16 |
(byte)command[8]<<8 |
(byte)command[7])
* ( 180.0 / 2147483648.0 ) );
time=(uint)((byte)command[14]<<24 |
(byte)command[13]<<16 |
(byte)command[12]<<8 |
(byte)command[11]);
if((byte)command[23]==1)
isNewSegment=true;
else
isNewSegment=false;
int h=(byte)command[18]<<24 |
(byte)command[17]<<16 |
(byte)command[16]<<8 |
(byte)command[15];
int exp=(h & 0x7f800000)/(2<<22);
int frac=h & 0x7fffff;
height=1+(float)frac/((float)(2<<22));
height *= 2<<(exp-128);
For every new track in the GPS database a message is sent containing the name of a new track and its ID, as seen earlier in the message examples. Next is a snippet from the code that is used to get the track name:
name=""
for(int i=5;i<=(byte)command[2]+1;i++)
name+=Convert.ToChar((byte)command[i]).ToString();
This is just an outline of what the protocol seems to be according to my analysis. For more information on other types of data that can be transferred, you may refer to this.
Having the protocol interpreter done, the next step was to build the data analysis tools, which was not as easy as expected due to the limitations found while comparing the .NET Framework to the .NET Compact Framework. The main feature usually used in graphing applications is vector graphics, achieved with the aid of matrix-transformations such as scale and translation. As a consequence, all the drawing routines had to be created from scratch.
For serial communications, I grabbed routines from Microsoft and the progress bar used to indicate the transfer progress from a GPS unit was taken from the OpenNETCF Smart Device Extensions project, which adds a fancy gradient background to the control.
Three important classes used for communication between a Pocket PC and a Garmin GPS unit are included in the source code: Garmin
, Track
and TrackPoint
. The first one is responsible for communicating with the unit and storing the information received into tracks of type Track
, each made of points of type TrackPoint
, and stored by each track as ArrayList
s.
Making the Garmin
class communicate with the GPS seemed to be trivial at first, but came out to be much more complex than expected.
I tried two methods of communication before getting to the one that I am now using, which hasn't failed even once throughout my tests with an iPaq H2215 device. However, when I tried it on an older model also from HP, the Jornada 548, I couldn't get it to work, probably because of some difference in the hardware implementation of the serial port. Next, I will describe each of the methods that sometimes worked but were not stable and their problems, and also the reliable one that is currently in use:
- Parsing as data was received:
The first one I tried consisted of parsing the data as it was received from the GPS and creating Track
s and TrackPoint
s according to what was received. The problem with this was as the serial port is asynchronous, I couldn't find out when the message was over while still not skipping any bytes that are the first ones from the next message unless I wrote a complex routine that would do the trick. After adding some delays to make sure that all the data had come and still not getting the expected results, I decided to try another method.
- Parsing as data was received and flooding the GPS with send next commands:
By using this method, whenever the data came in, a send next command was sent to the GPS so as to avoid the problems created by using the artificial delays of the previous method. This method never got to work due to the bytes being skipped.
- The currently-in-use working method:
In this method, I mix the second one with a new concept to make the code reliable. Whenever data is received it is added to an ArrayList
and, when an EOF (End of File) is received a function is called to split the messages from the array of bytes stored in the ArrayList
and then the Track
and its TrackPoint
s are added. By using this method, the saving and loading of files was made much easier, as the process of loading data from the GPS or from a file would be the same. To save data I need to just copy the bytes in the ArrayList
to the storage memory and to load it, do just the reverse and call the function that splits the messages accordingly.
Creating a code that would flawlessly break the bytes at every message-end took many hours until I got the idea of an algorithm that would do the trick. As mentioned earlier, when an information-byte has a value of 0x10, it is repeated. So, if we count the number of adjacent 0x10 present in the actual message we'll always get an even number as there will never be a single 0x10, they come in pairs. However, if there's an ending 0x10 byte we'll get an odd number because the ending-byte is always sent on its own. That's it! Breaking the messages was as simple as counting adjacent 0x10 and checking the result. If it is even we should go on. Otherwise, we have found a message break:
private static System.Collections.ArrayList SplitBytes(
System.Collections.ArrayList arrayListBytes)
{
System.Collections.ArrayList retval=
new System.Collections.ArrayList(10);
System.Collections.ArrayList tempBytes;
int count=0;
for(int i= 0;i<arrayListBytes.Count;i++)
{
if((byte)arrayListBytes[i]==0x10)
{
tempBytes=new System.Collections.ArrayList(8);
tempBytes.Add(arrayListBytes[i]);
for(int x=i+1;;x++)
{
i=x;
tempBytes.Add(arrayListBytes[x]);
if((byte)arrayListBytes[x]==0x10)
++count;
else if((byte)arrayListBytes[x]==0x03)
{
if(count%2==1)
break;
}
else
count=0;
}
retval.Add(tempBytes);
}
}
return retval;
}
In order to connect your Pocket PC to a GPS unit software-wise you just need to create a variable of type Garmin
and call its .GetTracks
.
Apart from the software, you'll have to establish a real physical connection between the device's serial port and the GPS. For this, I personally recommend Pfranc plugs. I got mine here in Brazil with no problems and they work just great. On the other end of the cable you'll have the Pocket PC. In my case, I had an iPaq which comes with a power adapter. I used this adapter and added some extra wires to it so that it became a charging and serial port connector. Below is an image showing the HP iPaq universal 22-pin connector diagram that shows the wires to connect to the GPS. If you need assistance in creating the cables feel free to e-mail me, but keep in mind that the connections you make on your own could void your warranty and damage your equipment. I got mine to work with no problems at all, but there is a possibility of damaging both the devices. Do whatever you do at your own risk.
Solder-side view of the iPaq power adapter and the pins to solder.
eTrex Pfranc plug, frontal view from the latch.
This is the result of the modifications made to the original plugs.
One interesting feature added to the sample application is the ability to save a spreadsheet file which can be read by most of the spreadsheet editors available in the market. It saves files using the known CSV (Comma Separated Values) format. However, while implementing the save function, I discovered a problem with the CSV format which I couldn't overcome: The decimal-separator character seems to depend on the current system's regional settings, and as a consequence we can't guarantee that a saved file will be read properly by a configuration. Their might be some way to elegantly fix this problem, but the way I figured out works satisfactorily: I use Excel formulae to overcome this issue. Non-integer numbers are multiplied by 1010 and Excel is made to calculate the result of the division of such a number by 1010. This way the decimal separator will always be displayed according to each system's configuration and still will be made possible for reading by whichever configuration. Let's see an example of the formula we use for the altitude measurement in Excel: =7749426479104/10000000000 which accounts for approximately 774.9426 meters.
- 12.03.2005: version 1.06
- 12.10.2005: version 1.07
- Fixed an issue in
Plotter
in which track segments made of only one point would make the program crash.