Просмотр исходного кода

Squashed 'gps_nmea/' changes from ecd900bfe..3758a8da3

3758a8da3 upd gps
9cb005d1b add automatic 5v power in gps app
dbd100e56 update gps uart
25662dbe0 fixing some big bugs
07e9c8227 combine 1
d5df4d803 move base pack here
REVERT: ecd900bfe Clean up changing baud rate logic.
REVERT: bdff18e48 Properly initialize gps_uart struct members.
REVERT: 565e93beb Merge pull request #24 from dl9sec/main
REVERT: cc8b25305 Update README.md
REVERT: e3cd90314 Remove unnecessary enum identifier.
REVERT: 5c70f6ecc Add include guard to constants.h.
REVERT: 7112bef29 Update module compatibility information. Closes #21.
REVERT: ec7716630 Merge pull request #23 from ezod/4800baud
REVERT: fb7946f04 Add 4800 baud rate. Closes #20.
REVERT: a27b41c61 Merge pull request #22 from ezod/mph
REVERT: d875fb5e8 Add setting for speed display in miles per hour. Closes #19.
REVERT: 88e732486 Add uFBT build instructions. Closes #15.
REVERT: aa8a4b729 Remove YMMV warning about NEO-6M.
REVERT: 734e2c47a Add Royaltek RBT-2100LP to supported module list.
REVERT: 0a86422a9 Add installation instructions.
REVERT: dc3aa4f82 Ignore .clang-format autogenerated by ufbt.
REVERT: b36bc47cd Remove unused sdk-index-url context.
REVERT: 7a45a4f5a Add build status badge.
REVERT: 96f0f59d2 Add GitHub action to build FAPs for dev and release SDK.
REVERT: 397292975 Add missing parenthesis.
REVERT: 53c610a07 Fix link on issue reference.
REVERT: e9317fca7 Add note about NEO-6M not working for some people.
REVERT: f433916e2 Add usage section to README for new interactions. Closes #11.
REVERT: 47b3e0a77 Remove contributing section, as it is no longer relevant.
REVERT: d28dfe9e8 Merge pull request #10 from xMasterX/ul-updates
REVERT: a9c1301fe Delete minmea.h
REVERT: e21c13271 Delete minmea.c
REVERT: 53aa016ad Import minmea as a submodule and add FAP library definition.
REVERT: 6147cb9aa Ignore .vscode from uFBT.
REVERT: 841db2df8 Set default baud rate to 9600.
REVERT: 6128470a4 Merge branch 'main' into ul-updates
REVERT: f6fa1e730 Apply ./fbt format (excluding third-party sources).
REVERT: 606389c3d Add all current updates
REVERT: aaea283a6 Add comments to serial readline loop.
REVERT: 5b748296a Add support for GLL NMEA sentence.
REVERT: 397a8f165 Improve context object pointer handling.
REVERT: 333405688 Update mutex to new API.
REVERT: e7bf036d8 Add link to Lab401 tutorial video by RocketGod. Closes #4.
REVERT: dc9fa7aa0 Add note about PRs for compatible module list.
REVERT: b73b3c644 Remove binary from source tree; to be added to GitHub release.
REVERT: d400dc67b Change app category to GPIO.
REVERT: 0a524e645 Add u-blox NEO-6M to compatible module list.
REVERT: e8bdc1a28 Add list of confirmed compatible modules.
REVERT: 4ab211401 FAP: API version 11.5.
REVERT: e22f4fc0c FAP: API version 10.1.
REVERT: 502ff349e FAP: API version 7.5.
REVERT: 5e0810363 FAP: API version 7.2.
REVERT: 123583ad3 Add default handler to input even switch statement.
REVERT: 03d8ffe30 Add contributing section to README.
REVERT: f5dbde55a Initialize time fields to zero.
REVERT: f0882961e Scale down wiring diagram.
REVERT: c50785555 Add wiring diagram.
REVERT: 4f1ed0273 Expand build instructions.
REVERT: 27b213dd3 Add UI image.
REVERT: 60f86aeb3 Add color-coded LED blinks on successful NMEA message parse.
REVERT: b9269b425 Improve UI layout.
REVERT: 87de23254 Add time fields to GPS status struct.
REVERT: 1014b5d9a Add several more GPS fields to status struct and display.
REVERT: f30f21914 Code style formatting overhaul.
REVERT: 47fadfc51 Update UART code to use new stream buffer API. Closes #1.
REVERT: 75940c72c Update README with new FAP target.
REVERT: 58fa5b904 Rename main app source file.
REVERT: b3cd0e706 Update application manifest and add icon.
REVERT: 8aff5d295 Move sources to root of repository.
REVERT: 6e654f9a3 Mention and link minmea in README.
REVERT: c4909c604 Create README.
REVERT: fe98f6125 Import and FAP conversion of GPS plugin from flipperzero-firmware fork. Does not build yet as stream_buffer.h API is not yet available for FAP.
REVERT: 2feee6c55 Initial commit

git-subtree-dir: gps_nmea
git-subtree-split: 3758a8da3bfea0efbc3c870fb3b9b937112cfa31
Willy-JL 2 лет назад
Родитель
Сommit
19571758ef
13 измененных файлов с 972 добавлено и 153 удалено
  1. 0 34
      .github/workflows/build.yml
  2. 0 57
      .gitignore
  3. 0 3
      .gitmodules
  4. 14 45
      README.md
  5. 3 7
      application.fam
  6. 19 5
      gps.c
  7. 1 1
      gps_uart.c
  8. BIN
      img/1.png
  9. BIN
      img/2.png
  10. BIN
      img/3.png
  11. 0 1
      lib/minmea
  12. 640 0
      minmea.c
  13. 295 0
      minmea.h

