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" 
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 " 
197+         }
198+         strncat (serverRequest, credentials, SERVER_BUFFER_SIZE);
199+         strncat (serverRequest, " \r\n " 
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 == ' $' 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