
Creating a GPS tracker is an exciting project, especially for beginners interested in electronics and software development. It’s a project that brings childhood curiosity about tracking everything around us to life. While the hardware setup is relatively simple, the real challenge lies in the software. But that’s no longer a concern, thanks to the introduction of the GeoLinker API.
Table of Contents
At Circuit Digest Cloud, we’ve launched an easy-to-use, free API called GeoLinker. This API enables you to plot and route the location data sent from your device to a server, making it effortless to visualize and track positions.
In this tutorial, we’ll build a GPS tracker using the ESP32 microcontroller and the Neo-6M GPS module as the main hardware components, with GeoLinker as the software component. Let's dive in and get started with this exciting project! We have previously built many projects using ESP32 you can also check them out if you want to explore more.
How Does ESP32 GPS Tracker Work?
In this project, we are going to build a simple GPS tracker that is easier to implement at both the hardware and software levels. For hardware, we will be using the well-known ESP32 Development Board along with the Neo-6M GPS Module. On the software side, we will utilize the GeoLinker API, developed by Circuit Digest Cloud.

We will fetch GPS data from the Neo-6M GPS module using the ESP32 and send this data to a map plotting and routing API via a simple HTTP request. Each data point will be appended to the database. By logging into our Circuit Digest Cloud account, we can view the live plotted data along with the route tracking.
An extra feature implemented in this project is online/offline data handling. Specifically, when the internet is unavailable, the data is stored in a local variable. Once the connection is restored, the stored data is pushed to the server first, followed by the newly fetched data. This is quite an interesting and useful feature, especially in areas where internet connectivity is unreliable.
With this, let's move on to the components required for this project.
Components Required
Only a minimal number of components are required for this project. However, it is important to verify the functionality of each component before use. As always, selecting components is your choice, depending on your budget and creativity, with no limitations on customization. Below are the components we have used to build a GPS tracker using esp32.
- ESP32 Development Board - 1
- LEDs - 2
- 1K Resistor - 2
- BreadBoard - 1
- Neo-6M GPS Module - 1
- Connecting Wires - As required
There is no specific reason for selecting the Neo-6M GPS module apart from its cost-effectiveness. If you are using the Neo-6M, as I did, and if your GPS module is not working check out the linked article for troubleshooting. You can also use any other GPS module that is compatible with the ESP32 to complete this project. Keeping theory Things apart let's move to the actual project, starting from software.
GeoLinker Setup for GPS Tracker
Like most CircuitDigest Cloud APIs, GeoLinker also requires an API key for authorization. Follow the steps below to receive your free API key.
If you are new to CircuitDigest Cloud, you need to register a new account by following the steps below.
Logging in to the "Circuit Digest Cloud"

Step 1: Visit the Circuit Digest.Cloud Home Page. Click the "Login" button located at the top right corner to be redirected to the login page.
Step 2: If you already have an account, log in using your existing credentials. If not, go to the registration page to create an account by filling in the required details. Once completed, click "Register Now" to sign up.
Step 3: After registering, use your email ID and password to log in on the login page.
Next, Step is to generate the API Key.
Generating API Key

Step 4: Once logged in, click on "My Account" at the top right corner.
Step 5: You will be directed to a page where you can generate your API Key. Enter the captcha text in the provided box, then click the "Submit" button.
Step 6: If the captcha is correct, you'll see a table displaying your API Key along with its expiration date and usage count.
Currently, there is a limit of 100 uses per key. Once you reach this limit, you can generate another key, giving you an additional 100 uses. This usage limit is in place to prevent server overload.
With this, we are ready to move to hardware.
ESP32 GPS Tracker Circuit Diagram
The circuit diagram is quite self-explanatory, as there are only a few connections, making it simple to understand. The complete circuit diagram to build a GPS tracker using ESP32 is shown below. The ESP32 is powered via its USB port, while the GPS module receives a stable 5V supply from the ESP32. Communication between the two modules is established through UART, where the GPS module's TX (Transmit) pin connects to the ESP32's RX (IO16), and the GPS RX (Receive) pin connects to the ESP32's TX (IO17). If you need more help you can also check out our article on how to use NEO-6M GPS module with ESP32 and learn more.

