Project Overview & Hardware Decision Matrix

To build a reliable, long-range Raspberry Pi GPS transmitter for off-grid tracking, telemetry, or wildlife monitoring, you need to bypass cellular infrastructure and local WiFi limits. The definitive hardware stack for this in 2026 is pairing a Raspberry Pi Zero 2 W with a u-blox NEO-6M GPS module and an Adafruit RFM95W (SX1276) LoRa transceiver. This combination yields 10km+ line-of-sight range, draws under 250mA during transmission, and requires zero monthly subscription fees.

Board Variant Target: The wiring, UART configuration, and Python code in this guide are explicitly written for the Raspberry Pi Zero 2 W running Raspberry Pi OS (Bookworm or Bullseye). If you are using a Pi 4 or Pi 5, the GPIO pinout remains the same, but the UART device path and power supply requirements will differ.

Hardware Decision Tree: Which Tracker Should You Build?

Don't default to a Raspberry Pi if a simpler microcontroller will do. Use this decision matrix to lock in your exact hardware pick:

RequirementRecommended HardwareWhy?
Need < 1km range, high bandwidth (video), and WiFi is availableESP32-CAM + WiFiPi is overkill; ESP32 handles streaming natively.
Need global tracking, budget allows $15+/mo data plansPi 4 + SIM7600 4G HATCellular is the only way to track across continents.
Need 5-15km range, zero recurring cost, low power, custom payloadsPi Zero 2 W + RFM95W LoRa + NEO-6MDEFAULT PICK. Best balance of Linux processing power and sub-GHz RF penetration.

Exact Parts List (Default Pick)

  • Compute: Raspberry Pi Zero 2 W ($15 - $20)
  • GPS: u-blox NEO-6M module with active patch antenna and EEPROM ($12 - $15)
  • Radio: Adafruit RFM95W LoRa Radio Breakout - 915MHz (US/AU) or 868MHz (EU) ($20)
  • Storage: 16GB SanDisk Extreme microSD card ($8)
  • Power: 5V 2.5A USB-C/Micro-USB power supply, or a 5V LiPo UPS HAT for field use
  • Wiring: Female-to-female jumper wires, protoboard

Wiring the Raspberry Pi GPS Transmitter

The Pi Zero 2 W uses SPI for the LoRa module and UART for the GPS. Below is the exact pin mapping. Double-check your LoRa module frequency; transmitting on 915MHz in the EU or 868MHz in the US violates FCC/CE spectrum regulations.

Component PinPi Zero 2 W GPIO (Physical Pin)Function
RFM95W VIN3V3 Power (Pin 1)Logic and Radio Power (3.3V)
RFM95W GNDGround (Pin 6)Common Ground
RFM95W SCKGPIO 11 / SCLK (Pin 23)SPI Clock
RFM95W MISOGPIO 9 / MISO (Pin 21)SPI Master-In-Slave-Out
RFM95W MOSIGPIO 10 / MOSI (Pin 19)SPI Master-Out-Slave-In
RFM95W CSGPIO 7 / CE1 (Pin 26)SPI Chip Select
RFM95W RSTGPIO 25 (Pin 22)Radio Hardware Reset
NEO-6M VCC5V Power (Pin 2 or 4)GPS Power (Requires 5V for active antenna LNA)
NEO-6M GNDGround (Pin 9)Common Ground
NEO-6M TXGPIO 15 / RXD (Pin 10)GPS Transmit to Pi Receive
NEO-6M RXGPIO 14 / TXD (Pin 8)GPS Receive from Pi Transmit
Critical UART Configuration: By default, the Pi Zero 2 W routes the hardware PL011 UART to the Bluetooth chip, leaving the unstable mini-UART on the GPIO pins. The mini-UART will drop GPS NMEA sentences when the CPU clock scales. You must open /boot/firmware/config.txt (or /boot/config.txt on older OS versions) and add these two lines at the bottom:
enable_uart=1
dtoverlay=disable-bt
Reboot after saving. This reclaims the PL011 hardware UART on GPIO 14/15, accessible at /dev/ttyAMA0. For more on Pi serial configuration, see the official Raspberry Pi configuration documentation.

Python Code: Parsing NMEA and Transmitting via LoRa

This script reads raw NMEA sentences from the GPS, parses the latitude and longitude using pynmea2, and broadcasts them via LoRa every 10 seconds. Install the required libraries first: pip3 install pyserial pynmea2 adafruit-circuitpython-rfm9x.

import time
import sys
import serial
import pynmea2
import board
import busio
import digitalio
import adafruit_rfm9x

# --- Hardware Pin Definitions (Pi Zero 2 W) ---
RADIO_CS = digitalio.DigitalInOut(board.CE1)
RADIO_RESET = digitalio.DigitalInOut(board.D25)
SPI = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO)

# Set to 915.0 for US/AU, 868.0 for EU
RADIO_FREQ_MHZ = 915.0 
GPS_PORT = '/dev/ttyAMA0'
BAUD_RATE = 9600

def init_radio():
    try:
        rfm9x = adafruit_rfm9x.RFM9x(SPI, RADIO_CS, RADIO_RESET, RADIO_FREQ_MHZ)
        rfm9x.tx_power = 23  # Max transmit power for RFM95W
        rfm9x.signal_bandwidth = 125000
        rfm9x.spreading_factor = 7
        rfm9x.coding_rate = 5
        print('LoRa Radio initialized successfully.')
        return rfm9x
    except RuntimeError as e:
        print(f'FATAL: Radio init failed: {e}')
        sys.exit(1)