+ 0 - 34
.github/workflows/build.yml

@@ -1,34 +0,0 @@
-name: "FAP Build"
-on:
-  push:
-    branches:
-      - main 
-  pull_request:
-  schedule:
-    - cron: "1 1 * * *"
-jobs:
-  ufbt-build-action:
-    runs-on: ubuntu-latest
-    strategy:
-      matrix:
-        include:
-          - name: dev channel
-            sdk-channel: dev
-          - name: release channel
-            sdk-channel: release
-    name: 'ufbt: Build for ${{ matrix.name }}'
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v3
-        with:
-          submodules: recursive
-      - name: Build with ufbt
-        uses: flipperdevices/flipperzero-ufbt-action@v0.1.2
-        id: build-app
-        with:
-          sdk-channel: ${{ matrix.sdk-channel }}
-      - name: Upload app artifacts
-        uses: actions/upload-artifact@v3
-        with:
-          name: ${{ github.event.repository.name }}-${{ steps.build-app.outputs.suffix }}
-          path: ${{ steps.build-app.outputs.fap-artifacts }}

+ 0 - 57
.gitignore

@@ -1,57 +0,0 @@
-# Prerequisites
-*.d
-
-# Object files
-*.o
-*.ko
-*.obj
-*.elf
-
-# Linker output
-*.ilk
-*.map
-*.exp
-
-# Precompiled Headers
-*.gch
-*.pch
-
-# Libraries
-*.lib
-*.a
-*.la
-*.lo
-
-# Shared objects (inc. Windows DLLs)
-*.dll
-*.so
-*.so.*
-*.dylib
-
-# Executables
-*.exe
-*.out
-*.app
-*.i*86
-*.x86_64
-*.hex
-
-# Debug files
-*.dSYM/
-*.su
-*.idb
-*.pdb
-
-# Kernel Module Compile Results
-*.mod*
-*.cmd
-.tmp_versions/
-modules.order
-Module.symvers
-Mkfile.old
-dkms.conf
-
-# uFBT
-.vscode
-.clang-format
-dist/

+ 0 - 3
.gitmodules

@@ -1,3 +0,0 @@
-[submodule "lib/minmea"]
-	path = lib/minmea
-	url = git@github.com:kosma/minmea.git

+ 14 - 45
README.md

@@ -1,21 +1,9 @@
 # GPS for Flipper Zero
 