Two LEDs are used for status indication,
- A yellow LED connected to IO19 via a 1KΩ resistor indicates GPS data reception.
- A green LED connected to IO18 via a 1KΩ resistor signals network connectivity.
With this, the circuit connections are complete. Below, you can see the assembly of the hardware components.

While assembling the components, carefully check for proper connections between the components and the breadboard, as improper wiring might cause unidentified issues.
Next, we can move on to the coding part.
Arduino Code
Before moving to the brief code explanation, let's look at the code overview. The complete code to build a GPS tracker with esp32 is given at the end of this article.
This code is primarily designed for the ESP32 microcontroller. With a few modifications, it can also be used for other microcontrollers running the Arduino core. It connects to a GPS module via UART1, reads and processes GPS data (latitude, longitude, altitude, timestamp), connects to Wi-Fi to send GPS data to a cloud server, and stores GPS data locally if Wi-Fi is unavailable, uploading it later.
Now, let's break down the code, starting with the utilized libraries.
Libraries Used:
- WiFi.h: Allows the ESP32 to connect to a Wi-Fi network.
- HTTPClient.h: Enables HTTP communication with a cloud server.
- ArduinoJson.h: Handles JSON formatting for sending GPS data.
- vector: Stores GPS data when offline for later upload.
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <vector>
Variables and Constants Used
This section includes various categories such as Wi-Fi & server configurations, GPS module setup, data structures for GPS information, and other general variables.
const char* ssid = "xxxx"; // WiFi SSID - Replace your own
const char* password = "xxxx"; // WiFi Password - Replace your own
const char* serverUrl = "https://www.circuitdigest.cloud/geolinker"; // Server URL
const char* apiKey = "xxxxxxxxxxxx"; // API key for authentication
HardwareSerial gpsSerial(1);
const int RXPin = 16; // GPS TX connected to ESP32 RX
const int TXPin = 17; // GPS RX connected to ESP32 TX
struct GPSRawData {
double latitude;
char latitudeDir;
double longitude;
char longitudeDir;
int satellites;
double altitude;
int hours, minutes, seconds;
int day, month, year;
String timestamp; // Combined timestamp with date and time
};
struct GPSData {
double latitude;
double longitude;
String timestamp;
};
const unsigned long uploadInterval = 10000;
unsigned long lastUploadTime = 0;
const int networkStatusLED = 18;
const int gpsStatusLED = 19;
bool gpsDataValid = false;
GPSData latestGPSData;
GPSRawData latestGPSRawData;
std::vector<GPSData> offlineData;
Among the above variables, only a few need to be replaced with your own values: ssid, password, apiKey, and IO pins (GPS UART Pins, Network, and GPS Status LED pins).
Following that, two structures are initialized:
- GPSRawData: Stores all the raw data received from the GPS module.
- GPSData: Stores essential information for uploading to the server and for offline storage.
Additionally, the std::vector data type is used. This is a dynamic array provided by the standard C++ library, which can resize dynamically as elements are added or removed, unlike regular arrays. For more information, visit: Modern C++: std::vector and std::array.
Setup Function
The setup function initializes essential components.
- Initializes serial communication at 115200 baud for debugging.
- Sets GPS communication to 9600 baud.
void setup() {
Serial.begin(115200);
gpsSerial.begin(9600, SERIAL_8N1, RXPin, TXPin);
- Configures status LEDs as outputs.
pinMode(networkStatusLED, OUTPUT);
pinMode(gpsStatusLED, OUTPUT);
- Attempts to connect to Wi-Fi. The network LED blinks while connecting and stays ON once connected.
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
digitalWrite(networkStatusLED, LOW);
delay(500);
digitalWrite(networkStatusLED, HIGH);
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi connected!");
digitalWrite(networkStatusLED, HIGH);
}
Loop Function
The loop function is slightly complex. You can refer to the flowchart for better understanding:

