Sensors / GNSS

Examples

Extracting latitude, longitude, and timestamp from a real $GPGGA sentence read over UART.

A sample sentence

$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47

Fields, in order: UTC time (123519 = 12:35:19), latitude (4807.038,N), longitude (01131.000,E), fix quality (1), satellites used (08), HDOP (0.9), altitude (545.4,M), geoid separation, then an empty DGPS field, and finally the checksum after *.

Checksum validation

The checksum is the XOR of every byte between $ and *, compared against the two hex digits that follow.

bool nmea_checksum_ok(const char *sentence) {
    if (sentence[0] != '$') return false;

    const char *star = strchr(sentence, '*');
    if (!star) return false;

    uint8_t checksum = 0;
    for (const char *p = sentence + 1; p < star; p++)
        checksum ^= (uint8_t)*p;

    uint8_t expected = (uint8_t)strtol(star + 1, NULL, 16);
    return checksum == expected;
}

Parsing latitude/longitude and time

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    int hour, minute, second;
    double latitude;   // decimal degrees, +north / -south
    double longitude;  // decimal degrees, +east / -west
    int fix_quality;
} gnss_fix_t;

static double dm_to_decimal(const char *dm, char hemisphere) {
    double raw = atof(dm);              // e.g. 4807.038
    int degrees = (int)(raw / 100);     // 48
    double minutes = raw - (degrees * 100); // 07.038
    double decimal = degrees + minutes / 60.0;
    if (hemisphere == 'S' || hemisphere == 'W')
        decimal = -decimal;
    return decimal;
}

bool parse_gpgga(const char *sentence, gnss_fix_t *out) {
    if (!nmea_checksum_ok(sentence)) return false;
    if (strncmp(sentence, "$GPGGA", 6) != 0 &&
        strncmp(sentence, "$GNGGA", 6) != 0) return false;

    char time_field[16], lat_field[16], lat_hemi[2];
    char lon_field[16], lon_hemi[2];
    int fix_quality;

    // field order: time, lat, N/S, lon, E/W, fix quality, ...
    int n = sscanf(sentence,
        "$%*3s,%15[^,],%15[^,],%1[^,],%15[^,],%1[^,],%d",
        time_field, lat_field, lat_hemi, lon_field, lon_hemi, &fix_quality);
    if (n < 6) return false;

    out->fix_quality = fix_quality;
    if (fix_quality == 0) return false; // fields below aren't meaningful yet

    out->hour   = (time_field[0] - '0') * 10 + (time_field[1] - '0');
    out->minute = (time_field[2] - '0') * 10 + (time_field[3] - '0');
    out->second = (time_field[4] - '0') * 10 + (time_field[5] - '0');

    out->latitude  = dm_to_decimal(lat_field, lat_hemi[0]);
    out->longitude = dm_to_decimal(lon_field, lon_hemi[0]);
    return true;
}
What I learned

Checking fix_quality == 0 and bailing out early matters more than it looks like it should — a receiver with no fix yet still happily sends well-formed $GPGGA sentences with empty or stale latitude/longitude fields, and a parser that doesn't check fix quality first will silently hand back garbage coordinates that look plausible.