From 56177f0e1c98111d938ec0f2f65494e2e715ccc9 Mon Sep 17 00:00:00 2001 From: Kosma Moczek Date: Fri, 14 Feb 2014 15:22:18 +0100 Subject: [PATCH] initial commit --- .gitignore | 5 + Makefile | 23 +++ README.md | 78 ++++++++++ example.c | 46 ++++++ minmea.c | 353 ++++++++++++++++++++++++++++++++++++++++++ minmea.h | 138 +++++++++++++++++ tests.c | 447 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 1090 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 example.c create mode 100644 minmea.c create mode 100644 minmea.h create mode 100644 tests.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..615178d --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.o +*~ +minmea +tests +example diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..af80e1d --- /dev/null +++ b/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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..8c0100f --- /dev/null +++ b/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. diff --git a/example.c b/example.c new file mode 100644 index 0000000..c1b67e3 --- /dev/null +++ b/example.c @@ -0,0 +1,46 @@ +#include +#include +#include + +#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: */ diff --git a/minmea.c b/minmea.c new file mode 100644 index 0000000..ef4261a --- /dev/null +++ b/minmea.c @@ -0,0 +1,353 @@ +#include "minmea.h" + +#include +#include +#include +#include + +#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: */ diff --git a/minmea.h b/minmea.h new file mode 100644 index 0000000..98c5eb3 --- /dev/null +++ b/minmea.h @@ -0,0 +1,138 @@ +#ifndef MINMEA_H +#define MINMEA_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include +#include +#include +#include + +#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: */ diff --git a/tests.c b/tests.c new file mode 100644 index 0000000..ae8e5d9 --- /dev/null +++ b/tests.c @@ -0,0 +1,447 @@ +#include +#include +#include +#include + +#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: */