Community GPS driver

Me too. It has to be easy to understand for a beginner.

I made a post earlier in this thread about the use of the split method appose to cycling through an array of char. And MarkH gave me this explanation;

First off I feel that holding the NMEA sentence in a char array instead of a string is wrong. With a char array you missing out on great built in string handling methods like; IndexOf, Split, Substring, ToLower, ToUpper, Trim … C#s string handling routines are one of the great features compared to Arduino.

When it comes to speed, I found it strange that cycling through an array of char, should be faster than the built in function of split, I decided to make two small test programs and compare.
To rule out any difference from the reading data from the GPS itself I hardcoded a NMEA sentence.
I used a snipped from the NmeaGps.cs out on codeplex and built my test program around that. (that said, -I had a hard time getting it to work, -has the code on codeplex been tested at all?)

My while loop test program looks like this;


using System;
using System.Threading;
using System.Text;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;
using GHIElectronics.NETMF.FEZ;

namespace While_test
{
    public class Program
    {
        public static void Main()
        {
            double millisec;

            string NMEA = "$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A\r$GPRMC,123519,A,480"; // part of the buffer
            char[] buffer = NMEA.ToCharArray();  //build our dummy buffer
            int seekPos = 0;  //Set seekPos at $

            char[] sentence;
            char[] checksum;
            int sentencePos;

            // found our first sentence;
            sentence = new char[300];
            checksum = new char[2];
            sentencePos = 0;

            Int64 start = DateTime.Now.Ticks;  //start the timer

            sentence[sentencePos++] = (char)buffer[seekPos++];
            while (buffer[sentencePos] != '*' && buffer[sentencePos] != '\r' && buffer[sentencePos] != '\n')
            {
                sentence[sentencePos++] = (char)buffer[seekPos++];
            }

            if (buffer[seekPos++] == '*')
            {
                checksum[0] = (char)buffer[seekPos++];
                checksum[1] = (char)buffer[seekPos++];
            }

            Int64 UsedTime = (DateTime.Now.Ticks - start);  //string handeling complete
            millisec = (double)UsedTime / 10000;
            Debug.Print("While test\nTime used: " + millisec.ToString("F5") + "ms");
            Debug.Print("Sentence: " + new string(sentence));
            Debug.Print("Checksum: " + new string(checksum));

            Thread.Sleep(-1);
        }



    }
}



My other test program using the split function looks like this;


using System;
using System.Threading;
using System.Text;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;

using GHIElectronics.NETMF.FEZ;

namespace Split_test
{
    public class Program
    {
        public static void Main()
        {
            double millisec;

            // Test 1 ------------------------
            string NMEA = "$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A";  // clean sentence
            Int64 start = DateTime.Now.Ticks;
            string[] cs = NMEA.Split('*');
            if (cs.Length == 2)
            {
                //we have a checksum
            }
            Int64 UsedTime = (DateTime.Now.Ticks - start);
            millisec = (double)UsedTime / 10000;
            Debug.Print("Split test\nTime used: " + millisec.ToString("F5") + "ms");
            Debug.Print("Sentence: " + cs[0]);
            Debug.Print("Checksum: " + cs[1] + "\n");

            Thread.Sleep(-1);
        }

    }
}

The results are as follows

While test
Time used: 7.64970ms
Sentence: $GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W
Checksum: 6A

Split test
Time used: 0.56690ms
Sentence: $GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W
Checksum: 6A

So according to this, the split method is 13.5 times faster than cycling through the array to find the checksum marker ‘*

So guys, is my findings correct or am I missing something?

Good point about the stuff on CodePlex not being tested. That’s a good goal, to get a stable release out, maybe within a month.

I have a serial GPS coming pretty soon, so I will be able to take part in development a little more.

As for string handling, yeah, I agree that we need to use the built in string handling functions. The first approach is not efficient and is too wordy.

A lot of the time the .NET implementation is much better/faster than anyone trying to “do it better/faster” :slight_smile: They really did pour a lot of time at MS into making the framework very fast and reasonably easy to understand. :slight_smile:

Here is my suggestion for an NMEA checksum validation. As far as I can see there is not yet a checksum validation in the source files at codeplex.


private bool ValidChecksum(string NMEA)
        {
            // Method assumes a full NMEA sentence without carriage return at the end like this;
            // $GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A
            // returnes false if checksum fails

            bool Status = false;
            int Checksum = 0;
            int NMEA_Checksum = -1;
            string[] Element = NMEA.Split('*');
            if (Element.Length == 2)
            {
                NMEA_Checksum = Convert.ToInt32(Element[1], 16);
                foreach (byte Character in Element[0].Substring(1)) //bypass the '$' and XOR the rest of the string
                {
                    Checksum ^= Character;
                }

                if (NMEA_Checksum == Checksum) Status = true;
            }
            return Status;
        }

