From 2c47c098db1f6cc28f91fd8a7cf18c63718b87cc Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Sun, 12 Dec 2021 13:58:32 -0700 Subject: [PATCH] Add example 16: NTRIP Client with GGA support --- .../Example16_NTRIPClient_WithGGA.ino | 400 ++++++++++++++++++ .../Example16_NTRIPClient_WithGGA/secrets.h | 17 + 2 files changed, 417 insertions(+) create mode 100644 examples/ZED-F9P/Example16_NTRIPClient_WithGGA/Example16_NTRIPClient_WithGGA.ino create mode 100644 examples/ZED-F9P/Example16_NTRIPClient_WithGGA/secrets.h diff --git a/examples/ZED-F9P/Example16_NTRIPClient_WithGGA/Example16_NTRIPClient_WithGGA.ino b/examples/ZED-F9P/Example16_NTRIPClient_WithGGA/Example16_NTRIPClient_WithGGA.ino new file mode 100644 index 0000000..689527f --- /dev/null +++ b/examples/ZED-F9P/Example16_NTRIPClient_WithGGA/Example16_NTRIPClient_WithGGA.ino @@ -0,0 +1,400 @@ +/* + Use ESP32 WiFi to get RTCM data from RTK2Go (caster) as a Client, and transmit GGA (needed for some Casters) + By: SparkFun Electronics / Nathan Seidle + Date: November 18th, 2021 + License: MIT. See license file for more information but you can + basically do whatever you want with this code. + + This example shows how to obtain RTCM data from a NTRIP Caster over WiFi + and push it over I2C to a ZED-F9x. + It's confusing, but the Arduino is acting as a 'client' to a 'caster'. In this case we will + use RTK2Go.com as our caster because it is free. See the NTRIPServer example to see how + to push RTCM data to the caster. + + The rover's location will be broadcast to the Caster every 10s via GGA setence. + + You will need to have a valid mountpoint available. To see available mountpoints go here: http://rtk2go.com:2101/ + + This is a proof of concept to show how to connect to a caster via HTTP. + + For more information about NTRIP Clients and the differences between Rev1 and Rev2 of the protocol + please see: https://www.use-snip.com/kb/knowledge-base/ntrip-rev1-versus-rev2-formats/ + + "In broad protocol terms, the NTRIP client must first connect (get an HTTP “OK” reply) and only then + should it send the sentence. NTRIP protocol revision 2 (which does not have very broad industry + acceptance at this time) does allow sending the sentence in the original header." + https://www.use-snip.com/kb/knowledge-base/subtle-issues-with-using-ntrip-client-nmea-183-strings/ + + Feel like supporting open source hardware? + Buy a board from SparkFun! + ZED-F9P RTK2: https://www.sparkfun.com/products/16481 + RTK Surveyor: https://www.sparkfun.com/products/18443 + RTK Express: https://www.sparkfun.com/products/18442 + + Hardware Connections: + Plug a Qwiic cable into the GNSS and a ESP32 Thing Plus + If you don't have a platform with a Qwiic connection use the SparkFun Qwiic Breadboard Jumper (https://www.sparkfun.com/products/14425) + Open the serial monitor at 115200 baud to see the output +*/ +#include +#include "secrets.h" + +#include //http://librarymanager/All#SparkFun_u-blox_GNSS +SFE_UBLOX_GNSS myGNSS; + +//The ESP32 core has a built in base64 library but not every platform does +//We'll use an external lib if necessary. +#if defined(ARDUINO_ARCH_ESP32) +#include "base64.h" //Built-in ESP32 library +#else +#include //nfriendly library from https://github.com/adamvr/arduino-base64, will work with any platform +#endif + +//Global variables +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +long lastReceivedRTCM_ms = 0; //5 RTCM messages take approximately ~300ms to arrive at 115200bps +int maxTimeBeforeHangup_ms = 10000; //If we fail to get a complete RTCM frame after 10s, then disconnect from caster + +bool transmitLocation = true; //By default we will transmit the units location via GGA sentence. +int timeBetweenGGAUpdate_ms = 10000; //GGA is required for Rev2 NTRIP casters. Don't transmit but once every 10 seconds +long lastTransmittedGGA_ms = 0; + +//Used for GGA sentence parsing from incoming NMEA +bool ggaSentenceStarted = false; +bool ggaSentenceComplete = false; +bool ggaTransmitComplete = false; //Goes true once we transmit GGA to the caster + +char ggaSentence[128] = {0}; +byte ggaSentenceSpot = 0; +int ggaSentenceEndSpot = 0; +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +void setup() +{ + Serial.begin(115200); + Serial.println(F("NTRIP testing")); + + Wire.begin(); //Start I2C + + while(myGNSS.begin() == false) //Connect to the Ublox module using Wire port + { + Serial.println(F("u-blox GPS not detected at default I2C address. Please check wiring. Freezing.")); + delay(2000); + //while (1); + } + Serial.println(F("u-blox module connected")); + + myGNSS.setI2COutput(COM_TYPE_UBX | COM_TYPE_NMEA); //Set the I2C port to output both NMEA and UBX messages + myGNSS.setPortInput(COM_PORT_I2C, COM_TYPE_UBX | COM_TYPE_NMEA | COM_TYPE_RTCM3); //Be sure RTCM3 input is enabled. UBX + RTCM3 is not a valid state. + + myGNSS.enableNMEAMessage(UBX_NMEA_GGA, COM_PORT_I2C); //Verify the GGA sentence is enabled + + myGNSS.setNavigationFrequency(1); //Set output in Hz. + + Serial.print(F("Connecting to local WiFi")); + WiFi.begin(ssid, password); + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print(F(".")); + } + Serial.println(); + + Serial.print(F("WiFi connected with IP: ")); + Serial.println(WiFi.localIP()); + + while (Serial.available()) Serial.read(); +} + +void loop() +{ + if (Serial.available()) + { + beginClient(); + while (Serial.available()) Serial.read(); //Empty buffer of any newline chars + } + + Serial.println(F("Press any key to start NTRIP Client.")); + + delay(1000); +} + +//Connect to NTRIP Caster, receive RTCM, and push to ZED module over I2C +void beginClient() +{ + WiFiClient ntripClient; + long rtcmCount = 0; + + Serial.println(F("Subscribing to Caster. Press key to stop")); + delay(10); //Wait for any serial to arrive + while (Serial.available()) Serial.read(); //Flush + + while (Serial.available() == 0) + { + myGNSS.checkUblox(); + + //Connect if we are not already. Limit to 5s between attempts. + if (ntripClient.connected() == false) + { + Serial.print(F("Opening socket to ")); + Serial.println(casterHost); + + if (ntripClient.connect(casterHost, casterPort) == false) //Attempt connection + { + Serial.println(F("Connection to caster failed")); + return; + } + else + { + Serial.print(F("Connected to ")); + Serial.print(casterHost); + Serial.print(F(": ")); + Serial.println(casterPort); + + Serial.print(F("Requesting NTRIP Data from mount point ")); + Serial.println(mountPoint); + + const int SERVER_BUFFER_SIZE = 512; + char serverRequest[SERVER_BUFFER_SIZE]; + + snprintf(serverRequest, + SERVER_BUFFER_SIZE, + "GET /%s HTTP/1.0\r\nUser-Agent: NTRIP SparkFun u-blox Client v1.0\r\n", + mountPoint); + + char credentials[512]; + if (strlen(casterUser) == 0) + { + strncpy(credentials, "Accept: */*\r\nConnection: close\r\n", sizeof(credentials)); + } + else + { + //Pass base64 encoded user:pw + char userCredentials[sizeof(casterUser) + sizeof(casterUserPW) + 1]; //The ':' takes up a spot + snprintf(userCredentials, sizeof(userCredentials), "%s:%s", casterUser, casterUserPW); + + Serial.print(F("Sending credentials: ")); + Serial.println(userCredentials); + +#if defined(ARDUINO_ARCH_ESP32) + //Encode with ESP32 built-in library + base64 b; + String strEncodedCredentials = b.encode(userCredentials); + char encodedCredentials[strEncodedCredentials.length() + 1]; + strEncodedCredentials.toCharArray(encodedCredentials, sizeof(encodedCredentials)); //Convert String to char array + snprintf(credentials, sizeof(credentials), "Authorization: Basic %s\r\n", encodedCredentials); +#else + //Encode with nfriendly library + int encodedLen = base64_enc_len(strlen(userCredentials)); + char encodedCredentials[encodedLen]; //Create array large enough to house encoded data + base64_encode(encodedCredentials, userCredentials, strlen(userCredentials)); //Note: Input array is consumed +#endif + } + strncat(serverRequest, credentials, SERVER_BUFFER_SIZE); + strncat(serverRequest, "\r\n", SERVER_BUFFER_SIZE); + + Serial.print(F("serverRequest size: ")); + Serial.print(strlen(serverRequest)); + Serial.print(F(" of ")); + Serial.print(sizeof(serverRequest)); + Serial.println(F(" bytes available")); + + Serial.println(F("Sending server request:")); + Serial.println(serverRequest); + ntripClient.write(serverRequest, strlen(serverRequest)); + + //Wait for response + unsigned long timeout = millis(); + while (ntripClient.available() == 0) + { + if (millis() - timeout > 5000) + { + Serial.println(F("Caster timed out!")); + ntripClient.stop(); + return; + } + delay(10); + } + + //Check reply + bool connectionSuccess = false; + char response[512]; + int responseSpot = 0; + while (ntripClient.available()) + { + if (responseSpot == sizeof(response) - 1) break; + + response[responseSpot++] = ntripClient.read(); + if (strstr(response, "200") > 0) //Look for '200 OK' + connectionSuccess = true; + if (strstr(response, "401") > 0) //Look for '401 Unauthorized' + { + Serial.println(F("Hey - your credentials look bad! Check you caster username and password.")); + connectionSuccess = false; + } + } + response[responseSpot] = '\0'; + + Serial.print(F("Caster responded with: ")); + Serial.println(response); + + if (connectionSuccess == false) + { + Serial.print(F("Failed to connect to ")); + Serial.println(casterHost); + return; + } + else + { + Serial.print(F("Connected to ")); + Serial.println(casterHost); + lastReceivedRTCM_ms = millis(); //Reset timeout + ggaTransmitComplete = true; //Reset to start polling for new GGA data + } + } //End attempt to connect + } //End connected == false + + if (ntripClient.connected() == true) + { + uint8_t rtcmData[512 * 4]; //Most incoming data is around 500 bytes but may be larger + rtcmCount = 0; + + //Print any available RTCM data + while (ntripClient.available()) + { + //Serial.write(ntripClient.read()); //Pipe to serial port is fine but beware, it's a lot of binary data + rtcmData[rtcmCount++] = ntripClient.read(); + if (rtcmCount == sizeof(rtcmData)) break; + } + + if (rtcmCount > 0) + { + lastReceivedRTCM_ms = millis(); + + //Push RTCM to GNSS module over I2C + myGNSS.pushRawData(rtcmData, rtcmCount, false); + Serial.print(F("RTCM pushed to ZED: ")); + Serial.println(rtcmCount); + } + } + + //Provide the caster with our current position as needed + if (ntripClient.connected() == true + && transmitLocation == true + && (millis() - lastTransmittedGGA_ms) > timeBetweenGGAUpdate_ms + && ggaSentenceComplete == true + && ggaTransmitComplete == false + ) + { + Serial.print(F("Pushing GGA to server: ")); + Serial.println(ggaSentence); + + lastTransmittedGGA_ms = millis(); + + //Push our current GGA sentence to caster + ntripClient.print(ggaSentence); + ntripClient.print("\r\n"); + + ggaTransmitComplete = true; + + //Wait for response + unsigned long timeout = millis(); + while (ntripClient.available() == 0) + { + if (millis() - timeout > 5000) + { + Serial.println(F("Caster timed out!")); + ntripClient.stop(); + return; + } + delay(10); + } + + //Check reply + bool connectionSuccess = false; + char response[512]; + int responseSpot = 0; + while (ntripClient.available()) + { + if (responseSpot == sizeof(response) - 1) break; + + response[responseSpot++] = ntripClient.read(); + if (strstr(response, "200") > 0) //Look for '200 OK' + connectionSuccess = true; + if (strstr(response, "401") > 0) //Look for '401 Unauthorized' + { + Serial.println(F("Hey - your credentials look bad! Check you caster username and password.")); + connectionSuccess = false; + } + } + response[responseSpot] = '\0'; + + Serial.print(F("Caster responded with: ")); + Serial.println(response); + } + + //Close socket if we don't have new data for 10s + if (millis() - lastReceivedRTCM_ms > maxTimeBeforeHangup_ms) + { + Serial.println(F("RTCM timeout. Disconnecting...")); + if (ntripClient.connected() == true) + ntripClient.stop(); + return; + } + + delay(10); + } + + Serial.println(F("User pressed a key")); + Serial.println(F("Disconnecting...")); + ntripClient.stop(); +} + +//This function gets called from the SparkFun u-blox Arduino Library +//As each NMEA character comes in you can specify what to do with it +//We will look for and copy the GGA sentence +void SFE_UBLOX_GNSS::processNMEA(char incoming) +{ + //Take the incoming char from the u-blox I2C port and check to see if we should record it or not + if (incoming == '$' && ggaTransmitComplete == true) + { + ggaSentenceStarted = true; + ggaSentenceSpot = 0; + ggaSentenceEndSpot = sizeof(ggaSentence); + ggaSentenceComplete = false; + } + + if (ggaSentenceStarted == true) + { + ggaSentence[ggaSentenceSpot++] = incoming; + + //Make sure we don't go out of bounds + if (ggaSentenceSpot == sizeof(ggaSentence)) + { + //Start over + ggaSentenceStarted = false; + } + //Verify this is the GGA setence + else if (ggaSentenceSpot == 5 && incoming != 'G') + { + //Ignore this sentence, start over + ggaSentenceStarted = false; + } + else if (incoming == '*') + { + //We're near the end. Keep listening for two more bytes to complete the CRC + ggaSentenceEndSpot = ggaSentenceSpot + 2; + } + else if (ggaSentenceSpot == ggaSentenceEndSpot) + { + ggaSentence[ggaSentenceSpot] = '\0'; //Terminate this string + ggaSentenceComplete = true; + ggaTransmitComplete = false; //We are ready for transmission + + //Serial.print("GGA Parsed - "); + //Serial.println(ggaSentence); + + //Start over + ggaSentenceStarted = false; + } + } +} diff --git a/examples/ZED-F9P/Example16_NTRIPClient_WithGGA/secrets.h b/examples/ZED-F9P/Example16_NTRIPClient_WithGGA/secrets.h new file mode 100644 index 0000000..3a6becf --- /dev/null +++ b/examples/ZED-F9P/Example16_NTRIPClient_WithGGA/secrets.h @@ -0,0 +1,17 @@ +//Your WiFi credentials +const char ssid[] = "TRex"; +const char password[] = "parachutes"; + +//RTK2Go works well and is free +const char casterHost[] = "rtk2go.com"; +const uint16_t casterPort = 2101; +const char casterUser[] = "myEmail@test.com"; //User must provide their own email address to use RTK2Go +const char casterUserPW[] = ""; +const char mountPoint[] = "bldr_SparkFun1"; //The mount point you want to get data from + +//Emlid Caster also works well and is free +//const char casterHost[] = "caster.emlid.com"; +//const uint16_t casterPort = 2101; +//const char casterUser[] = "u99696"; //User name and pw must be obtained through their web portal +//const char casterUserPW[] = "466zez"; +//const char mountPoint[] = "MP1979"; //The mount point you want to get data from