├── Adafruit Feather └── README.md ├── Raspberry Pi └── README.md ├── TTGO └── README.md ├── Sodaq ├── sodaq-one-ttnmapper │ ├── README.md │ ├── decoder.js │ ├── Sodaq_UBlox_GPS.h │ ├── sodaq-one.ino │ └── Sodaq_UBlox_GPS.cpp └── sodaq-universal-tracker │ └── decoder.js ├── Pycom └── README.md ├── Arduino ├── README.md └── MKR WAN 1300 │ └── LoRaPing │ ├── arduino_secrets.h │ └── LoRaPing.ino ├── README.md ├── RAK811 └── README.md ├── LICENSE └── Digital Matter └── decoder.js /Adafruit Feather/README.md: -------------------------------------------------------------------------------- 1 | https://github.com/Bjoerns-TB/ttn-gps-tracker 2 | -------------------------------------------------------------------------------- /Raspberry Pi/README.md: -------------------------------------------------------------------------------- 1 | # Raspberry Pi 2 | 3 | https://github.com/thomaswesenberg/LoRaWAN-device-GPS-RaspBerry-Pi 4 | -------------------------------------------------------------------------------- /TTGO/README.md: -------------------------------------------------------------------------------- 1 | https://github.com/bitconnector/ttn_mapper_t-beam 2 | 3 | https://github.com/cyberman54/ESP32-Paxcounter 4 | -------------------------------------------------------------------------------- /Sodaq/sodaq-one-ttnmapper/README.md: -------------------------------------------------------------------------------- 1 | ## Prerequisites 2 | You need to have the [RN2483 library](https://github.com/jpmeijers/RN2483-Arduino-Library) installed. 3 | 4 | -------------------------------------------------------------------------------- /Pycom/README.md: -------------------------------------------------------------------------------- 1 | # Some examples meant for the LoPy and other Pycom devices 2 | 3 | https://github.com/ttn-be/ttnmapper 4 | https://github.com/computenodes/pycom-gps-logger 5 | -------------------------------------------------------------------------------- /Arduino/README.md: -------------------------------------------------------------------------------- 1 | # Arduino devices 2 | 3 | Many examples for Arduino devices exist, many on the RN2xx3 library page 4 | 5 | https://github.com/jpmeijers/RN2483-Arduino-Library/tree/master/examples 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gps-node-examples 2 | Example code for end devices with GPSes on board, transmitting their coordinates in the payload of the LoRaWAN packet in a format that is compatible with TTN Mapper. 3 | -------------------------------------------------------------------------------- /Arduino/MKR WAN 1300/LoRaPing/arduino_secrets.h: -------------------------------------------------------------------------------- 1 | // Replace with keys obtained from TheThingsNetwork console 2 | #define SECRET_APP_EUI "xxxxxxxxxxxxx" 3 | #define SECRET_APP_KEY "yyyyyyyyyyyyyyyyyyyyyyy" 4 | -------------------------------------------------------------------------------- /RAK811/README.md: -------------------------------------------------------------------------------- 1 | # RAK811 GPS tracker module 2 | 3 | Complete example: 4 | https://github.com/jcaridadhdez/RAK811-tracker 5 | 6 | Minimal example for only 868MHz: 7 | https://github.com/jpmeijers/RAK811-tracker 8 | -------------------------------------------------------------------------------- /Sodaq/sodaq-one-ttnmapper/decoder.js: -------------------------------------------------------------------------------- 1 | function Decoder(bytes, port) { 2 | // Decode an uplink message from a buffer 3 | // (array) of bytes to an object of fields. 4 | var decoded = {}; 5 | 6 | // if (port === 1) decoded.led = bytes[0]; 7 | decoded.lat = ((bytes[0]<<16)>>>0) + ((bytes[1]<<8)>>>0) + bytes[2]; 8 | decoded.lat = (decoded.lat / 16777215.0 * 180) - 90; 9 | 10 | decoded.lon = ((bytes[3]<<16)>>>0) + ((bytes[4]<<8)>>>0) + bytes[5]; 11 | decoded.lon = (decoded.lon / 16777215.0 * 360) - 180; 12 | 13 | var altValue = ((bytes[6]<<8)>>>0) + bytes[7]; 14 | var sign = bytes[6] & (1 << 7); 15 | if(sign) 16 | { 17 | decoded.alt = 0xFFFF0000 | altValue; 18 | } 19 | else 20 | { 21 | decoded.alt = altValue; 22 | } 23 | 24 | decoded.hdop = bytes[8] / 10.0; 25 | 26 | return decoded; 27 | } 28 | -------------------------------------------------------------------------------- /Sodaq/sodaq-universal-tracker/decoder.js: -------------------------------------------------------------------------------- 1 | function Decoder(bytes, port) { 2 | var epoch = (bytes[3] << 24) | (bytes[2] << 16) | (bytes[1] << 8) | bytes[0]; 3 | var batvolt = bytes[4]; 4 | var boardtemp = bytes[5]; 5 | var lat = (bytes[9] << 24) | (bytes[8] << 16) | (bytes[7] << 8) | bytes[6]; 6 | var long = (bytes[13] << 24) | (bytes[12] << 16) | (bytes[11] << 8) | bytes[10]; 7 | var alt = (bytes[15] << 8) | bytes[14]; 8 | var speed = (bytes[17] << 8) | bytes[16]; 9 | var course = bytes[18]; 10 | var numsat = bytes[19]; 11 | var fix = bytes[20]; 12 | return { 13 | epoch: epoch, 14 | batvolt: batvolt, 15 | boardtemp: boardtemp, 16 | lat: lat/10000000.0, 17 | long: long/10000000.0, 18 | alt: alt, 19 | speed: speed, 20 | course: course, 21 | numsat: numsat, 22 | fix: fix, 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 ttnmapper 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Arduino/MKR WAN 1300/LoRaPing/LoRaPing.ino: -------------------------------------------------------------------------------- 1 | /* 2 | Lora Ping 3 | This sketch demonstrates how to constantly send data with the MKR WAN 1300/1310 LoRa module. 4 | It is useful only for LoRa range testing, as the power usage is too high for normal use. 5 | This example code is in the public domain. 6 | */ 7 | 8 | #include 9 | 10 | LoRaModem modem; 11 | 12 | // Uncomment if using the Murata chip as a module 13 | // LoRaModem modem(Serial1); 14 | 15 | #include "arduino_secrets.h" 16 | // Please enter your sensitive data in the Secret tab or arduino_secrets.h 17 | String appEui = SECRET_APP_EUI; 18 | String appKey = SECRET_APP_KEY; 19 | 20 | void setup() { 21 | Serial.begin(115200); 22 | // while (!Serial); 23 | // change this to your regional band (eg. US915, AS923, ...) 24 | if (!modem.begin(EU868)) { 25 | Serial.println("Failed to start module"); 26 | while (1) {} 27 | }; 28 | Serial.print("Your module version is: "); 29 | Serial.println(modem.version()); 30 | Serial.print("Your device EUI is: "); 31 | Serial.println(modem.deviceEUI()); 32 | 33 | int connected = modem.joinOTAA(appEui, appKey); 34 | if (!connected) { 35 | Serial.println("Something went wrong. Do you have coverage of the network?"); 36 | while (1) {} 37 | } 38 | 39 | modem.minPollInterval(60); 40 | modem.setADR(true); 41 | } 42 | 43 | void loop() { 44 | modem.dataRate(5); 45 | int err; 46 | modem.beginPacket(); 47 | err = modem.endPacket(true); 48 | Serial.print("TX result="); 49 | Serial.println(err); 50 | delay(10000); 51 | } 52 | -------------------------------------------------------------------------------- /Digital Matter/decoder.js: -------------------------------------------------------------------------------- 1 | // Decode an uplink message from an array of bytes to an object of fields 2 | function Decoder(bytes, port) 3 | { 4 | var decoded = {}; 5 | if (port === 1) 6 | { 7 | decoded.type = "position"; 8 | 9 | decoded.latitudeDeg = bytes[0] + bytes[1] * 256 + 10 | bytes[2] * 65536 + bytes[3] * 16777216; 11 | if (decoded.latitudeDeg >= 0x80000000) 12 | decoded.latitudeDeg -= 0x100000000; 13 | decoded.latitudeDeg /= 1e7; 14 | 15 | decoded.longitudeDeg = bytes[4] + bytes[5] * 256 + 16 | bytes[6] * 65536 + bytes[7] * 16777216; 17 | if (decoded.longitudeDeg >= 0x80000000) 18 | decoded.longitudeDeg -= 0x100000000; 19 | decoded.longitudeDeg /= 1e7; 20 | 21 | decoded.inTrip = ((bytes[8] & 0x1) !== 0) ? true : false; 22 | decoded.fixFailed = ((bytes[8] & 0x2) !== 0) ? true : false; 23 | decoded.headingDeg = (bytes[8] >> 2) * 5.625; 24 | 25 | decoded.speedKmph = bytes[9]; 26 | decoded.batV = bytes[10] * 0.025; 27 | } 28 | else if (port === 2) 29 | { 30 | decoded.type = "downlink ack"; 31 | 32 | decoded.sequence = (bytes[0] & 0x7F); 33 | decoded.accepted = ((bytes[0] & 0x80) !== 0) ? true : false; 34 | decoded.fwMaj = bytes[1]; 35 | decoded.fwMin = bytes[2]; 36 | } 37 | else if (port === 3) 38 | { 39 | decoded.type = "stats"; 40 | 41 | decoded.initialBatV = 4.0 + 0.100 * (bytes[0] & 0xF); 42 | decoded.txCount = 32 * ((bytes[0] >> 4) + (bytes[1] & 0x7F) * 16); 43 | decoded.tripCount = 32 * ((bytes[1] >> 7) + (bytes[2] & 0xFF) * 2 44 | + (bytes[3] & 0x0F) * 512); 45 | decoded.gpsSuccesses = 32 * ((bytes[3] >> 4) + (bytes[4] & 0x3F) * 16); 46 | decoded.gpsFails = 32 * ((bytes[4] >> 6) + (bytes[5] & 0x3F) * 4); 47 | decoded.aveGpsFixS = 1 * ((bytes[5] >> 6) + (bytes[6] & 0x7F) * 4); 48 | decoded.aveGpsFailS = 1 * ((bytes[6] >> 7) + (bytes[7] & 0xFF) * 2); 49 | decoded.aveGpsFreshenS = 1 * ((bytes[7] >> 8) + (bytes[8] & 0xFF) * 1); 50 | decoded.wakeupsPerTrip = 1 * ((bytes[8] >> 8) + (bytes[9] & 0x7F) * 1); 51 | decoded.uptimeWeeks = 1 * ((bytes[9] >> 7) + (bytes[10] & 0xFF) * 2); 52 | } 53 | return decoded; 54 | } 55 | -------------------------------------------------------------------------------- /Sodaq/sodaq-one-ttnmapper/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 | 26 | class Sodaq_UBlox_GPS 27 | { 28 | public: 29 | Sodaq_UBlox_GPS(); 30 | 31 | void init(int8_t enable_pin); 32 | bool scan(bool leave_on=false, uint32_t timeout=20000); 33 | String getDateTimeString(); 34 | double getLat() { return _lat; } 35 | double getLon() { return _lon; } 36 | double getAlt() { return _alt; } 37 | double getHDOP() { return _hdop; } 38 | uint8_t getNumberOfSatellites() { return _numSatellites; } 39 | uint16_t getYear() { return (uint16_t)_yy + 2000; } // 2016.. 40 | uint8_t getMonth() { return _MM; } // 1.. 41 | uint8_t getDay() { return _dd; } // 1.. 42 | uint8_t getHour() { return _hh; } // 0.. 43 | uint8_t getMinute() { return _mm; } // 0.. 44 | uint8_t getSecond() { return _ss; } // 0.. 45 | 46 | // How many extra lines must scan see before it stops? This is merely for debugging 47 | void setMinNumOfLines(size_t num) { _minNumOfLines = num; } 48 | 49 | // The minimum number of satellites to satisfy scan 50 | void setMinNumSatellites(size_t num) { _minNumSatellites = num; } 51 | 52 | // Sets the optional "Diagnostics and Debug" stream. 53 | void setDiag(Stream &stream) { _diagStream = &stream; } 54 | void setDiag(Stream *stream) { _diagStream = stream; } 55 | 56 | private: 57 | void on(); 58 | void off(); 59 | 60 | // Read one byte 61 | uint8_t read(); 62 | bool readLine(uint32_t timeout = 10000); 63 | bool parseLine(const char * line); 64 | bool parseGPGGA(const String & line); 65 | bool parseGPGSA(const String & line); 66 | bool parseGPRMC(const String & line); 67 | bool parseGPGSV(const String & line); 68 | bool parseGPGLL(const String & line); 69 | bool parseGPVTG(const String & line); 70 | bool parseGPTXT(const String & line); 71 | bool computeCrc(const char * line, bool do_logging=false); 72 | uint8_t getHex2(const char * s, size_t index); 73 | String num2String(int num, size_t width); 74 | String getField(const String & data, int index); 75 | double convertDegMinToDecDeg(const String & data); 76 | 77 | void setDateTime(const String & date, const String & time); 78 | 79 | void beginTransmission(); 80 | void endTransmission(); 81 | 82 | void resetValues(); 83 | 84 | int8_t _enablePin; 85 | 86 | // The (optional) stream to show debug information. 87 | Stream * _diagStream; 88 | 89 | uint8_t _addr; 90 | 91 | size_t _minNumOfLines; 92 | 93 | // Minimum number of satellites to satisfy scan(). Zero means any number is OK. 94 | size_t _minNumSatellites; 95 | 96 | bool _seenLatLon; 97 | bool _seenAlt; 98 | uint8_t _numSatellites; 99 | double _lat; 100 | double _lon; 101 | double _alt; 102 | double _hdop; 103 | 104 | bool _seenTime; 105 | uint8_t _yy; 106 | uint8_t _MM; 107 | uint8_t _dd; 108 | uint8_t _hh; 109 | uint8_t _mm; 110 | uint8_t _ss; 111 | 112 | bool _trans_active; 113 | 114 | static const char _fieldSep; 115 | char * _inputBuffer; 116 | size_t _inputBufferSize; 117 | }; 118 | 119 | extern Sodaq_UBlox_GPS sodaq_gps; 120 | 121 | #endif // _SODAQ_UBLOX_GPS_H 122 | -------------------------------------------------------------------------------- /Sodaq/sodaq-one-ttnmapper/sodaq-one.ino: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: JP Meijers 3 | * Date: 2016-09-02 4 | * 5 | * Transmit GPS coordinates via TTN. This happens as fast as possible, while still keeping to 6 | * the 1% duty cycle rules enforced by the RN2483's built in LoRaWAN stack. Even though this is 7 | * allowed by the radio regulations of the 868MHz band, the fair use policy of TTN may prohibit this. 8 | * 9 | * CHECK THE RULES BEFORE USING THIS PROGRAM! 10 | * 11 | * CHANGE ADDRESS! 12 | * Change the device address, network (session) key, and app (session) key to the values 13 | * that are registered via the TTN dashboard. 14 | * The appropriate line is "myLora.initABP(XXX);" or "myLora.initOTAA(XXX);" 15 | * When using ABP, it is advised to enable "relax frame count". 16 | * 17 | * This sketch assumes you are using the Sodaq One V4 node in its original configuration. 18 | * 19 | * This program makes use of the Sodaq UBlox library, but with minor changes to include altitude and HDOP. 20 | * 21 | * LED indicators: 22 | * Blue: Busy transmitting a packet 23 | * Green waiting for a new GPS fix 24 | * Red: GPS fix taking a long time. Try to go outdoors. 25 | * 26 | * To decode the binary payload, you can use the following 27 | * javascript decoder function. It should work with the TTN console. 28 | * 29 | function Decoder(bytes, port) { 30 | // Decode an uplink message from a buffer 31 | // (array) of bytes to an object of fields. 32 | var decoded = {}; 33 | 34 | // if (port === 1) decoded.led = bytes[0]; 35 | decoded.lat = ((bytes[0]<<16)>>>0) + ((bytes[1]<<8)>>>0) + bytes[2]; 36 | decoded.lat = (decoded.lat / 16777215.0 * 180) - 90; 37 | 38 | decoded.lon = ((bytes[3]<<16)>>>0) + ((bytes[4]<<8)>>>0) + bytes[5]; 39 | decoded.lon = (decoded.lon / 16777215.0 * 360) - 180; 40 | 41 | var altValue = ((bytes[6]<<8)>>>0) + bytes[7]; 42 | var sign = bytes[6] & (1 << 7); 43 | if(sign) 44 | { 45 | decoded.alt = 0xFFFF0000 | altValue; 46 | } 47 | else 48 | { 49 | decoded.alt = altValue; 50 | } 51 | 52 | decoded.hdop = bytes[8] / 10.0; 53 | 54 | return decoded; 55 | } 56 | * 57 | */ 58 | #include 59 | #include "Sodaq_UBlox_GPS.h" 60 | #include 61 | 62 | //create an instance of the rn2xx3 library, 63 | //giving Serial1 as stream to use for communication with the radio 64 | rn2xx3 myLora(Serial1); 65 | 66 | String toLog; 67 | uint8_t txBuffer[9]; 68 | uint32_t LatitudeBinary, LongitudeBinary; 69 | uint16_t altitudeGps; 70 | uint8_t hdopGps; 71 | int dr = 5; 72 | 73 | void setup() 74 | { 75 | delay(3000); 76 | 77 | SerialUSB.begin(57600); 78 | Serial1.begin(57600); 79 | 80 | // make sure usb serial connection is available, 81 | // or after 10s go on anyway for 'headless' use of the node. 82 | while ((!SerialUSB) && (millis() < 10000)); 83 | 84 | SerialUSB.println("SODAQ LoRaONE TTN Mapper starting"); 85 | 86 | initialize_radio(); 87 | 88 | //transmit a startup message 89 | myLora.tx("TTN Mapper on Sodaq One"); 90 | 91 | // Enable next line to enable debug information of the sodaq_gps 92 | //sodaq_gps.setDiag(SerialUSB); 93 | 94 | // initialize GPS 95 | sodaq_gps.init(GPS_ENABLE); 96 | 97 | // Set the datarate/spreading factor at which we communicate. 98 | // DR5 is the fastest and best to use. DR0 is the slowest. 99 | myLora.setDR(dr); 100 | 101 | // LED pins as outputs. HIGH=Off, LOW=On 102 | pinMode(LED_BLUE, OUTPUT); 103 | digitalWrite(LED_BLUE, HIGH); 104 | pinMode(LED_RED, OUTPUT); 105 | digitalWrite(LED_RED, HIGH); 106 | pinMode(LED_GREEN, OUTPUT); 107 | digitalWrite(LED_GREEN, HIGH); 108 | } 109 | 110 | void initialize_radio() 111 | { 112 | delay(100); //wait for the RN2xx3's startup message 113 | while(Serial1.available()){ 114 | Serial1.read(); 115 | } 116 | 117 | //print out the HWEUI so that we can register it via ttnctl 118 | String hweui = myLora.hweui(); 119 | while(hweui.length() != 16) 120 | { 121 | SerialUSB.println("Communication with RN2xx3 unsuccessful. Power cycle the Sodaq One board."); 122 | delay(10000); 123 | hweui = myLora.hweui(); 124 | } 125 | SerialUSB.println("When using OTAA, register this DevEUI: "); 126 | SerialUSB.println(hweui); 127 | SerialUSB.println("RN2xx3 firmware version:"); 128 | SerialUSB.println(myLora.sysver()); 129 | 130 | //configure your keys and join the network 131 | SerialUSB.println("Trying to join TTN"); 132 | bool join_result = false; 133 | 134 | //ABP: initABP(String addr, String AppSKey, String NwkSKey); 135 | join_result = myLora.initABP("02017201", "8D7FFEF938589D95AAD928C2E2E7E48F", "AE17E567AECC8787F749A62F5541D522"); 136 | 137 | //OTAA: initOTAA(String AppEUI, String AppKey); 138 | //join_result = myLora.initOTAA("70B3D57ED00001A6", "A23C96EE13804963F8C2BD6285448198"); 139 | 140 | while(!join_result) 141 | { 142 | SerialUSB.println("Unable to join. Are your keys correct, and do you have TTN coverage?"); 143 | delay(60000); //delay a minute before retry 144 | digitalWrite(LED_BLUE, LOW); 145 | join_result = myLora.init(); 146 | digitalWrite(LED_BLUE, HIGH); 147 | } 148 | 149 | SerialUSB.println("Successfully joined TTN"); 150 | 151 | } 152 | 153 | void loop() 154 | { 155 | SerialUSB.println("Waiting for GPS fix"); 156 | 157 | digitalWrite(LED_GREEN, LOW); 158 | // Keep the GPS enabled after we do a scan, increases accuracy 159 | sodaq_gps.scan(true); 160 | digitalWrite(LED_GREEN, HIGH); 161 | 162 | // if the latitude is 0, we likely do not have a GPS fix yet, so wait longer 163 | while(sodaq_gps.getLat()==0.0) 164 | { 165 | SerialUSB.println("Latitude still 0.0, doing another scan"); 166 | 167 | digitalWrite(LED_RED, LOW); 168 | // Keep the GPS enabled after we do a scan, increases accuracy 169 | sodaq_gps.scan(true); 170 | digitalWrite(LED_RED, HIGH); 171 | } 172 | 173 | LatitudeBinary = ((sodaq_gps.getLat() + 90) / 180) * 16777215; 174 | LongitudeBinary = ((sodaq_gps.getLon() + 180) / 360) * 16777215; 175 | 176 | txBuffer[0] = ( LatitudeBinary >> 16 ) & 0xFF; 177 | txBuffer[1] = ( LatitudeBinary >> 8 ) & 0xFF; 178 | txBuffer[2] = LatitudeBinary & 0xFF; 179 | 180 | txBuffer[3] = ( LongitudeBinary >> 16 ) & 0xFF; 181 | txBuffer[4] = ( LongitudeBinary >> 8 ) & 0xFF; 182 | txBuffer[5] = LongitudeBinary & 0xFF; 183 | 184 | altitudeGps = sodaq_gps.getAlt(); 185 | txBuffer[6] = ( altitudeGps >> 8 ) & 0xFF; 186 | txBuffer[7] = altitudeGps & 0xFF; 187 | 188 | hdopGps = sodaq_gps.getHDOP()*10; 189 | txBuffer[8] = hdopGps & 0xFF; 190 | 191 | toLog = ""; 192 | for(size_t i = 0; i. 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")) { 163 | return parseGPGGA(data); 164 | } 165 | 166 | if (data.startsWith("GPGSA")) { 167 | return parseGPGSA(data); 168 | } 169 | 170 | if (data.startsWith("GPRMC")) { 171 | return parseGPRMC(data); 172 | } 173 | 174 | if (data.startsWith("GPGSV")) { 175 | return parseGPGSV(data); 176 | } 177 | 178 | if (data.startsWith("GPGLL")) { 179 | return parseGPGLL(data); 180 | } 181 | 182 | if (data.startsWith("GPVTG")) { 183 | return parseGPVTG(data); 184 | } 185 | 186 | if (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 | setDateTime(date, time); 298 | 299 | return true; 300 | } 301 | 302 | /*! 303 | * Parse GPGSV line 304 | * See also section 24.12 of u-blox 7, Receiver Description. Document number: GPS.G7-SW-12001-B 305 | * 306 | * 0 $GPGSV 307 | * 1 numMsg digit Number of messages, total number of GSV messages being output 308 | * 2 msgNum digit Number of this message 309 | * 3 numSV num Number of satellites in view 310 | * 311 | * 4 sv num Satellite ID 312 | * 5 elv num Elevation (range 0-90) 313 | * 6 az num Azimuth, (range 0-359) 314 | * 7 cno num Signal strength (C/N0, range 0-99), blank when not tracking 315 | * 316 | * fields 4..7 are repeated for each satellite in this message 317 | */ 318 | bool Sodaq_UBlox_GPS::parseGPGSV(const String & line) 319 | { 320 | debugPrintLn("parseGPGSV"); 321 | debugPrintLn(String(">> ") + line); 322 | 323 | // We could/should only use msgNum == 1. However, all messages should have 324 | // the same numSV. 325 | _numSatellites = getField(line, 3).toInt(); 326 | 327 | return true; 328 | } 329 | 330 | /*! 331 | * Parse GPGLL line 332 | * Latitude and longitude, with time of position fix and status 333 | */ 334 | bool Sodaq_UBlox_GPS::parseGPGLL(const String & line) 335 | { 336 | // Not (yet) used 337 | debugPrintLn("parseGPGLL"); 338 | debugPrintLn(String(">> ") + line); 339 | return false; 340 | } 341 | 342 | /*! 343 | * Parse GPVTG line 344 | * Course over ground and Ground speed 345 | */ 346 | bool Sodaq_UBlox_GPS::parseGPVTG(const String & line) 347 | { 348 | // Not (yet) used 349 | debugPrintLn("parseGPVTG"); 350 | debugPrintLn(String(">> ") + line); 351 | return false; 352 | } 353 | 354 | /*! 355 | * Parse GPTXT line 356 | * See also section 24.13 of u-blox 7, Receiver Description. Document number: GPS.G7-SW-12001-B 357 | * $GPTXT,01,01,02,ANTSTATUS=INIT*25 358 | * ... 359 | * $GPTXT,01,01,02,ANTSTATUS=OK*3B 360 | * 361 | * 0 $GPTXT 362 | * 1 numMsg num Total number of messages in this transmission 363 | * 2 msgNum num Message number in this transmission 364 | * 3 msgType num Text identifier, 00: Error, 01: Warning, 02: Notice, 07: User 365 | * 4 text string Any ASCII text 366 | * 13 checksum 2 hex digits Checksum 367 | */ 368 | bool Sodaq_UBlox_GPS::parseGPTXT(const String & line) 369 | { 370 | //debugPrintLn("parseGPTXT"); 371 | //debugPrintLn(String(">> ") + line); 372 | debugPrintLn(String("TXT: \"") + getField(line, 4) + "\""); 373 | return true; 374 | } 375 | 376 | /*! 377 | * Compute and verify the checksum 378 | * 379 | * Each line must start with '$' 380 | * Each line must end with '*' 381 | */ 382 | bool Sodaq_UBlox_GPS::computeCrc(const char * line, bool do_logging) 383 | { 384 | if (do_logging) { 385 | debugPrint(line); 386 | } 387 | size_t len = strlen(line); 388 | if (len < 4) { 389 | if (do_logging) { 390 | debugPrint(" Invalid short: "); 391 | debugPrintLn(len); 392 | } 393 | return false; 394 | } 395 | if (line[0] != '$') { 396 | if (do_logging) { 397 | debugPrintLn(" Invalid$"); 398 | } 399 | return false; 400 | } 401 | if (line[len - 3] != '*') { 402 | if (do_logging) { 403 | debugPrintLn(" Invalid*"); 404 | } 405 | return false; 406 | } 407 | 408 | uint8_t crc = 0; 409 | for (size_t i = 1; i < len - 3; ++i) { 410 | crc ^= line[i]; 411 | } 412 | 413 | uint8_t crc1 = getHex2(line, len - 2); 414 | if (crc != crc1) { 415 | if (do_logging) { 416 | debugPrint(" INVALID CRC "); 417 | debugPrint(crc1, HEX); 418 | debugPrint(" EXPECTED "); 419 | debugPrintLn(crc, HEX); 420 | } 421 | return false; 422 | } 423 | 424 | //debugSerial.println(" OK"); 425 | return true; 426 | } 427 | 428 | uint8_t Sodaq_UBlox_GPS::getHex2(const char * s, size_t index) 429 | { 430 | uint8_t val = 0; 431 | char c; 432 | c = s[index]; 433 | if (c >= '0' && c <= '9') { 434 | val += c - '0'; 435 | } else if (c >= 'a' && c <= 'f') { 436 | val += c - 'a' + 10; 437 | } else if (c >= 'A' && c <= 'F') { 438 | val += c - 'A' + 10; 439 | } 440 | val <<= 4; 441 | c = s[++index]; 442 | if (c >= '0' && c <= '9') { 443 | val += c - '0'; 444 | } else if (c >= 'a' && c <= 'f') { 445 | val += c - 'a' + 10; 446 | } else if (c >= 'A' && c <= 'F') { 447 | val += c - 'A' + 10; 448 | } 449 | return val; 450 | } 451 | 452 | String Sodaq_UBlox_GPS::num2String(int num, size_t width) 453 | { 454 | String out; 455 | out = num; 456 | while (out.length() < width) { 457 | out = String("0") + out; 458 | } 459 | return out; 460 | } 461 | 462 | String Sodaq_UBlox_GPS::getField(const String & data, int index) 463 | { 464 | int found = 0; 465 | int strIndex[] = { 0, -1 }; 466 | int maxIndex = data.length() - 1; 467 | 468 | for (int i = 0; i <= maxIndex && found <= index; i++) { 469 | if (data.charAt(i) == _fieldSep || i == maxIndex) { 470 | found++; 471 | strIndex[0] = strIndex[1] + 1; 472 | strIndex[1] = (i == maxIndex) ? i + 1 : i; 473 | } 474 | } 475 | 476 | return found > index ? data.substring(strIndex[0], strIndex[1]) : ""; 477 | } 478 | 479 | /* 480 | * Convert lat/long degree-minute format to decimal-degrees 481 | * 482 | * This code is from 483 | * http://arduinodev.woofex.net/2013/02/06/adafruit_gps_forma/ 484 | * 485 | * According to the NMEA Standard, Latitude and Longitude are output in the format Degrees, Minutes and 486 | * (Decimal) Fractions of Minutes. To convert to Degrees and Fractions of Degrees, or Degrees, Minutes, Seconds 487 | * and Fractions of seconds, the 'Minutes' and 'Fractional Minutes' parts need to be converted. In other words: If 488 | * the GPS Receiver reports a Latitude of 4717.112671 North and Longitude of 00833.914843 East, this is 489 | * Latitude 47 Degrees, 17.112671 Minutes 490 | * Longitude 8 Degrees, 33.914843 Minutes 491 | * or 492 | * Latitude 47 Degrees, 17 Minutes, 6.76026 Seconds 493 | * Longitude 8 Degrees, 33 Minutes, 54.89058 Seconds 494 | * or 495 | * Latitude 47.28521118 Degrees 496 | * Longitude 8.56524738 Degrees 497 | */ 498 | double Sodaq_UBlox_GPS::convertDegMinToDecDeg(const String & data) 499 | { 500 | double degMin = data.toFloat(); 501 | double min = 0.0; 502 | double decDeg = 0.0; 503 | 504 | //get the minutes, fmod() requires double 505 | min = fmod((double) degMin, 100.0); 506 | 507 | //rebuild coordinates in decimal degrees 508 | degMin = (int) (degMin / 100); 509 | decDeg = degMin + (min / 60); 510 | 511 | return decDeg; 512 | } 513 | 514 | void Sodaq_UBlox_GPS::setDateTime(const String & date, const String & time) 515 | { 516 | if (time.length() == 9 && date.length() == 6) { 517 | _hh = time.substring(0, 2).toInt(); 518 | _mm = time.substring(2, 4).toInt(); 519 | _ss = time.substring(4, 6).toInt(); 520 | _dd = date.substring(0, 2).toInt(); 521 | _MM = date.substring(2, 4).toInt(); 522 | _yy = date.substring(4, 6).toInt(); 523 | _seenTime = true; 524 | } 525 | } 526 | 527 | /*! 528 | * Read one NMEA frame 529 | */ 530 | bool Sodaq_UBlox_GPS::readLine(uint32_t timeout) 531 | { 532 | if (!_inputBuffer) { 533 | return false; 534 | } 535 | 536 | uint32_t start = millis(); 537 | char c; 538 | char *ptr = _inputBuffer; 539 | size_t cnt = 0; 540 | *ptr = '\0'; 541 | 542 | c = 0; 543 | while (!is_timedout(start, timeout)) { 544 | c = (char)read(); 545 | if (c == '$') { 546 | break; 547 | } 548 | } 549 | if (c != '$') { 550 | return false; 551 | } 552 | *ptr++ = c; 553 | ++cnt; 554 | 555 | c = 0; 556 | while (!is_timedout(start, timeout)) { 557 | c = (char)read(); 558 | if (c == '\r') { 559 | continue; 560 | } 561 | if (c == '\n') { 562 | break; 563 | } 564 | if (cnt < _inputBufferSize - 1) { 565 | *ptr++ = c; 566 | ++cnt; 567 | } 568 | } 569 | *ptr = '\0'; 570 | if (c != '\n') { 571 | return false; 572 | } 573 | endTransmission(); 574 | return true; 575 | } 576 | 577 | uint8_t Sodaq_UBlox_GPS::read() 578 | { 579 | beginTransmission(); 580 | 581 | uint8_t b = 0xFF; 582 | uint8_t nr_bytes; 583 | nr_bytes = Wire.requestFrom(_addr, 1, false); 584 | if (nr_bytes == 1) { 585 | b = Wire.read(); 586 | } 587 | return b; 588 | } 589 | 590 | void Sodaq_UBlox_GPS::beginTransmission() 591 | { 592 | if (_trans_active) { 593 | return; 594 | } 595 | Wire.beginTransmission(_addr); 596 | _trans_active = true; 597 | } 598 | 599 | void Sodaq_UBlox_GPS::endTransmission() 600 | { 601 | if (!_trans_active) { 602 | return; 603 | } 604 | Wire.endTransmission(); 605 | _trans_active = false; 606 | } 607 | 608 | void Sodaq_UBlox_GPS::on() 609 | { 610 | digitalWrite(_enablePin, GPS_ENABLE_ON); 611 | } 612 | 613 | void Sodaq_UBlox_GPS::off() 614 | { 615 | digitalWrite(_enablePin, GPS_ENABLE_OFF); 616 | } 617 | --------------------------------------------------------------------------------