There is however a method for adding checksum to a sentence. This seems rather useless to me. When will it ever be used?


        /// <summary>
        /// Add the NMEA checksum to the message.
        /// </summary>
        /// <param name="message">Message that just requires a checksum on the end</param>
        /// <returns>Full message with the checksum added.</returns>
        protected string NmeaChecksum(string message)
        {
            if (message.Substring(0,1) != "$")
            {
                message = "$" + message;
            }

            if (message.Substring(message.Length - 1, 1) == "*")
            {
                message = message.Substring(0, message.Length - 1);
            }

            int checkSum = 0;

            for (int i = 1; i < message.Length; i++)
            {
                checkSum ^= (int)message[i];
            }

            string checksum = checkSum.ToString("X");

            if (checksum.Length == 1) // no PadLeft method on string.
                return message + "*0" + checksum; 

            return message + "*" + checksum; 
        }


The gps devices support the sending of information to them via the serial bus. The checksum is needed to ensure that the message is received on it’s end properly.

The code in the codeplex is still “in progress” :smiley: patience grasshopper :smiley: (lol I’ve waited all my life so far to say that). The issue regarding the speed of the code is important because we don’t know what else will be running on the device along with this driver. So the intent is to make an easy to use, efficient as practical driver.

Geir, can you provide any info about the memory usage of the 2 approaches?

Here is a GC dump;

While test
Time used: 7.64940ms
Sentence: $GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W
Checksum: 6A
GC: 1msec 16320 bytes used, 48060 bytes available
Type 0F (STRING ): 108 bytes
Type 11 (CLASS ): 576 bytes
Type 12 (VALUETYPE ): 36 bytes
Type 13 (SZARRAY ): 960 bytes
Type 15 (FREEBLOCK ): 48060 bytes
Type 16 (CACHEDBLOCK ): 132 bytes
Type 17 (ASSEMBLY ): 7416 bytes
Type 18 (WEAKCLASS ): 48 bytes
Type 19 (REFLECTION ): 24 bytes
Type 1B (DELEGATE_HEAD ): 72 bytes
Type 1D (OBJECT_TO_EVENT ): 24 bytes
Type 1E (BINARY_BLOB_HEAD ): 5340 bytes
Type 1F (THREAD ): 384 bytes
Type 20 (SUBTHREAD ): 48 bytes
Type 21 (STACK_FRAME ): 528 bytes
Type 27 (FINALIZER_HEAD ): 24 bytes
Type 31 (IO_PORT ): 36 bytes
Type 34 (APPDOMAIN_HEAD ): 72 bytes
Type 36 (APPDOMAIN_ASSEMBLY ): 492 bytes
48060

Split test
Time used: 0.56600ms
Sentence: $GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W
Checksum: 6A

GC: 1msec 15588 bytes used, 48792 bytes available
Type 0F (STRING ): 216 bytes
Type 11 (CLASS ): 576 bytes
Type 12 (VALUETYPE ): 36 bytes
Type 13 (SZARRAY ): 180 bytes
Type 15 (FREEBLOCK ): 48792 bytes
Type 16 (CACHEDBLOCK ): 132 bytes
Type 17 (ASSEMBLY ): 7416 bytes
Type 18 (WEAKCLASS ): 48 bytes
Type 19 (REFLECTION ): 24 bytes
Type 1B (DELEGATE_HEAD ): 72 bytes
Type 1D (OBJECT_TO_EVENT ): 24 bytes
Type 1E (BINARY_BLOB_HEAD ): 5340 bytes
Type 1F (THREAD ): 384 bytes
Type 20 (SUBTHREAD ): 48 bytes
Type 21 (STACK_FRAME ): 468 bytes
Type 27 (FINALIZER_HEAD ): 24 bytes
Type 31 (IO_PORT ): 36 bytes
Type 34 (APPDOMAIN_HEAD ): 72 bytes
Type 36 (APPDOMAIN_ASSEMBLY ): 492 bytes
48792

By the looks of it, you guys are planning one massive GPS driver and thats great, but wont this be a bit heavy on the memory consumption?
Maybe there should be a lightweight version of it for us that needs only the basic stuff and would like to conserve memory to other parts of our application.

The source tree is big. You will only need a few files from the source tree to actually make it run.

