├── .gitignore ├── .github ├── dependabot.yml └── workflows │ └── platformio.yml ├── library.properties ├── .travis.yml ├── platformio.ini ├── make-zip.sh ├── Readme.md ├── examples └── test_ublox_gps │ └── test_ublox_gps.ino └── src ├── Sodaq_UBlox_GPS.h └── Sodaq_UBlox_GPS.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.zip 3 | 4 | # Doxygen stuff 5 | html/ 6 | latex/ 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Set update schedule for GitHub Actions 2 | version: 2 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | -------------------------------------------------------------------------------- /library.properties: -------------------------------------------------------------------------------- 1 | name=Sodaq_UBlox_GPS 2 | version=0.9.6 3 | author=keestux,SODAQ 4 | maintainer=Kees Bakker 5 | sentence=An Arduino library for the UBlox EVA7M (as available on LoRaONE). 6 | paragraph=It reads GPS coordinate, time, number of satellites, etc. 7 | category=Communication 8 | url=https://github.com/SodaqMoja/Sodaq_UBlox_GPS 9 | architectures=samd 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | 5 | # Cache PlatformIO packages using Travis CI container-based infrastructure 6 | sudo: false 7 | cache: 8 | directories: 9 | - "~/.platformio" 10 | 11 | env: 12 | matrix: 13 | - PLATFORMIO_CI_SRC=examples/test_ublox_gps 14 | 15 | install: 16 | - pip install -U platformio 17 | - pio update 18 | 19 | script: 20 | - platformio ci --lib="./src" --project-conf platformio.ini 21 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; http://docs.platformio.org/page/projectconf.html 10 | 11 | [env:sodaq_autonomo] 12 | platform = atmelsam 13 | board = sodaq_one 14 | framework = arduino 15 | 16 | ;lib_deps = 17 | ; Sodaq_wdt 18 | -------------------------------------------------------------------------------- /make-zip.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Make a ZIP file for distribution / publishing. 4 | 5 | #VERSION=$(git describe --abbrev=0 --tags 2> /dev/null) 6 | VERSION=$(git describe --tags 2> /dev/null) 7 | [ -z "${VERSION}" ] && { echo "ERROR: Cannot determine version. Did you do a git-tag?"; exit 1; } 8 | 9 | FILES=$(git ls-files|grep -v .gitignore) 10 | [ -z "${FILES}" ] && { echo "ERROR: No files to put in ZIP."; exit 1; } 11 | 12 | DIRNAME="$(basename $(pwd))" 13 | ZIPNAME="${DIRNAME}-${VERSION}" 14 | 15 | TDIR=/tmp/$DIRNAME 16 | [ -d "${TDIR}" ] && { echo "ERROR: Directory '${TDIR}' exists. Please remove."; exit 1; } 17 | 18 | mkdir -p ${TDIR} 19 | git ls-files | 20 | grep -v .gitignore | 21 | cpio -pmud ${TDIR}/ 22 | (cd /tmp && zip -r $ZIPNAME.zip $DIRNAME) 23 | mv /tmp/$ZIPNAME.zip . 24 | ln -sf -T $ZIPNAME.zip ${DIRNAME}.zip 25 | rm -fr ${TDIR} 26 | -------------------------------------------------------------------------------- /.github/workflows/platformio.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 1 * * SUN' 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | os: [ubuntu-20.04] 15 | python-version: [3.7] 16 | example: 17 | - "examples/test_ublox_gps" 18 | runs-on: ${{ matrix.os }} 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v2 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | - name: Cache pip 26 | uses: actions/cache@v2 27 | with: 28 | path: ~/.cache/pip 29 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} 30 | restore-keys: | 31 | ${{ runner.os }}-pip- 32 | - name: Cache PlatformIO 33 | uses: actions/cache@v2 34 | with: 35 | path: ~/.platformio 36 | key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} 37 | - name: Install PlatformIO 38 | run: | 39 | python -m pip install --upgrade pip 40 | pip install --upgrade platformio 41 | - name: Build examples 42 | env: 43 | PLATFORMIO_CI_SRC: ${{ matrix.example }} 44 | run: | 45 | pio ci --lib="./src" --project-conf platformio.ini 46 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Sodaq_UBlox_GPS 2 | 3 | Arduino library for using the UBlox EVA7M. 4 | 5 | [![CI](https://github.com/SodaqMoja/Sodaq_UBlox_GPS/workflows/CI/badge.svg)](https://github.com/SodaqMoja/Sodaq_UBlox_GPS/actions?query=workflow%3ACI) 6 | 7 | ## Usage 8 | 9 | Quick example: 10 | 11 | ```c 12 | #include 13 | #include 14 | 15 | #define MySerial SERIAL_PORT_MONITOR 16 | 17 | void setup() 18 | { 19 | delay(3000); 20 | while (!SerialUSB) { 21 | // Wait for USB to connect 22 | } 23 | 24 | MySerial.begin(57600); 25 | do_flash_led(LED_BLUE); 26 | 27 | MySerial.println("SODAQ LoRaONE test_gps is starting ..."); 28 | 29 | sodaq_gps.init(GPS_ENABLE); 30 | sodaq_gps.setDiag(MySerial); 31 | } 32 | 33 | void loop() 34 | { 35 | MySerial.println("waiting in loop() ..."); 36 | // Try to get a GPS scan and when we get a fix we print our location. We are 37 | // passing true to keep the GPS enabled after a scan 38 | if (sodaq_gps.scan(true)) 39 | { 40 | MySerial.print("We are at latitude "); 41 | MySerial.print(sodaq_gps.getLat(), 13); 42 | MySerial.print(" longitude "); 43 | MySerial.print(sodaq_gps.getLon(), 13); 44 | MySerial.print(" altitude "); 45 | MySerial.print(sodaq_gps.getAlt(), 1); 46 | MySerial.print(" and HDOP "); 47 | MySerial.println(sodaq_gps.getHDOP(), 2); 48 | } 49 | delay(60000); 50 | } 51 | 52 | ``` 53 | 54 | Method|Description 55 | ------|------ 56 | **init (int8_t enable_pin)**|Initializes the UBlox, and switches it on. 57 | **scan (bool leave_on=false, uint32_t timeout=20000)**|Scans all NMEA messages until lat/long is seen. It returns true when a fix is found. 58 | 59 | 60 | ## Contributing 61 | 62 | 1. Fork it! 63 | 2. Create your feature branch: `git checkout -b my-new-feature` 64 | 3. Commit your changes: `git commit -am 'Add some feature'` 65 | 4. Push to the branch: `git push origin my-new-feature` 66 | 5. Submit a pull request 67 | 68 | ## License 69 | 70 | Copyright (c) 2016 SODAQ. All rights reserved. 71 | 72 | This file is part of Sodaq_UBlox_GPS. 73 | 74 | Sodaq_UBlox_GPS is free software: you can redistribute it and/or 75 | modify it under the terms of the GNU Lesser General Public License as 76 | published by the Free Software Foundation, either version 3 of the 77 | License, or(at your option) any later version. 78 | 79 | Sodaq_UBlox_GPS is distributed in the hope that it will be useful, but 80 | WITHOUT ANY WARRANTY; without even the implied warranty of 81 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 82 | Lesser General Public License for more details. 83 | 84 | You should have received a copy of the GNU Lesser General Public 85 | License along with Sodaq_UBlox_GPS. If not, see 86 | . 87 | -------------------------------------------------------------------------------- /examples/test_ublox_gps/test_ublox_gps.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #define MySerial SERIAL_PORT_MONITOR 6 | #define ARRAY_DIM(arr) (sizeof(arr) / sizeof(arr[0])) 7 | 8 | // List of interval values to be used in loop() 9 | // to measure how long it takes to get a fix. 10 | uint32_t intervals[] = { 11 | 12 | // Do a few tests with 1 minute delay 13 | 1UL * 60 * 1000, 14 | 1UL * 60 * 1000, 15 | 1UL * 60 * 1000, 16 | 17 | // Try a few longer delays 18 | 2UL * 60 * 1000, 19 | 2UL * 60 * 1000, 20 | 5UL * 60 * 1000, 21 | 5UL * 60 * 1000, 22 | 23 | // Slowly increase the delays 24 | 15UL * 60 * 1000, 25 | 30UL * 60 * 1000, 26 | 1UL * 60 * 60 * 1000, 27 | 3UL * 60 * 60 * 1000, 28 | 4UL * 60 * 60 * 1000, 29 | 8UL * 60 * 60 * 1000, 30 | }; 31 | size_t interval_ix = 0; 32 | 33 | void find_fix(uint32_t delay_until); 34 | void do_flash_led(int pin); 35 | 36 | void setup() 37 | { 38 | delay(3000); 39 | while (!SerialUSB) { 40 | // Wait for USB to connect 41 | } 42 | 43 | MySerial.begin(57600); 44 | 45 | digitalWrite(LED_RED, HIGH); 46 | pinMode(LED_RED, OUTPUT); 47 | digitalWrite(LED_GREEN, HIGH); 48 | pinMode(LED_GREEN, OUTPUT); 49 | digitalWrite(LED_BLUE, HIGH); 50 | pinMode(LED_BLUE, OUTPUT); 51 | 52 | do_flash_led(LED_RED); 53 | do_flash_led(LED_GREEN); 54 | do_flash_led(LED_BLUE); 55 | 56 | MySerial.println("SODAQ LoRaONE test_gps is starting ..."); 57 | 58 | sodaq_gps.init(GPS_ENABLE); 59 | 60 | // This is for debugging to see more details, more messages 61 | // Use this in combination with setDiag() 62 | //sodaq_gps.setMinNumOfLines(10); 63 | 64 | // Uncomment the next line if you want to see the incoming $GPxxx messages 65 | //sodaq_gps.setDiag(MySerial); 66 | 67 | // First time finding a fix 68 | find_fix(0); 69 | } 70 | 71 | void loop() 72 | { 73 | uint32_t wait_ms = intervals[interval_ix]; 74 | if (++interval_ix > ARRAY_DIM(intervals)) { 75 | interval_ix = 0; 76 | } 77 | find_fix(wait_ms); 78 | } 79 | 80 | /*! 81 | * Find a GPS fix, but first wait a while 82 | */ 83 | void find_fix(uint32_t delay_until) 84 | { 85 | MySerial.println(String("delay ... ") + delay_until + String("ms")); 86 | delay(delay_until); 87 | 88 | uint32_t start = millis(); 89 | uint32_t timeout = 900L * 1000; 90 | MySerial.println(String("waiting for fix ..., timeout=") + timeout + String("ms")); 91 | if (sodaq_gps.scan(false, timeout)) { 92 | MySerial.println(String(" time to find fix: ") + (millis() - start) + String("ms")); 93 | MySerial.println(String(" datetime = ") + sodaq_gps.getDateTimeString()); 94 | MySerial.println(String(" lat = ") + String(sodaq_gps.getLat(), 7)); 95 | MySerial.println(String(" lon = ") + String(sodaq_gps.getLon(), 7)); 96 | MySerial.println(String(" num sats = ") + String(sodaq_gps.getNumberOfSatellites())); 97 | } else { 98 | MySerial.println("No Fix"); 99 | } 100 | } 101 | 102 | void do_flash_led(int pin) 103 | { 104 | for (size_t i = 0; i < 2; ++i) { 105 | delay(100); 106 | digitalWrite(pin, LOW); 107 | delay(100); 108 | digitalWrite(pin, HIGH); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Sodaq_UBlox_GPS.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 SODAQ. All rights reserved. 3 | * 4 | * This file is part of Sodaq_UBlox_GPS. 5 | * 6 | * Sodaq_UBlox_GPS is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Lesser General Public License as published 8 | * by the Free Software Foundation, either version 3 of the License, or (at 9 | * your option) any later version. 10 | * 11 | * Sodaq_UBlox_GPS is distributed in the hope that it will be useful, but 12 | * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 14 | * License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with Sodaq_UBlox_GPS. If not, see . 18 | */ 19 | 20 | #ifndef _SODAQ_UBLOX_GPS_H 21 | #define _SODAQ_UBLOX_GPS_H 22 | 23 | #include 24 | #include 25 | #include 26 | 27 | class Sodaq_UBlox_GPS 28 | { 29 | public: 30 | Sodaq_UBlox_GPS(); 31 | 32 | void init(int8_t enable_pin); 33 | bool scan(bool leave_on = false, uint32_t timeout = 20000); 34 | String getDateTimeString(); 35 | double getLat() { return _lat; } 36 | double getLon() { return _lon; } 37 | double getAlt() { return _alt; } 38 | double getSpeed() { return _speed; } 39 | double getHDOP() { return _hdop; } 40 | uint8_t getNumberOfSatellites() { return _numSatellites; } 41 | uint16_t getYear() { return (uint16_t)_yy + 2000; } // 2016.. 42 | uint8_t getMonth() { return _MM; } // 1.. 43 | uint8_t getDay() { return _dd; } // 1.. 44 | uint8_t getHour() { return _hh; } // 0.. 45 | uint8_t getMinute() { return _mm; } // 0.. 46 | uint8_t getSecond() { return _ss; } // 0.. 47 | 48 | // How many extra lines must scan see before it stops? This is merely for debugging 49 | void setMinNumOfLines(size_t num) { _minNumOfLines = num; } 50 | 51 | // The minimum number of satellites to satisfy scan 52 | void setMinNumSatellites(size_t num) { _minNumSatellites = num; } 53 | 54 | // Sets the optional "Diagnostics and Debug" stream. 55 | void setDiag(Stream &stream) { _diagStream = &stream; } 56 | void setDiag(Stream *stream) { _diagStream = stream; } 57 | 58 | private: 59 | void on(); 60 | void off(); 61 | 62 | // Read one byte 63 | uint8_t read(); 64 | bool readLine(uint32_t timeout = 10000); 65 | bool parseLine(const char * line); 66 | bool parseGPGGA(const String & line); 67 | bool parseGPGSA(const String & line); 68 | bool parseGPRMC(const String & line); 69 | bool parseGPGSV(const String & line); 70 | bool parseGPGLL(const String & line); 71 | bool parseGPVTG(const String & line); 72 | bool parseGPTXT(const String & line); 73 | bool computeCrc(const char * line, bool do_logging = false); 74 | uint8_t getHex2(const char * s, size_t index); 75 | String num2String(int num, size_t width); 76 | String getField(const String & data, int index); 77 | double convertDegMinToDecDeg(const String & data); 78 | 79 | void setDateTime(const String & date, const String & time); 80 | 81 | void beginTransmission(); 82 | void endTransmission(); 83 | 84 | void resetValues(); 85 | 86 | int8_t _enablePin; 87 | 88 | // The (optional) stream to show debug information. 89 | Stream * _diagStream; 90 | 91 | uint8_t _addr; 92 | 93 | size_t _minNumOfLines; 94 | 95 | // Minimum number of satellites to satisfy scan(). Zero means any number is OK. 96 | size_t _minNumSatellites; 97 | 98 | bool _seenLatLon; 99 | bool _seenAlt; 100 | uint8_t _numSatellites; 101 | double _lat; 102 | double _speed; 103 | double _lon; 104 | double _alt; 105 | double _hdop; 106 | 107 | bool _seenTime; 108 | uint8_t _yy; 109 | uint8_t _MM; 110 | uint8_t _dd; 111 | uint8_t _hh; 112 | uint8_t _mm; 113 | uint8_t _ss; 114 | 115 | bool _trans_active; 116 | 117 | static const char _fieldSep; 118 | char * _inputBuffer; 119 | size_t _inputBufferSize; 120 | }; 121 | 122 | extern Sodaq_UBlox_GPS sodaq_gps; 123 | 124 | #endif // _SODAQ_UBLOX_GPS_H 125 | -------------------------------------------------------------------------------- /src/Sodaq_UBlox_GPS.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 SODAQ. All rights reserved. 3 | * 4 | * This file is part of Sodaq_UBlox_GPS. 5 | * 6 | * Sodaq_UBlox_GPS is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU Lesser General Public License as published 8 | * by the Free Software Foundation, either version 3 of the License, or (at 9 | * your option) any later version. 10 | * 11 | * Sodaq_UBlox_GPS is distributed in the hope that it will be useful, but 12 | * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 14 | * License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public License 17 | * along with Sodaq_UBlox_GPS. If not, see . 18 | */ 19 | 20 | #include 21 | #include 22 | #include "Sodaq_UBlox_GPS.h" 23 | 24 | #define DEBUG 1 25 | #ifdef DEBUG 26 | #define debugPrintLn(...) { if (this->_diagStream) this->_diagStream->println(__VA_ARGS__); } 27 | #define debugPrint(...) { if (this->_diagStream) this->_diagStream->print(__VA_ARGS__); } 28 | #else 29 | #define debugPrintLn(...) 30 | #define debugPrint(...) 31 | #endif 32 | 33 | static const size_t INPUT_BUFFER_SIZE = 128; // TODO Check UBlox manual ReceiverDescProtSpec 34 | static const uint8_t UBlox_I2C_addr = 0x42; 35 | 36 | #define GPS_ENABLE_ON HIGH 37 | #define GPS_ENABLE_OFF LOW 38 | 39 | Sodaq_UBlox_GPS sodaq_gps; 40 | const char Sodaq_UBlox_GPS::_fieldSep = ','; 41 | 42 | static inline bool is_timedout(uint32_t from, uint32_t nr_ms) __attribute__((always_inline)); 43 | static inline bool is_timedout(uint32_t from, uint32_t nr_ms) 44 | { 45 | return (millis() - from) > nr_ms; 46 | } 47 | 48 | Sodaq_UBlox_GPS::Sodaq_UBlox_GPS() 49 | { 50 | _enablePin = -1; 51 | 52 | _diagStream = 0; 53 | _addr = UBlox_I2C_addr; 54 | 55 | _minNumOfLines = 0; 56 | _minNumSatellites = 0; 57 | 58 | resetValues(); 59 | 60 | _trans_active = false; 61 | _inputBuffer = static_cast(malloc(INPUT_BUFFER_SIZE)); 62 | _inputBufferSize = INPUT_BUFFER_SIZE; 63 | } 64 | 65 | void Sodaq_UBlox_GPS::resetValues() 66 | { 67 | _seenLatLon = false; 68 | _seenAlt = false; 69 | _numSatellites = 0; 70 | _lat = 0; 71 | _lon = 0; 72 | 73 | _seenTime = false; 74 | _hh = 0; 75 | _mm = 0; 76 | _ss = 0; 77 | _yy = 0; 78 | _MM = 0; 79 | _dd = 0; 80 | } 81 | 82 | void Sodaq_UBlox_GPS::init(int8_t enable_pin) 83 | { 84 | _enablePin = enable_pin; 85 | Wire.begin(); 86 | digitalWrite(_enablePin, GPS_ENABLE_OFF); 87 | pinMode(_enablePin, OUTPUT); 88 | } 89 | 90 | /*! 91 | * Read the UBlox device until a fix is seen, or until 92 | * a timeout has been reached. 93 | */ 94 | bool Sodaq_UBlox_GPS::scan(bool leave_on, uint32_t timeout) 95 | { 96 | bool retval = false; 97 | uint32_t start = millis(); 98 | resetValues(); 99 | 100 | on(); 101 | delay(500); // TODO Is this needed? 102 | 103 | size_t fix_count = 0; 104 | while (!is_timedout(start, timeout)) { 105 | if (!readLine()) { 106 | // TODO Maybe quit? 107 | continue; 108 | } 109 | parseLine(_inputBuffer); 110 | 111 | // Which conditions are required to quit the scan? 112 | if (_seenLatLon 113 | && _seenTime 114 | && _seenAlt 115 | && (_minNumSatellites == 0 || _numSatellites >= _minNumSatellites)) { 116 | ++fix_count; 117 | if (fix_count >= _minNumOfLines) { 118 | retval = true; 119 | break; 120 | } 121 | } 122 | } 123 | 124 | if (_numSatellites > 0) { 125 | debugPrintLn(String("[scan] num sats = ") + _numSatellites); 126 | } 127 | if (_seenTime) { 128 | debugPrintLn(String("[scan] datetime = ") + getDateTimeString()); 129 | } 130 | if (_seenLatLon) { 131 | debugPrintLn(String("[scan] lat = ") + String(_lat, 7)); 132 | debugPrintLn(String("[scan] lon = ") + String(_lon, 7)); 133 | } 134 | 135 | if (!leave_on) { 136 | off(); 137 | } 138 | return retval; 139 | } 140 | 141 | String Sodaq_UBlox_GPS::getDateTimeString() 142 | { 143 | return num2String(getYear(), 4) 144 | + num2String(getMonth(), 2) 145 | + num2String(getDay(), 2) 146 | + num2String(getHour(), 2) 147 | + num2String(getMinute(), 2) 148 | + num2String(getSecond(), 2); 149 | } 150 | 151 | bool Sodaq_UBlox_GPS::parseLine(const char * line) 152 | { 153 | //debugPrintLn(String("= ") + line); 154 | if (!computeCrc(line, false)) { 155 | // Redo the check, with logging 156 | computeCrc(line, true); 157 | return false; 158 | } 159 | String data = line + 1; 160 | data.remove(data.length() - 3, 3); // Strip checksum * 161 | 162 | if (data.startsWith("GPGGA") || data.startsWith("GNGGA")) { 163 | return parseGPGGA(data); 164 | } 165 | 166 | if (data.startsWith("GPGSA") || data.startsWith("GNGSA")) { 167 | return parseGPGSA(data); 168 | } 169 | 170 | if (data.startsWith("GPRMC") || data.startsWith("GNRMC")) { 171 | return parseGPRMC(data); 172 | } 173 | 174 | if (data.startsWith("GPGSV")) { 175 | return parseGPGSV(data); 176 | } 177 | 178 | if (data.startsWith("GPGLL") || data.startsWith("GNGLL")) { 179 | return parseGPGLL(data); 180 | } 181 | 182 | if (data.startsWith("GPVTG") || data.startsWith("GNVTG")) { 183 | return parseGPVTG(data); 184 | } 185 | 186 | if (data.startsWith("GPTXT") || data.startsWith("$GPTXT")) { 187 | return parseGPTXT(data); 188 | } 189 | 190 | debugPrintLn(String("?? >> ") + line); 191 | return false; 192 | } 193 | 194 | /*! 195 | * Read the coordinates using $GPGGA 196 | * See also section 24.3 of u-blox 7, Receiver Description. Document number: GPS.G7-SW-12001-B 197 | * 198 | * See section "Decode of selected position sentences" at 199 | * http://www.gpsinformation.org/dale/nmea.htm 200 | * The most important NMEA sentences include the GGA which provides the 201 | * current Fix data, the RMC which provides the minimum gps sentences 202 | * information, and the GSA which provides the Satellite status data. 203 | * 204 | * 0 $GPGGA 205 | * 1 time hhmmss.ss UTC time 206 | * 2 lat ddmm.mmmmm Latitude (degrees & minutes) 207 | * 3 NS char North/South indicator 208 | * 4 long dddmm.mmmmm Longitude (degrees & minutes) 209 | * 5 EW char East/West indicator 210 | * 6 quality digit Quality indicator for position fix: 0 No Fix, 6 Estimated, 1 Auto GNSS, 2 Diff GNSS 211 | * 7 numSV num Number of satellites used 212 | * 8 HDOP num Horizontal Dilution of Precision 213 | * 9 alt num Altitude above mean sea level 214 | * 10 uAlt char Altitude units: meters 215 | * 11 sep num Geoid separation: difference between geoid and mean sea level 216 | * 12 uSep char Separation units: meters 217 | * 13 diffAge num Age of differential corrections 218 | * 14 diffStation num ID of station providing differential corrections 219 | * 15 checksum 2 hex digits 220 | */ 221 | bool Sodaq_UBlox_GPS::parseGPGGA(const String & line) 222 | { 223 | debugPrintLn("parseGPGGA"); 224 | debugPrintLn(String(">> ") + line); 225 | if (getField(line, 6) != "0") { 226 | _lat = convertDegMinToDecDeg(getField(line, 2)); 227 | if (getField(line, 3) == "S") { 228 | _lat = -_lat; 229 | } 230 | _lon = convertDegMinToDecDeg(getField(line, 4)); 231 | if (getField(line, 5) == "W") { 232 | _lon = -_lon; 233 | } 234 | _seenLatLon = true; 235 | 236 | _hdop = getField(line, 8).toFloat(); 237 | if (getField(line, 10) == "M") { 238 | _alt = getField(line, 9).toFloat(); 239 | _seenAlt = true; 240 | } 241 | } 242 | 243 | _numSatellites = getField(line, 7).toInt(); 244 | return true; 245 | } 246 | 247 | /*! 248 | * Parse GPGSA line 249 | * GNSS DOP and Active Satellites 250 | */ 251 | bool Sodaq_UBlox_GPS::parseGPGSA(const String & line) 252 | { 253 | // Not (yet) used 254 | debugPrintLn("parseGPGSA"); 255 | debugPrintLn(String(">> ") + line); 256 | return false; 257 | } 258 | 259 | /*! 260 | * Read the coordinates using $GPRMC 261 | * See also section 24.13 of u-blox 7, Receiver Description. Document number: GPS.G7-SW-12001-B 262 | * 263 | * 0 $GPRMC 264 | * 1 time hhmmss.ss UTC time 265 | * 2 status char Status, V = Navigation receiver warning, A = Data valid 266 | * 3 lat ddmm.mmmmm Latitude (degrees & minutes) 267 | * 4 NS char North/South 268 | * 5 long dddmm.mmmmm Longitude (degrees & minutes) 269 | * 6 EW char East/West 270 | * 7 spd num Speed over ground 271 | * 8 cog num Course over ground 272 | * 9 date ddmmyy Date in day, month, year format 273 | * 10 mv num Magnetic variation value 274 | * 11 mvEW char Magnetic variation E/W indicator 275 | * 12 posMode char Mode Indicator: 'N' No Fix, 'E' Estimate, 'A' Auto GNSS, 'D' Diff GNSS 276 | * 13 checksum 2 hex digits Checksum 277 | */ 278 | bool Sodaq_UBlox_GPS::parseGPRMC(const String & line) 279 | { 280 | debugPrintLn("parseGPRMC"); 281 | debugPrintLn(String(">> ") + line); 282 | 283 | if (getField(line, 2) == "A" && getField(line, 12) != "N") { 284 | _lat = convertDegMinToDecDeg(getField(line, 3)); 285 | if (getField(line, 4) == "S") { 286 | _lat = -_lat; 287 | } 288 | _lon = convertDegMinToDecDeg(getField(line, 5)); 289 | if (getField(line, 6) == "W") { 290 | _lon = -_lon; 291 | } 292 | _seenLatLon = true; 293 | } 294 | 295 | String time = getField(line, 1); 296 | String date = getField(line, 9); 297 | 298 | String speed = getField(line, 7); 299 | _speed = speed.toDouble(); 300 | 301 | setDateTime(date, time); 302 | 303 | return true; 304 | } 305 | 306 | /*! 307 | * Parse GPGSV line 308 | * See also section 24.12 of u-blox 7, Receiver Description. Document number: GPS.G7-SW-12001-B 309 | * 310 | * 0 $GPGSV 311 | * 1 numMsg digit Number of messages, total number of GSV messages being output 312 | * 2 msgNum digit Number of this message 313 | * 3 numSV num Number of satellites in view 314 | * 315 | * 4 sv num Satellite ID 316 | * 5 elv num Elevation (range 0-90) 317 | * 6 az num Azimuth, (range 0-359) 318 | * 7 cno num Signal strength (C/N0, range 0-99), blank when not tracking 319 | * 320 | * fields 4..7 are repeated for each satellite in this message 321 | */ 322 | bool Sodaq_UBlox_GPS::parseGPGSV(const String & line) 323 | { 324 | debugPrintLn("parseGPGSV"); 325 | debugPrintLn(String(">> ") + line); 326 | 327 | // We could/should only use msgNum == 1. However, all messages should have 328 | // the same numSV. 329 | _numSatellites = getField(line, 3).toInt(); 330 | 331 | return true; 332 | } 333 | 334 | /*! 335 | * Parse GPGLL line 336 | * Latitude and longitude, with time of position fix and status 337 | */ 338 | bool Sodaq_UBlox_GPS::parseGPGLL(const String & line) 339 | { 340 | // Not (yet) used 341 | debugPrintLn("parseGPGLL"); 342 | debugPrintLn(String(">> ") + line); 343 | return false; 344 | } 345 | 346 | /*! 347 | * Parse GPVTG line 348 | * Course over ground and Ground speed 349 | */ 350 | bool Sodaq_UBlox_GPS::parseGPVTG(const String & line) 351 | { 352 | // Not (yet) used 353 | debugPrintLn("parseGPVTG"); 354 | debugPrintLn(String(">> ") + line); 355 | return false; 356 | } 357 | 358 | /*! 359 | * Parse GPTXT line 360 | * See also section 24.13 of u-blox 7, Receiver Description. Document number: GPS.G7-SW-12001-B 361 | * $GPTXT,01,01,02,ANTSTATUS=INIT*25 362 | * ... 363 | * $GPTXT,01,01,02,ANTSTATUS=OK*3B 364 | * 365 | * 0 $GPTXT 366 | * 1 numMsg num Total number of messages in this transmission 367 | * 2 msgNum num Message number in this transmission 368 | * 3 msgType num Text identifier, 00: Error, 01: Warning, 02: Notice, 07: User 369 | * 4 text string Any ASCII text 370 | * 13 checksum 2 hex digits Checksum 371 | */ 372 | bool Sodaq_UBlox_GPS::parseGPTXT(const String & line) 373 | { 374 | //debugPrintLn("parseGPTXT"); 375 | //debugPrintLn(String(">> ") + line); 376 | debugPrintLn(String("TXT: \"") + getField(line, 4) + "\""); 377 | return true; 378 | } 379 | 380 | /*! 381 | * Compute and verify the checksum 382 | * 383 | * Each line must start with '$' 384 | * Each line must end with '*' 385 | */ 386 | bool Sodaq_UBlox_GPS::computeCrc(const char * line, bool do_logging) 387 | { 388 | if (do_logging) { 389 | debugPrint(line); 390 | } 391 | size_t len = strlen(line); 392 | if (len < 4) { 393 | if (do_logging) { 394 | debugPrint(" Invalid short: "); 395 | debugPrintLn(len); 396 | } 397 | return false; 398 | } 399 | if (line[0] != '$') { 400 | if (do_logging) { 401 | debugPrintLn(" Invalid$"); 402 | } 403 | return false; 404 | } 405 | if (line[len - 3] != '*') { 406 | if (do_logging) { 407 | debugPrintLn(" Invalid*"); 408 | } 409 | return false; 410 | } 411 | 412 | uint8_t crc = 0; 413 | for (size_t i = 1; i < len - 3; ++i) { 414 | crc ^= line[i]; 415 | } 416 | 417 | uint8_t crc1 = getHex2(line, len - 2); 418 | if (crc != crc1) { 419 | if (do_logging) { 420 | debugPrint(" INVALID CRC "); 421 | debugPrint(crc1, HEX); 422 | debugPrint(" EXPECTED "); 423 | debugPrintLn(crc, HEX); 424 | } 425 | return false; 426 | } 427 | 428 | //debugSerial.println(" OK"); 429 | return true; 430 | } 431 | 432 | uint8_t Sodaq_UBlox_GPS::getHex2(const char * s, size_t index) 433 | { 434 | uint8_t val = 0; 435 | char c; 436 | c = s[index]; 437 | if (c >= '0' && c <= '9') { 438 | val += c - '0'; 439 | } 440 | else if (c >= 'a' && c <= 'f') { 441 | val += c - 'a' + 10; 442 | } 443 | else if (c >= 'A' && c <= 'F') { 444 | val += c - 'A' + 10; 445 | } 446 | val <<= 4; 447 | c = s[++index]; 448 | if (c >= '0' && c <= '9') { 449 | val += c - '0'; 450 | } 451 | else if (c >= 'a' && c <= 'f') { 452 | val += c - 'a' + 10; 453 | } 454 | else if (c >= 'A' && c <= 'F') { 455 | val += c - 'A' + 10; 456 | } 457 | return val; 458 | } 459 | 460 | String Sodaq_UBlox_GPS::num2String(int num, size_t width) 461 | { 462 | String out; 463 | out = num; 464 | while (out.length() < width) { 465 | out = String("0") + out; 466 | } 467 | return out; 468 | } 469 | 470 | String Sodaq_UBlox_GPS::getField(const String & data, int index) 471 | { 472 | int found = 0; 473 | int strIndex[] = { 0, -1 }; 474 | int maxIndex = data.length() - 1; 475 | 476 | for (int i = 0; i <= maxIndex && found <= index; i++) { 477 | if (data.charAt(i) == _fieldSep || i == maxIndex) { 478 | found++; 479 | strIndex[0] = strIndex[1] + 1; 480 | strIndex[1] = (i == maxIndex) ? i + 1 : i; 481 | } 482 | } 483 | 484 | return found > index ? data.substring(strIndex[0], strIndex[1]) : ""; 485 | } 486 | 487 | /* 488 | * Convert lat/long degree-minute format to decimal-degrees 489 | * 490 | * This code is from 491 | * http://arduinodev.woofex.net/2013/02/06/adafruit_gps_forma/ 492 | * 493 | * According to the NMEA Standard, Latitude and Longitude are output in the format Degrees, Minutes and 494 | * (Decimal) Fractions of Minutes. To convert to Degrees and Fractions of Degrees, or Degrees, Minutes, Seconds 495 | * and Fractions of seconds, the 'Minutes' and 'Fractional Minutes' parts need to be converted. In other words: If 496 | * the GPS Receiver reports a Latitude of 4717.112671 North and Longitude of 00833.914843 East, this is 497 | * Latitude 47 Degrees, 17.112671 Minutes 498 | * Longitude 8 Degrees, 33.914843 Minutes 499 | * or 500 | * Latitude 47 Degrees, 17 Minutes, 6.76026 Seconds 501 | * Longitude 8 Degrees, 33 Minutes, 54.89058 Seconds 502 | * or 503 | * Latitude 47.28521118 Degrees 504 | * Longitude 8.56524738 Degrees 505 | */ 506 | double Sodaq_UBlox_GPS::convertDegMinToDecDeg(const String & data) 507 | { 508 | double degMin = data.toFloat(); 509 | double min = 0.0; 510 | double decDeg = 0.0; 511 | 512 | //get the minutes, fmod() requires double 513 | min = fmod((double)degMin, 100.0); 514 | 515 | //rebuild coordinates in decimal degrees 516 | degMin = (int)(degMin / 100); 517 | decDeg = degMin + (min / 60); 518 | 519 | return decDeg; 520 | } 521 | 522 | void Sodaq_UBlox_GPS::setDateTime(const String & date, const String & time) 523 | { 524 | if (time.length() == 9 && date.length() == 6) { 525 | _hh = time.substring(0, 2).toInt(); 526 | _mm = time.substring(2, 4).toInt(); 527 | _ss = time.substring(4, 6).toInt(); 528 | _dd = date.substring(0, 2).toInt(); 529 | _MM = date.substring(2, 4).toInt(); 530 | _yy = date.substring(4, 6).toInt(); 531 | _seenTime = true; 532 | } 533 | } 534 | 535 | /*! 536 | * Read one NMEA frame 537 | */ 538 | bool Sodaq_UBlox_GPS::readLine(uint32_t timeout) 539 | { 540 | if (!_inputBuffer) { 541 | return false; 542 | } 543 | 544 | uint32_t start = millis(); 545 | char c; 546 | char *ptr = _inputBuffer; 547 | size_t cnt = 0; 548 | *ptr = '\0'; 549 | 550 | c = 0; 551 | while (!is_timedout(start, timeout)) { 552 | c = (char)read(); 553 | if (c == '$') { 554 | break; 555 | } 556 | } 557 | if (c != '$') { 558 | return false; 559 | } 560 | *ptr++ = c; 561 | ++cnt; 562 | 563 | c = 0; 564 | while (!is_timedout(start, timeout)) { 565 | c = (char)read(); 566 | if (c == '\r') { 567 | continue; 568 | } 569 | if (c == '\n') { 570 | break; 571 | } 572 | if (cnt < _inputBufferSize - 1) { 573 | *ptr++ = c; 574 | ++cnt; 575 | } 576 | } 577 | *ptr = '\0'; 578 | if (c != '\n') { 579 | return false; 580 | } 581 | endTransmission(); 582 | return true; 583 | } 584 | 585 | uint8_t Sodaq_UBlox_GPS::read() 586 | { 587 | beginTransmission(); 588 | 589 | uint8_t b = 0xFF; 590 | uint8_t nr_bytes; 591 | nr_bytes = Wire.requestFrom(_addr, 1, false); 592 | if (nr_bytes == 1) { 593 | b = Wire.read(); 594 | } 595 | return b; 596 | } 597 | 598 | void Sodaq_UBlox_GPS::beginTransmission() 599 | { 600 | if (_trans_active) { 601 | return; 602 | } 603 | Wire.beginTransmission(_addr); 604 | _trans_active = true; 605 | } 606 | 607 | void Sodaq_UBlox_GPS::endTransmission() 608 | { 609 | if (!_trans_active) { 610 | return; 611 | } 612 | Wire.endTransmission(); 613 | _trans_active = false; 614 | } 615 | 616 | void Sodaq_UBlox_GPS::on() 617 | { 618 | digitalWrite(_enablePin, GPS_ENABLE_ON); 619 | } 620 | 621 | void Sodaq_UBlox_GPS::off() 622 | { 623 | digitalWrite(_enablePin, GPS_ENABLE_OFF); 624 | } 625 | --------------------------------------------------------------------------------