1. A static string is used to store incoming GPS data.
void loop() {
static String gpsData = "";
2. Reads GPS data character-by-character and calls processGPSData() when a complete NMEA sentence is received.
while (gpsSerial.available()) {
char c = gpsSerial.read();
gpsData += c;
if (c == '\n') {
processGPSData(gpsData);
gpsData = "";
}
}
3. Checks if 10 seconds have passed since the last upload.
if (millis() - lastUploadTime >= uploadInterval) {
lastUploadTime = millis(00);
4. If GPS data is valid and Wi-Fi is connected, proceeds with the upload.
if (gpsDataValid) {
if (WiFi.status() == WL_CONNECTED) {
digitalWrite(networkStatusLED, HIGH);
5. Uploads previously stored data if Wi-Fi is available. Clears the buffer if all data is successfully uploaded.
bool offlineUploadSuccess = true;
for (auto& data : offlineData) {
if (!sendGPSData(data)) {
offlineUploadSuccess = false;
break;
}
}
if (offlineUploadSuccess) {
offlineData.clear();
}
6. If the upload fails, store the data for later.
if (!sendGPSData(latestGPSData)) {
Serial.println("Failed to upload latest GPS data. Storing locally.");
offlineData.push_back(latestGPSData);
digitalWrite(networkStatusLED, LOW);
} else {
Serial.println("Latest GPS data uploaded successfully.");
}
7. If Wi-Fi is disconnected, stores data and attempts to reconnect. If GPS data is invalid, skips the upload.
} else {
Serial.println("WiFi not connected. Storing data locally.");
offlineData.push_back(latestGPSData);
digitalWrite(networkStatusLED, LOW);
WiFi.disconnect();
WiFi.reconnect();
}
} else {
Serial.println("Invalid GPS data. Skipping upload.");
}
}
}
Other Supportive Functions
Apart from the main logic, there are helper functions for uploading and processing GPS data.

Uploading GPS Data
Initializes an HTTP request, sets headers for JSON format, and API authentication.
bool sendGPSData(GPSData data) {
HTTPClient http;
http.begin(serverUrl);
http.addHeader("Content-Type", "application/json");
http.addHeader("Authorization", apiKey);
Creates a JSON payload with GPS data.
String payload = R"({
"timestamp": [")" + String(data.timestamp) + R"("],
"lat": [)" + String(data.latitude, 6) + R"(],
"long": [)" + String(data.longitude, 6) + R"(]
})";
Sends the HTTP POST request and returns true if successful.
int httpResponseCode = http.POST(payload);
Serial.print("Server response code: ");
Serial.println(httpResponseCode);
http.end();
return (httpResponseCode == 200 || httpResponseCode == 201);
}
Processing GPS Data
- Extracts GPS data from NMEA sentences ($GPGGA, $GPRMC).
- Calls appropriate parsing functions.
void processGPSData(String raw) {
if (raw.startsWith("$GPGGA")) {
parseGPGGA(raw);
convertAndPrintLocalDateTime();
} else if (raw.startsWith("$GPRMC")) {
parseGPRMC(raw);
}
}
With this, the code explanation is complete. The whole code is available at the bottom of this article. Now, the only pending task is uploading it to the ESP32, and the magic happens.
ESP32 GPS Tracker - Demo and Testing
After successfully uploading the code, we can check its functionality. As always, you can refer to the flowchart to understand the expected output.
Before proceeding with the demonstration, you need to understand some important details about the response.
Regarding the hardware;
Regarding the software;
In our project, the concept is simple: GPS data is fetched and sent to the server. In case of an unreliable network, the data is stored locally and pushed once an internet connection is available. That's exactly what we tested during a trial run. So, let's see all the details below.
Initially, during the testing phase, we couldn't get a proper GPS signal due to limitations in an urban area. To resolve this, we used an external antenna for testing, and it worked flawlessly.

Once everything was working, we started our journey from the office. You can see a small movement from the office on the map. As you might know, the system currently pushes data every 10 seconds.

As everything was going fine, after a few minutes, we decided to test the online/offline feature. To do this, we turned off the mobile hotspot and left it off for 1 minute. As shown in the image below, I disconnected the mobile hotspot, forcing the device into offline mode.

Once it went offline, the data started storing in a local dynamic array. During the one-minute offline period, each data point was safely stored without any data loss.

As you can see in the above image, once I reconnected to the internet, the entire set of stored data was pushed to the server one by one within seconds, and the system switched back to online mode.
After 20 minutes, we completed our testing and returned to the office.

