In this guide, we will be learning how to use a GPS module with an Arduino and C++. We will be looking at; how GPS works, how to wire up and code an Arduino to use the GPS module, and as a final step we will be putting it all together in a quick little project - a device that takes your current position and points you in the direction of a target destination anywhere on earth, also telling you how far away it is.
We also have a version of this guide for the Raspberry Pi Pico and MicroPython if that is more your style.
Let's get into it!
How does GPS Work?
GPS is one of the most incredible things we as a species have made. Figuring out where you are used to be a skill our ancestors spent a considerable portion of their lives mastering, but now you can do it far more precisely with a GPS module (like in the image on the right) that costs a little more than a cup of coffee. But how does it work? How does such an inexpensive module have the ability to locate you anywhere on Earth?
Well, this is thanks to a system of strategically placed GPS satellites orbiting around the Earth, about 30 of them. And these satellites are constantly beaming out radio messages containing the exact time the signal was sent, and where the satellite is in orbit. These satellites aren't stationary - they're constantly moving in a medium Earth orbit at around 20,000 km above us, completing two full orbits every day. Although they are constantly moving, their strategic placement means that every part of the earth can see 4 to 15 of them.
With that multi-billion dollars of equipment already in space, this inexpensive device just listens for those messages and uses that information to figure out how far away each satellite is, which it calculates using the speed of light. The module measures the time delay between when the signal was sent (as reported by the satellite) and when it was received. Since radio waves travel at the speed of light (roughly 300,000 km per second), the delay tells us the distance. Pretty clever, right?
If it can get this distance from 4 or more satellites at once, it can figure out its latitude, longitude, altitude, and even get a very accurate time reading from the satellite's atomic clock. That's another incredible thing - this module is going to let your project read the time from an atomic clock in space!
A simplified example of how this locating with distances works: imagine that I was lost somewhere in Australia. But I knew that I was exactly 2,000 km away from Sydney, and I also knew I was exactly 1,450 km away from Cairns. If I drew a 2,000 km circle around Sydney, I know that I could be anywhere on that line. If I drew another circle around Cairns, this time 1,450 km, I could find the place where they both intersect and know that I am in Alice Springs! It gets a bit more complicated in three dimensions, but it's the same idea - if you know how far away the satellites are and where they are, you can find where you are. Remember though, we need 4 satellites for our module to work, not 2 like our cities example!
This process is called trilateration (not triangulation, which is slightly different) and the GPS module we're using handles all this complex math for us, giving us a simple readout of coordinates. And the best part? There is no subscription required for this, and it's a 1-way system as we are just intercepting signals from these satellites - we know where the satellites are, but the satellites have no idea about us!
What You Will Need
To follow along with this guide you will need:
- An Arduino Uno: We are using the Uno R4, but this will work with the older Uno R3 or any Arduino Uno. With a bit of effort, this can also be adapted to work with an ESP32.
- A U-blox NEO-6M GPS Module
- Jumper Wires to connect it all.
Connecting and Reading the GPS
First things first, let's connect our GPS to the Arduino. Ensure you have soldered the header pins onto your GPS module. We will then connect the ground pin of the GPS to a ground pin of the Arduino, and the Vcc pin of the GPS to the 5 Volt power pin of the Arduino - this is going to supply power to our GPS module.
This GPS module can be powered with either 3.3 volts or 5 volts, there is no operational difference, just a handy bit of information if you want to power it with 3.3 Volts instead.
The GPS module will send its data to our Arduino via UART. Pins 0 and 1 of the Arduino are usually used for UART communication as those are the pins that are connected to the Arduino's UART hardware. However, when we plug the Arduino into a computer and print out information to the serial monitor it also uses these pins - so we will get a conflict here if we try to use both at the same time. This issue has been fixed with the newer Arduino R4 models as the USB serial and UART through pins 1 and 0 use different hardware.
Regardless, and for the sake of being able to run the same code on any version of the Uno, we will instead be using Pins 2 and 3 for our UART connection, and using SoftwareSerial to simulate that UART hardware. We will take a look at this in more detail in a bit.
Another important thing to mention is the Arduino uses 5-Volt logic, and the GPS uses 3.3-Volt logic. If we send a 5-Volt signal from the Arduino to the 3.3-Volt GPS, we risk damaging our module. However, if we send a 3.3-Volt signal from the GPS to the 5-Volt Arduino, it is going to be able to read it just fine. Because of this, we are only going to connect the TX (the transmitter pin) of the GPS to the RX (the receiver pin) of the Arduino (which will send data from the GPS to the Arduino).
In this guide, we do not need to make the other connection to send data in the opposite direction so we won't make that connection. If you wish to make this other connection (the GPS's RX to the Arduino's TX) for any reason, you will need to use a bi-directional logic level converter.
Now that we have it wired up, let's jump into the Arduino IDE and code this up. Plug your Arduino into your computer, select the board and COM port, create a new sketch, and paste in the following demo code:
#include <SoftwareSerial.h> const int GPS_RX_PIN = 2; // Arduino pin connected to GPS TX const int GPS_TX_PIN = 3; // Arduino pin connected to GPS RX // Initialize the software serial port SoftwareSerial gpsSerial(GPS_RX_PIN, GPS_TX_PIN); void setup() { // Initialize software serial for GPS communication gpsSerial.begin(9600); // Initialize the hardware serial port to communicate with the computer Serial.begin(9600); } void loop() { // Check if data is available from GPS if (gpsSerial.available()) { // Read the available data char gps_reading = gpsSerial.read(); // Pass it to the Serial Monitor Serial.write(gps_reading); } }
This is some very stock-standard code that reads the UART connection and prints it to the serial monitor, the only twist being the usage of SoftwareSerial. At the top of the code we import the library, then we set up serial (and call it gpsSerial) using pins 2 and 3. By doing this we are telling the Arduino to emulate the UART hardware by virtually simulating it with code. This does use some of the Arduino's processing power as it has to deal with the overhead of running this simulation - but for our needs, this performance dip is going to be largely negligible, especially on the Uno R4. Outside of this dip in performance, there are very few functional differences. This is an extremely handy method of dealing with the Pin conflict for any UART projects on an Uno R3.
Compile and upload the code to the Arduino, and open up the serial monitor to see the messages coming from the GPS module. Chances are, yours might look like the text below with a lot of commas, and not much information. To analyse these messages it might be easy to unplug the Arduino to prevent new messages from being printed.
This is actually what the GPS outputs when it hasn't gotten a "lock" onto enough satellites yet (it isn't receiving a good enough signal from at least 4 satellites). When writing this I am sitting in our giant metal warehouse so it's a little difficult to receive those signals, and we aren't able to get our position yet. You can confirm this by seeing a 'V' in the status field in the top line of the message. When your GPS module can see enough satellites and can start finding your location, this will instead change to the letter 'A', and the red LED on the module will also start flashing.
It may take your module a few minutes to get a lock if it hasn't been powered on for a while, and if you are like us and inside a giant metal building, you might need to move closer to a window or head outside to get a lock onto those satellites. We found that if we took it outside and powered the GPS module for a few minutes to get a lock, we were able to then work on it inside and it kept that lock.
This also brings up a conversation about accuracy and operating conditions. Essentially, the more that's between you and the satellites in space, the less accurate it will be. When we were outside with a clear view of the sky, the GPS module was accurate to less than a meter. However, when it was REALLY cloudy outside (almost storming), and we were in between a few buildings and under some trees, it was accurate to about 2 or 3 meters. And when we bring it inside the giant metal warehouse, that blows it out to a dozen, sometimes maybe 50 meters off - it all depends on your conditions. But the more things between you and the satellites, the less accurate your GPS will be.
After Going outside and getting a lock, we were able to start receiving a full message as shown on the right, we have also gone ahead and highlighted where the essential information is - this is not mandatory to learn, just a helpful guide if you ever need it. Much of what is not highlighted is information about the satellites that the GPS is receiving from.
All of the information that we want is in there but it's quite hard to use in the format it is in. This format is called the NMEA format and is a standard for GPS outputs. While this is helpful for devices built for this standard, we now have the task of manipulating this string to extract the information we need. This can be quite a difficult task so don't be afraid to turn to an LLM like ChatGPT, Claude or Deep Seek to help you write code for this section as they are quite well-versed with the NMEA format.
Another option is to use a library, and we have already written one for this specific module.
Using a GPS Library
We have written a basic library that greatly simplifies the process of using the module and you can download it here:
Download the zip file (do not unzip it), then in the Arduino IDE select Sketch > Include Library > Add .ZIP Library and then open GPSParser.zip.
This library not only hides a lot of the complexities of extracting the relevant information from the GPS string but also deals with the UART interactions for us as well. The code below is a demo of how to use the library:
// Arduino GPS Demo using GPSParser library #include <SoftwareSerial.h> #include "GPSParser.h" // Define RX and TX pins for the GPS module const int GPS_RX_PIN = 2; // Arduino pin connected to GPS TX const int GPS_TX_PIN = 3; // Arduino pin connected to GPS RX // Initialize the software serial port SoftwareSerial gpsSerial(GPS_RX_PIN, GPS_TX_PIN); // Create a GPS reader that handles all the complexity GPSReader gps(gpsSerial); void setup() { // Initialize software serial for GPS communication gpsSerial.begin(9600); // Initialize serial for debugging Serial.begin(9600); } void loop() { // Get the GPS data (automatically updates) GPSData gps_data = gps.get_data(); /* .has_fix .latitude .longitude .speed_knots .time .date .satellites .altitude .hdop .pdop .vdop */ // Print the GPS data Serial.print("Fix: "); Serial.print(gps_data.has_fix ? "Yes" : "No"); Serial.print(", Latitude: "); Serial.print(gps_data.latitude, 6); Serial.print(", Longitude: "); Serial.print(gps_data.longitude, 6); // Additional data Serial.print(", Satellites: "); Serial.print(gps_data.satellites); Serial.print(", Time: "); Serial.print(gps_data.time); Serial.print(", Date: "); Serial.println(gps_data.date); // Small delay delay(100); }
Paste in this sample code to the IDE, compile and upload, and you should see some more nicely formatted GPS data being printed to the serial monitor. In this case, it is printing whether the GPS has a lock or not, and its latitude and longitude (which will both be 0 if it doesn't have a lock).
The important commands in this code are the creation of the GPS object using the library, which takes in the SoftwareSerial UART connection we set up:
#include "GPSParser.h" // Create a GPS reader that handles all the complexity GPSReader gps(gpsSerial);
Once we have that object setup, the GPS library is going to receive the messages coming from the GPS module (which happens about once a second), extract all the information, and then format it neatly for us to access. To get the latest reading, we simply need to call the .get_data() command like so:
void loop() { // Get the GPS data (automatically updates) GPSData gps_data = gps.get_data();
Now we have all of that data stored in gps_data which is actually a structure holding other variables. Below it is a comment with all the information stored in this structure. To use the information we simply need to tack it on the end of gps_data(). For example, if we wanted to get .time, we would simply use gps_data.time. The demo code has some examples of printing all of these out:
/* .has_fix .latitude .longitude .speed_knots .time .date .satellites .altitude .hdop .pdop .vdop */ // Print the GPS data Serial.print("Fix: "); Serial.print(gps_data.has_fix ? "Yes" : "No"); Serial.print(", Latitude: "); Serial.print(gps_data.latitude, 6); Serial.print(", Longitude: "); Serial.print(gps_data.longitude, 6); // Additional data Serial.print(", Satellites: "); Serial.print(gps_data.satellites); Serial.print(", Time: "); Serial.print(gps_data.time); Serial.print(", Date: "); Serial.println(gps_data.date); }
Most of the information stored in this structure is self-explanatory, but for clarification of the tricky ones; ".satellites" is the number of satellites the GPS is connected to and ".hdop", ".vdop", and ".pdop" are ratings of the horizontal, vertical and overall positional accuracy of the GPS readings. These are called the dilution of precision and essentially the lower the number is, the more accurate it is. Anything under 5 is a good reading.
An important note here is that this library outputs coordinates in the positive/negative decimal degrees (DD) format. This is different to the raw UART output format the GPS uses which is degrees decimal minutes (DDM) which looks like this:
3256.44393,S,15143.09072,E
There are a lot of different systems of representing coordinates and some are more fit for certain uses than others, but for our projects, the DD format will likely be the best choice. The same GPS reading from the Library will look like this:
-32.94073, 151.71818
These 2 numbers are not only easier to understand as a person, but also make the math relating to coordinates easier (in a little bit we will be using this math to calculate the distance between coordinates). Below is an image showing this DD coordinate system. Latitude is a measurement of your distance from the equator, and longitude is a measurement of how far away you are from the Prime Meridiam, a single line drawn from the north pole to the south pole cutting through parts of West Africa. This line is also where all timezones are based on, you are currently either a certain amount of hours ahead of this time, or behind it and your GPS will give the time in this zone. This DD system can also use the north/south and east/west format like in the image below, but this library will use the positive/negative format.
With this library and an understanding of how to use it, you are now able to add an accurate reading of location, altitude, and time to your project! Before we finish though, let's take a look at this module in action with a short and sweet project.
Project: Navigate Anywhere in the World
We currently have an Arduino that can find our location anywhere on the surface of the planet, and just to show how powerful of a concept that is, we are going to combine it with a few other simple components to build a device that will take a target GPS coordinate and inform us how far away that destination is, and what direction we will need to head. To follow along with this little project you will also need a:
- Battery or method of powering the Arduino: We love these Dupont AA battery packs for simplicity (which pair fantastically with rechargeable NIMH batteries).
- SSD1306 OLED: We are using the Piicodev OLED which we assemble in-house, but any generic OLED display will work. If you do use a different display, the connection method will differ a little from this guide as we are using the Piicodev connection system. The Piicodev system is also designed around libraries we wrote for it in MicroPython, but the core components on the board will work with an appropriate C++ library, like the ones we will be using.
- MMC5603 Magnetometer: This is a module that acts like a compass and will allow our Pico to know which direction it is pointing in
You will also need a method of mounting this all which could be a piece of cardboard or a 3d printed case. In this guide, we used a spare piece of acrylic sheet we had lying around and used double-sided tape and screws to mount everything.
If you are using the I2C modules with the Qwicc connector on the Arduino, you might want to grab some Piicodev cables to connect them (they are compatible with the Qwiic connector system).
To assemble this, start by placing or mounting all of the components, and then connect the ground pin of the battery pack to a ground pin of the Arduino, and the positive pin to the Arduino's Vin Pin. This is going to allow us to power the project, and we can turn it off and on with the switch on the battery pack.
Next, connect the GPS module to the Arduino exactly like we did previously. Then connect the I2C modules together with a Piicodev cable. If you are using the Arduino R4, you can connect these modules to the Qwiic connector on the bottom of the board with another Piicodev cable (it's hidden by the GPS wire a little in the image on the right). These should be "daisy-chained" together as one run of sensors - connect the Arduino to the OLED screen, then the other side of the OLED screen to the magnetometer.
If you are not using an R4 or don't have the Qwiic connector on your board, you will need a prototyping cable that adapts the Qwiic-style connector to Dupont connectors. Connect the red wire to the Arduino's 3.3 Volt power pin, the black wire to the Arduino's ground, the blue wire to the Arduino's SDA pin, and the yellow to the SCL pin.
We are going to need to install some libraries to operate the OLED and magnetometer. These libraries can be installed through the library manager on the left sidebar of the Arduino IDE. Search for, and install these libraries:
- Adafruit MMC56x3
- Adafruit SSD1306
We need to do one final step before running the project, and that is to get calibration values for the magnetometer. To do so we are going to need to find the maximum and minimum readings in the X, Y, and Z axis' which can be found by running the following code:
#include <Adafruit_MMC56x3.h> /* Assign a unique ID to this sensor at the same time */ Adafruit_MMC5603 mag = Adafruit_MMC5603(12345); float MagMinX, MagMaxX; float MagMinY, MagMaxY; float MagMinZ, MagMaxZ; long lastDisplayTime; void setup(void) { Serial.begin(9600); while (!Serial) delay(10); // will pause Zero, Leonardo, etc until serial console opens Serial.println("Adafruit_MMC5603 Magnetometer Calibration"); Serial.println(""); /* Initialise the sensor */ if (!mag.begin(MMC56X3_DEFAULT_ADDRESS, &Wire1)) { // I2C mode /* There was a problem detecting the MMC5603 ... check your connections */ Serial.println("Ooops, no MMC5603 detected ... Check your wiring!"); while (1) delay(10); } lastDisplayTime = millis(); MagMinX = MagMaxX = MagMinY = MagMaxY = MagMinZ = MagMaxZ = 0; } void loop(void) { /* Get a new sensor event */ sensors_event_t magEvent; mag.getEvent(&magEvent); if (magEvent.magnetic.x < MagMinX) MagMinX = magEvent.magnetic.x; if (magEvent.magnetic.x > MagMaxX) MagMaxX = magEvent.magnetic.x; if (magEvent.magnetic.y < MagMinY) MagMinY = magEvent.magnetic.y; if (magEvent.magnetic.y > MagMaxY) MagMaxY = magEvent.magnetic.y; if (magEvent.magnetic.z < MagMinZ) MagMinZ = magEvent.magnetic.z; if (magEvent.magnetic.z > MagMaxZ) MagMaxZ = magEvent.magnetic.z; if ((millis() - lastDisplayTime) > 1000) // display once/second { Serial.print("Mag Minimums: "); Serial.print(MagMinX); Serial.print(" ");Serial.print(MagMinY); Serial.print(" "); Serial.print(MagMinZ); Serial.println(); Serial.print("Mag Maximums: "); Serial.print(MagMaxX); Serial.print(" ");Serial.print(MagMaxY); Serial.print(" "); Serial.print(MagMaxZ); Serial.println(); Serial.println(); lastDisplayTime = millis(); } }
IMPORTANT: The Qwiic connector on the Uno R4 uses "Wire1" for its I2C connection. If you are NOT using this connection, it will instead use "Wire" for the I2C connection. If you are NOT using the Qwiic connection, you will need to change the following line:
if (!mag.begin(MMC56X3_DEFAULT_ADDRESS, &Wire1)) { // I2C mode
To look like:
if (!mag.begin(MMC56X3_DEFAULT_ADDRESS, &Wire)) { // I2C mode
Paste this code in and upload it to the board. Open the serial monitor and you should see a readout of the maximum and minimum magnetometer values being recorded. Ensure your device is at least 30 cm (1 foot) away from keyboards, monitors, or any other electrical device that might interfere with the readings, and then rotate your device around continuously in all directions. Take note of all 6 values that are printed to the serial monitor. If your project gets some weird or wonky directional readings, redo this step and get new calibration values.
Now we can finally copy and paste the demo code into the IDE:
#include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> #include <Adafruit_MMC56x3.h> #include <Wire.h> #include <SoftwareSerial.h> #include <math.h> #include "GPSParser.h" // OLED display configuration #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 64 #define OLED_RESET -1 #define SCREEN_ADDRESS 0x3C Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire1, OLED_RESET); // GPS pins configuration const int GPS_RX_PIN = 2; // Arduino pin connected to GPS TX const int GPS_TX_PIN = 3; // Arduino pin connected to GPS RX // Initialize the software serial port for GPS SoftwareSerial gpsSerial(GPS_RX_PIN, GPS_TX_PIN); // Create a GPS reader GPSReader gps(gpsSerial); // Initialize compass/magnetometer Adafruit_MMC5603 compass = Adafruit_MMC5603(12345); // Calibration values for magnetometer float magXmax = 0.00; float magYmax = 0.00; float magZmax = 0.00; float magXmin = 0.00; float magYmin = 0.00; float magZmin = 0.00; // Calculated offsets (hard iron calibration) float magXoffset = (magXmax + magXmin) / 2.0; // -27.015 float magYoffset = (magYmax + magYmin) / 2.0; // -3.675 float magZoffset = (magZmax + magZmin) / 2.0; // -34.255 // Calculate scaling factors (soft iron calibration) float avgDelta; float magXscale, magYscale, magZscale; // Set your destination coordinates here (latitude, longitude) const float DESTINATION_LAT = -32.940931; // Latitude in decimal degrees const float DESTINATION_LON = 151.718029; // Longitude in decimal degrees // Variables to store the latest GPS information float latest_distance = 0; float latest_bearing = 0; // Function to calculate distance between two GPS coordinates float calculate_distance(float lat1, float lon1, float lat2, float lon2) { // Convert decimal degrees to radians lat1 = lat1 * M_PI / 180.0; lon1 = lon1 * M_PI / 180.0; lat2 = lat2 * M_PI / 180.0; lon2 = lon2 * M_PI / 180.0; // Haversine formula float dlat = lat2 - lat1; float dlon = lon2 - lon1; float a = sin(dlat/2) * sin(dlat/2) + cos(lat1) * cos(lat2) * sin(dlon/2) * sin(dlon/2); float c = 2 * atan2(sqrt(a), sqrt(1-a)); // Earth radius in meters float r = 6371000; float distance = c * r; return distance; } // Function to calculate bearing between two GPS coordinates float calculate_bearing(float lat1, float lon1, float lat2, float lon2) { // Convert decimal degrees to radians lat1 = lat1 * M_PI / 180.0; lon1 = lon1 * M_PI / 180.0; lat2 = lat2 * M_PI / 180.0; lon2 = lon2 * M_PI / 180.0; // Calculate the bearing float dlon = lon2 - lon1; float y = sin(dlon) * cos(lat2); float x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dlon); float initial_bearing = atan2(y, x); // Convert from radians to degrees initial_bearing = initial_bearing * 180.0 / M_PI; float bearing = fmod((initial_bearing + 360.0), 360.0); return bearing; } // Draw an arrow pointing in the specified angle (in degrees) void draw_arrow(float angle, int center_x, int center_y, int size) { // Convert angle to radians float rad_angle = angle * M_PI / 180.0; // Calculate arrow tip int tip_x = center_x + size * sin(rad_angle); int tip_y = center_y - size * cos(rad_angle); // Calculate base points for arrow float base_angle_1 = rad_angle + 150.0 * M_PI / 180.0; float base_angle_2 = rad_angle - 150.0 * M_PI / 180.0; int base_x1 = center_x + (size/2) * sin(base_angle_1); int base_y1 = center_y - (size/2) * cos(base_angle_1); int base_x2 = center_x + (size/2) * sin(base_angle_2); int base_y2 = center_y - (size/2) * cos(base_angle_2); // Draw the arrow display.drawLine(center_x, center_y, tip_x, tip_y, SSD1306_WHITE); display.drawLine(tip_x, tip_y, base_x1, base_y1, SSD1306_WHITE); display.drawLine(tip_x, tip_y, base_x2, base_y2, SSD1306_WHITE); } // Get compass heading in degrees (0-360) float read_heading() { // Get magnetometer event sensors_event_t event; compass.getEvent(&event); // Apply calibration (hard iron) float x = event.magnetic.x - magXoffset; float y = event.magnetic.y - magYoffset; float z = event.magnetic.z - magZoffset; // Apply calibration (soft iron) x *= magXscale; y *= magYscale; z *= magZscale; // Calculate heading float heading = (atan2(-x, y) * 180.0) / M_PI; // Normalize to 0-360 if (heading < 0) { heading = 360.0 + heading; } return heading; } void setup() { // Initialize serial for debugging Serial.begin(9600); Serial.println("GPS Navigation Starting..."); // Initialize I2C and OLED display if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) { Serial.println(F("SSD1306 allocation failed")); for(;;); // Don't proceed, loop forever } // Initialize GPS gpsSerial.begin(9600); // Initialize magnetometer if (!compass.begin(MMC56X3_DEFAULT_ADDRESS, &Wire1)) { Serial.println("Ooops, no MMC5603 detected ... Check your wiring!"); while (1) delay(10); } // Calculate scaling factors for magnetometer float xRange = magXmax - magXmin; float yRange = magYmax - magYmin; float zRange = magZmax - magZmin; // Calculate average range for scaling avgDelta = (xRange + yRange + zRange) / 3.0; // Set scales (with safety checks) magXscale = (xRange > 0.1) ? avgDelta / xRange : 1.0; magYscale = (yRange > 0.1) ? avgDelta / yRange : 1.0; magZscale = (zRange > 0.1) ? avgDelta / zRange : 1.0; // Clear the display display.clearDisplay(); display.display(); // Show startup message display.setTextSize(1); display.setTextColor(SSD1306_WHITE); display.setCursor(0, 0); display.println("GPS Navigation System"); display.println("Initializing..."); display.display(); delay(2000); } void loop() { // Clear the display display.clearDisplay(); // Get compass heading float heading = read_heading(); // Get current GPS data GPSData gps_data = gps.get_data(); // If we have valid GPS data, calculate distance and bearing if (gps_data.has_fix) { // Calculate distance to destination latest_distance = calculate_distance(gps_data.latitude, gps_data.longitude, DESTINATION_LAT, DESTINATION_LON); // Calculate bearing to destination latest_bearing = calculate_bearing(gps_data.latitude, gps_data.longitude, DESTINATION_LAT, DESTINATION_LON); // Calculate the difference between bearing to destination and current heading float relative_angle = fmod((latest_bearing - heading + 360.0), 360.0); // Draw the arrow at the center of the display draw_arrow(relative_angle, 30, 26, 26); // Show distance display.setTextSize(1); display.setTextColor(SSD1306_WHITE); if (latest_distance >= 1000) { display.setCursor(4, 57); display.print(latest_distance/1000, 1); display.print(" km"); } else { display.setCursor(4, 57); display.print((int)latest_distance); display.print(" m"); } // Show altitude display.setCursor(94, 57); display.print(gps_data.altitude, 1); // Show current coordinates display.setCursor(64, 2); display.print(gps_data.latitude, 6); display.setCursor(64, 12); display.print(gps_data.longitude, 6); // Show destination coordinates display.setCursor(64, 30); display.print(DESTINATION_LAT, 6); display.setCursor(64, 40); display.print(DESTINATION_LON, 6); // Draw the partial octagon around the arrow display.drawLine(47, 0, 58, 11, SSD1306_WHITE); display.drawLine(58, 11, 58, 38, SSD1306_WHITE); display.drawLine(58, 38, 47, 49, SSD1306_WHITE); display.drawLine(0, 11, 11, 0, SSD1306_WHITE); display.drawLine(0, 11, 0, 38, SSD1306_WHITE); display.drawLine(0, 38, 11, 49, SSD1306_WHITE); // Draw the partial box around altitude display.drawLine(86, 54, 86, 64, SSD1306_WHITE); display.drawLine(86, 54, 128, 54, SSD1306_WHITE); // Draw the line between the current coords and the target display.drawLine(70, 24, 128, 24, SSD1306_WHITE); } else { // No GPS fix - show "Waiting for GPS" and satellite count if available display.setTextSize(1); display.setTextColor(SSD1306_WHITE); display.setCursor(14, 20); display.print("Waiting for GPS"); display.setCursor(44, 35); display.print("No Fix"); // Show satellite count display.setCursor(24, 50); display.print("Satellites: "); display.print(gps_data.satellites); } // Print debug information to the serial console Serial.print("Satellites: "); Serial.print(gps_data.satellites); Serial.print(", Position: "); Serial.print(gps_data.latitude, 6); Serial.print(", "); Serial.print(gps_data.longitude, 6); Serial.print(", Altitude: "); Serial.print(gps_data.altitude); Serial.print(" m, Fix: "); Serial.print(gps_data.has_fix ? "Yes" : "No"); Serial.print(", Heading: "); Serial.println(heading); // Update the display display.display(); // Update at 10 Hz delay(100); }
Before running this code, you will need to enter the 6 minimum and maximum calibration values (starting at line 30 in the demo code). If you are NOT using the Qwiic connector, you will also need to change all mentions of "Wire1" to "Wire", exactly like the calibration code. This will need to be changed twice in the code; when the screen is initialised in line 14, and when the magnetometer is initialised in line 161.
Upload that code to the board, and you should be greeted by a screen that waits till the GPS has a lock which also displays how many satellites it is connected to. Once it has a lock, you will see the screen on the right displaying the direction and distance to the target (by default set to the Core Electronics office), as well as your location, altitude and the target's coordinate.
And if you want to navigate to somewhere else other than the Core Electronics office (we don't see why you wouldn't want to), you can simply edit the target GPS coordinates in lines 47 and 48:
// Set your destination coordinates here (latitude, longitude) const float DESTINATION_LAT = -32.940931; // Latitude in decimal degrees const float DESTINATION_LON = 151.718029; // Longitude in decimal degrees
One of the easiest ways to get a target GPS coordinate would be with Google Maps. If you click on any point on earth, it will give you the coordinates of that point in the positive/negative DD format the code uses.
And with that, you now have a device that can navigate you to any location on the surface of the earth! There is something pretty profound to be considered here. With a few inexpensive off-the-shelf components and a few hundred lines of code, we are able to slap together this fun little project. And although this seems like a cool gizmo to make, if you presented this to someone 50 years ago, it would be a technilogical marvel and would be beyond what is possible with that age of technology. If you took it back 100 years ago to when people navigate with the stars, it would be akin to magic - and all of that is thanks to a few dozen satellites in space, and our simple GPS module.
And with that, you now have a device that can navigate you to any location on the surface of the earth, overcoming the need to master a skill that took our ancestors many years to learn. And all of that is thanks to the humble GPS module.