-[![FAP Build](https://github.com/ezod/flipperzero-gps/actions/workflows/build.yml/badge.svg)](https://github.com/ezod/flipperzero-gps/actions/workflows/build.yml)
+A simple Flipper Zero application for NMEA 0183 serial GPS modules, such as the
+- Adafruit Ultimate GPS Breakout.
 
-A simple Flipper Zero application for NMEA 0183 serial GPS modules.
-
-![ui](ui.png)
-
-Heavy lifting (NMEA parsing) provided by [minmea].
-
-## Installation
-
-1. Navigate to the [FAP Build](https://github.com/ezod/flipperzero-gps/actions/workflows/build.yml)
-   GitHub action workflow, and select the most recent run.
-2. The FAP is built for both the `dev` and `release` channels of the official
-   firmware. Download the artifact corresponding to your firmware version.
-3. Extract `gps_nmea.fap` from the ZIP file to `apps/GPIO` on your Flipper
-   Zero SD card.
+Heavy lifting (NMEA parsing) provided by minmea.
 
 ## Usage
 
@@ -23,7 +11,7 @@ This is a single-screen app, and a few interactions are provided via the
 hardware buttons:
 
 - Long press the up button to change the **baud rate**. The default baud rate
-  is 9600, but 4800, 19200, 38400, 57600, and 115200 baud are also supported.
+  is 9600, but 19200, 38400, 57600, and 115200 baud are also supported.
 - Long press the right button to change **speed units** from knots to
   kilometers per hour.
 - Press the OK button to set the **backlight** to always on mode. Press it
@@ -35,43 +23,24 @@ hardware buttons:
 Connect the GPS module to power and the USART using GPIO pins 9 (3.3V), 11
 (GND), 13 (TX), and 14 (RX), as appropriate.
 
-![wiring](wiring.png)
 
-See the [tutorial video](https://www.youtube.com/watch?v=5vSGFzEBp-k) from
-Lab401 by [RocketGod](https://github.com/RocketGod-git) for a visual guide to
+See the tutorial video - https://www.youtube.com/watch?v=5vSGFzEBp-k from
+Lab401 by RocketGod - https://github.com/RocketGod-git for a visual guide to
 the hardware setup.
 
-### Confirmed Compatible Modules
+## Confirmed Compatible Modules
 
-* [Adafruit Ultimate GPS Breakout]
-* ATGM336H
+* Adafruit Ultimate GPS Breakout
 * Beitian BN-180
-* Beitian BN-220
-* Beitian BN-280
-* Beitian BN-880
-* Beitian BK-250
-* Beitian BK-357
-* Beitian BK-880Q
-* Beitian BE-280
-* Beitian BN-280ZF
-* Beitian BN-357ZF
 * Royaltek RBT-2100LP
-* [u-blox NEO-6M]
-* [u-blox NEO-7M]
-* [Uputronics u-blox MAX-M8C Pico]
+* u-blox NEO-6M
 
 If you have verified this application working with a module not listed here,
 please submit a PR adding it to the list.
 
-## Building
-
-This application can be compiled using [uFBT]. Run `ufbt` in the root directory
-of the repository.
+## Links
 
-[Adafruit Ultimate GPS Breakout]: https://www.adafruit.com/product/746
-[minmea]: https://github.com/kosma/minmea
-[qFlipper]: https://flipperzero.one/update
-[u-blox NEO-6M]: https://www.u-blox.com/en/product/neo-6-series
-[u-blox NEO-7M]: https://www.u-blox.com/en/product/neo-7-series
-[uFBT]: https://github.com/flipperdevices/flipperzero-ufbt
-[Uputronics u-blox MAX-M8C Pico]: https://store.uputronics.com/index.php?route=product/product&product_id=72
+Original repo link - https://github.com/ezod/flipperzero-gps
+Adafruit Ultimate GPS Breakout: https://www.adafruit.com/product/746
+minmea: https://github.com/kosma/minmea
+u-blox NEO-6M: https://www.u-blox.com/en/product/neo-6-series

+ 3 - 7
application.fam

@@ -8,11 +8,7 @@ App(
     order=35,
     fap_icon="gps_10px.png",
     fap_category="GPIO",
-    fap_private_libs=[
-        Lib(
-            name="minmea",
-            sources=["minmea.c"],
-            cdefines=["timegm=mktime"],
-        ),
-    ],
+    fap_author="@ezod & @xMasterX",
+    fap_version="1.2",
+    fap_description="Works with GPS modules via UART, using NMEA protocol.",
 )

+ 19 - 5
gps.c

@@ -2,6 +2,7 @@
 #include "constants.h"
 
 #include <furi.h>
+#include <furi_hal_power.h>
 #include <gui/gui.h>
 #include <string.h>
 
@@ -94,6 +95,14 @@ int32_t gps_app(void* p) {
 
     FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(PluginEvent));
 
+    uint8_t attempts = 0;
+    bool otg_was_enabled = furi_hal_power_is_otg_enabled();
+    while(!furi_hal_power_is_otg_enabled() && attempts++ < 5) {
+        furi_hal_power_enable_otg();
+        furi_delay_ms(10);
+    }
+    furi_delay_ms(200);
+
     GpsUart* gps_uart = gps_uart_enable();
 
     gps_uart->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
@@ -159,6 +168,8 @@ int32_t gps_app(void* p) {
 
                         gps_uart_init_thread(gps_uart);
                         gps_uart->changing_baudrate = true;
+                        furi_mutex_release(gps_uart->mutex);
+                        view_port_update(view_port);
                         break;
                     case InputKeyRight:
                         gps_uart->speed_units++;
@@ -175,11 +186,10 @@ int32_t gps_app(void* p) {
                 }
             }
         }
-
-        view_port_update(view_port);
-        furi_mutex_release(gps_uart->mutex);
-
-        if(gps_uart->changing_baudrate) {
+        if(!gps_uart->changing_baudrate) {
+            furi_mutex_release(gps_uart->mutex);
+            view_port_update(view_port);
+        } else {
             furi_delay_ms(1000);
             gps_uart->changing_baudrate = false;
         }
@@ -194,5 +204,9 @@ int32_t gps_app(void* p) {
     furi_mutex_free(gps_uart->mutex);
     gps_uart_disable(gps_uart);
 
+    if(furi_hal_power_is_otg_enabled() && !otg_was_enabled) {
+        furi_hal_power_disable_otg();
+    }
+
     return 0;
 }

+ 1 - 1
gps_uart.c

@@ -1,6 +1,6 @@
 #include <string.h>
 
-#include <minmea.h>
+#include "minmea.h"
 #include "gps_uart.h"
 
 typedef enum {




+ 0 - 1
lib/minmea

@@ -1 +0,0 @@
-Subproject commit 85439b97dd4984c5efb84ce954b85088e781dae8

+ 640 - 0
minmea.c

@@ -0,0 +1,640 @@
+/*
+ * Copyright © 2014 Kosma Moczek <kosma@cloudyourcar.com>
+ * This program is free software. It comes without any warranty, to the extent
+ * permitted by applicable law. You can redistribute it and/or modify it under
+ * the terms of the Do What The Fuck You Want To Public License, Version 2, as
+ * published by Sam Hocevar. See the COPYING file for more details.
+ */
+
+#include "minmea.h"
+
+#include <stdlib.h>
+#include <string.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' + 10;
+    if(c >= 'a' && c <= 'f') return c - 'a' + 10;
+    return -1;
+}
+
+uint8_t minmea_checksum(const char* sentence) {
+    // Support senteces with or without the starting dollar sign.
+    if(*sentence == '$') sentence++;
+
+    uint8_t checksum = 0x00;
+
+    // The optional checksum is an XOR of all bytes between "$" and "*".
+    while(*sentence && *sentence != '*') checksum ^= *sentence++;
+
+    return checksum;
+}
+
+bool minmea_check(const char* sentence, bool strict) {
+    uint8_t checksum = 0x00;
+
+    // A valid sentence starts with "$".
+    if(*sentence++ != '$') return false;
+
+    // The optional checksum is an XOR of all bytes between "$" and "*".
+    while(*sentence && *sentence != '*' && isprint((unsigned char)*sentence))
+        checksum ^= *sentence++;
+
+    // If checksum is present...
+    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;
+    } else if(strict) {
+        // Discard non-checksummed frames in strict mode.
+        return false;
+    }
+
+    // The only stuff allowed at this point is a newline.
+    while(*sentence == '\r' || *sentence == '\n') {
+        sentence++;
+    }
+
+    if(*sentence) {
+        return false;
+    }
+
+    return true;
+}
+
+bool minmea_scan(const char* sentence, const char* format, ...) {
+    bool result = false;
+    bool optional = false;
+
+    if(sentence == NULL) return false;
+
+    va_list ap;
+    va_start(ap, format);
+
+    const char* field = sentence;
+#define next_field()                                 \
+    do {                                             \
+        /* Progress to the next field. */            \
+        while(minmea_isfield(*sentence)) sentence++; \
+        /* Make sure there is a field there. */      \
+        if(*sentence == ',') {                       \
+            sentence++;                              \
+            field = sentence;                        \
+        } else {                                     \
+            field = NULL;                            \
+        }                                            \
+    } while(0)
+
+    while(*format) {
+        char type = *format++;
+
+        if(type == ';') {
+            // All further fields are optional.
+            optional = true;
+            continue;
+        }
+
+        if(!field && !optional) {
+            // Field requested but we ran out if input. Bail out.
+            goto parse_error;
+        }
+
+        switch(type) {
+        case 'c': { // Single character field (char).
+            char value = '\0';
+
+            if(field && minmea_isfield(*field)) value = *field;
+
+            *va_arg(ap, char*) = value;
+        } break;
+
+        case 'd': { // Single character direction field (int).
+            int value = 0;
+
+            if(field && minmea_isfield(*field)) {
+                switch(*field) {
+                case 'N':
+                case 'E':
+                    value = 1;
+                    break;
+                case 'S':
+                case 'W':
+                    value = -1;
+                    break;
+                default:
+                    goto parse_error;
+                }
+            }
+
+            *va_arg(ap, int*) = value;
+        } break;
+
+        case 'f': { // Fractional value with scale (struct minmea_float).
+            int sign = 0;
+            int_least32_t value = -1;
+            int_least32_t scale = 0;
+
+            if(field) {
+                while(minmea_isfield(*field)) {
+                    if(*field == '+' && !sign && value == -1) {
+                        sign = 1;
+                    } else if(*field == '-' && !sign && value == -1) {
+                        sign = -1;
+                    } else if(isdigit((unsigned char)*field)) {
+                        int digit = *field - '0';
+                        if(value == -1) value = 0;
+                        if(value > (INT_LEAST32_MAX - digit) / 10) {
+                            /* we ran out of bits, what do we do? */
+                            if(scale) {
+                                /* truncate extra precision */
+                                break;
+                            } else {
+                                /* integer overflow. bail out. */
+                                goto parse_error;
+                            }
+                        }
+                        value = (10 * value) + digit;
+                        if(scale) scale *= 10;
+                    } else if(*field == '.' && scale == 0) {
+                        scale = 1;
+                    } else if(*field == ' ') {
+                        /* Allow spaces at the start of the field. Not NMEA
+                             * conformant, but some modules do this. */
+                        if(sign != 0 || value != -1 || scale != 0) goto parse_error;
+                    } else {
+                        goto parse_error;
+                    }
+                    field++;
+                }
+            }
+
+            if((sign || scale) && value == -1) goto parse_error;
+
+            if(value == -1) {
+                /* No digits were scanned. */
+                value = 0;
+                scale = 0;
+            } else if(scale == 0) {
+                /* No decimal point. */
+                scale = 1;
+            }
+            if(sign) value *= sign;
+
+            *va_arg(ap, struct minmea_float*) = (struct minmea_float){value, scale};
+        } break;
+
+        case 'i': { // Integer value, default 0 (int).
+            int value = 0;
+
+            if(field) {
+                char* endptr;
+                value = strtol(field, &endptr, 10);
+                if(minmea_isfield(*endptr)) goto parse_error;
+            }
+
+            *va_arg(ap, int*) = value;
+        } break;
+
+        case 's': { // String value (char *).
+            char* buf = va_arg(ap, char*);
+
+            if(field) {
+                while(minmea_isfield(*field)) *buf++ = *field++;
+            }
+
+            *buf = '\0';
+        } break;
+
+        case 't': { // NMEA talker+sentence identifier (char *).
+            // This field is always mandatory.
+            if(!field) goto parse_error;
+
+            if(field[0] != '$') goto parse_error;
+            for(int f = 0; f < 5; f++)
+                if(!minmea_isfield(field[1 + f])) goto parse_error;
+
+            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;
+
+            if(field && minmea_isfield(*field)) {
+                // Always six digits.
+                for(int f = 0; f < 6; f++)
+                    if(!isdigit((unsigned char)field[f])) goto parse_error;
+
+                char dArr[] = {field[0], field[1], '\0'};
+                char mArr[] = {field[2], field[3], '\0'};
+                char yArr[] = {field[4], field[5], '\0'};
+                d = strtol(dArr, NULL, 10);
+                m = strtol(mArr, NULL, 10);
+                y = strtol(yArr, NULL, 10);
+            }
+
+            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;
+
+            if(field && minmea_isfield(*field)) {
+                // Minimum required: integer time.
+                for(int f = 0; f < 6; f++)
+                    if(!isdigit((unsigned char)field[f])) goto parse_error;
+
+                char hArr[] = {field[0], field[1], '\0'};
+                char iArr[] = {field[2], field[3], '\0'};
+                char sArr[] = {field[4], field[5], '\0'};
+                h = strtol(hArr, NULL, 10);
+                i = strtol(iArr, NULL, 10);
+                s = strtol(sArr, NULL, 10);
+                field += 6;
+
+                // Extra: fractional time. Saved as microseconds.
+                if(*field++ == '.') {
+                    uint32_t value = 0;
+                    uint32_t scale = 1000000LU;
+                    while(isdigit((unsigned char)*field) && scale > 1) {
+                        value = (value * 10) + (*field++ - '0');
+                        scale /= 10;
+                    }
+                    u = value * scale;
+                } else {
+                    u = 0;
+                }
+            }
+
+            time_->hours = h;
+            time_->minutes = i;
+            time_->seconds = s;
+            time_->microseconds = u;
+        } break;
+
+        case '_': { // Ignore the field.
+        } break;
+
+        default: { // Unknown.
+            goto parse_error;
+        }
+        }
+
+        next_field();
+    }
+
+    result = true;
+
+parse_error:
+    va_end(ap);
+    return result;
+}
+
+bool minmea_talker_id(char talker[3], const char* sentence) {
+    char type[6];
+    if(!minmea_scan(sentence, "t", type)) return false;
+
+    talker[0] = type[0];
+    talker[1] = type[1];
+    talker[2] = '\0';
+
+    return true;
+}
+
+enum minmea_sentence_id minmea_sentence_id(const char* sentence, bool strict) {
+    if(!minmea_check(sentence, strict)) return MINMEA_INVALID;
+
+    char type[6];
+    if(!minmea_scan(sentence, "t", type)) return MINMEA_INVALID;
+
+    if(!strcmp(type + 2, "GBS")) return MINMEA_SENTENCE_GBS;
+    if(!strcmp(type + 2, "GGA")) return MINMEA_SENTENCE_GGA;
+    if(!strcmp(type + 2, "GLL")) return MINMEA_SENTENCE_GLL;
+    if(!strcmp(type + 2, "GSA")) return MINMEA_SENTENCE_GSA;
+    if(!strcmp(type + 2, "GST")) return MINMEA_SENTENCE_GST;
+    if(!strcmp(type + 2, "GSV")) return MINMEA_SENTENCE_GSV;
+    if(!strcmp(type + 2, "RMC")) return MINMEA_SENTENCE_RMC;
+    if(!strcmp(type + 2, "VTG")) return MINMEA_SENTENCE_VTG;
+    if(!strcmp(type + 2, "ZDA")) return MINMEA_SENTENCE_ZDA;
+
+    return MINMEA_UNKNOWN;
+}
+
+bool minmea_parse_gbs(struct minmea_sentence_gbs* frame, const char* sentence) {
+    // $GNGBS,170556.00,3.0,2.9,8.3,,,,*5C
+    char type[6];
+    if(!minmea_scan(
+           sentence,
+           "tTfffifff",
+           type,
+           &frame->time,
+           &frame->err_latitude,
+           &frame->err_longitude,
+           &frame->err_altitude,
+           &frame->svid,
+           &frame->prob,
+           &frame->bias,
+           &frame->stddev))
+        return false;
+    if(strcmp(type + 2, "GBS")) return false;
+
+    return true;
+}
+
+bool minmea_parse_rmc(struct minmea_sentence_rmc* 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,
+           &latitude_direction,
+           &frame->longitude,
+           &longitude_direction,
+           &frame->speed,
+           &frame->course,
+           &frame->date,
+           &frame->variation,
+           &variation_direction))
+        return false;
+    if(strcmp(type + 2, "RMC")) return false;
+
+    frame->valid = (validity == 'A');
+    frame->latitude.value *= latitude_direction;
+    frame->longitude.value *= longitude_direction;
+    frame->variation.value *= variation_direction;
+
+    return true;
+}
+
+bool minmea_parse_gga(struct minmea_sentence_gga* 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,
+           "tTfdfdiiffcfcf_",
+           type,
+           &frame->time,
+           &frame->latitude,
+           &latitude_direction,
+           &frame->longitude,
+           &longitude_direction,
+           &frame->fix_quality,
+           &frame->satellites_tracked,
+           &frame->hdop,
+           &frame->altitude,
+           &frame->altitude_units,
+           &frame->height,
+           &frame->height_units,
+           &frame->dgps_age))
+        return false;
+    if(strcmp(type + 2, "GGA")) return false;
+
+    frame->latitude.value *= latitude_direction;
+    frame->longitude.value *= longitude_direction;
+
+    return true;
+}
+
+bool minmea_parse_gsa(struct minmea_sentence_gsa* frame, const char* sentence) {
+    // $GPGSA,A,3,04,05,,09,12,,,24,,,,,2.5,1.3,2.1*39
+    char type[6];
+
+    if(!minmea_scan(
+           sentence,
+           "tciiiiiiiiiiiiifff",
+           type,
+           &frame->mode,
+           &frame->fix_type,
+           &frame->sats[0],
+           &frame->sats[1],
+           &frame->sats[2],
+           &frame->sats[3],
+           &frame->sats[4],
+           &frame->sats[5],
+           &frame->sats[6],
+           &frame->sats[7],
+           &frame->sats[8],
+           &frame->sats[9],
+           &frame->sats[10],
+           &frame->sats[11],
+           &frame->pdop,
+           &frame->hdop,
+           &frame->vdop))
+        return false;
+    if(strcmp(type + 2, "GSA")) return false;
+
+    return true;
+}
+
+bool minmea_parse_gll(struct minmea_sentence_gll* frame, const char* sentence) {
+    // $GPGLL,3723.2475,N,12158.3416,W,161229.487,A,A*41$;
+    char type[6];
+    int latitude_direction;
+    int longitude_direction;
+
+    if(!minmea_scan(
+           sentence,
+           "tfdfdTc;c",
+           type,
+           &frame->latitude,
+           &latitude_direction,
+           &frame->longitude,
+           &longitude_direction,
+           &frame->time,
+           &frame->status,
+           &frame->mode))
+        return false;
+    if(strcmp(type + 2, "GLL")) return false;
+
+    frame->latitude.value *= latitude_direction;
+    frame->longitude.value *= longitude_direction;
+
+    return true;
+}
+
+bool minmea_parse_gst(struct minmea_sentence_gst* frame, const char* sentence) {
+    // $GPGST,024603.00,3.2,6.6,4.7,47.3,5.8,5.6,22.0*58
+    char type[6];
+
+    if(!minmea_scan(
+           sentence,
+           "tTfffffff",
+           type,
+           &frame->time,
+           &frame->rms_deviation,
+           &frame->semi_major_deviation,
+           &frame->semi_minor_deviation,
+           &frame->semi_major_orientation,
+           &frame->latitude_error_deviation,
+           &frame->longitude_error_deviation,
+           &frame->altitude_error_deviation))
+        return false;
+    if(strcmp(type + 2, "GST")) return false;
+
+    return true;
+}
+
+bool minmea_parse_gsv(struct minmea_sentence_gsv* frame, const char* sentence) {
+    // $GPGSV,3,1,11,03,03,111,00,04,15,270,00,06,01,010,00,13,06,292,00*74
+    // $GPGSV,3,3,11,22,42,067,42,24,14,311,43,27,05,244,00,,,,*4D
+    // $GPGSV,4,2,11,08,51,203,30,09,45,215,28*75
+    // $GPGSV,4,4,13,39,31,170,27*40
+    // $GPGSV,4,4,13*7B
+    char type[6];
+
+    if(!minmea_scan(
+           sentence,
+           "tiii;iiiiiiiiiiiiiiii",
+           type,
+           &frame->total_msgs,
+           &frame->msg_nr,
+           &frame->total_sats,
+           &frame->sats[0].nr,
+           &frame->sats[0].elevation,
+           &frame->sats[0].azimuth,
+           &frame->sats[0].snr,
+           &frame->sats[1].nr,
+           &frame->sats[1].elevation,
+           &frame->sats[1].azimuth,
+           &frame->sats[1].snr,
+           &frame->sats[2].nr,
+           &frame->sats[2].elevation,
+           &frame->sats[2].azimuth,
+           &frame->sats[2].snr,
+           &frame->sats[3].nr,
+           &frame->sats[3].elevation,
+           &frame->sats[3].azimuth,
+           &frame->sats[3].snr)) {
+        return false;
+    }
+    if(strcmp(type + 2, "GSV")) return false;
+
+    return true;
+}
+
+bool minmea_parse_vtg(struct minmea_sentence_vtg* frame, const char* sentence) {
+    // $GPVTG,054.7,T,034.4,M,005.5,N,010.2,K*48
+    // $GPVTG,156.1,T,140.9,M,0.0,N,0.0,K*41
+    // $GPVTG,096.5,T,083.5,M,0.0,N,0.0,K,D*22
+    // $GPVTG,188.36,T,,M,0.820,N,1.519,K,A*3F
+    char type[6];
+    char c_true, c_magnetic, c_knots, c_kph, c_faa_mode;
+
+    if(!minmea_scan(
+           sentence,
+           "t;fcfcfcfcc",
+           type,
+           &frame->true_track_degrees,
+           &c_true,
+           &frame->magnetic_track_degrees,
+           &c_magnetic,
+           &frame->speed_knots,
+           &c_knots,
+           &frame->speed_kph,
+           &c_kph,
+           &c_faa_mode))
+        return false;
+    if(strcmp(type + 2, "VTG")) return false;
+    // values are only valid with the accompanying characters
+    if(c_true != 'T') frame->true_track_degrees.scale = 0;
+    if(c_magnetic != 'M') frame->magnetic_track_degrees.scale = 0;
+    if(c_knots != 'N') frame->speed_knots.scale = 0;
+    if(c_kph != 'K') frame->speed_kph.scale = 0;
+    frame->faa_mode = (enum minmea_faa_mode)c_faa_mode;
+
+    return true;
+}
+
+bool minmea_parse_zda(struct minmea_sentence_zda* frame, const char* sentence) {
+    // $GPZDA,201530.00,04,07,2002,00,00*60
+    char type[6];
+
+    if(!minmea_scan(
+           sentence,
+           "tTiiiii",
+           type,
+           &frame->time,
+           &frame->date.day,
+           &frame->date.month,
+           &frame->date.year,
+           &frame->hour_offset,
+           &frame->minute_offset))
+        return false;
+    if(strcmp(type + 2, "ZDA")) return false;
+
+    // check offsets
+    if(abs(frame->hour_offset) > 13 || frame->minute_offset > 59 || frame->minute_offset < 0)
+        return false;
+
+    return true;
+}
+
+int minmea_getdatetime(
+    struct tm* tm,
+    const struct minmea_date* date,
+    const struct minmea_time* time_) {
+    if(date->year == -1 || time_->hours == -1) return -1;
+
+    memset(tm, 0, sizeof(*tm));
+    if(date->year < 80) {
+        tm->tm_year = 2000 + date->year - 1900; // 2000-2079
+    } else if(date->year >= 1900) {
+        tm->tm_year = date->year - 1900; // 4 digit year, use directly
+    } else {
+        tm->tm_year = date->year; // 1980-1999
+    }
+    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;
+
+    return 0;
+}
+
+int minmea_gettime(
+    struct timespec* ts,
+    const struct minmea_date* date,
+    const struct minmea_time* time_) {
+    struct tm tm;
+    if(minmea_getdatetime(&tm, date, time_)) return -1;
+
+    time_t timestamp = mktime(&tm); /* See README.md if your system lacks timegm(). */
+    if(timestamp != (time_t)-1) {
+        ts->tv_sec = timestamp;
+        ts->tv_nsec = time_->microseconds * 1000;
+        return 0;
+    } else {
+        return -1;
+    }
+}
+
+/* vim: set ts=4 sw=4 et: */