def init_gps():
    try:
        ser = serial.Serial(GPS_PORT, baudrate=BAUD_RATE, timeout=1.0)
        print(f'GPS Serial port {GPS_PORT} opened.')
        return ser
    except serial.SerialException as e:
        print(f'FATAL: Could not open GPS port: {e}')
        sys.exit(1)

def main():
    radio = init_radio()
    gps_serial = init_gps()
    
    print('Starting Raspberry Pi GPS Transmitter loop...')
    
    while True:
        try:
            line = gps_serial.readline().decode('ascii', errors='replace').strip()
            if line.startswith('$GPRMC'):
                msg = pynmea2.parse(line)
                if msg.is_valid:
                    lat = msg.latitude
                    lon = msg.longitude
                    payload = f'GPS:{lat:.5f},{lon:.5f}'
                    
                    print(f'Transmitting: {payload}')
                    radio.send(bytes(payload, 'utf-8'))
                    
                    # LoRa duty cycle and thermal management delay
                    time.sleep(10) 
        except pynmea2.ParseError:
            # Ignore corrupted NMEA checksums
            continue
        except UnicodeDecodeError:
            continue
        except KeyboardInterrupt:
            print('Shutting down transmitter.')
            gps_serial.close()
            break
        except Exception as e:
            print(f'Unexpected error in main loop: {e}')
            time.sleep(2)

if __name__ == '__main__':
    main()

Debugging: Serial Timeouts and Radio Init Failures

When building a Raspberry Pi GPS transmitter, hardware initialization is where 90% of failures occur. Here are the exact error strings you will see, ranked by their most likely causes, and how to fix them.

Error 1: RuntimeError: Failed to find RFM9x radio chip, check wiring!

This means the Adafruit library sent an SPI read command to the SX1276 silicon and received no response or garbage data.

  1. Cause: SPI Interface Disabled. Run sudo raspi-config, navigate to Interface Options > SPI, and enable it. Reboot.
  2. Cause: Chip Select (CS) Mismatch. The code uses board.CE1 (Physical Pin 26). If you wired the RFM95W CS pin to Physical Pin 24 (CE0), the Pi is talking to a different SPI device. Move the wire to Pin 26 or change the code to board.CE0.
  3. Cause: Broken MISO Line. SPI is a bus. If the MISO (Master-In-Slave-Out) wire is loose, the Pi can send commands but cannot read the radio's confirmation registers. Check continuity on the MISO jumper.

Error 2: serial.serialutil.SerialException: [Errno 2] could not open port /dev/ttyAMA0

The Python script cannot find the hardware UART device node.

  1. Cause: Bluetooth Overlay Not Disabled. You forgot to add dtoverlay=disable-bt to config.txt, meaning /dev/ttyAMA0 is still bound to the internal Bluetooth chip. Add the overlay and reboot.
  2. Cause: Wrong Device Path. If you are on a very old OS version or using a different Pi model, the hardware UART might be mapped to /dev/serial0. Run ls -l /dev/serial* to verify the symlink target.
  3. Cause: Permission Denied (Often confused with Errno 2). If the error says [Errno 13] Permission denied, your user is not in the dialout group. Fix it with: sudo usermod -a -G dialout $USER and log out/in.

Error 3: GPS Yields Garbage Characters or pynmea2.ParseError on Every Line

You are receiving data, but it looks like ÿÿ$GPRMC... or fails checksum validation constantly.

  1. Cause: Mini-UART Clock Scaling. As mentioned in the wiring section, if you are reading from /dev/ttyS0 instead of /dev/ttyAMA0, CPU frequency scaling will destroy the 9600 baud timing. Switch to the PL011 UART.
  2. Cause: Baud Rate Mismatch. The NEO-6M defaults to 9600 baud. If you bought a pre-configured module flashed to 115200, change the BAUD_RATE variable in the Python script to match.
The First 3 Things to Check When It Fails:
1. Run lsmod | grep spi to ensure the SPI kernel module is loaded.
2. Run cat /dev/ttyAMA0 in the terminal. You should see raw NMEA text scrolling. If it's blank, your GPS TX/RX pins are swapped or the GPS has no 5V power.
3. Verify the LoRa antenna is screwed on before powering up. Transmitting without an antenna attached will fry the SX1276 RF front-end in seconds.

Extending and Simplifying the Build

Once your base Raspberry Pi GPS transmitter is online, you will likely want to adapt it for specific field conditions. Here is how to scale the project up or down.

How to Simplify (For Ultra-Low Power)

The Raspberry Pi Zero 2 W draws roughly 120mA at idle, which will drain a standard 18650 Li-ion cell in under 15 hours. If your use case is strict battery-powered tracking where Linux processing is unnecessary, drop the Pi entirely. Swap to an Arduino Pro Mini (3.3V/8MHz) paired with the same NEO-6M and RFM95W. Using the RadioHead library and putting the ATmega328P to sleep between GPS fixes will drop your average current draw to under 15mA, extending battery life to weeks. See the Adafruit Ultimate GPS guide for microcontroller-specific NMEA parsing techniques.

How to Extend (Adding Mesh and Sensors)

If you want to build a network of these transmitters rather than a single point-to-point link, rewrite the LoRa payload to conform to the Meshtastic protocol, or simply run Meshtastic firmware on an ESP32 and use the Pi strictly as a telemetry injector via MQTT. For environmental tracking, wire a BME280 I2C sensor to the Pi's SDA/SCL pins (GPIO 2 and 3). You can append temperature and barometric pressure to the LoRa payload string: f'ENV:{lat},{lon},{temp}C,{press}hPa'. Ensure your receiving gateway's Python script is updated to split the payload by commas and route the environmental data to a database like InfluxDB.

For deeper documentation on the CircuitPython RFM9x library parameters used in the code above, refer to the Adafruit CircuitPython RFM9x API documentation.