Hence, we successfully demonstrated and verified the working of the device and its application. With future updates in GeoLinker, it will be even easier to use as a generic GPS plotting and routing platform.
During this demonstration, we faced certain challenges that I would like to share here.
- We need to verify the accuracy of the data before sending it to the server because the server is designed to handle only decimal coordinates. There is no need to worry about the timestamp, as it is just a string, and you can feed any value of your choice.
- The routing API can only plot nearby consecutive coordinates. If the data fluctuates too much, you might not get a proper routing output on the map, but the plotting will still be done.
- The use of a local dynamic array is limited by hardware specifications. It is recommended to use separate flash memory for better data retention, even if the device loses power.
- Using the Neo6M GPS module in cities or high-traffic areas with its built-in antenna might be challenging. Therefore, using an external antenna is a better option.
GitHub Repository with Code and Circuit
The complete code for this GPS Tracker using ESP32 project is provided at the bottom of this page. Additionally, you can find the source code in our GitHub repository linked below.
Projects Using GPS Tracker for Data Visualization
Previously, we have used GPS trackers in various projects to collect and visualize location data. If you want to explore these projects, check out the links below.
GPS Visualizer to Upload Data and Visualize GPS Maps for Arduino, ESP32 & other Embedded Devices
This tutorial introduces the CircuitDigest Cloud GeoLinker API, a free tool designed to simplify GPS data handling. It allows users to upload GPS data from devices like Arduino, ESP32, NodeMCU, and Raspberry Pi, and visualize the data on a map with routes and timestamps.
In this project, a NodeMCU, NEO-6M GPS module, and OLED display are integrated to create a GPS location tracker. The system sets up a local web server to display location details, including a direct link to view the location on Google Maps.
Lora Based GPS Tracker using Arduino and LoRa Shield
This project demonstrates building a GPS tracking system using LoRa technology for long-range communication. A transmitter reads location data from a NEO-6M GPS module and transmits it wirelessly over LoRa, making it suitable for applications requiring extended range and low power consumption.
How to Use GPS module with STM32F103C8 to Get Location Coordinates
This tutorial covers interfacing a GPS module with an STM32F103C8 microcontroller to retrieve location coordinates. The project includes displaying the coordinates on a 16x2 LCD and provides guidance on programming the STM32 for GPS data acquisition.
DIY Location Tracker using GSM SIM800 and Arduino
This project involves creating a location tracker using an Arduino and a SIM800 GSM module. The system receives calls from users, retrieves GPS coordinates, and sends the location data back via SMS, allowing real-time tracking without the need for internet connectivity.
Complete Project Code
/*
ESP32 GPS Tracker with Offline Data Storage and Wi-Fi Upload
Features:
- Connects to a GPS module via UART1 (RX:16, TX:17)
- Reads GPS data (latitude, longitude, altitude, timestamp, satellites)
- Connects to Wi-Fi and uploads GPS data to a cloud server
- Stores GPS data locally when Wi-Fi is unavailable and uploads later
- Uses LEDs to indicate GPS signal and Wi-Fi connection status
- Automatically reconnects to Wi-Fi if disconnected
Functions:
- setup(): Initializes Wi-Fi, GPS module, and status LEDs
- loop(): Continuously reads GPS data, processes it, and uploads at intervals
- processGPSData(): Extracts GPS information from raw NMEA sentences
- sendGPSData(): Sends formatted GPS data to the cloud server via HTTP POST
- parseGPGGA(), parseGPRMC(): Extracts relevant GPS details from NMEA sentences
- convertAndPrintLocalDateTime(): Converts UTC time to local format
*/
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <vector> // Include vector library for storing offline GPS data
// WiFi credentials
const char* ssid = "xxxx"; // WiFi SSID - Replace your own
const char* password = "xxxx"; // WiFi Password - Replace your own
// Server details for sending GPS data
const char* serverUrl = "https://www.circuitdigest.cloud/geolinker"; // Server URL
const char* apiKey = "xxxxxxxxxxxx"; //12 character API key for authentication
// GPS module connection using UART1
HardwareSerial gpsSerial(1);
const int RXPin = 16; // GPS TX connected to ESP32 RX
const int TXPin = 17; // GPS RX connected to ESP32 TX
// Structure to hold detailed GPS data
struct GPSRawData {
double latitude;
char latitudeDir;
double longitude;
char longitudeDir;
int satellites;
double altitude;
int hours, minutes, seconds;
int day, month, year;
String timestamp; // Combined timestamp with date and time
};
// Structure to store simplified GPS data for transmission
struct GPSData {
double latitude;
double longitude;
String timestamp;
};
// Upload interval and last upload timestamp
const unsigned long uploadInterval = 10000; // Data sent every 10 seconds
unsigned long lastUploadTime = 0;
// LED indicators for network and GPS status
const int networkStatusLED = 18; // Indicates WiFi status
const int gpsStatusLED = 19; // Indicates GPS data validity
bool gpsDataValid = false; // Flag to check if GPS data is valid
GPSData latestGPSData; // Stores the latest valid GPS data
GPSRawData latestGPSRawData; // Stores raw GPS data
// Buffer to store GPS data when offline
std::vector<GPSData> offlineData;
void setup() {
Serial.begin(115200);
gpsSerial.begin(9600, SERIAL_8N1, RXPin, TXPin); // Initialize GPS module
// Initialize LEDs
pinMode(networkStatusLED, OUTPUT);
pinMode(gpsStatusLED, OUTPUT);
// Connect to WiFi
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
digitalWrite(networkStatusLED, LOW); // Turn off LED when not connected
delay(500);
digitalWrite(networkStatusLED, HIGH); // Blink LED while connecting
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi connected!");
digitalWrite(networkStatusLED, HIGH); // Turn on LED when connected
}
void loop() {
static String gpsData = "";
// Read and process GPS data from the module
while (gpsSerial.available()) {
char c = gpsSerial.read();
gpsData += c;
if (c == '\n') { // When a full GPS sentence is received
processGPSData(gpsData);
gpsData = "";
}
}
// Periodically send GPS data to the server
if (millis() - lastUploadTime >= uploadInterval) {
lastUploadTime = millis();
if (gpsDataValid) { // Only send if GPS data is valid
if (WiFi.status() == WL_CONNECTED) {
digitalWrite(networkStatusLED, HIGH); // Indicate WiFi is active
// Send any stored offline data first
bool offlineUploadSuccess = true;
for (auto& data : offlineData) {
if (!sendGPSData(data)) { // Stop if sending fails
offlineUploadSuccess = false;
break;
}
}
if (offlineUploadSuccess) {
offlineData.clear(); // Clear offline buffer if all data is uploaded
}
// Send the latest GPS data
if (!sendGPSData(latestGPSData)) {
Serial.println("Failed to upload latest GPS data. Storing locally.");
offlineData.push_back(latestGPSData); // Store data for later upload
digitalWrite(networkStatusLED, LOW);
} else {
digitalWrite(networkStatusLED, HIGH);
Serial.println("Latest GPS data uploaded successfully.");
}
} else {
// If WiFi is offline, store GPS data for later upload
Serial.println("WiFi not connected. Storing data locally.");
offlineData.push_back(latestGPSData);
digitalWrite(networkStatusLED, LOW);
WiFi.disconnect();
WiFi.reconnect();
}
} else {
Serial.println("Invalid GPS data. Skipping upload.");
}
}
}
// Function to send GPS data to the server
bool sendGPSData(GPSData data) {
HTTPClient http;
http.begin(serverUrl);
http.addHeader("Content-Type", "application/json");
http.addHeader("Authorization", apiKey);
// Create JSON payload for GPS data
String payload = R"({
"timestamp": [
")" + String(data.timestamp)
+ R"("],
"lat": [
)" + String(data.latitude, 6)
+ R"(],
"long": [
)" + String(data.longitude, 6)
+ R"(]
})";
int httpResponseCode = http.POST(payload); // Send data via HTTP POST request
Serial.print("Server response code: ");
Serial.println(httpResponseCode);
http.end();
return (httpResponseCode == 200 || httpResponseCode == 201); // Success if 200 or 201
}
// Function to process received GPS data
void processGPSData(String raw) {
if (raw.startsWith("$GPGGA")) { // GPGGA contains location and satellite data
parseGPGGA(raw);
convertAndPrintLocalDateTime(); // Convert UTC to local time
} else if (raw.startsWith("$GPRMC")) { // GPRMC contains date and speed info
parseGPRMC(raw);
}
}
// Function to parse GPGGA data and extract latitude, longitude, and altitude
void parseGPGGA(String gpgga) {
String tokens[15];
int tokenIndex = 0;
// Split GPGGA string into individual tokens
int startIndex = 0;
for (int i = 0; i < gpgga.length(); i++) {
if (gpgga[i] == ',' || gpgga[i] == '*') {
tokens[tokenIndex++] = gpgga.substring(startIndex, i);
startIndex = i + 1;
}
}
// Extract meaningful GPS data
if (tokenIndex > 1) {
String utcTime = tokens[1];
latestGPSRawData.hours = utcTime.substring(0, 2).toInt();
latestGPSRawData.minutes = utcTime.substring(2, 4).toInt();
latestGPSRawData.seconds = utcTime.substring(4, 6).toInt();
latestGPSRawData.latitude = nmeaToDecimal(tokens[2]);
latestGPSData.latitude = nmeaToDecimal(tokens[2]);
latestGPSRawData.latitudeDir = tokens[3].charAt(0);
latestGPSRawData.longitude = nmeaToDecimal(tokens[4]);
latestGPSData.longitude = nmeaToDecimal(tokens[4]);
latestGPSRawData.longitudeDir = tokens[5].charAt(0);
latestGPSRawData.satellites = tokens[7].toInt();
latestGPSRawData.altitude = tokens[9].toDouble();
if (latestGPSRawData.satellites >= 4) { // Ensure enough satellites are available
if (latestGPSData.latitude != 0 || latestGPSData.longitude != 0) {
gpsDataValid = true;
digitalWrite(gpsStatusLED, HIGH); // Indicate valid GPS data
} else {
gpsDataValid = false;
digitalWrite(gpsStatusLED, LOW);
}
} else {
gpsDataValid = false;
digitalWrite(gpsStatusLED, LOW);
}
}
}
// Function to parse GPRMC data and extract the date
void parseGPRMC(String gprmc) {
String tokens[15];
int tokenIndex = 0;
// Split GPRMC string into tokens
int startIndex = 0;
for (int i = 0; i < gprmc.length(); i++) {
if (gprmc[i] == ',' || gprmc[i] == '*') {
tokens[tokenIndex++] = gprmc.substring(startIndex, i);
startIndex = i + 1;
}
}
// Extract date information
if (tokenIndex > 9) {
String utcDate = tokens[9];
latestGPSRawData.day = utcDate.substring(0, 2).toInt();
latestGPSRawData.month = utcDate.substring(2, 4).toInt();
latestGPSRawData.year = 2000 + utcDate.substring(4, 6).toInt();
}
}
// Function to convert UTC time and date to local time and update the timestamp
void convertAndPrintLocalDateTime() {
int offsetHours = 5;
int offsetMinutes = 30;
latestGPSRawData.minutes += offsetMinutes;
latestGPSRawData.hours += offsetHours;
if (latestGPSRawData.minutes >= 60) {
latestGPSRawData.minutes -= 60;
latestGPSRawData.hours++;
}
if (latestGPSRawData.hours >= 24) {
latestGPSRawData.hours -= 24;
latestGPSRawData.day++;
}
char timeBuffer[20];
snprintf(timeBuffer, sizeof(timeBuffer), "%04d-%02d-%02d %02d:%02d:%02d",
latestGPSRawData.year, latestGPSRawData.month, latestGPSRawData.day,
latestGPSRawData.hours, latestGPSRawData.minutes, latestGPSRawData.seconds);
latestGPSRawData.timestamp = String(timeBuffer);
latestGPSData.timestamp = String(timeBuffer);
Serial.print("TimeStamp: ");
Serial.println(latestGPSRawData.timestamp);
}
// Function to determine the number of days in a given month
int daysInMonth(int month, int year) {
if (month == 2) {
// Check for leap year
return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)) ? 29 : 28;
}
return (month == 4 || month == 6 || month == 9 || month == 11) ? 30 : 31;
}
// Function to convert NMEA coordinates to decimal degrees
double nmeaToDecimal(String nmeaCoord) {
if (nmeaCoord == "") return 0.0;
double raw = nmeaCoord.toDouble();
int degrees = int(raw / 100);
double minutes = raw - (degrees * 100);
return degrees + (minutes / 60.0);
}
// Function to print the latest GPS data
void printLatestGPSData() {
Serial.println("");
Serial.print("Latitude: ");
Serial.println(latestGPSData.latitude, 6);
Serial.print("Longitude: ");
Serial.println(latestGPSData.longitude, 6);
Serial.print("Timestamp: ");
Serial.println(latestGPSData.timestamp);
Serial.println("");
}