Umm guys, so the string manipulation is faster, but how do you plan on getting it to the string? The code you showed there geir isnt comparing apples with apples. The code you show includes reading from the buffer in the first sample but not the second.

Perhaps change your test to read from the buffer (which is a byte array) into your string and then split it. It’s going to end up being a string in any case so it can be split up for getting the elements out so it may be quicker to split it rather than read two bytes if they exist.

I’m always happy to be wrong and find a better way to do something, i’ve only just got my FEZ so the way i do things may seem right in my mind but not be practical when they are implemented - that’s why we have multiple developers on these things.

Hehe, well it’s designed for all devices including the EMX so if it’s to fat then we’ll trim some meat of into a slimmer version.

If it gets too fat, then it’s always possible to split the various protocols out to their own assembly with a single assembly containing all the shared types.

I don’t think it will be goo big through, it’s only basic string manipulation and reading after all, with a bit of basic maths for speed.

MarkH,
Based on your comment

[quote]Umm guys, so the string manipulation is faster, but how do you plan on getting it to the string? The code you showed there geir isnt comparing apples with apples. The code you show includes reading from the buffer in the first sample but not the second.

Perhaps change your test to read from the buffer (which is a byte array) into your string and then split it. It’s going to end up being a string in any case so it can be split up for getting the elements out so it may be quicker to split it rather than read two bytes if they exist.
[/quote]

Ive changed the code to convert a char array to string and then do the split. Still your method is far slower


public static void Main()
        {
            double millisec;

            // Test 1 ------------------------
            string strBuffer = "$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A";  // clean sentence
            char[] buffer = strBuffer.ToCharArray();  //build our dummy buffer

            Int64 start = DateTime.Now.Ticks;
            string NMEA = new string(buffer);
            string[] cs = NMEA.Split('*');
            if (cs.Length == 2)
            {
                //we have a checksum
            }
            Int64 UsedTime = (DateTime.Now.Ticks - start);
            millisec = (double)UsedTime / 10000;
            Debug.Print("Split test\nTime used: " + millisec.ToString("F5") + "ms");
            Debug.Print("Sentence: " + cs[0]);
            Debug.Print("Checksum: " + cs[1] + "\n");
            Debug.Print(Debug.GC(true).ToString());
            Thread.Sleep(-1);
        }

Here is the timing and GC dump

Split test
Time used: 0.65150ms
Sentence: $GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W
Checksum: 6A

GC: 1msec 15864 bytes used, 48516 bytes available
Type 0F (STRING ): 300 bytes
Type 11 (CLASS ): 576 bytes
Type 12 (VALUETYPE ): 36 bytes
Type 13 (SZARRAY ): 336 bytes
Type 15 (FREEBLOCK ): 48516 bytes
Type 16 (CACHEDBLOCK ): 132 bytes
Type 17 (ASSEMBLY ): 7428 bytes
Type 18 (WEAKCLASS ): 48 bytes
Type 19 (REFLECTION ): 24 bytes
Type 1B (DELEGATE_HEAD ): 72 bytes
Type 1D (OBJECT_TO_EVENT ): 24 bytes
Type 1E (BINARY_BLOB_HEAD ): 5340 bytes
Type 1F (THREAD ): 384 bytes
Type 20 (SUBTHREAD ): 48 bytes
Type 21 (STACK_FRAME ): 492 bytes
Type 27 (FINALIZER_HEAD ): 24 bytes
Type 31 (IO_PORT ): 36 bytes
Type 34 (APPDOMAIN_HEAD ): 72 bytes
Type 36 (APPDOMAIN_ASSEMBLY ): 492 bytes
48516

Ok so my tests:
Split test
Time used: 1738.09940ms


public static void Main()
        {
            double millisec;
            string NMEA = "$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A\r$GPRMC,123519,A,480"; // part of the buffer
            char[] buffer = NMEA.ToCharArray();  //build our dummy buffer
            
            Int64 start = DateTime.Now.Ticks;
            
            for (int i = 0; i < 1000; i++)
            {
                
                int seekPos = 0;  //Set seekPos at $

                char[] sentence;
                char[] checksum;
                int sentencePos;

                // found our first sentence;
                sentence = new char[300];
                checksum = new char[2];
                sentencePos = 0;



                sentence[sentencePos++] = (char)buffer[seekPos++];
                while (buffer[sentencePos] != '\r' && buffer[sentencePos] != '\n')// && buffer[sentencePos] != '*')
                {
                    sentence[sentencePos++] = (char)buffer[seekPos++];
                }

                string ss = new string(sentence);

                string[] cs = ss.Split('*');
                if (cs.Length == 2)
                {
                    //we have a checksum
                }
            }

            Int64 UsedTime = (DateTime.Now.Ticks - start);
            millisec = (double)UsedTime / 10000;
            Debug.Print("Split test\nTime used: " + millisec.ToString("F5") + "ms");
            Thread.Sleep(-1);
        }

