How to parse position information from GPGGA NMEA sentence with a PVM plugin?

Configuring custom parsing for NMEA-formatted GPS data to convert it into JSON message with standardized parameters.

Say, you have an external GNSS receiver that is connected to the tracker via serial port (e.g. RS232) and sends the detected position in NMEA-0183 format using the tracker as a gateway. As soon as flespi is ignorant about the connected equipment and the format of payload it transfers, the payload generated by the external equipment can't be parsed at the channel level and is stored in the payload.text message parameter as a whole:

{
  "ident": "352625333222111",
  "payload.text": "$GPGGA,063818.80,4904.1868922,N,2837.9695321,E,4,18,0.7,273.610,M,31.5,M,2.0,0008*73",
  "server.timestamp": 1650636570.426424,
  "timestamp": 1650636570.426424
}

Fetching the position information from the payload is possible using the “msg-pvm-code” plugin. The following code is a basic example showing how to do it:

optional .payload.text ==> input:        // Take payload.text parameter if present
split[",", error=false]:           // and split by commas
item ==> $sentence_id            // Sentence Identifier
switch[$sentence_id]:
"$GPGGA":
item ==> skip                               // UTC Time
item ==> nmea_latitude ==> $lat             // Latitude
item ==> map["N"=1,"S"=-1] ==> $lat_sign    // North/South
item ==> nmea_longitude ==> $lon            // Longitude
item ==> map["E"=1,"W"=-1] ==> $lon_sign    // East/West
$lat * $lat_sign ==> #position.latitude
$lon * $lon_sign ==> #position.longitude
item ==> skip                               // Fix Quality
item ==> %uint8 ==> #position.satellites    // Satellites
item ==> %double ==> #position.hdop         // HDOP
item ==> %double ==> #position.altitude     // Altitude
item ==> <"M">

What does the plugin code do?

The composition of GPGGA sentences is described in the NMEA-0183 standard. The sentence consists of a series of comma-separated fields with position data. What we need to do is to save the values of each field into corresponding parameters. 

The first line of code takes payload.text parameter if it’s present in the initial message [operator optional] and puts its value into the input buffer for parsing [operator input]. The next actions are performed with the GPGGA sentence in the input buffer.

Operator split breaks up the sentence into the fields by “,”. In the code below each field is referred to as an item. Attribute error=false instructs the operator to skip unparsed items silently, without raising a parsing error.

The following code takes the fields (items) one by one and does particular actions according to the meaning of the field.

The first item is a "Sentence Identifier". NMEA standard describes various sentences, and parsing depends on the Sentence Identifier. That’s why we use operator switch here: if the Sentence Identifier is $GPGGA, then the subsequent items are parsed with the block of code inside the "$GPGGA" section. To add parsing of other NMEA sentences you just need to add corresponding sections for the switch operator.

The following items are parsed one by one according to the purpose of each field:

  • item “Time” is skipped

  • item “Latitude” is converted from NMEA format to degrees [operator nmea_latitude] and saved into variable $lat for later use

  • item “North/South” is converted to latitude sign [operator map] and stored into $lat_sign variable

  • items “Longitude” and “East/West” are processed similarly

  • once all position components are collected, it’s possible to register position parameters: latitude value in degrees ($lat variable) is multiplied by the latitude sign ($lat_sign variable) and stored as the position.latitude parameter in the resulting message. The same happens with longitude.

  • next item “Fix Quality” is skipped

  • item “Satellites” is converted to integer and saved into the position.satellites parameter

  • items “HDOP” and “Altitude” and converted to double and saved into corresponding parameters

  • the next item checks the Altitude units of measurement — it is expected to be always “M” (meters)

  • and all the remaining items are just skipped

Assign the plugin to device(s) (Plugins tab on the Device card) and the code will be applied to every message of the assigned devices. The resulting message will look like this:

{
  "ident": "352625333222111",
  "payload.text": "$GPGGA,063818.80,4904.1868922,N,2837.9695321,E,4,18,0.7,273.610,M,31.5,M,2.0,0008*73",
  "position.altitude": 273.61,
  "position.hdop": 0.7,
  "position.latitude": 49.069782,
  "position.longitude": 28.632826,
  "position.satellites": 18,
  "server.timestamp": 1650636570.426424,
  "timestamp": 1650636570.426424
}

Bonus: how to parse time from GPGGA sentence?

UTC time field of GPGGA sentence contains only time component in format hhmmss.sss, without a date. That’s why parsing UTC time into position.timestamp parameter will be a little bit tricky but still possible.

We will take the number of days from the timestamp parameter of the initial message:

.timestamp ==> %uint32 ==> this / 86400 ==> $days 

The number of hours, minutes and seconds — from "UTC Time" field of the sentence:

	item:                            // UTC Time: hhmmss.sss
[size=2] ==> %text ==> %uint8 ==> $hours
[size=2] ==> %text ==> %uint8 ==> $minutes
%text ==> %double ==> $seconds

And finally put it all together and store into resulting message as position.timestamp parameter:

	86400 * $days + 3600 * $hours + 60 * $minutes + $seconds ==> #position.timestamp

The code with modifications applied will look like this:

.timestamp ==> %uint32 ==> this / 86400 ==> $days
optional .payload.text ==> input:    // Take payload.text parameter if present
split[",", error=false]:         // and split by commas
item ==> $sentence_id        // Sentence Identifier
switch[$sentence_id]:
"$GPGGA":
item:                // UTC Time: hhmmss.sss
[size=2] ==> %text ==> %uint8 ==> $hours
[size=2] ==> %text ==> %uint8 ==> $minutes
%text ==> %double ==> $seconds
86400 * $days + 3600 * $hours + 60 * $minutes + $seconds ==> #position.timestamp
item ==> nmea_latitude ==> $lat         // Latitude
item ==> map["N"=1, "S"=-1] ==> $lat_sign    // North/South
item ==> nmea_longitude ==> $lon         // Longitude
item ==> map["E"=1,"W"=-1] ==> $lon_sign     // East/West
$lat * $lat_sign ==> #position.latitude
$lon * $lon_sign ==> #position.longitude
item ==> skip                 // Fix Quality
item ==> %uint8 ==> #position.satellites     // Satellites
item ==> %double ==> #position.hdop         // HDOP
item ==> %double ==> #position.altitude      // Altitude
item ==> <"M">

The resulting message will have a position.timestamp parameter that corresponds to the time from GPGGA sentence within the current day:

{
  "ident": "352625333222111",
  "payload.text": "$GPGGA,063818.80,4904.1868922,N,2837.9695321,E,4,18,0.7,273.610,M,31.5,M,2.0,0008*73",
  "position.altitude": 273.61,
  "position.hdop": 0.7,
  "position.latitude": 49.069782,
  "position.longitude": 28.632826,
  "position.satellites": 18,
  "position.timestamp": 1650609498.80,
  "server.timestamp": 1650636570.426424,
  "timestamp": 1650636570.426424
}

See also
Using plugins to resolve position coordinates into address using Here reverse geocoding API and add it into the device messages.