+ 295 - 0
minmea.h

@@ -0,0 +1,295 @@
+/*
+ * Copyright © 2014 Kosma Moczek <kosma@cloudyourcar.com>
+ * This program is free software. It comes without any warranty, to the extent
+ * permitted by applicable law. You can redistribute it and/or modify it under
+ * the terms of the Do What The Fuck You Want To Public License, Version 2, as
+ * published by Sam Hocevar. See the COPYING file for more details.
+ */
+
+#ifndef MINMEA_H
+#define MINMEA_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <ctype.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <time.h>
+#include <math.h>
+#ifdef MINMEA_INCLUDE_COMPAT
+#include <minmea_compat.h>
+#endif
+
+#ifndef MINMEA_MAX_SENTENCE_LENGTH
+#define MINMEA_MAX_SENTENCE_LENGTH 80
+#endif
+
+enum minmea_sentence_id {
+    MINMEA_INVALID = -1,
+    MINMEA_UNKNOWN = 0,
+    MINMEA_SENTENCE_GBS,
+    MINMEA_SENTENCE_GGA,
+    MINMEA_SENTENCE_GLL,
+    MINMEA_SENTENCE_GSA,
+    MINMEA_SENTENCE_GST,
+    MINMEA_SENTENCE_GSV,
+    MINMEA_SENTENCE_RMC,
+    MINMEA_SENTENCE_VTG,
+    MINMEA_SENTENCE_ZDA,
+};
+
+struct minmea_float {
+    int_least32_t value;
+    int_least32_t scale;
+};
+
+struct minmea_date {
+    int day;
+    int month;
+    int year;
+};
+
+struct minmea_time {
+    int hours;
+    int minutes;
+    int seconds;
+    int microseconds;
+};
+
+struct minmea_sentence_gbs {
+    struct minmea_time time;
+    struct minmea_float err_latitude;
+    struct minmea_float err_longitude;
+    struct minmea_float err_altitude;
+    int svid;
+    struct minmea_float prob;
+    struct minmea_float bias;
+    struct minmea_float stddev;
+};
+
+struct minmea_sentence_rmc {
+    struct minmea_time time;
+    bool valid;
+    struct minmea_float latitude;
+    struct minmea_float longitude;
+    struct minmea_float speed;
+    struct minmea_float course;
+    struct minmea_date date;
+    struct minmea_float variation;
+};
+
+struct minmea_sentence_gga {
+    struct minmea_time time;
+    struct minmea_float latitude;
+    struct minmea_float longitude;
+    int fix_quality;
+    int satellites_tracked;
+    struct minmea_float hdop;
+    struct minmea_float altitude;
+    char altitude_units;
+    struct minmea_float height;
+    char height_units;
+    struct minmea_float dgps_age;
+};
+
+enum minmea_gll_status {
+    MINMEA_GLL_STATUS_DATA_VALID = 'A',
+    MINMEA_GLL_STATUS_DATA_NOT_VALID = 'V',
+};
+
+// FAA mode added to some fields in NMEA 2.3.
+enum minmea_faa_mode {
+    MINMEA_FAA_MODE_AUTONOMOUS = 'A',
+    MINMEA_FAA_MODE_DIFFERENTIAL = 'D',
+    MINMEA_FAA_MODE_ESTIMATED = 'E',
+    MINMEA_FAA_MODE_MANUAL = 'M',
+    MINMEA_FAA_MODE_SIMULATED = 'S',
+    MINMEA_FAA_MODE_NOT_VALID = 'N',
+    MINMEA_FAA_MODE_PRECISE = 'P',
+};
+
+struct minmea_sentence_gll {
+    struct minmea_float latitude;
+    struct minmea_float longitude;
+    struct minmea_time time;
+    char status;
+    char mode;
+};
+
+struct minmea_sentence_gst {
+    struct minmea_time time;
+    struct minmea_float rms_deviation;
+    struct minmea_float semi_major_deviation;
+    struct minmea_float semi_minor_deviation;
+    struct minmea_float semi_major_orientation;
+    struct minmea_float latitude_error_deviation;
+    struct minmea_float longitude_error_deviation;
+    struct minmea_float altitude_error_deviation;
+};
+
+enum minmea_gsa_mode {
+    MINMEA_GPGSA_MODE_AUTO = 'A',
+    MINMEA_GPGSA_MODE_FORCED = 'M',
+};
+
+enum minmea_gsa_fix_type {
+    MINMEA_GPGSA_FIX_NONE = 1,
+    MINMEA_GPGSA_FIX_2D = 2,
+    MINMEA_GPGSA_FIX_3D = 3,
+};
+
+struct minmea_sentence_gsa {
+    char mode;
+    int fix_type;
+    int sats[12];
+    struct minmea_float pdop;
+    struct minmea_float hdop;
+    struct minmea_float vdop;
+};
+
+struct minmea_sat_info {
+    int nr;
+    int elevation;
+    int azimuth;
+    int snr;
+};
+
+struct minmea_sentence_gsv {
+    int total_msgs;
+    int msg_nr;
+    int total_sats;
+    struct minmea_sat_info sats[4];
+};
+
+struct minmea_sentence_vtg {
+    struct minmea_float true_track_degrees;
+    struct minmea_float magnetic_track_degrees;
+    struct minmea_float speed_knots;
+    struct minmea_float speed_kph;
+    enum minmea_faa_mode faa_mode;
+};
+
+struct minmea_sentence_zda {
+    struct minmea_time time;
+    struct minmea_date date;
+    int hour_offset;
+    int minute_offset;
+};
+
+/**
+ * Calculate raw sentence checksum. Does not check sentence integrity.
+ */
+uint8_t minmea_checksum(const char* sentence);
+
+/**
+ * Check sentence validity and checksum. Returns true for valid sentences.
+ */
+bool minmea_check(const char* sentence, bool strict);
+
+/**
+ * Determine talker identifier.
+ */
+bool minmea_talker_id(char talker[3], const char* sentence);
+
+/**
+ * Determine sentence identifier.
+ */
+enum minmea_sentence_id minmea_sentence_id(const char* sentence, bool strict);
+
+/**
+ * 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 (struct minmea_float *)
+ * i - decimal, default zero (int *)
+ * s - string (char *)
+ * t - talker identifier and type (char *)
+ * D - date (struct minmea_date *)
+ * T - time stamp (struct minmea_time *)
+ * _ - ignore this field
+ * ; - following fields are optional
+ * Returns true on success. See library source code for details.
+ */
+bool minmea_scan(const char* sentence, const char* format, ...);
+
+/*
+ * Parse a specific type of sentence. Return true on success.
+ */
+bool minmea_parse_gbs(struct minmea_sentence_gbs* frame, const char* sentence);
+bool minmea_parse_rmc(struct minmea_sentence_rmc* frame, const char* sentence);
+bool minmea_parse_gga(struct minmea_sentence_gga* frame, const char* sentence);
+bool minmea_parse_gsa(struct minmea_sentence_gsa* frame, const char* sentence);
+bool minmea_parse_gll(struct minmea_sentence_gll* frame, const char* sentence);
+bool minmea_parse_gst(struct minmea_sentence_gst* frame, const char* sentence);
+bool minmea_parse_gsv(struct minmea_sentence_gsv* frame, const char* sentence);
+bool minmea_parse_vtg(struct minmea_sentence_vtg* frame, const char* sentence);
+bool minmea_parse_zda(struct minmea_sentence_zda* frame, const char* sentence);
+
+/**
+ * Convert GPS UTC date/time representation to a UNIX calendar time.
+ */
+int minmea_getdatetime(
+    struct tm* tm,
+    const struct minmea_date* date,
+    const struct minmea_time* time_);
+
+/**
+ * Convert GPS UTC date/time representation to a UNIX timestamp.
+ */
+int minmea_gettime(
+    struct timespec* ts,
+    const struct minmea_date* date,
+    const struct minmea_time* time_);
+
+/**
+ * Rescale a fixed-point value to a different scale. Rounds towards zero.
+ */
+static inline int_least32_t minmea_rescale(const struct minmea_float* f, int_least32_t new_scale) {
+    if(f->scale == 0) return 0;
+    if(f->scale == new_scale) return f->value;
+    if(f->scale > new_scale)
+        return (f->value + ((f->value > 0) - (f->value < 0)) * f->scale / new_scale / 2) /
+               (f->scale / new_scale);
+    else
+        return f->value * (new_scale / f->scale);
+}
+
+/**
+ * Convert a fixed-point value to a floating-point value.
+ * Returns NaN for "unknown" values.
+ */
+static inline float minmea_tofloat(const struct minmea_float* f) {
+    if(f->scale == 0) return NAN;
+    return (float)f->value / (float)f->scale;
+}
+
+/**
+ * Convert a raw coordinate to a floating point DD.DDD... value.
+ * Returns NaN for "unknown" values.
+ */
+static inline float minmea_tocoord(const struct minmea_float* f) {
+    if(f->scale == 0) return NAN;
+    if(f->scale > (INT_LEAST32_MAX / 100)) return NAN;
+    if(f->scale < (INT_LEAST32_MIN / 100)) return NAN;
+    int_least32_t degrees = f->value / (f->scale * 100);
+    int_least32_t minutes = f->value % (f->scale * 100);
+    return (float)degrees + (float)minutes / (60 * f->scale);
+}
+
+/**
+ * Check whether a character belongs to the set of characters allowed in a
+ * sentence data field.
+ */
+static inline bool minmea_isfield(char c) {
+    return isprint((unsigned char)c) && c != ',' && c != '*';
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* MINMEA_H */
+
+/* vim: set ts=4 sw=4 et: */