Extra Reads test
Time used: 1844.10540ms

public static void Main()
        {
            double millisec;
            string NMEA = "$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A\r$GPRMC,123519,A,480"; // part of the buffer
            char[] buffer = NMEA.ToCharArray();  //build our dummy buffer
            
            Int64 start = DateTime.Now.Ticks;
            
            for (int i = 0; i < 1000; i++)
            {
                
                int seekPos = 0;  //Set seekPos at $

                char[] sentence;
                char[] checksum;
                int sentencePos;

                // found our first sentence;
                sentence = new char[300];
                checksum = new char[2];
                sentencePos = 0;



                sentence[sentencePos++] = (char)buffer[seekPos++];
                while (buffer[sentencePos] != '\r' && buffer[sentencePos] != '\n' && buffer[sentencePos] != '*')
                {
                    sentence[sentencePos++] = (char)buffer[seekPos++];
                }

                if (buffer[seekPos++] == '*')
                {
                    checksum[0] = (char)buffer[seekPos++];
                    checksum[1] = (char)buffer[seekPos++];
                }
            }

            Int64 UsedTime = (DateTime.Now.Ticks - start);
            millisec = (double)UsedTime / 10000;
            Debug.Print("Extra Reads test\nTime used: " + millisec.ToString("F5") + "ms");
            Thread.Sleep(-1);
        }

Based on Geir’s previous tests, it’s not surprising that the second way is slower, however when running it 1000x with both tests simulating reading the string in from the GPS it’s not much slower (5.7%) so my thinking wasn’t too far off :wink:

as to:

It will be used a lot by me:

  • Setting baud rate of the remote device after initial connection
  • Setting what sentences to send
  • Setting update rate

You can’t do 5hz never mind 10hz at 19200 with 3 sentences getting sent, so i change up to 115200 and change which sentences i want, and tell it 4/5/10hz depending on the device. My MTK’s are capable of 5hz or 10hz but by default only send 1hz.

On a side note, is anyone actually going to work on the GPS driver or just debate the half written, untested, unusable code that’s already there?

I’m actually amazed that your second version isn’t the fastest. After all, you are breaking out of the while loop on ‘*’ so its position is known. Whereas the split version first has to build the string, then split it.
And in your second version, you are still stuck with an array of chars :wink:

Here is an example where I’m taking the buffer of chars. Building a string out of it. Then cut out a full NMEA sentence and split on ‘*’. This is still faster (1275ms) but much more readable and easy to follow in my opinion


public static void Main()
        {
            double millisec;

            string strBuffer = "$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A\r$GPRMC,123519,A,480";  // clean sentence
            char[] buffer = strBuffer.ToCharArray();  //build our dummy buffer

            Int64 start = DateTime.Now.Ticks;
            for (int i = 0; i < 1000; i++)
            {
                string NMEA = new string(buffer);               //Build a string from char buffer
                NMEA = NMEA.Substring(0,NMEA.IndexOf('\r'));    //Get a clean NMEA sentense
                string[] cs = NMEA.Split('*');                  //Split at '*' for sentence and checksum
                if (cs.Length == 2)
                {
                    //we have a checksum
                }  
            }

            Int64 UsedTime = (DateTime.Now.Ticks - start);
            millisec = (double)UsedTime / 10000;
            Debug.Print("Split test\nTime used: " + millisec.ToString("F5") + "ms");
            Debug.Print(Debug.GC(true).ToString());
            Thread.Sleep(-1);

        }

Geir,

I must admit i was suprised the second one wasnt faster too, considering it needs to read those two bytes for the first string.

The reason i did the samples the way i did is because unfortunately the serial port doesn’t grace us with a ReadAll which comes out as a string (as far as i’m aware). So in the real application you need to convert the character array (the buffer from the server) over to read the string if we’re going to keep the samples consistent.

Your version is much more readable :slight_smile: I wrote the other one at 1am with some serious neck strain issues going on, i admit it’s not the best code.

The problem with converting the whole buffer to a string is that we’re not sure what the gps is going to end it’s line with, most end with \r\n but some end with just \n, and a rare few just \r (like on an older mac os.) It’s difficult to support everything. Plus with only reading the data out of the buffer we need, we can add new data to the buffer so as to not loose any partial sentences that may have been received at the time of reading. I’d hate to throw away half of a majr sentence on a 1hz gps.

