Decoding PPS data

Updated: October 26, 2022

For the purposes of the decoder functions of this API, an object is a container that holds zero or more attributes of any type, each of which can be referenced by name. An array is a container that holds zero or more values that are referenced by position. Unlike arrays in C, an array here can use a different data type for each element. When the JSON encoding is used, individual PPS attributes can themselves be objects or arrays, which can be nested.

The PPS decoder functions allow both PPS- and JSON-encoded data to be parsed. Once a data string has been parsed, the pps_decoder_t structure maintains a representation of this data as a tree. Immediately upon parsing the data, you are positioned at the root of the tree. Using the PPS decoder functions you can:

The decoder is always positioned within some object or array, either at one of its elements or off the end at a special element of type PPS_TYPE_NONE. Many of the decoder functions take a name argument that indicates the PPS attribute or object property name to look for. If the name is NULL, the current element is used. When extracting data from arrays, the name must always be NULL because array elements don't have names. When you successfully extract a data element, for example by calling pps_decoder_get_int() or after you call pps_decoder_pop(), the position automatically moves to the next element. So, for example, you could extract all elements of an array of numbers using code like this:

while ( pps_decoder_type(decoder, NULL) != PPS_TYPE_NONE ) {
   pps_decoder_get_double(decoder, NULL, &values[i++]);
}

Let's look at a complete example, using the following PPS data:

@gps
city::Ottawa
speed:n:65.412
position:json:{"latitude":45.6512,"longitude":-75.9041}

To extract this data, you might use code like this:

const char *city;
double lat, lon, speed;
pps_decoder_t decoder;
            
pps_decoder_initialize(&decoder, NULL);
pps_decoder_parse_pps_str(&decoder, buffer);
pps_decoder_push(&decoder, NULL);
pps_decoder_get_double(&decoder, "speed", &speed);
pps_decoder_get_string(&decoder, "city", &city);
            
pps_decoder_push(&decoder, "position");
pps_decoder_get_double(&decoder, "latitude", &lat);
pps_decoder_get_double(&decoder, "longitude", &lon);
pps_decoder_pop(&decoder);
            
pps_decoder_pop(&decoder);
            
if ( pps_decoder_status(&decoder, false) == PPS_DECODER_OK ) {
    . . .
}
pps_decoder_cleanup(&decoder);

Let's take a look at each of these function calls:

pps_decoder_initialize()
Initializes the pps_decoder_t structure from an unknown state. You can skip the subsequent call to pps_decoder_parse_pps_str() and just pass the data to be parsed in this call, but typically you'll be parsing multiple buffers of PPS data, so you'll need both steps.
pps_decoder_parse_pps_str()
Parses the PPS data into the decoder's internal data structures. This process modifies the buffer that's passed in, and the internal structure uses pointers into this buffer, so the contents of the buffer must remain valid and unchanged until the parsing results are no longer needed.
pps_decoder_push()
Causes the decoder to descend into the gps object to allow the values of the attributes to be extracted. In this particularly simple situation, this step might seem unnecessary. But if, for example, the data came from reading the .all special object, which returns multiple objects, it's important to indicate which particular object you're extracting data from.
pps_decoder_get_double(), pps_decoder_get_string()
Extract data from the PPS object, in this case the speed and city attributes. Note that the order of these calls doesn't have to match the order of the attributes in the original PPS data.
pps_decoder_push(), pps_decoder_get_double(), pps_decoder_pop()
In this case, the latitude and longitude are both contained in a single JSON-encoded PPS attribute. Therefore, before extracting latitude and longitude, we have to descend into the object they're contained in by calling pps_decoder_push(). Having pushed into the object, we can extract the two values just as if they had been PPS attributes. After the position data has been extracted, we then call pps_decoder_pop() to go back a level in case we need to extract more PPS attributes.
pps_decoder_pop()
Performs the reverse action of the initial pps_decoder_push() and pops out of an object (in this case the gps object) to its parent. Calling pps_decoder_pop() in this case isn't really required, but if we had read .all and there were multiple objects contained in the data, we would need this call to advance to the next object.
pps_decoder_status()
Returns the status of the decoder object. If this is PPS_DECODER_OK, then all parsing and data extraction occurred successfully. The preceding calls have the effect that if they fail, they will update the decoder status, so that you can perform a series of operations and check the status at the end, rather than checking at each stage. In this case, the second parameter to pps_decoder_status() is false, meaning that the status shouldn't be reset to PPS_DECODER_OK when the function returns. Parsing more data using pps_decoder_parse_pps_str() also has the effect of resetting the status (unless it fails).
pps_decoder_cleanup()
Once you've finished with a decoder object, pps_decoder_cleanup() frees any memory that was allocated. You need to call this only when you no longer require the decoder; if you have more data to parse, you can simply call pps_decoder_parse_pps_str() again.