/* 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 #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 snprintf(credentials, sizeof(credentials), "Authorization: Basic %s\r\n", encodedCredentials); } 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; } } }