Kosma Moczek 11 жил өмнө
commit
56177f0e1c
7 өөрчлөгдсөн 1090 нэмэгдсэн , 0 устгасан
  1. 5 0
      .gitignore
  2. 23 0
      Makefile
  3. 78 0
      README.md
  4. 46 0
      example.c
  5. 353 0
      minmea.c
  6. 138 0
      minmea.h
  7. 447 0
      tests.c

+ 5 - 0
.gitignore

@@ -0,0 +1,5 @@
+*.o
+*~
+minmea
+tests
+example

+ 23 - 0
Makefile

@@ -0,0 +1,23 @@
+CFLAGS = -g -Wall -Wextra -Werror
+LDFLAGS = -lcheck
+
+all: scan-build test example
+	@echo "+++ All good."""
+
+test: tests
+	@echo "+++ Running Check test suite..."
+	./tests
+
+scan-build: clean
+	@echo "+++ Running Clang Static Analyzer..."
+	scan-build $(MAKE) tests
+
+clean:
+	$(RM) tests *.o
+
+tests: tests.o minmea.o
+example: example.o minmea.o
+tests.o: tests.c minmea.h
+minmea.o: minmea.c minmea.h
+
+.PHONY: all test scan-build clean

+ 78 - 0
README.md

@@ -0,0 +1,78 @@
+# minmea, the lightweight GPS NMEA 0183 parser library
+
+Minmea is a minimalistic GPS parser library intended for resource-constrained
+platforms, especially microcontrollers and other embedded systems.
+
+## Features
+
+* Written in ISO C99.
+* No dynamic memory allocation.
+* No floating point usage in the core library.
+* Supports both fixed and floating point values.
+* One source file and one header - can't get any simpler.
+* Easily extendable to support new sentences.
+* Complete with a test suite and static analysis.
+
+## Supported sentences
+
+* ``$GPRMC``
+* ``$GPGGA``
+
+Adding support for more sentences is trivial; see ``minmea.c`` source.
+
+## Example
+
+```c
+    char line[MINMEA_MAX_LENGTH];
+    while (fgets(line, sizeof(line), stdin) != NULL) {
+        printf("%s", line);
+        switch (minmea_type(line)) {
+            case MINMEA_GPRMC: {
+                struct minmea_gprmc frame;
+                if (minmea_parse_gprmc(&frame, line)) {
+                    printf("+++ raw coordinates and speed: (%d/%d,%d/%d) %d/%d\n",
+                            frame.latitude, frame.latitude_scale,
+                            frame.longitude, frame.longitude_scale,
+                            frame.speed, frame.speed_scale);
+                    printf("+++ fixed-point coordinates and speed scaled to three decimal places: (%d,%d) %d\n",
+                            minmea_rescale(frame.latitude, frame.latitude_scale, 1000),
+                            minmea_rescale(frame.longitude, frame.longitude_scale, 1000),
+                            minmea_rescale(frame.speed, frame.speed_scale, 1000));
+                    printf("+++ floating point degree coordinates and speed: (%f,%f) %f\n",
+                            minmea_coord(frame.latitude, frame.latitude_scale),
+                            minmea_coord(frame.longitude, frame.longitude_scale),
+                            minmea_float(frame.speed, frame.speed_scale));
+                }
+            } break;
+
+            case MINMEA_GPGGA: {
+                struct minmea_gpgga frame;
+                if (minmea_parse_gpgga(&frame, line)) {
+                    printf("$GPGGA: fix quality: %d\n", frame.fix_quality);
+                }
+            } break;
+
+            default: {
+            } break;
+        }
+    }
+```
+
+## Integration with your project
+
+Simply add ``minmea.[ch]`` to your project, ``#include "minmea.h"`` and you're
+good to go.
+
+## Running unit tests
+
+Building and running the tests requires the following:
+
+* Check Framework (http://check.sourceforge.net/).
+* Clang Static Analyzer (http://clang-analyzer.llvm.org/).
+
+If you have both in your ``$PATH``, running the tests should be as simple as
+typing ``make``.
+
+## Bugs
+
+There are plenty. Report them on GitHub, or - even better - open a pull request.

+ 46 - 0
example.c

@@ -0,0 +1,46 @@
+#include <stdio.h>
+#include <string.h>
+#include <ctype.h>
+
+#include "minmea.h"
+
+int main()
+{
+    char line[MINMEA_MAX_LENGTH];
+    while (fgets(line, sizeof(line), stdin) != NULL) {
+        printf("%s", line);
+        switch (minmea_type(line)) {
+            case MINMEA_GPRMC: {
+                struct minmea_gprmc frame;
+                if (minmea_parse_gprmc(&frame, line)) {
+                    printf("+++ raw coordinates and speed: (%d/%d,%d/%d) %d/%d\n",
+                            frame.latitude, frame.latitude_scale,
+                            frame.longitude, frame.longitude_scale,
+                            frame.speed, frame.speed_scale);
+                    printf("+++ fixed-point coordinates and speed scaled to three decimal places: (%d,%d) %d\n",
+                            minmea_rescale(frame.latitude, frame.latitude_scale, 1000),
+                            minmea_rescale(frame.longitude, frame.longitude_scale, 1000),
+                            minmea_rescale(frame.speed, frame.speed_scale, 1000));
+                    printf("+++ floating point degree coordinates and speed: (%f,%f) %f\n",
+                            minmea_coord(frame.latitude, frame.latitude_scale),
+                            minmea_coord(frame.longitude, frame.longitude_scale),
+                            minmea_float(frame.speed, frame.speed_scale));
+                }
+            } break;
+
+            case MINMEA_GPGGA: {
+                struct minmea_gpgga frame;
+                if (minmea_parse_gpgga(&frame, line)) {
+                    printf("$GPGGA: fix quality: %d\n", frame.fix_quality);
+                }
+            } break;
+
+            default: {
+            } break;
+        }
+    }
+
+    return 0;
+}
+
+/* vim: ts=4 sw=4 et: */

+ 353 - 0
minmea.c

@@ -0,0 +1,353 @@
+#include "minmea.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <stdarg.h>
+
+#define boolstr(s) ((s) ? "true" : "false")
+
+static int hex2int(char c)
+{
+    if (c >= '0' && c <= '9')
+        return c - '0';
+    if (c >= 'A' && c <= 'F')
+        return c - 'A';
+    if (c >= 'a' && c <= 'f')
+        return c - 'a';
+    return -1;
+}
+
+bool minmea_check(const char *sentence)
+{
+    uint8_t checksum = 0x00;
+
+    // Sequence length is limited.
+    if (strlen(sentence) > MINMEA_MAX_LENGTH + 3)
+        return false;
+
+    // A valid sentence starts with "$".
+    if (*sentence++ != '$')
+        return false;
+
+    // The optional checksum is an XOR of all bytes between "$" and "*".
+    while (*sentence && *sentence != '*' && isprint(*sentence))
+        checksum ^= *sentence++;
+
+    if (*sentence == '*') {
+        // Extract checksum.
+        sentence++;
+        int upper = hex2int(*sentence++);
+        if (upper == -1)
+            return false;
+        int lower = hex2int(*sentence++);
+        if (lower == -1)
+            return false;
+        int expected = upper << 4 | lower;
+
+        // Check for checksum mismatch.
+        if (checksum != expected)
+            return false;
+    }
+
+    // The only stuff allowed at this point is a newline.
+    if (*sentence && strcmp(sentence, "\n") && strcmp(sentence, "\r\n"))
+        return false;
+
+    return true;
+}
+
+static inline bool minmea_isfield(char c) {
+    return isprint(c) && c != ',' && c != '*';
+}
+
+bool minmea_scan(const char *sentence, const char *format, ...)
+{
+    bool result = false;
+    va_list ap;
+    va_start(ap, format);
+
+    const char *field = sentence;
+#define next_field() \
+    do { \
+        while (minmea_isfield(*sentence++)) {} \
+        field = sentence; \
+    } while (0)
+
+    while (*format) {
+        char type = *format++;
+
+        switch (type) {
+            case 'c': { // Single character field (char).
+                char value = '\0';
+
+                if (minmea_isfield(*field))
+                    value = *field;
+                else
+                    value = '\0';
+
+                *va_arg(ap, char *) = value;
+            } break;
+
+            case 'd': { // Single character direction field (int).
+                int value = 0;
+
+                if (minmea_isfield(*field)) {
+                    switch (*field) {
+                        case 'N':
+                        case 'E':
+                            value = 1;
+                            break;
+                        case 'S':
+                        case 'W':
+                            value = -1;
+                            break;
+                        default:
+                            goto end;
+                    }
+                }
+
+                *va_arg(ap, int *) = value;
+            } break;
+
+            case 'f': { // Fractional value with scale (int, int).
+                int sign = 0;
+                int value = -1;
+                int scale = 0;
+
+                while (minmea_isfield(*field)) {
+                    if (*field == '+' && !sign && value == -1) {
+                        sign = 1;
+                    } else if (*field == '-' && !sign && value == -1) {
+                        sign = -1;
+                    } else if (isdigit(*field)) {
+                        if (value == -1)
+                            value = 0;
+                        value = (10 * value) + (*field - '0');
+                        if (scale)
+                            scale *= 10;
+                    } else if (*field == '.' && scale == 0) {
+                        scale = 1;
+                    } else {
+                        goto end;
+                    }
+                    field++;
+                }
+
+                if ((sign || scale) && value == -1)
+                    goto end;
+
+                if (value == -1) {
+                    value = 0;
+                    scale = 0;
+                }
+                if (sign)
+                    value *= sign;
+
+                *va_arg(ap, int *) = value;
+                *va_arg(ap, int *) = scale;
+            } break;
+
+            case 'i': { // Integer value, default 0 (int).
+                int value;
+
+                char *endptr;
+                value = strtol(field, &endptr, 10);
+                if (minmea_isfield(*endptr))
+                    goto end;
+
+                *va_arg(ap, int *) = value;
+            } break;
+
+            case 's': { // String value (char *).
+                char *buf = va_arg(ap, char *);
+
+                while (minmea_isfield(*field))
+                    *buf++ = *field++;
+                *buf = '\0';
+            } break;
+
+            case 't': { // NMEA talker+sequence identifier (char *).
+                if (field[0] != '$')
+                    goto end;
+                for (int i=0; i<5; i++)
+                    if (!minmea_isfield(field[1+i]))
+                        goto end;
+
+                char *buf = va_arg(ap, char *);
+                memcpy(buf, field+1, 5);
+                buf[5] = '\0';
+            } break;
+
+            case 'D': { // Date (int, int, int), -1 if empty.
+                struct minmea_date *date = va_arg(ap, struct minmea_date *);
+
+                int d = -1, m = -1, y = -1;
+                // Always six digits.
+                for (int i=0; i<6; i++)
+                    if (!isdigit(field[i]))
+                        goto end_D;
+
+                d = strtol((char[]) {field[0], field[1], '\0'}, NULL, 10);
+                m = strtol((char[]) {field[2], field[3], '\0'}, NULL, 10);
+                y = strtol((char[]) {field[4], field[5], '\0'}, NULL, 10);
+
+            end_D:
+                date->day = d;
+                date->month = m;
+                date->year = y;
+            } break;
+
+            case 'T': { // Time (int, int, int, int), -1 if empty.
+                struct minmea_time *time = va_arg(ap, struct minmea_time *);
+
+                int h = -1, i = -1, s = -1, u = -1;
+                // Minimum required: integer time.
+                for (int i=0; i<6; i++)
+                    if (!isdigit(field[i]))
+                        goto end_T;
+
+                h = strtol((char[]) {field[0], field[1], '\0'}, NULL, 10);
+                i = strtol((char[]) {field[2], field[3], '\0'}, NULL, 10);
+                s = strtol((char[]) {field[4], field[5], '\0'}, NULL, 10);
+                field += 6;
+
+                // Extra: fractional time. Saved as microseconds.
+                if (*field++ == '.') {
+                    int value = 0;
+                    int scale = 1000000;
+                    while (isdigit(*field) && scale > 1) {
+                        value = (value * 10) + (*field++ - '0');
+                        scale /= 10;
+                    }
+                    u = value * scale;
+                } else {
+                    u = 0;
+                }
+
+            end_T:
+                time->hours = h;
+                time->minutes = i;
+                time->seconds = s;
+                time->microseconds = u;
+            } break;
+
+            case '_': { // Ignore the field.
+            } break;
+
+            default: { // Unknown.
+                goto end;
+            } break;
+        }
+
+        // Advance to next field.
+        next_field();
+    }
+
+    result = true;
+
+end:
+    va_end(ap);
+    return result;
+}
+
+enum minmea_type minmea_type(const char *sentence)
+{
+    if (!minmea_check(sentence))
+        return MINMEA_INVALID;
+
+    char type[6];
+    if (!minmea_scan(sentence, "t", type))
+        return MINMEA_INVALID;
+
+    if (!strcmp(type, "GPRMC"))
+        return MINMEA_GPRMC;
+    if (!strcmp(type, "GPGGA"))
+        return MINMEA_GPGGA;
+
+    return MINMEA_UNKNOWN;
+}
+
+bool minmea_parse_gprmc(struct minmea_gprmc *frame, const char *sentence)
+{
+    // $GPRMC,081836,A,3751.65,S,14507.36,E,000.0,360.0,130998,011.3,E*62
+    char type[6];
+    char validity;
+    int latitude_direction;
+    int longitude_direction;
+    int variation_direction;
+    if (!minmea_scan(sentence, "tTcfdfdffDfd",
+            type,
+            &frame->time,
+            &validity,
+            &frame->latitude, &frame->latitude_scale, &latitude_direction,
+            &frame->longitude, &frame->longitude_scale, &longitude_direction,
+            &frame->speed, &frame->speed_scale,
+            &frame->course, &frame->course_scale,
+            &frame->date,
+            &frame->variation, &frame->variation_scale, &variation_direction))
+        return false;
+    if (strcmp(type, "GPRMC"))
+        return false;
+
+    frame->valid = (validity == 'A');
+    frame->latitude *= latitude_direction;
+    frame->longitude *= longitude_direction;
+    frame->variation *= variation_direction;
+
+    return true;
+}
+
+bool minmea_parse_gpgga(struct minmea_gpgga *frame, const char *sentence)
+{
+    // $GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47
+    char type[6];
+    int latitude_direction;
+    int longitude_direction;
+
+    if (!minmea_scan(sentence, "tTfdfdiiffcfci_",
+            type,
+            &frame->time,
+            &frame->latitude, &frame->latitude_scale, &latitude_direction,
+            &frame->longitude, &frame->longitude_scale, &longitude_direction,
+            &frame->fix_quality,
+            &frame->satellites_tracked,
+            &frame->hdop, &frame->hdop_scale,
+            &frame->altitude, &frame->altitude_scale, &frame->altitude_units,
+            &frame->height, &frame->height_scale, &frame->height_units,
+            &frame->dgps_age))
+        return false;
+    if (strcmp(type, "GPGGA"))
+        return false;
+
+    frame->latitude *= latitude_direction;
+    frame->longitude *= longitude_direction;
+
+    return true;
+}
+
+int minmea_gettimeofday(struct timeval *tv, const struct minmea_date *date, const struct minmea_time *time)
+{
+    if (date->year == -1 || time->hours == -1)
+        return -1;
+
+    struct tm tm;
+    tm.tm_year = 2000 + date->year - 1900;
+    tm.tm_mon = date->month - 1;
+    tm.tm_mday = date->day;
+    tm.tm_hour = time->hours;
+    tm.tm_min = time->minutes;
+    tm.tm_sec = time->seconds;
+    tm.tm_isdst = 0;
+    time_t timestamp = timegm(&tm);
+
+    if (timestamp != -1) {
+        tv->tv_sec = timestamp;
+        tv->tv_usec = time->microseconds;
+        return 0;
+    } else {
+        return -1;
+    }
+}
+
+/* vim: set ts=4 sw=4 et: */

+ 138 - 0
minmea.h

@@ -0,0 +1,138 @@
+#ifndef MINMEA_H
+#define MINMEA_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdio.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <errno.h>
+#include <time.h>
+#include <math.h>
+#include <sys/time.h>
+
+#define MINMEA_MAX_LENGTH 80
+
+enum minmea_type {
+    MINMEA_INVALID = -1,
+    MINMEA_UNKNOWN = 0,
+    MINMEA_GPRMC,
+    MINMEA_GPGGA,
+};
+
+struct minmea_date {
+    int day;
+    int month;
+    int year;
+};
+
+struct minmea_time {
+    int hours;
+    int minutes;
+    int seconds;
+    int microseconds;
+};
+
+struct minmea_gprmc {
+    struct minmea_time time;
+    bool valid;
+    int latitude, latitude_scale;
+    int longitude, longitude_scale;
+    int speed, speed_scale;
+    int course, course_scale;
+    struct minmea_date date;
+    int variation, variation_scale;
+};
+
+struct minmea_gpgga {
+    struct minmea_time time;
+    int latitude, latitude_scale;
+    int longitude, longitude_scale;
+    int fix_quality;
+    int satellites_tracked;
+    int hdop, hdop_scale;
+    int altitude, altitude_scale; char altitude_units;
+    int height, height_scale; char height_units;
+    int dgps_age;
+};
+
+/**
+ * Check sequence validity and checksum. Returns true for valid sequences.
+ */
+bool minmea_check(const char *sentence);
+
+/**
+ * Determine sequence type.
+ */
+enum minmea_type minmea_type(const char *sequence);
+
+/**
+ * Scanf-like processor for NMEA sentences. Supports the following formats:
+ * c - single character (char *)
+ * d - direction, returned as 1/-1, default 0 (int *)
+ * f - fractional, returned as value + scale (int *, int *)
+ * i - decimal, default zero (int *)
+ * s - string (char *)
+ * t - talker identifier and type (char *)
+ * T - date/time stamp (int *, int *, int *)
+ * Returns true on success. See library source code for details.
+ */
+bool minmea_scan(const char *sentence, const char *format, ...);
+
+/*
+ * Parse a specific type of frame. Return true on success.
+ */
+bool minmea_parse_gprmc(struct minmea_gprmc *frame, const char *sentence);
+bool minmea_parse_gpgga(struct minmea_gpgga *frame, const char *sentence);
+
+/**
+ * Convert GPS UTC date/time representation to a UNIX timestamp.
+ */
+int minmea_gettimeofday(struct timeval *tv, const struct minmea_date *date, const struct minmea_time *time);
+
+/**
+ * Rescale signed integer value to a different full-scale value, with rounding.
+ * The "from" value in the numerator cancels out with the denominator, leaving
+ * just 1/2 - which makes integer truncation act as rounding. Returns zero for
+ * invalid values.
+ */
+static inline int minmea_rescale(int value, int from, int to)
+{
+    if (from == 0)
+        return 0;
+    return (value * to + from / 2) / from;
+}
+
+/**
+ * Convert a fixed-point value to a floating-point value.
+ * Returns NaN for "unknown" values.
+ */
+static inline float minmea_float(int value, int scale)
+{
+    if (scale == 0)
+        return NAN;
+    return (float) value / (float) scale;
+}
+
+/**
+ * Convert a raw coordinate to a floating point DD.DDD... value.
+ * Returns NaN for "unknown" values.
+ */
+static inline float minmea_coord(int value, int scale)
+{
+    if (scale == 0)
+        return NAN;
+    int degrees = value / (scale * 100);
+    int minutes = value % (scale * 100);
+    return (float) degrees + (float) minutes / (60 * scale);
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* MINMEA_H */
+
+/* vim: set ts=4 sw=4 et: */

+ 447 - 0
tests.c

@@ -0,0 +1,447 @@
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+#include <check.h>
+
+#include "minmea.h"
+
+static const char *valid_sequences[] = {
+    "$GPTXT,xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+    "$GPTXT,01,01,02,ANTSTATUS=INIT*25",
+    "$GPRMC,,V,,,,,,,,,,N*53",
+    "$GPVTG,,,,,,,,,N*30",
+    "$GPGGA,,,,,,0,00,99.99,,,,,,*48",
+    "$GPGSA,A,1,,,,,,,,,,,,,99.99,99.99,99.99*30",
+    "$GPGLL,,,,,,V,N*64",
+    NULL,
+};
+
+static const char *invalid_sequences[] = {
+    "$GPTXT,xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+        "xxxxxxxxxxxxxxxxxxxxxxxxxxx",
+    "$GPTXT,01,01,02,ANTSTATUS=INIT*26",
+    "$GPRMC,,V,,,,,,,,,,N*532",
+    "$GPVTG,,,,\xff,,,,,N*30",
+    "$$GPGGA,,,,,,0,00,99.99,,,,,,*48",
+    "GPGSA,A,1,,,,,,,,,,,,,99.99,99.99,99.99*30",
+    "gps: $GPGLL,,,,,,V,N",
+    NULL,
+};
+
+START_TEST(test_minmea_check)
+{
+    for (const char **sequence=valid_sequences; *sequence; sequence++)
+        ck_assert_msg(minmea_check(*sequence) == true, *sequence);
+
+    for (const char **sequence=invalid_sequences; *sequence; sequence++)
+        ck_assert_msg(minmea_check(*sequence) == false, *sequence);
+}
+END_TEST
+
+START_TEST(test_minmea_scan_c)
+{
+    char ch;
+
+    ck_assert(minmea_scan("A,123.45", "c", &ch) == true);
+    ck_assert_int_eq(ch, 'A');
+
+    ck_assert(minmea_scan("WUT,123.45", "c", &ch) == true);
+    ck_assert_int_eq(ch, 'W');
+
+    ck_assert(minmea_scan(",123.45", "c", &ch) == true);
+    ck_assert_int_eq(ch, '\0');
+}
+END_TEST
+
+START_TEST(test_minmea_scan_d)
+{
+    int direction;
+
+    ck_assert(minmea_scan("K", "d", &direction) == false);
+
+    ck_assert(minmea_scan("", "d", &direction) == true);
+    ck_assert(minmea_scan(",foo", "d", &direction) == true);
+    ck_assert_int_eq(direction, 0);
+    ck_assert(minmea_scan("N", "d", &direction) == true);
+    ck_assert_int_eq(direction, 1);
+    ck_assert(minmea_scan("S,foo", "d", &direction) == true);
+    ck_assert_int_eq(direction, -1);
+    ck_assert(minmea_scan("W", "d", &direction) == true);
+    ck_assert_int_eq(direction, -1);
+    ck_assert(minmea_scan("E,foo", "d", &direction) == true);
+    ck_assert_int_eq(direction, 1);
+}
+END_TEST
+
+START_TEST(test_minmea_scan_f)
+{
+    int value, scale;
+
+    ck_assert(minmea_scan("-", "f", &value, &scale) == false);
+    ck_assert(minmea_scan("10-", "f", &value, &scale) == false);
+    ck_assert(minmea_scan("+-10", "f", &value, &scale) == false);
+    ck_assert(minmea_scan("12..45", "f", &value, &scale) == false);
+    ck_assert(minmea_scan("blah", "f", &value, &scale) == false);
+    ck_assert(minmea_scan("12.3.4", "f", &value, &scale) == false);
+
+    ck_assert(minmea_scan(",", "f", &value, &scale) == true);
+    ck_assert_int_eq(scale, 0);
+    ck_assert(minmea_scan("", "f", &value, &scale) == true);
+    ck_assert_int_eq(scale, 0);
+
+    ck_assert(minmea_scan("15.345", "f", &value, &scale) == true);
+    ck_assert_int_eq(value, 15345);
+    ck_assert_int_eq(scale, 1000);
+
+    ck_assert(minmea_scan("-1.23,V", "f", &value, &scale) == true);
+    ck_assert_int_eq(value, -123);
+    ck_assert_int_eq(scale, 100);
+}
+END_TEST
+
+START_TEST(test_minmea_scan_s)
+{
+    char value[MINMEA_MAX_LENGTH];
+
+    ck_assert(minmea_scan("foo,bar,baz", "s", value) == true);
+    ck_assert_str_eq(value, "foo");
+    ck_assert(minmea_scan(",bar,baz", "s", value) == true);
+    ck_assert_str_eq(value, "");
+}
+END_TEST
+
+START_TEST(test_minmea_scan_t)
+{
+    char buf[7];
+    buf[sizeof(buf)-1] = 0x42;
+
+    ck_assert(minmea_scan("$GPRM,foo,bar,baz", "t", buf) == false);
+    ck_assert(minmea_scan("GPRMC,foo,bar,baz", "t", buf) == false);
+
+    ck_assert(minmea_scan("$GPRMC,foo,bar,baz", "t", buf) == true);
+    ck_assert_str_eq(buf, "GPRMC");
+
+    ck_assert(buf[sizeof(buf)-1] == 0x42);
+}
+END_TEST
+
+START_TEST(test_minmea_scan_D)
+{
+    struct minmea_date date;
+
+    ck_assert(minmea_scan("$GPXXX,311299", "_D", &date) == true);
+    ck_assert_int_eq(date.day, 31);
+    ck_assert_int_eq(date.month, 12);
+    ck_assert_int_eq(date.year, 99);
+
+    ck_assert(minmea_scan("$GPXXX,,,,,,,,,nope", "_D", &date) == true);
+    ck_assert_int_eq(date.day, -1);
+    ck_assert_int_eq(date.month, -1);
+    ck_assert_int_eq(date.year, -1);
+}
+END_TEST
+
+START_TEST(test_minmea_scan_T)
+{
+    struct minmea_time time;
+
+    ck_assert(minmea_scan("$GPXXX,235960", "_T", &time) == true);
+    ck_assert_int_eq(time.hours, 23);
+    ck_assert_int_eq(time.minutes, 59);
+    ck_assert_int_eq(time.seconds, 60);
+    ck_assert_int_eq(time.microseconds, 0);
+
+    ck_assert(minmea_scan("$GPXXX,213700.001", "_T", &time) == true);
+    ck_assert_int_eq(time.hours, 21);
+    ck_assert_int_eq(time.minutes, 37);
+    ck_assert_int_eq(time.seconds, 0);
+    ck_assert_int_eq(time.microseconds, 1000);
+
+    ck_assert(minmea_scan("$GPXXX,,,,,,,nope", "_T", &time) == true);
+    ck_assert_int_eq(time.hours, -1);
+    ck_assert_int_eq(time.minutes, -1);
+    ck_assert_int_eq(time.seconds, -1);
+    ck_assert_int_eq(time.microseconds, -1);
+}
+END_TEST
+
+START_TEST(test_minmea_scan_complex1)
+{
+    const char *sentence = "$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n";
+    char type[6];
+    struct minmea_time time;
+    int latitude, latitude_scale, latitude_direction;
+    int longitude, longitude_scale, longitude_direction;
+    int fix_quality;
+    int satellites;
+    int hdop, hdop_scale;
+    int altitude, altitude_scale; char altitude_units;
+    int height, height_scale; char height_units;
+    ck_assert(minmea_scan(sentence, "tTfdfdiiffcfc__",
+        type,
+        &time,
+        &latitude, &latitude_scale, &latitude_direction,
+        &longitude, &longitude_scale, &longitude_direction,
+        &fix_quality,
+        &satellites,
+        &hdop, &hdop_scale,
+        &altitude, &altitude_scale, &altitude_units,
+        &height, &height_scale, &height_units) == true);
+    ck_assert_str_eq(type, "GPGGA");
+    ck_assert_int_eq(time.hours, 12);
+    ck_assert_int_eq(time.minutes, 35);
+    ck_assert_int_eq(time.seconds, 19);
+    ck_assert_int_eq(latitude, 4807038);
+    ck_assert_int_eq(latitude_scale, 1000);
+    ck_assert_int_eq(latitude_direction, 1);
+    ck_assert_int_eq(longitude, 1131000);
+    ck_assert_int_eq(longitude_scale, 1000);
+    ck_assert_int_eq(longitude_direction, 1);
+    ck_assert_int_eq(fix_quality, 1);
+    ck_assert_int_eq(satellites, 8);
+    ck_assert_int_eq(hdop, 9);
+    ck_assert_int_eq(hdop_scale, 10);
+    ck_assert_int_eq(altitude, 5454);
+    ck_assert_int_eq(altitude_scale, 10);
+    ck_assert_int_eq(altitude_units, 'M');
+    ck_assert_int_eq(height, 469);
+    ck_assert_int_eq(height_scale, 10);
+    ck_assert_int_eq(height_units, 'M');
+
+}
+END_TEST
+
+START_TEST(test_minmea_scan_complex2)
+{
+    const char *sentence = "$GPBWC,081837,,,,,,T,,M,,N,*13";
+    char type[6];
+    struct minmea_time time;
+    int latitude, latitude_scale, latitude_direction;
+    int longitude, longitude_scale, longitude_direction;
+    int bearing_true, bearing_true_scale; char bearing_true_mark;
+    int bearing_magnetic, bearing_magnetic_scale; char bearing_magnetic_mark;
+    int distance, distance_scale; char distance_units;
+    char name[MINMEA_MAX_LENGTH];
+    ck_assert(minmea_scan(sentence, "tTfdfdfcfcfcs",
+        type,
+        &time,
+        &latitude, &latitude_scale, &latitude_direction,
+        &longitude, &longitude_scale, &longitude_direction,
+        &bearing_true, &bearing_true_scale, &bearing_true_mark,
+        &bearing_magnetic, &bearing_magnetic_scale, &bearing_magnetic_mark,
+        &distance, &distance_scale, &distance_units,
+        name) == true);
+    ck_assert_str_eq(type, "GPBWC");
+    ck_assert_int_eq(time.hours, 8);
+    ck_assert_int_eq(time.minutes, 18);
+    ck_assert_int_eq(time.seconds, 37);
+    ck_assert_int_eq(latitude_scale, 0);
+    ck_assert_int_eq(latitude_direction, 0);
+    ck_assert_int_eq(longitude_scale, 0);
+    ck_assert_int_eq(longitude_direction, 0);
+    ck_assert_int_eq(bearing_true_scale, 0);
+    ck_assert_int_eq(bearing_true_mark, 'T');
+    ck_assert_int_eq(bearing_magnetic_scale, 0);
+    ck_assert_int_eq(bearing_magnetic_mark, 'M');
+    ck_assert_int_eq(distance_scale, 0);
+    ck_assert_int_eq(distance_units, 'N');
+    ck_assert_str_eq(name, "");
+}
+END_TEST
+
+START_TEST(test_minmea_parse_gprmc1)
+{
+    const char *sentence = "$GPRMC,081836.75,A,3751.65,S,14507.36,E,000.0,360.0,130998,011.3,E";
+    struct minmea_gprmc frame = {};
+    struct minmea_gprmc expected = {
+        .time = { 8, 18, 36, 750000 },
+        .valid = true,
+        .latitude = -375165,
+        .latitude_scale = 100,
+        .longitude = 1450736,
+        .longitude_scale = 100,
+        .speed = 0,
+        .speed_scale = 10,
+        .course = 3600,
+        .course_scale = 10,
+        .date = { 13, 9, 98 },
+        .variation = 113,
+        .variation_scale = 10,
+    };
+    ck_assert(minmea_check(sentence) == true);
+    ck_assert(minmea_parse_gprmc(&frame, sentence) == true);
+    ck_assert(!memcmp(&frame, &expected, sizeof(frame)));
+}
+END_TEST
+
+START_TEST(test_minmea_parse_gprmc2)
+{
+    const char *sentence = "$GPRMC,,A,3751.65,N,14507.36,W,,,,,";
+    struct minmea_gprmc frame = {};
+    struct minmea_gprmc expected = {
+        .time = { -1, -1, -1, -1 },
+        .valid = true,
+        .latitude = 375165,
+        .latitude_scale = 100,
+        .longitude = -1450736,
+        .longitude_scale = 100,
+        .date = { -1, -1, -1 },
+    };
+    ck_assert(minmea_check(sentence) == true);
+    ck_assert(minmea_parse_gprmc(&frame, sentence) == true);
+    ck_assert(!memcmp(&frame, &expected, sizeof(frame)));
+}
+END_TEST
+
+START_TEST(test_minmea_parse_gpgga1)
+{
+    const char *sentence = "$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47";
+    struct minmea_gpgga frame = {};
+    struct minmea_gpgga expected = {
+        .time = { 12, 35, 19, 0 },
+        .latitude = 4807038,
+        .latitude_scale = 1000,
+        .longitude = 1131000,
+        .longitude_scale = 1000,
+        .fix_quality = 1,
+        .satellites_tracked = 8,
+        .hdop = 9,
+        .hdop_scale = 10,
+        .altitude = 5454,
+        .altitude_scale = 10,
+        .altitude_units = 'M',
+        .height = 469,
+        .height_scale = 10,
+        .height_units = 'M',
+        .dgps_age = 0,
+    };
+    ck_assert(minmea_check(sentence) == true);
+    ck_assert(minmea_parse_gpgga(&frame, sentence) == true);
+    ck_assert(!memcmp(&frame, &expected, sizeof(frame)));
+}
+END_TEST
+
+START_TEST(test_minmea_usage1)
+{
+    const char *sentences[] = {
+        "$GPRMC,081836,A,3751.65,S,14507.36,E,000.0,360.0,130998,011.3,E*62",
+        "$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47",
+        NULL,
+    };
+
+    for (const char **sentence=sentences; *sentence; sentence++) {
+        switch (minmea_type(*sentence)) {
+            case MINMEA_GPRMC: {
+                struct minmea_gprmc frame;
+                ck_assert(minmea_parse_gprmc(&frame, *sentence) == true);
+            } break;
+            
+            case MINMEA_GPGGA: {
+                struct minmea_gpgga frame;
+                ck_assert(minmea_parse_gpgga(&frame, *sentence) == true);
+            } break;
+            
+            default: {
+            } break;
+        }
+    }
+}
+END_TEST
+
+START_TEST(test_minmea_gettimeofday)
+{
+    struct minmea_date date = { 14, 2, 14 };
+    struct minmea_time time = { 13, 0, 9, 123456 };
+    struct timeval tv;
+    ck_assert(minmea_gettimeofday(&tv, &date, &time) == 0);
+    ck_assert_int_eq(tv.tv_sec, 1392382809);
+    ck_assert_int_eq(tv.tv_usec, 123456);
+
+    date.year = -1;
+    ck_assert(minmea_gettimeofday(&tv, &date, &time) != 0);
+    date.year = 2014;
+
+    time.hours = -1;
+    ck_assert(minmea_gettimeofday(&tv, &date, &time) != 0);
+}
+END_TEST
+
+START_TEST(test_minmea_rescale)
+{
+    ck_assert(minmea_rescale(42, 0, 3) == 0);
+    ck_assert(minmea_rescale(1234, 10, 1) == 123);
+    ck_assert(minmea_rescale(1235, 10, 1) == 124);
+    ck_assert(minmea_rescale(1234, 10, 1000) == 123400);
+}
+END_TEST
+
+START_TEST(test_minmea_float)
+{
+    ck_assert(isnan(minmea_float(42, 0)));
+    ck_assert(minmea_float(7, 1) == 7.0);
+    ck_assert(minmea_float(-200, 100) == -2.0);
+    ck_assert(minmea_float(15, 10) == 1.5);
+}
+END_TEST
+
+START_TEST(test_minmea_coord)
+{
+    ck_assert(isnan(minmea_coord(42, 0)));
+    ck_assert(minmea_coord(4200, 1) == 42.0);
+    ck_assert(minmea_coord(420000, 100) == 42.0);
+    ck_assert(minmea_coord(423000, 100) == 42.5);
+}
+END_TEST
+
+Suite *minmea_suite(void)
+{
+    Suite *s = suite_create ("minmea");
+  
+    TCase *tc_check = tcase_create("minmea_check");
+    tcase_add_test(tc_check, test_minmea_check);
+    suite_add_tcase(s, tc_check);
+
+    TCase *tc_scan = tcase_create("minmea_scan");
+    tcase_add_test(tc_scan, test_minmea_scan_c);
+    tcase_add_test(tc_scan, test_minmea_scan_d);
+    tcase_add_test(tc_scan, test_minmea_scan_f);
+    tcase_add_test(tc_scan, test_minmea_scan_s);
+    tcase_add_test(tc_scan, test_minmea_scan_t);
+    tcase_add_test(tc_scan, test_minmea_scan_D);
+    tcase_add_test(tc_scan, test_minmea_scan_T);
+    tcase_add_test(tc_scan, test_minmea_scan_complex1);
+    tcase_add_test(tc_scan, test_minmea_scan_complex2);
+    suite_add_tcase(s, tc_scan);
+  
+    TCase *tc_parse = tcase_create("minmea_parse");
+    tcase_add_test(tc_parse, test_minmea_parse_gprmc1);
+    tcase_add_test(tc_parse, test_minmea_parse_gprmc2);
+    tcase_add_test(tc_parse, test_minmea_parse_gpgga1);
+    suite_add_tcase(s, tc_parse);
+
+    TCase *tc_usage = tcase_create("minmea_usage");
+    tcase_add_test(tc_usage, test_minmea_usage1);
+    suite_add_tcase(s, tc_usage);
+
+    TCase *tc_utils = tcase_create("minmea_utils");
+    tcase_add_test(tc_utils, test_minmea_gettimeofday);
+    tcase_add_test(tc_utils, test_minmea_rescale);
+    tcase_add_test(tc_utils, test_minmea_float);
+    tcase_add_test(tc_utils, test_minmea_coord);
+    suite_add_tcase(s, tc_utils);
+
+    return s;
+}
+
+int main()
+{
+    int number_failed;
+    Suite *s = minmea_suite();
+    SRunner *sr = srunner_create(s);
+    srunner_run_all(sr, CK_NORMAL);
+    number_failed = srunner_ntests_failed(sr);
+    srunner_free(sr);
+    return (number_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
+}
+
+/* vim: set ts=4 sw=4 et: */