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\n User-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\n Connection: 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+ }
0 commit comments