Skip to content

Commit 7a2e4a2

Browse files
committed
Add NTRIP Examples
Fix #17
1 parent b23284f commit 7a2e4a2

File tree

4 files changed

+663
-0
lines changed

4 files changed

+663
-0
lines changed
Lines changed: 379 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,379 @@
1+
/*
2+
Connect to NTRIP Caster to obtain corrections
3+
By: Nathan Seidle
4+
SparkFun Electronics
5+
Date: January 31, 2025
6+
License: MIT. Please see LICENSE.md for more information.
7+
8+
This example shows how to connect to an NTRIP Caster and push RTCM to the UM980 to
9+
obtain an RTK Fix.
10+
11+
Feel like supporting open source hardware?
12+
Buy a board from SparkFun!
13+
SparkFun Triband GNSS RTK Breakout - UM980 (GPS-23286) https://www.sparkfun.com/products/23286
14+
15+
Hardware Connections:
16+
Connect RX2 of the UM980 to pin 4 on the ESP32
17+
Connect TX2 of the UM980 to pin 13 on the ESP32
18+
To make this easier, a 4-pin locking JST cable can be purchased here: https://www.sparkfun.com/products/17240
19+
Note: Almost any ESP32 pins can be used for serial.
20+
Connect a dual or triband GNSS antenna: https://www.sparkfun.com/products/21801
21+
*/
22+
23+
#include <WiFi.h>
24+
#include "secrets.h"
25+
26+
#include <SparkFun_Unicore_GNSS_Arduino_Library.h> //http://librarymanager/All#SparkFun_Unicore_GNSS
27+
UM980 myGNSS;
28+
29+
#define pin_UART_TX 4
30+
#define pin_UART_RX 13
31+
32+
HardwareSerial SerialGNSS(1); //Use UART1 on the ESP32
33+
34+
//The ESP32 core has a built in base64 library but not every platform does
35+
//We'll use an external lib if necessary.
36+
#if defined(ARDUINO_ARCH_ESP32)
37+
#include "base64.h" //Built-in ESP32 library
38+
#else
39+
#include <Base64.h> //nfriendly library from https://github.com/adamvr/arduino-base64, will work with any platform
40+
#endif
41+
42+
//Global variables
43+
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
44+
long lastReceivedRTCM_ms = 0; //5 RTCM messages take approximately ~300ms to arrive at 115200bps
45+
int maxTimeBeforeHangup_ms = 10000; //If we fail to get a complete RTCM frame after 10s, then disconnect from caster
46+
47+
bool transmitLocation = true; //By default we will transmit the units location via GGA sentence.
48+
int timeBetweenGGAUpdate_ms = 10000; //GGA is required for Rev2 NTRIP casters. Don't transmit but once every 10 seconds
49+
long lastTransmittedGGA_ms = 0;
50+
51+
//Used for GGA sentence parsing from incoming NMEA
52+
bool ggaSentenceStarted = false;
53+
bool ggaSentenceComplete = false;
54+
bool ggaTransmitComplete = false; //Goes true once we transmit GGA to the caster
55+
56+
char ggaSentence[128] = {0};
57+
byte ggaSentenceSpot = 0;
58+
int ggaSentenceEndSpot = 0;
59+
//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
60+
61+
void setup()
62+
{
63+
Serial.begin(115200);
64+
delay(250);
65+
Serial.println();
66+
Serial.println("SparkFun UM980 Example");
67+
68+
//We must start the serial port before using it in the library
69+
SerialGNSS.begin(115200, SERIAL_8N1, pin_UART_RX, pin_UART_TX);
70+
71+
//myGNSS.enableDebugging(); // Print all debug to Serial
72+
73+
if (myGNSS.begin(SerialGNSS) == false) //Give the serial port over to the library
74+
{
75+
Serial.println("UM980 failed to respond. Check ports and baud rates.");
76+
while (1);
77+
}
78+
Serial.println("UM980 detected!");
79+
80+
myGNSS.disableOutput(); // Disables all messages on this port
81+
82+
myGNSS.setModeRoverSurvey();
83+
84+
//Enable the basic 5 NMEA sentences including GGA for the NTRIP Caster at 1Hz
85+
myGNSS.setNMEAPortMessage("GPGGA", 1);
86+
myGNSS.setNMEAPortMessage("GPGSA", 1);
87+
myGNSS.setNMEAPortMessage("GPGST", 1);
88+
myGNSS.setNMEAPortMessage("GPGSV", 1);
89+
myGNSS.setNMEAPortMessage("GPRMC", 1);
90+
91+
myGNSS.saveConfiguration(); //Save the current configuration into non-volatile memory (NVM)
92+
93+
Serial.println("GNSS Configuration complete");
94+
95+
Serial.print("Connecting to local WiFi");
96+
WiFi.begin(ssid, password);
97+
while (WiFi.status() != WL_CONNECTED)
98+
{
99+
delay(500);
100+
Serial.print(".");
101+
}
102+
Serial.println();
103+
104+
Serial.print("WiFi connected with IP: ");
105+
Serial.println(WiFi.localIP());
106+
107+
//Clear any serial characters from the buffer
108+
while (Serial.available())
109+
Serial.read();
110+
}
111+
112+
void loop()
113+
{
114+
if (Serial.available())
115+
{
116+
beginClient();
117+
while (Serial.available())
118+
Serial.read(); //Empty buffer of any newline chars
119+
}
120+
121+
Serial.println("Press any key to start NTRIP Client.");
122+
123+
delay(1000);
124+
}
125+
126+
//Connect to the NTRIP Caster, receive RTCM, and push it to the GNSS module
127+
void beginClient()
128+
{
129+
WiFiClient ntripClient;
130+
long rtcmCount = 0;
131+
132+
Serial.println("Subscribing to Caster. Press key to stop");
133+
delay(10); //Wait for any serial to arrive
134+
while (Serial.available())
135+
Serial.read(); //Flush
136+
137+
// Break if we receive a character from the user
138+
while (Serial.available() == 0)
139+
{
140+
//Connect if we are not already. Limit to 5s between attempts.
141+
if (ntripClient.connected() == false)
142+
{
143+
Serial.print("Opening socket to ");
144+
Serial.println(casterHost);
145+
146+
if (ntripClient.connect(casterHost, casterPort) == false) //Attempt connection
147+
{
148+
Serial.println("Connection to caster failed");
149+
return;
150+
}
151+
else
152+
{
153+
Serial.print("Connected to ");
154+
Serial.print(casterHost);
155+
Serial.print(": ");
156+
Serial.println(casterPort);
157+
158+
Serial.print("Requesting NTRIP Data from mount point ");
159+
Serial.println(mountPoint);
160+
161+
const int SERVER_BUFFER_SIZE = 512;
162+
char serverRequest[SERVER_BUFFER_SIZE + 1];
163+
164+
snprintf(serverRequest,
165+
SERVER_BUFFER_SIZE,
166+
"GET /%s HTTP/1.0\r\nUser-Agent: NTRIP SparkFun UM980 Client v1.0\r\n",
167+
mountPoint);
168+
169+
char credentials[512];
170+
if (strlen(casterUser) == 0)
171+
{
172+
strncpy(credentials, "Accept: */*\r\nConnection: close\r\n", sizeof(credentials));
173+
}
174+
else
175+
{
176+
//Pass base64 encoded user:pw
177+
char userCredentials[sizeof(casterUser) + sizeof(casterUserPW) + 1]; //The ':' takes up a spot
178+
snprintf(userCredentials, sizeof(userCredentials), "%s:%s", casterUser, casterUserPW);
179+
180+
Serial.print("Sending credentials: ");
181+
Serial.println(userCredentials);
182+
183+
#if defined(ARDUINO_ARCH_ESP32)
184+
//Encode with ESP32 built-in library
185+
base64 b;
186+
String strEncodedCredentials = b.encode(userCredentials);
187+
char encodedCredentials[strEncodedCredentials.length() + 1];
188+
strEncodedCredentials.toCharArray(encodedCredentials, sizeof(encodedCredentials)); //Convert String to char array
189+
#else
190+
//Encode with nfriendly library
191+
int encodedLen = base64_enc_len(strlen(userCredentials));
192+
char encodedCredentials[encodedLen]; //Create array large enough to house encoded data
193+
base64_encode(encodedCredentials, userCredentials, strlen(userCredentials)); //Note: Input array is consumed
194+
#endif
195+
196+
snprintf(credentials, sizeof(credentials), "Authorization: Basic %s\r\n", encodedCredentials);
197+
}
198+
strncat(serverRequest, credentials, SERVER_BUFFER_SIZE);
199+
strncat(serverRequest, "\r\n", SERVER_BUFFER_SIZE);
200+
201+
Serial.print("serverRequest size: ");
202+
Serial.print(strlen(serverRequest));
203+
Serial.print(" of ");
204+
Serial.print(sizeof(serverRequest));
205+
Serial.println(" bytes available");
206+
207+
Serial.println("Sending server request:");
208+
Serial.println(serverRequest);
209+
ntripClient.write(serverRequest, strlen(serverRequest));
210+
211+
//Wait for response
212+
unsigned long timeout = millis();
213+
while (ntripClient.available() == 0)
214+
{
215+
if (millis() - timeout > 5000)
216+
{
217+
Serial.println("Caster timed out!");
218+
ntripClient.stop();
219+
return;
220+
}
221+
delay(10);
222+
}
223+
224+
//Check reply
225+
bool connectionSuccess = false;
226+
char response[512];
227+
int responseSpot = 0;
228+
while (ntripClient.available())
229+
{
230+
if (responseSpot == sizeof(response) - 1)
231+
break;
232+
233+
response[responseSpot++] = ntripClient.read();
234+
if (strstr(response, "200") != nullptr) //Look for '200 OK'
235+
connectionSuccess = true;
236+
if (strstr(response, "401") != nullptr) //Look for '401 Unauthorized'
237+
{
238+
Serial.println("Hey - your credentials look bad! Check you caster username and password.");
239+
connectionSuccess = false;
240+
}
241+
}
242+
response[responseSpot] = '\0';
243+
244+
Serial.print("Caster responded with: ");
245+
Serial.println(response);
246+
247+
if (connectionSuccess == false)
248+
{
249+
Serial.print("Failed to connect to ");
250+
Serial.println(casterHost);
251+
return;
252+
}
253+
else
254+
{
255+
Serial.print("Connected to ");
256+
Serial.println(casterHost);
257+
lastReceivedRTCM_ms = millis(); //Reset timeout
258+
ggaTransmitComplete = true; //Reset to start polling for new GGA data
259+
}
260+
} //End attempt to connect
261+
} //End connected == false
262+
263+
if (ntripClient.connected() == true)
264+
{
265+
uint8_t rtcmData[512 * 4]; //Most incoming data is around 500 bytes but may be larger
266+
rtcmCount = 0;
267+
268+
//Print any available RTCM data
269+
while (ntripClient.available())
270+
{
271+
//Serial.write(ntripClient.read()); //Pipe to serial port is fine but beware, it's a lot of binary data
272+
rtcmData[rtcmCount++] = ntripClient.read();
273+
if (rtcmCount == sizeof(rtcmData))
274+
break;
275+
}
276+
277+
if (rtcmCount > 0)
278+
{
279+
lastReceivedRTCM_ms = millis();
280+
281+
//Write incoming RTCM directly to UM980
282+
SerialGNSS.write(rtcmData, rtcmCount);
283+
Serial.print("RTCM pushed to GNSS: ");
284+
Serial.println(rtcmCount);
285+
}
286+
}
287+
288+
//Write incoming NMEA back out to serial port and check for incoming GGA sentence
289+
while (SerialGNSS.available())
290+
{
291+
byte incoming = SerialGNSS.read();
292+
processNMEA(incoming);
293+
Serial.write(incoming);
294+
}
295+
296+
//Provide the caster with our current position as needed
297+
if (ntripClient.connected() == true
298+
&& transmitLocation == true
299+
&& (millis() - lastTransmittedGGA_ms) > timeBetweenGGAUpdate_ms
300+
&& ggaSentenceComplete == true
301+
&& ggaTransmitComplete == false)
302+
{
303+
Serial.print("Pushing GGA to server: ");
304+
Serial.println(ggaSentence);
305+
306+
lastTransmittedGGA_ms = millis();
307+
308+
//Push our current GGA sentence to caster
309+
ntripClient.print(ggaSentence);
310+
ntripClient.print("\r\n");
311+
312+
ggaTransmitComplete = true;
313+
}
314+
315+
//Close socket if we don't have new data for 10s
316+
if (millis() - lastReceivedRTCM_ms > maxTimeBeforeHangup_ms)
317+
{
318+
Serial.println("RTCM timeout. Disconnecting...");
319+
if (ntripClient.connected() == true)
320+
ntripClient.stop();
321+
return;
322+
}
323+
324+
delay(10);
325+
}
326+
327+
Serial.println("User pressed a key");
328+
Serial.println("Disconnecting...");
329+
ntripClient.stop();
330+
}
331+
332+
//As each NMEA character comes in you can specify what to do with it
333+
//We will look for and copy the GGA sentence
334+
void processNMEA(char incoming)
335+
{
336+
//Take the incoming char from the GNSS and check to see if we should record it or not
337+
if (incoming == '$' && ggaTransmitComplete == true)
338+
{
339+
ggaSentenceStarted = true;
340+
ggaSentenceSpot = 0;
341+
ggaSentenceEndSpot = sizeof(ggaSentence);
342+
ggaSentenceComplete = false;
343+
}
344+
345+
if (ggaSentenceStarted == true)
346+
{
347+
ggaSentence[ggaSentenceSpot++] = incoming;
348+
349+
//Make sure we don't go out of bounds
350+
if (ggaSentenceSpot == sizeof(ggaSentence))
351+
{
352+
//Start over
353+
ggaSentenceStarted = false;
354+
}
355+
//Verify this is the GGA setence
356+
else if (ggaSentenceSpot == 5 && incoming != 'G')
357+
{
358+
//Ignore this sentence, start over
359+
ggaSentenceStarted = false;
360+
}
361+
else if (incoming == '*')
362+
{
363+
//We're near the end. Keep listening for two more bytes to complete the CRC
364+
ggaSentenceEndSpot = ggaSentenceSpot + 2;
365+
}
366+
else if (ggaSentenceSpot == ggaSentenceEndSpot)
367+
{
368+
ggaSentence[ggaSentenceSpot] = '\0'; //Terminate this string
369+
ggaSentenceComplete = true;
370+
ggaTransmitComplete = false; //We are ready for transmission
371+
372+
//Serial.print("GGA Parsed - ");
373+
//Serial.println(ggaSentence);
374+
375+
//Start over
376+
ggaSentenceStarted = false;
377+
}
378+
}
379+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//Your WiFi credentials
2+
const char ssid[] = "Roving";
3+
const char password[] = "sparkfun";
4+
5+
//RTK2Go works well and is free
6+
const char casterHost[] = "rtk2go.com";
7+
const uint16_t casterPort = 2101;
8+
const char casterUser[] = "yes you have to fill this [email protected]"; //User must provide their own email address to use RTK2Go
9+
const char casterUserPW[] = "";
10+
const char mountPoint[] = "bldr_SparkFun1"; //The mount point you want to get data from
11+
12+
//Emlid Caster also works well and is free
13+
//const char casterHost[] = "caster.emlid.com";
14+
//const uint16_t casterPort = 2101;
15+
//const char casterUser[] = "u99696"; //User name and pw must be obtained through their web portal
16+
//const char casterUserPW[] = "466zez";
17+
//const char mountPoint[] = "MP1979"; //The mount point you want to get data from

0 commit comments

Comments
 (0)