Processing NMEA 0183 GPS strings

Here’s a simple example that parses NMEA 0183 data in the background, and delivers every field of each string into an element in a table. Each field can then be accessed easily using something like GGA.Latitude or ZDA.Month for example.

If you un-comment the line in the ReadData() function, this is what the output looks like:

03/31/2013 06:19:07 PM: $GPZDA,071907.00,31,03,2013,*6F
03/31/2013 06:19:07 PM: $GPGGA,071907.00,3744.5607,S,14447.1022,E,1,06,1.18,00073,M,-004,M,*59
03/31/2013 06:19:08 PM: $GPZDA,071908.00,31,03,2013,*60
03/31/2013 06:19:08 PM: $GPGGA,071908.00,3744.5607,S,14447.1022,E,1,05,1.57,00073,M,-004,M,*5E

`// NMEA 0183 processor using Trimble Condor C1011 GPS at 1s intervals

// Many NMEA fields have variable lengths, and fixed-position indexing
// should not be used. In order to extract field information, the string
// must be traversed looking for comma de-limiters.

// Only the GGA and ZDA strings are shown here as examples. Add more as required.

// sjm 20130331

GPS <- hardware.uart12; // simplify code and speed up operation

GGA <- {UTC="",
Latitude="",
NS="",
Longitude="",
EW="",
Quality="",
NumSats="",
HDOP="",
Alti="",
AUnits="",
Geoid="“
GUnits=”",
Age="",
Diff="",
Checksum=""
};

ZDA <- {UTC="",
Day="",
Month="",
Year="",
Empty1="",
Empty2="",
Checksum=""
}

// Extract individual sub-strings for each field by looking for commas
function ParseGGA(s)
{
local i=0;
local tmpstr="";

i=7;
while (s[i]!=',')
{
    tmpstr+=s[i].tochar();
    i++;
}
GGA.UTC = tmpstr;
tmpstr="";
i++;    // skip comma
while (s[i]!=',')
{
    tmpstr+=s[i].tochar();
    i++;
}
GGA.Latitude=tmpstr;
tmpstr="";
i++;    // skip comma
while (s[i]!=',')
{
    tmpstr+=s[i].tochar();
    i++;
}
GGA.NS = tmpstr;
tmpstr="";
i++;    // skip comma
while (s[i]!=',')
{
    tmpstr+=s[i].tochar();
    i++;
}
GGA.Longitude = tmpstr;
tmpstr="";
i++;    // skip comma
while (s[i]!=',')
{
    tmpstr+=s[i].tochar();
    i++;
}
GGA.EW = tmpstr;
tmpstr="";
i++;    // skip comma
while (s[i]!=',')
{
    tmpstr+=s[i].tochar();
    i++;
}
GGA.Quality = tmpstr;
tmpstr="";
i++;    // skip comma
while (s[i]!=',')
{
    tmpstr+=s[i].tochar();
    i++;
}
GGA.NumSats = tmpstr;
tmpstr="";
i++;    // skip comma
while (s[i]!=',')
{
    tmpstr+=s[i].tochar();
    i++;
}
GGA.HDOP = tmpstr;
tmpstr="";
i++;    // skip comma
while (s[i]!=',')
{
    tmpstr+=s[i].tochar();
    i++;
}
GGA.Alti = tmpstr;
tmpstr="";
i++;    // skip comma
    while (s[i]!=',')
{
    tmpstr+=s[i].tochar();
    i++;
}
GGA.AUnits = tmpstr;
tmpstr="";
i++;    // skip comma
while (s[i]!=',')
{
    tmpstr+=s[i].tochar();
    i++;
}
GGA.Geoid = tmpstr;
tmpstr="";
i++;    // skip comma
while (s[i]!=',')
{
    tmpstr+=s[i].tochar();
    i++;
}
GGA.GUnits = tmpstr;
tmpstr="";
i++;    // skip comma
while (s[i]!=',')
{
    tmpstr+=s[i].tochar();
    i++;
}
GGA.Age = tmpstr;
tmpstr="";
i++;    // skip comma
while (s[i]!='*')   // note, check for star
{
    tmpstr+=s[i].tochar();
    i++;
}
GGA.Diff = tmpstr;
tmpstr="";
i++;    // skip star
GGA.Checksum = s.slice(i,i+2);

}

// Extract individual sub-strings for each field by looking for commas
function ParseZDA(s)
{
local i=0;
local tmpstr="";

i=7;
while (s[i]!=',')
{
    tmpstr+=s[i].tochar();
    i++;
}
ZDA.UTC = tmpstr;
tmpstr="";
i++;    // skip comma
while (s[i]!=',')
{
    tmpstr+=s[i].tochar();
    i++;
}
ZDA.Day=tmpstr;
tmpstr="";
i++;    // skip comma
while (s[i]!=',')
{
    tmpstr+=s[i].tochar();
    i++;
}
ZDA.Month = tmpstr;
tmpstr="";
i++;    // skip comma
while (s[i]!=',')
{
    tmpstr+=s[i].tochar();
    i++;
}
ZDA.Year = tmpstr;
tmpstr="";
i++;    // skip comma
while (s[i]!=',')
{
    tmpstr+=s[i].tochar();
    i++;
}
ZDA.Empty1 = tmpstr;
tmpstr="";
i++;    // skip comma
while (s[i]!='*')   // note, check for star
{
    tmpstr+=s[i].tochar();
    i++;
}
ZDA.Empty2 = tmpstr;
tmpstr="";
i++;    // skip star
ZDA.Checksum = s.slice(i,i+2);

}

// We’ve tested the string is valid, and checksum has been verified
// now we determine which kind of message is being sent, and
// process it accordingly
function CheckHeader(s)
{
local header = s.slice(3,6);
if (header==“GGA”) ParseGGA(s);
if (header==“ZDA”) ParseZDA(s);
// add more as required …
}

// Checksum is the XOR of all bytes between $ and * (non-inclusive)
// We compare the value embedded in the string with the calculated value
// If they match, we assume we have received a valid string
function VerifyString(s)
{
local i;
local CalcCS=0; // calculated checksum
local len = s.len()-3; // don’t include checksum bytes or star
for (i=1;i<len;i++) // don’t include $
CalcCS = CalcCS ^ s[i];

local EmbedCS;      // checksum embedded in string - take two hex
                    // chars and convert into an integer
local MSB=s[len+1];
local LSB=s[len+2];
if (MSB > 0x40) MSB -= 0x37; else MSB -= 0x30;
if (LSB > 0x40) LSB -= 0x37; else LSB -= 0x30;
EmbedCS = MSB*16 + LSB;

return((CalcCS == EmbedCS) ? 0 : -1);

}

// Firstly verify the checksum is valid, then extract each field into
// an entry within the table
function ProcessString(s)
{
local error=0;
local result = VerifyString(s);
if (result == 0)
CheckHeader(s);
else
{
server.log(“Checksum error”);
error=-1;
}
return(error);
}

// Callback that’s activated once data is available in the UART RX buffer
start <- 0;
DataString <- “”;
function ReadData()
{
local len = 0;
local ch = GPS.read();
while (ch != -1) // while there is data in the buffer
{
if (ch == ‘$’) start = 1;
if (start)
{
DataString += ch.tochar();
len = DataString.len();
if ((len > 3) && (DataString[len-3]==’*’)) // avoid negative index runtime error
{
start = 0;
//server.log(DataString); // uncomment for “live” updates
ProcessString(DataString);
DataString="";
}
}
ch = GPS.read();
}
}

// Extract some random fields from the table and display it every ten
// seconds to show that the data is being updated in the background.
// In a real-world application, this is where you’d handle the data -
// display it on a screen, push it to the web etc…
function DisplayData()
{
if (ZDA.UTC != “”) server.log(ZDA.UTC);
if (GGA.Latitude != “”) server.log(GGA.Latitude);
if (GGA.Longitude != “”) server.log(GGA.Longitude);

imp.wakeup(10,DisplayData);

}

// Main
imp.configure(“GPS processor”, [], []);
GPS.configure(9600,8,PARITY_NONE,1,NO_TX,ReadData);
DisplayData();
`

