diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3e759b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,330 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +**/Properties/launchSettings.json + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8f29205 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## 1.0.0 - 2018-10-31 +- First release. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c6206fb --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 Mariusz Kacki + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..014d817 --- /dev/null +++ b/README.md @@ -0,0 +1,214 @@ +# FuGPS Library + +It is a simple Arduino library for parse NMEA sentences (e.g. GPS/GLONASS/GALILEO) from serial port or any other object derived from Stream class. +I wrote only a few basic functions for the simplicity of use. + +## Why another library? +I wrote it for fun, learning and for my next project (meteorological balloon). + +## Installation +This library is available on Arduino Library Manager (should be after some time). +If not, just download project from GitHub and put (after extraction) into Arduino library folder. +Library was written in Visual Micro, but can be compiled also on Arduino IDE. + +## Setup +```cpp +#include +#include + +NeoSWSerial in(8, 9); +FuGPS fuGPS(in); + +void setup() +{ + Serial.begin(38400); + in.begin(9600); + + // fuGPS.sendCommand(FUGPS_PMTK_SET_NMEA_BAUDRATE_9600); + // fuGPS.sendCommand(FUGPS_PMTK_SET_NMEA_UPDATERATE_1HZ); + fuGPS.sendCommand(FUGPS_PMTK_API_SET_NMEA_OUTPUT_RMCGGA); +} +``` +## Example 1 +Good for testing purpose. + +```cpp +void loop() +{ + // Valid NMEA message + if (fuGPS.read()) + { + byte tokenIdx = 0; + while (const char * token = fuGPS.getField(tokenIdx++)) + { + Serial.print("Token [" + String(tokenIdx) + "]: "); + Serial.println(token); + } + Serial.println(); + } +} +``` + +## Example 2 +Typical usage. + +```cpp +bool gpsAlive = false; + +void loop() +{ + // Valid NMEA message + if (fuGPS.read()) + { + // We don't know, which message was came first (GGA or RMC). + // Thats why some fields may be empty. + + gpsAlive = true; + + Serial.print("Quality: "); + Serial.println(fuGPS.Quality); + + Serial.print("Satellites: "); + Serial.println(fuGPS.Satellites); + + if (fuGPS.hasFix() == true) + { + // Data from GGA message + Serial.print("Accuracy (HDOP): "); + Serial.println(fuGPS.Accuracy); + + Serial.print("Altitude (above sea level): "); + Serial.println(fuGPS.Altitude); + + // Data from GGA or RMC + Serial.print("Location (decimal degrees): "); + Serial.println("https://www.google.com/maps/search/?api=1&query=" + String(fuGPS.Latitude, 6) + "," + String(fuGPS.Longitude, 6)); + } + } + + // Default is 10 seconds + if (fuGPS.isAlive() == false) + { + if (gpsAlive == true) + { + gpsAlive = false; + Serial.println("GPS module not responding with valid data."); + Serial.println("Check wiring or restart."); + } + } +} +``` + +## Example 3 +Suitable only with HardwareSerial (not tested). +```cpp +void loop() +{ + // Valid NMEA message + if (fuGPS.isValid()) + { + // Rest is the same... + } +} + +// SerialEvent occurs whenever a new data comes in the hardware serial RX. +void serialEvent() { + fuGPS.read(); +} +``` + +## Possible problems + +##### Dropping frames (messages) + +Please, don't use *SoftwareSerial* if you must capture more than one type of message, it is very ineffective. +You can experiment with setting proper input and output baud rate, but the best solution to me is to use better implementations of Serial: + +- **HardwareSerial (always the best way).** +- NeoSWSerial (ESP8266 not supported). +- AltSoftSerial (pins 8 & 9 for a Nano, ESP8266 not supported). + +## Compatibility +I tested on that I had, it means GY-NEO6MV2 and below modules. + +| MCU | Works | Notes +|--- |:---: |:-- +| Arduino Nano w/ ATmega328P | YES | +| Arduino Pro or Pro Mini w/ ATmega328P (3.3V, 8 MHz) | YES | +| Arduino Pro or Pro Mini w/ ATmega328P (5V, 16 MHz) | YES | +| ESP8266 | YES | WeMos D1 mini.
I had to implemenent my own strsep() function. +| ESP32 | ? | Waiting for shipment from AliExpress ;-) + +## Developing +I would rather not complicate the library by adding too many functionalities. +There are exists many other complex libraries and everyone can find something suitable for themselves. +I cared about simplicity and stability. + +## Debugging +There are two macros: + +```cpp +DPRINT(...) +DPRINTLN(...) +``` + +To use them, uncomment: + +```cpp +#define FUGPS_DEBUG +``` + +We don't need GPS module for testing. +My library needs Stream as input, so we need some object implementing this class. +I found this: +https://github.com/paulo-raca/ArduinoBufferedStreams. + +Now, code might look like this: + +```cpp +#include +#include + +LoopbackStream in; +FuGPS fuGPS(in); + +const char * gpsStream = + "$GPGGA,092750.000,5321.6802,N,00630.3372,W,1,8,1.03,61.7,M,55.2,M,,*76\r\n" + "$GPGSA,A,3,10,07,05,02,29,04,08,13,,,,,1.72,1.03,1.38*0A\r\n" + "$GPGSV,3,1,11,10,63,137,17,07,61,098,15,05,59,290,20,08,54,157,30*70\r\n" + "$GPGSV,3,2,11,02,39,223,19,13,28,070,17,26,23,252,,04,14,186,14*79\r\n" + "$GPGSV,3,3,11,29,09,301,24,16,09,020,,36,,,*76\r\n" + "$GPRMC,092750.000,A,5321.6802,N,00630.3372,W,0.02,31.66,280511,,,A*43\r\n" + "$GPGGA,092751.000,5321.6802,N,00630.3371,W,1,8,1.03,61.7,M,55.3,M,,*75\r\n" + "$GPGSA,A,3,10,07,05,02,29,04,08,13,,,,,1.72,1.03,1.38*0A\r\n" + "$GPGSV,3,1,11,10,63,137,17,07,61,098,15,05,59,290,20,08,54,157,30*70\r\n" + "$GPGSV,3,2,11,02,39,223,16,13,28,070,17,26,23,252,,04,14,186,15*77\r\n" + "$GPGSV,3,3,11,29,09,301,24,16,09,020,,36,,,*76\r\n" + "$GPRMC,092751.000,A,5321.6802,N,00630.3371,W,0.06,31.66,280511,,,A*45\r\n"; + +void setup() +{ + Serial.begin(38400); + Serial.println("Setup"); +} + +void loop() +{ + const char * pStart = gpsStream; + + while (*pStart) + { + in.write(*pStart++); + if (fuGPS.read()) + { + byte tokenIdx = 0; + while (const char * token = fuGPS.getField(tokenIdx++)) + { + Serial.print("Token [" + String(tokenIdx) + "]: "); + Serial.println(token); + } + Serial.println(); + } + } +} +``` diff --git a/examples/FuGPS_Debug/FuGPS_Debug.ino b/examples/FuGPS_Debug/FuGPS_Debug.ino new file mode 100644 index 0000000..6e8a996 --- /dev/null +++ b/examples/FuGPS_Debug/FuGPS_Debug.ino @@ -0,0 +1,45 @@ +#include +#include + +LoopbackStream in; +FuGPS fuGPS(in); + +const char * gpsStream = + "$GPGGA,092750.000,5321.6802,N,00630.3372,W,1,8,1.03,61.7,M,55.2,M,,*76\r\n" + "$GPGSA,A,3,10,07,05,02,29,04,08,13,,,,,1.72,1.03,1.38*0A\r\n" + "$GPGSV,3,1,11,10,63,137,17,07,61,098,15,05,59,290,20,08,54,157,30*70\r\n" + "$GPGSV,3,2,11,02,39,223,19,13,28,070,17,26,23,252,,04,14,186,14*79\r\n" + "$GPGSV,3,3,11,29,09,301,24,16,09,020,,36,,,*76\r\n" + "$GPRMC,092750.000,A,5321.6802,N,00630.3372,W,0.02,31.66,280511,,,A*43\r\n" + "$GPGGA,092751.000,5321.6802,N,00630.3371,W,1,8,1.03,61.7,M,55.3,M,,*75\r\n" + "$GPGSA,A,3,10,07,05,02,29,04,08,13,,,,,1.72,1.03,1.38*0A\r\n" + "$GPGSV,3,1,11,10,63,137,17,07,61,098,15,05,59,290,20,08,54,157,30*70\r\n" + "$GPGSV,3,2,11,02,39,223,16,13,28,070,17,26,23,252,,04,14,186,15*77\r\n" + "$GPGSV,3,3,11,29,09,301,24,16,09,020,,36,,,*76\r\n" + "$GPRMC,092751.000,A,5321.6802,N,00630.3371,W,0.06,31.66,280511,,,A*45\r\n"; + +void setup() +{ + Serial.begin(38400); + Serial.println("Setup"); +} + +void loop() +{ + const char * pStart = gpsStream; + + while (*pStart) + { + in.write(*pStart++); + if (fuGPS.read()) + { + byte tokenIdx = 0; + while (const char * token = fuGPS.getField(tokenIdx++)) + { + Serial.print("Token [" + String(tokenIdx) + "]: "); + Serial.println(token); + } + Serial.println(); + } + } +} diff --git a/examples/FuGPS_Test/FuGPS_Test.ino b/examples/FuGPS_Test/FuGPS_Test.ino new file mode 100644 index 0000000..bedc542 --- /dev/null +++ b/examples/FuGPS_Test/FuGPS_Test.ino @@ -0,0 +1,30 @@ +#include +#include + +NeoSWSerial in(8, 9); +FuGPS fuGPS(in); + +void setup() +{ + Serial.begin(38400); + in.begin(9600); + + // fuGPS.sendCommand(FUGPS_PMTK_SET_NMEA_BAUDRATE_9600); + // fuGPS.sendCommand(FUGPS_PMTK_SET_NMEA_UPDATERATE_1HZ); + fuGPS.sendCommand(FUGPS_PMTK_API_SET_NMEA_OUTPUT_RMCGGA); +} + +void loop() +{ + // Valid NMEA message + if (fuGPS.read()) + { + byte tokenIdx = 0; + while (const char * token = fuGPS.getField(tokenIdx++)) + { + Serial.print("Token [" + String(tokenIdx) + "]: "); + Serial.println(token); + } + Serial.println(); + } +} diff --git a/examples/FuGPS_Usage/FuGPS_Usage.ino b/examples/FuGPS_Usage/FuGPS_Usage.ino new file mode 100644 index 0000000..84b8caf --- /dev/null +++ b/examples/FuGPS_Usage/FuGPS_Usage.ino @@ -0,0 +1,59 @@ +#include +#include + +NeoSWSerial in(8, 9); +FuGPS fuGPS(in); +bool gpsAlive = false; + +void setup() +{ + Serial.begin(38400); + in.begin(9600); + + // fuGPS.sendCommand(FUGPS_PMTK_SET_NMEA_BAUDRATE_9600); + // fuGPS.sendCommand(FUGPS_PMTK_SET_NMEA_UPDATERATE_1HZ); + fuGPS.sendCommand(FUGPS_PMTK_API_SET_NMEA_OUTPUT_RMCGGA); +} + +void loop() +{ + // Valid NMEA message + if (fuGPS.read()) + { + // We don't know, which message was came first (GGA or RMC). + // Thats why some fields may be empty. + + gpsAlive = true; + + Serial.print("Quality: "); + Serial.println(fuGPS.Quality); + + Serial.print("Satellites: "); + Serial.println(fuGPS.Satellites); + + if (fuGPS.hasFix() == true) + { + // Data from GGA message + Serial.print("Accuracy (HDOP): "); + Serial.println(fuGPS.Accuracy); + + Serial.print("Altitude (above sea level): "); + Serial.println(fuGPS.Altitude); + + // Data from GGA or RMC + Serial.print("Location (decimal degrees): "); + Serial.println("https://www.google.com/maps/search/?api=1&query=" + String(fuGPS.Latitude, 6) + "," + String(fuGPS.Longitude, 6)); + } + } + + // Default is 10 seconds + if (fuGPS.isAlive() == false) + { + if (gpsAlive == true) + { + gpsAlive = false; + Serial.println("GPS module not responding with valid data."); + Serial.println("Check wiring or restart."); + } + } +} diff --git a/keywords.txt b/keywords.txt new file mode 100644 index 0000000..876e4e3 --- /dev/null +++ b/keywords.txt @@ -0,0 +1,73 @@ +####################################### +# Syntax Coloring Map For FuGPS Library +####################################### + +####################################### +# Datatypes (KEYWORD1) +####################################### + +FuGPS KEYWORD1 + +####################################### +# Methods and Functions (KEYWORD2) +####################################### + +checksum KEYWORD2 +parseDateTime KEYWORD2 +toDecimal KEYWORD2 +sendCommand KEYWORD2 +getField KEYWORD2 +getMessageId KEYWORD2 +getSentenceId KEYWORD2 +hasFix KEYWORD2 +isAlive KEYWORD2 +isValid KEYWORD2 +read KEYWORD2 +Hours KEYWORD2 +Minutes KEYWORD2 +Seconds KEYWORD2 +Days KEYWORD2 +Months KEYWORD2 +Years KEYWORD2 +Quality KEYWORD2 +Satellites KEYWORD2 +Accuracy KEYWORD2 +Altitude KEYWORD2 +Latitude KEYWORD2 +Longitude KEYWORD2 +Speed KEYWORD2 +Course KEYWORD2 + +####################################### +# Instances (KEYWORD2) +####################################### + +####################################### +# Constants (LITERAL1) +####################################### + +FUGPS_PMTK_CMD_HOT_START LITERAL1 +FUGPS_PMTK_CMD_WARM_START LITERAL1 +FUGPS_PMTK_CMD_COLD_START LITERAL1 +FUGPS_PMTK_CMD_FULL_COLD_START LITERAL1 + +FUGPS_PMTK_API_SET_NMEA_OUTPUT_DEFAULT LITERAL1 +FUGPS_PMTK_API_SET_NMEA_OUTPUT_RMCGGA LITERAL1 +FUGPS_PMTK_API_SET_NMEA_OUTPUT_NONE LITERAL1 + +FUGPS_PMTK_SET_NMEA_BAUDRATE_DEFAULT LITERAL1 +FUGPS_PMTK_SET_NMEA_BAUDRATE_4800 LITERAL1 +FUGPS_PMTK_SET_NMEA_BAUDRATE_9600 LITERAL1 +FUGPS_PMTK_SET_NMEA_BAUDRATE_14400 LITERAL1 +FUGPS_PMTK_SET_NMEA_BAUDRATE_19200 LITERAL1 +FUGPS_PMTK_SET_NMEA_BAUDRATE_38400 LITERAL1 +FUGPS_PMTK_SET_NMEA_BAUDRATE_57600 LITERAL1 +FUGPS_PMTK_SET_NMEA_BAUDRATE_115200 LITERAL1 + +FUGPS_PMTK_SET_NMEA_UPDATERATE_1HZ LITERAL1 +FUGPS_PMTK_SET_NMEA_UPDATERATE_5HZ LITERAL1 +FUGPS_PMTK_SET_NMEA_UPDATERATE_10HZ LITERAL1 + +FUGPS_NMEA_BUFFER_LENGTH LITERAL1 +FUGPS_MAX_TOKENS LITERAL1 +FUGPS_DEBUG LITERAL1 diff --git a/library.properties b/library.properties new file mode 100644 index 0000000..da4719f --- /dev/null +++ b/library.properties @@ -0,0 +1,9 @@ +name=FuGPS Library +version=1.0.0 +author=Mariusz Kacki +maintainer=Mariusz Kacki +sentence=Arduino library for parsing NMEA 0183 (GPS) messages. +paragraph=Works with any GPS module that sends the correct NMEA sentences through the serial port, including GPS/GLONASS/GALILEO/BEIDOU. +category=Sensors +url=https://github.com/fu-hsi/fugps +architectures=* diff --git a/src/FuGPS.cpp b/src/FuGPS.cpp new file mode 100644 index 0000000..25aab9d --- /dev/null +++ b/src/FuGPS.cpp @@ -0,0 +1,282 @@ +/** + * FuGPS Library for Arduino + * Copyright (c) 2018 Mariusz Kacki + * The MIT License (MIT) + * https://github.com/fu-hsi/fugps + */ + +#include "FuGPS.h" + +#ifdef FUGPS_DEBUG +unsigned int FuGPS::gga_counter = 0; +unsigned int FuGPS::rmc_counter = 0; +#endif + +FuGPS::FuGPS(const Stream & _stream) : + _stream(_stream), _state(0), _tokensCount(0), _fix(false), _lastRead(0), + Quality(0), Satellites(0), Accuracy(0), Altitude(0), Latitude(0), Longitude(0), Speed(0), Course(0) +{ +} + +byte FuGPS::checksum(const char * sentence) +{ + byte checksum = 0; + while (*sentence) + { + checksum ^= *sentence++; + } + return checksum; +} + +void FuGPS::parseDateTime(float value, byte & val1, byte & val2, byte & val3) +{ + val1 = (int)(value / 10000); + value -= val1 * 10000.0; + + val2 = (int)(value / 100); + value -= val2 * 100.0; + + val3 = (int)value; +} + +float FuGPS::toDecimal(float coordinate, char coordinateRef) +{ + float decimal = (int)(coordinate / 100) + ((coordinate - ((int)(coordinate / 100) * 100)) / 60); + if (coordinateRef == 'S' || coordinateRef == 'W') + { + return -decimal; + } + else + { + return decimal; + } +} + +void FuGPS::sendCommand(const char * command) +{ + _stream.println(command); +} + +const char * FuGPS::getMessageId() +{ + if (_tokensCount > 0) + { + return _tokens[0]; + } + else + { + return nullptr; + } +} + +const char * FuGPS::getSentenceId() +{ + if (_tokensCount > 0 && strlen(_tokens[0]) == 5) + { + return _tokens[0] + 2; + } + else + { + return nullptr; + } +} + +const char * FuGPS::getField(byte index) +{ + if (index < _tokensCount) + { + return _tokens[index]; + } + else + { + return nullptr; + } +} + +bool FuGPS::hasFix() +{ + return _fix; +} + +bool FuGPS::isAlive(unsigned int timeout) +{ + if (_lastRead == 0) + { + return false; + } + else + { + unsigned long timeDiff = millis() - _lastRead; + return timeDiff <= timeout; + } +} + +bool FuGPS::isValid() +{ + return _tokensCount; +} + +bool FuGPS::read() +{ + static byte idx = 0; + if (_stream.available()) + { + char ch = _stream.read(); + _currentBuff[idx++] = ch; + + switch (ch) + { + case '$': + _currentBuff[idx = 0] = '\0'; + _state = 0; + break; + + case '*': + case '\r': + _currentBuff[--idx] = '\0'; + _state ^= ch; + break; + + case '\n': + { + DPRINT(F("State: ")); + DPRINTLN(_state); + + _currentBuff[--idx] = '\0'; + + // Simple validation based on checksum + if (_state == 39) + { + char * pChecksum = (char *)(&_currentBuff[idx - 2]); + unsigned int givenChecksum; + sscanf((const char *)pChecksum, "%x", &givenChecksum); + + // Cut-off checksum segment + *pChecksum = '\0'; + + DPRINT(F("Checksum is ")); + if (checksum(_currentBuff) == givenChecksum) + { + DPRINTLN(F("valid")); + strcpy(_sentenceBuff, _currentBuff); + + process(); + return true; + } + else + { + DPRINTLN(F("invalid")); + } + } + else + { + DPRINTLN(F("Invalid NMEA message (missing $, * or ")); + }; + + _state = 0; + break; + } + + default: + break; + } + + if (idx >= FUGPS_NMEA_BUFFER_LENGTH) + { + DPRINTLN(F("Buffer overflow")); + _currentBuff[idx--] = '\0'; + } + } + return false; +} + +void FuGPS::process() +{ + _tokensCount = 0; + char * pStart = _sentenceBuff; + char * pString = _sentenceBuff; + + while (*pString != '\0') + { + if (*pString == ',') + { + *pString = '\0'; + _tokens[_tokensCount++] = pStart; + pStart = pString + 1; + } + pString++; + } + + // Count the last token + _tokens[_tokensCount++] = pStart; + + /** + * Little slower than above, but less code and more readable. + * For our misfortune, it does not count the empty values at the end in ESP2866. + * strtok() is considered unsafe, maybe strtok_r() will be better than my function? + * Both were not tested on ESP. + * + * char * token; + * while ((token = strsep(&pStart, ",")) != NULL) + * { + * _tokens[_tokensCount++] = token; + * } + */ + + DPRINT(F("Tokens: ")); + DPRINTLN(_tokensCount); + + // RMC - Recommended Minimum Specific GPS/Transit Data + if (_tokensCount == 13 && strcmp(getSentenceId(), "RMC") == 0) + { +#ifdef FUGPS_DEBUG + rmc_counter++; +#endif + float time = atoi(_tokens[1]); + parseDateTime(time, Hours, Minutes, Seconds); + + _fix = *_tokens[2] == 'A'; + + Latitude = atof(_tokens[3]); + Latitude = toDecimal(Latitude, *_tokens[4]); + + Longitude = atof(_tokens[5]); + Longitude = toDecimal(Longitude, *_tokens[6]); + + Speed = atof(_tokens[7]); + Course = atof(_tokens[8]); + + float _date = atof(_tokens[9]); + parseDateTime(_date, Days, Months, Years); + } + // GGA - Global Positioning System Fix Data + else if (_tokensCount == 15 && strcmp(getSentenceId(), "GGA") == 0) + { +#ifdef FUGPS_DEBUG + gga_counter++; +#endif + float time = atoi(_tokens[1]); + parseDateTime(time, Hours, Minutes, Seconds); + + Latitude = atof(_tokens[2]); + Latitude = toDecimal(Latitude, *_tokens[3]); + + Longitude = atof(_tokens[4]); + Longitude = toDecimal(Longitude, *_tokens[5]); + + Quality = atoi(_tokens[6]); + _fix = Quality > 0; + + Satellites = atoi(_tokens[7]); + Accuracy = atof(_tokens[8]); + Altitude = atof(_tokens[9]); + } + + _lastRead = millis(); + + DPRINT(F("GGA: ")); + DPRINT(gga_counter); + DPRINT(F(", RMC: ")); + DPRINTLN(rmc_counter); + DPRINTLN(); +} diff --git a/src/FuGPS.h b/src/FuGPS.h new file mode 100644 index 0000000..dc31385 --- /dev/null +++ b/src/FuGPS.h @@ -0,0 +1,137 @@ +/** + * FuGPS Library for Arduino + * Copyright (c) 2018 Mariusz Kacki + * The MIT License (MIT) + * https://github.com/fu-hsi/fugps + */ + +#ifndef _FUGPS_h +#define _FUGPS_h + +#if defined(ARDUINO) && ARDUINO >= 100 +#include "arduino.h" +#else +#include "WProgram.h" +#endif + +#define FUGPS_PMTK_CMD_HOT_START "$PMTK101*32" +#define FUGPS_PMTK_CMD_WARM_START "$PMTK102*31" +#define FUGPS_PMTK_CMD_COLD_START "$PMTK103*30" +#define FUGPS_PMTK_CMD_FULL_COLD_START "$PMTK104*37" + +#define FUGPS_PMTK_API_SET_NMEA_OUTPUT_DEFAULT "$PMTK314,-1*04" +#define FUGPS_PMTK_API_SET_NMEA_OUTPUT_RMCGGA "$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28" +#define FUGPS_PMTK_API_SET_NMEA_OUTPUT_NONE "$PMTK314,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28" + +#define FUGPS_PMTK_SET_NMEA_BAUDRATE_DEFAULT "$PMTK251,0*28" +#define FUGPS_PMTK_SET_NMEA_BAUDRATE_4800 "$PMTK251,4800*14" +#define FUGPS_PMTK_SET_NMEA_BAUDRATE_9600 "$PMTK251,9600*17" +#define FUGPS_PMTK_SET_NMEA_BAUDRATE_14400 "$PMTK251,14400*29" +#define FUGPS_PMTK_SET_NMEA_BAUDRATE_19200 "$PMTK251,19200*22" +#define FUGPS_PMTK_SET_NMEA_BAUDRATE_38400 "$PMTK251,38400*27" +#define FUGPS_PMTK_SET_NMEA_BAUDRATE_57600 "$PMTK251,57600*2C" +#define FUGPS_PMTK_SET_NMEA_BAUDRATE_115200 "$PMTK251,115200*1F" + +#define FUGPS_PMTK_SET_NMEA_UPDATERATE_1HZ "$PMTK220,1000*1F" +#define FUGPS_PMTK_SET_NMEA_UPDATERATE_5HZ "$PMTK220,200*2C" +#define FUGPS_PMTK_SET_NMEA_UPDATERATE_10HZ "$PMTK220,100*2F" + +// 80 characters of visible text (plus the line terminators). +#define FUGPS_NMEA_BUFFER_LENGTH 82 + +// 20 is more than we need for GGA and RMC +#define FUGPS_MAX_TOKENS 20 + +// Uncomment for debug +// #define FUGPS_DEBUG + +#ifdef FUGPS_DEBUG +#define DPRINT(...) Serial.print(__VA_ARGS__) +#define DPRINTLN(...) Serial.println(__VA_ARGS__) +#else +#define DPRINT(...) +#define DPRINTLN(...) +#endif + +class FuGPS +{ +private: + const Stream & _stream; + + char _currentBuff[FUGPS_NMEA_BUFFER_LENGTH + 1]; + char _sentenceBuff[FUGPS_NMEA_BUFFER_LENGTH + 1]; + char * _tokens[FUGPS_MAX_TOKENS]; + + byte _state = 0; + byte _tokensCount; + + bool _fix; + unsigned long _lastRead; + +protected: + void process(); + +public: +#ifdef FUGPS_DEBUG + static unsigned int gga_counter; + static unsigned int rmc_counter; +#endif + + FuGPS(const Stream & _stream); + + static byte checksum(const char * sentence); + static void parseDateTime(float data, byte & val1, byte & val2, byte & val3); + static float toDecimal(float coordinate, char coordinateRef); + + void sendCommand(const char* command); + + // Comma separated fields (Zero-based numbering) + const char * getField(byte index); + + // E.g. GRRMC + const char * getMessageId(); + + // E.g. RMC + const char * getSentenceId(); + + // Based on GGA and RMC messages data + bool hasFix(); + + // Checks if module still sends valid data (no matter what message) + bool isAlive(unsigned int timeout = 10000); + + // Checks if we have valid NMEA message (see isAlive()) + bool isValid(); + + // Reads one char (non blocking) + bool read(); + + byte Hours, Minutes, Seconds; + byte Days, Months, Years; + + // Fix Quality + byte Quality; + + // Number of Satellites + byte Satellites; + + // Horizontal Dilution of Precision (HDOP) + float Accuracy; + + // Altitude above mean sea level (m or ft) + float Altitude; + + // Latitude (decimal degrees) + float Latitude; + + // Longitude (decimal degrees) + float Longitude; + + // Speed in knots + float Speed; + + // True course (Track) + float Course; +}; + +#endif