Simple Guide: Build a Reverse Geolocator with Raspberry Pi Pico W and GPS Module

Create a Reverse Geolocator with Pico W and GPS Module

Introduction

In this guide, we will walk through how to build a reverse geolocator using the Raspberry Pi Pico W and a GPS module. This project allows you to fetch the human-readable address of your current location by processing GPS data and querying the OpenCage Geocoder API. Perfect for hobbyists and students, this tutorial makes use of MicroPython to program the Pico W.

Parts and Tools Required

What is GPS?

GPS, or Global Positioning System, is a satellite-based navigation system that provides location and time information anywhere on Earth. It works by using a network of satellites that transmit signals to GPS receivers, like the NEO-6M module used in this project. By analyzing the signals from multiple satellites, the GPS receiver calculates its exact position in terms of latitude, longitude, and altitude.

This technology has become essential in many applications, including navigation, surveying, and scientific research. In this project, we utilize GPS to determine our geographical coordinates, which are then converted into human-readable addresses using the OpenCage Geocoder API.

GPS Working Overview
Illustration of how GPS works, using signals from satellites to calculate location. (Image Source: Wikipedia)

Connections

The GPS module needs to be soldered to add headers before it can be connected to the Pico W. After soldering, use the following connections:

  • GPS Module TX to Pico W Pin 1 (RX)
  • GPS Module RX to Pico W Pin 0 (TX)
  • GPS Module VCC to Pico W 3.3V
  • GPS Module GND to Pico W GND

Ensure you solder the GPS module carefully to avoid damaging the headers or PCB.

Connections between Pico W and GPS module
High level overview of connections between the Pico W and GPS module.

Setting Up an OpenCage Account

To use the OpenCage Geocoder API for reverse geocoding, you need an API key. Follow these steps to set up an account:

  1. Visit the OpenCage Geocoder API website.
  2. Click on Sign Up to create a free account.
  3. After registering, log in to your account and navigate to the API Keys section.
  4. Generate a new API key and copy it. You will use this key in the code to authenticate your API requests.

Note: The free tier provides limited requests per day. Upgrade if you need more requests.

Code

Below is the complete code for this project. This code reads GPS data, processes it to extract coordinates, and uses the OpenCage Geocoder API to retrieve the corresponding city and country.


import machine
import time
import urequests
import network

# Initialize UART for GPS communication
uart = machine.UART(0, baudrate=9600, tx=machine.Pin(0), rx=machine.Pin(1))

def connect_internet(name, password):
    try:
        sta_if = network.WLAN(network.STA_IF)
        sta_if.active(True)
        sta_if.connect(name, password)

        for i in range(0, 10):
            if not sta_if.isconnected():
                time.sleep(1)
        print("Connected to Wi-Fi")
    except Exception as e:
        print('There was an issue connecting to WIFI')
        print(e)

def reverse_geocode(latitude, longitude):
    """
    Convert latitude and longitude to a human-readable address using OpenCage API.
    """
    api_key = "Put your key here"  # Your provided API key
    # Manually construct the URL with query parameters
    full_url = f"https://api.opencagedata.com/geocode/v1/json?q={latitude}+{longitude}&key={api_key}"
    
    # Validate coordinates
    if not (-90 <= latitude <= 90) or not (-180 <= longitude <= 180):
        print("Invalid GPS coordinates.")
        return "Unknown", "Unknown"
    
    try:
        print(f"Requesting URL: {full_url}")  # Debugging log
        response = urequests.get(full_url, timeout=5)
        
        # Check if the request was successful
        if response.status_code == 200:
            data = response.json()
            print(f"API Response: {data}")  # Debugging log
            
            if data.get('results'):
                components = data['results'][0].get('components', {})
                city = components.get('city') or components.get('town') or components.get('village') or "Unknown"
                country = components.get('country', "Unknown")
                return city, country
            else:
                print("No results found in API response.")
                return "Unknown", "Unknown"
        elif response.status_code == 403:
            print("Error: API key is invalid or blocked.")
            return "Unknown", "Unknown"
        elif response.status_code == 429:
            print("Error: Rate limit exceeded. Try again later.")
            return "Unknown", "Unknown"
        else:
            print(f"Unexpected API error: {response.status_code}, {response.text}")
            return "Unknown", "Unknown"
    except Exception as e:
        print(f"Error during API request: {e}")
        return "Unknown", "Unknown"



def validate_checksum(sentence):
    if '*' not in sentence:
        return False  # Invalid sentence format
    
    # Split the sentence into data and checksum parts
    data, checksum = sentence.split('*')
    
    if not checksum:
        return False
    
    # Calculate checksum by XORing all characters in the data part
    calculated_checksum = 0
    for char in data:
        calculated_checksum ^= ord(char)
    return calculated_checksum == int(checksum, 16)