split(DataString,",")?

Peter

Thanks Peter, but how do you handle empty fields using split?

For example, a=split(“ABC,DEF,GHI,JKL,MNO,PQ,RST”,",");
needs to return an array like:

a[0]=ABC
a[1]=DEF
a[2]=GHI
a[3]=NULL;
a[4]=NULL;
a[5]=NULL;
a[6]=NULL;
a[7]=JKL;
etc…

Using split the array only contains fields with data, and an NMEA string can often contain fields without data (eg drive through a tunnel, and your lat/long will become null).

This will actually return something like:
a[0]=ABC;
a[1]=DEF;
a[2]=GHI;
a[3]=JKL;
a[4]=MNO;
a[5]=PQ;
a[6]=RST;

which is not what we require.

Oh whoops, you’re quite right, that is a bit of a misfeature of split().

Peter

Regarding your checksum checking function, I think if the checksum includes characters that aren’t in the range of 0-9 or A-F (e.g. because of data corruption), the sentence should be rejected.

@sjm of course, you could write your own split; eg:

`function mysplit(a, b) {
local ret = [];
local field = “”;
foreach(c in a) {
if (c == b) {
// found separator, push field
ret.push(field);
field="";
} else {
// append to field
field+=c.tochar();
}
}
// Push the last field
ret.push(field);
return ret;
}

a <- mysplit(“ABC,DEF,GHI,JKL,MNO,PQ,RST”,’,’);
foreach(b in a) {
server.log(b);
}
`

…which logs this, pretty close to what you’re after. You could have nulls if you wanted, but I suspect it’s more order you’re after?

Sun Jul 28 2013 23:08:38 GMT-0700 (PDT): ABC
Sun Jul 28 2013 23:08:38 GMT-0700 (PDT): DEF
Sun Jul 28 2013 23:08:38 GMT-0700 (PDT): GHI
Sun Jul 28 2013 23:08:38 GMT-0700 (PDT):
Sun Jul 28 2013 23:08:38 GMT-0700 (PDT):
Sun Jul 28 2013 23:08:38 GMT-0700 (PDT):
Sun Jul 28 2013 23:08:38 GMT-0700 (PDT): JKL
Sun Jul 28 2013 23:08:38 GMT-0700 (PDT): MNO
Sun Jul 28 2013 23:08:38 GMT-0700 (PDT):
Sun Jul 28 2013 23:08:38 GMT-0700 (PDT): PQ
Sun Jul 28 2013 23:08:38 GMT-0700 (PDT):
Sun Jul 28 2013 23:08:38 GMT-0700 (PDT):
Sun Jul 28 2013 23:08:38 GMT-0700 (PDT):
Sun Jul 28 2013 23:08:38 GMT-0700 (PDT): RST