Mark - it’s an open source project. By law we have to sit and debate 90% of the time and code 10%. :wink:

I’m adding to it here and there as I can. I admit to not being the strongest coder so I’m trying to not get involved in the more complicated stuff so as to not screw anything up. :slight_smile:

Hopefully i’ll get some time to do some more work on it later this week. I’m hoping my biowire will arrive from Robotshop though which will put things on hold a little :slight_smile:

Hi MarkH
It would be great if there was an eventhandler that only got triggered on a user defined value like ‘\r so we didnt constantly have to handle the COM input. As far as I know the only way to handle the serial stream is by the eventhandler and do some work every time there is activity on the port.

Im attaching one way of doing this, but I feel there is a lot of overhead. I cant really think of a way to evaluate it, and see how much of the total processor capacity it consumes. Where is that task manager!! :smiley:

This is the way I think
I dont know how many characters there are at the serial port so I read them all and append them to a string called NMEA_stream.

If NMEA_stream has a start character in it ($), I cut away anything before it as that is useless information and will never be a complete sentence.

As the NMEA_stream gets more characters I look for the terminator ‘\r (in my case) [any ‘\n will be cut off at the next round]

When I get my ‘\r I cut out the sentence and leave the rest in the NMEA_stream as there might be characters there already for the next sentence.

Anyway, here is the code. Does it make sense to you?


namespace GPS_reader
{
    public class Program
    {
        
        public static string NMEA_stream;
        public static SerialPort GPSHandle;

        public static void Main()
        {
            Debug.EnableGCMessages(false);

            GPSHandle = new SerialPort("COM2", 4800);
            GPSHandle.Open();
            GPSHandle.Flush();
            GPSHandle.DataReceived += new SerialDataReceivedEventHandler(GPSHandle_DataReceived);

            Thread.Sleep(-1);
        }

        static void GPSHandle_DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            if (e.EventType == SerialData.Chars)
            {

                byte[] ReadBuffer = new byte[GPSHandle.BytesToRead];
                GPSHandle.Read(ReadBuffer, 0, ReadBuffer.Length);
                NMEA_stream = NMEA_stream + new String(Encoding.UTF8.GetChars(ReadBuffer));

                if (NMEA_stream.IndexOf('$') >= 0)
                {

                    NMEA_stream = NMEA_stream.Substring(NMEA_stream.IndexOf('$'));  //Trim off anything before the '$' as its useless
                    if (NMEA_stream.IndexOf('\r') > 0)
                    {
                        string NMEA = NMEA_stream.Substring(0, NMEA_stream.IndexOf('\r')); //Try to get a complete sentence

                        if (NMEA.Length > 5)
                        {
                            NMEA_stream = NMEA_stream.Substring(NMEA_stream.IndexOf('\r')); //Cut the sentence out of the NMEA_stream buffer and keep the rest
                            if (ValidChecksum(NMEA))
                            {
                                Debug.Print(NMEA); //We have a clean NMEA sentence to process
                            }
                        }
                    }
                }
            }
        }




        static bool ValidChecksum(string NMEA)
        {
            // Method assumes a full NMEA sentence without carriage return at the end like this;
            // $GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A
            // returnes false if checksum fails
            bool Status = false;
            int Checksum = 0;
            int NMEA_Checksum = -1;
            string[] Element = NMEA.Split('*');
            if (Element.Length == 2)
            {
                NMEA_Checksum = Convert.ToInt32(Element[1], 16);
                foreach (byte Character in Element[0].Substring(1)) //bypass the '$' and XOR the rest of the string
                {
                    Checksum ^= Character;
                }

                if (NMEA_Checksum == Checksum) Status = true;
            }
            return Status;
        }

    }
}

Great timing on starting up the codeplex project guys! Once i put my brandy new domino through some basic paces, I’m up for contributing. I’ll be using an EarthMate BT-20 GPS to start playing with the code you’ve already put up. Comes with a STA2051E ‘Vespucci’ chip, board pinouts, and fits nicely on the screwshield I also purchased. I’m a FEZ hardware newb, but have plenty of exp in C# and serial com with embedded devices. Stumbled across NetMF while looking into automating model train layouts. Too many possibilities have left little time for sleep since…

Anybody interested in a base class to parse standard NMEA-0183 compliant sentences? This would be called from NmeaGPS.DecodeGpsSentence() function and only handle the standard headers (RMC, GGA, GSV, etc), leaving device specific codes to be subclassed.

Eric