def extract_coordinates(sentence):
    """
    Extract coordinates and altitude from GPGGA sentence.
    Returns a tuple: (latitude, longitude, altitude) or None if invalid.
    """
    # Split the sentence by commas
    print(sentence)
    parts = sentence.split(",")

    # Validate the sentence
    if parts[0] != "GPGGA":
        raise ValueError("Input is not a valid GPGGA sentence.")

    # Check GPS signal validity
    gps_quality = parts[6]
    if gps_quality != "1":
        return None

    # Extract latitude
    raw_lat = parts[2]
    lat_direction = parts[3]
    latitude = float(raw_lat[:2]) + float(raw_lat[2:]) / 60  # Convert to decimal degrees
    if lat_direction == "S":
        latitude = -latitude

    # Extract longitude
    raw_lon = parts[4]
    lon_direction = parts[5]
    longitude = float(raw_lon[:3]) + float(raw_lon[3:]) / 60  # Convert to decimal degrees
    if lon_direction == "W":
        longitude = -longitude

    # Extract altitude
    altitude = float(parts[9])
    city, country = reverse_geocode(latitude, longitude)
    return latitude, longitude, altitude, city, country


def convert_to_decimal(raw, direction):
    """
    Convert raw NMEA latitude/longitude to decimal format.
    """
    if not raw:
        return None
    degrees = int(raw[:2])
    minutes = float(raw[2:])
    decimal = degrees + (minutes / 60)
    if direction in ['S', 'W']:
        decimal = -decimal
    return decimal

def read_gps(read_coordinates_only=False):
    buffer = b""
    while True:
        if uart.any():  # Check if there's data available in UART buffer
            data = uart.read()  # Read the data from UART
            if data:
                buffer += data  # Append data to buffer
                
                # Process complete NMEA sentences
                while b'\r\n' in buffer:  # Look for sentence endings
                    raw_line, buffer = buffer.split(b'\r\n', 1)  # Split buffer at sentence boundary
                    line = raw_line.decode('utf-8')  # Decode from bytes to string
                    if line.startswith('$'):
                        lines = line.split('$')
                        for line in lines:
                            if line == '' or not validate_checksum(line):
                                continue
                                
                            if read_coordinates_only:
                                if line.startswith('GPGGA'):
                                    coordinates = extract_coordinates(line)
                                    if coordinates:
                                        latitude, longitude, altitude, city, country = coordinates
                                        print(f"Latitude: {latitude}, Longitude: {longitude}, Altitude: {altitude} m, City: {city}, Country: {country}")
                                    else:
                                        print("GPS signal not available")
                            else:
                                print(line)  # Print the full NMEA sentence
                    
        time.sleep(1)  # Add a small delay to prevent CPU overuse

try:
    print("Reading GPS data...")
    # Pass `True` to only print coordinates and altitude, or `False` for all data
    connect_internet('SMA', 'mmmyellow')
    read_gps(read_coordinates_only=True)
except KeyboardInterrupt:
    print("Stopped")

        

Sample Log

Below is a sample output log showcasing the GPS data and reverse geocoding results:


GPGGA,042820.00,4152.68463,N,08739.23793,W,1,05,2.68,308.3,M,-33.8,M,,*62
Requesting URL: https://api.opencagedata.com/geocode/v1/json?q=41.87808+-87.65397&key=5b9111b71c5f47e194c1d9b8c7d896a9

Latitude: 41.87808, Longitude: -87.65397, Altitude: 308.3 m, City: Chicago, Country: United States
GPGGA,042821.00,4152.68536,N,08739.23883,W,1,05,2.68,310.1,M,-33.8,M,,*67
Requesting URL: https://api.opencagedata.com/geocode/v1/json?q=41.87809+-87.65398&key=5b9111b71c5f47e194c1d9b8c7d896a9

Latitude: 41.87809, Longitude: -87.65398, Altitude: 310.1 m, City: Chicago, Country: United States
GPGGA,042824.00,4152.68823,N,08739.24100,W,1,05,2.68,318.9,M,-33.8,M,,*6E
Requesting URL: https://api.opencagedata.com/geocode/v1/json?q=41.87814+-87.65401&key=5b9111b71c5f47e194c1d9b8c7d896a9

Latitude: 41.87814, Longitude: -87.65401, Altitude: 318.9 m, City: Chicago, Country: United States
GPGGA,042827.00,4152.69062,N,08739.24131,W,1,05,2.68,323.1,M,-33.8,M,,*63
Requesting URL: https://api.opencagedata.com/geocode/v1/json?q=41.87818+-87.65402&key=5b9111b71c5f47e194c1d9b8c7d896a9

Latitude: 41.87818, Longitude: -87.65402, Altitude: 323.1 m, City: Chicago, Country: United States
        

Testing

Once the connections are made and the code is uploaded to the Pico W, power it up. Open the serial console to view the GPS data and reverse geocoding results. You should see the latitude, longitude, altitude, city, and country printed out in real time.

Conclusion

Congratulations! You've successfully created a reverse geolocator using the Raspberry Pi Pico W and a GPS module. This project demonstrates the powerful capabilities of GPS and MicroPython, and there’s so much more you can explore. Consider expanding this project by logging data, integrating with other APIs, or adding real-time mapping features.

If you enjoyed this tutorial, subscribe to our YouTube channel for more hands-on tech projects and tutorials. Interested in taking your projects to the next level or need professional help? Hire us on Upwork through ShillehTek consulting. Let’s bring your ideas to life!

Thank you for following along, and we look forward to seeing what you create. Share your projects and experiences with us!

Create a free account to access full content.

All access to code and resources on ShillehTek.

Signup Now

Already a member? Sign In

Back to blog

Leave a comment

Please note, comments need to be approved before they are published.