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.
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:
| Requirement | Recommended Hardware | Why? |
|---|---|---|
| Need < 1km range, high bandwidth (video), and WiFi is available | ESP32-CAM + WiFi | Pi is overkill; ESP32 handles streaming natively. |
| Need global tracking, budget allows $15+/mo data plans | Pi 4 + SIM7600 4G HAT | Cellular is the only way to track across continents. |
| Need 5-15km range, zero recurring cost, low power, custom payloads | Pi Zero 2 W + RFM95W LoRa + NEO-6M | DEFAULT 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 Pin | Pi Zero 2 W GPIO (Physical Pin) | Function |
|---|---|---|
| RFM95W VIN | 3V3 Power (Pin 1) | Logic and Radio Power (3.3V) |
| RFM95W GND | Ground (Pin 6) | Common Ground |
| RFM95W SCK | GPIO 11 / SCLK (Pin 23) | SPI Clock |
| RFM95W MISO | GPIO 9 / MISO (Pin 21) | SPI Master-In-Slave-Out |
| RFM95W MOSI | GPIO 10 / MOSI (Pin 19) | SPI Master-Out-Slave-In |
| RFM95W CS | GPIO 7 / CE1 (Pin 26) | SPI Chip Select |
| RFM95W RST | GPIO 25 (Pin 22) | Radio Hardware Reset |
| NEO-6M VCC | 5V Power (Pin 2 or 4) | GPS Power (Requires 5V for active antenna LNA) |
| NEO-6M GND | Ground (Pin 9) | Common Ground |
| NEO-6M TX | GPIO 15 / RXD (Pin 10) | GPS Transmit to Pi Receive |
| NEO-6M RX | GPIO 14 / TXD (Pin 8) | GPS Receive from Pi Transmit |
/boot/firmware/config.txt (or /boot/config.txt on older OS versions) and add these two lines at the bottom:enable_uart=1dtoverlay=disable-btReboot 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.
- Cause: SPI Interface Disabled. Run
sudo raspi-config, navigate to Interface Options > SPI, and enable it. Reboot. - 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 toboard.CE0. - 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.
- Cause: Bluetooth Overlay Not Disabled. You forgot to add
dtoverlay=disable-bttoconfig.txt, meaning/dev/ttyAMA0is still bound to the internal Bluetooth chip. Add the overlay and reboot. - 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. Runls -l /dev/serial*to verify the symlink target. - 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 $USERand 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.
- Cause: Mini-UART Clock Scaling. As mentioned in the wiring section, if you are reading from
/dev/ttyS0instead of/dev/ttyAMA0, CPU frequency scaling will destroy the 9600 baud timing. Switch to the PL011 UART. - Cause: Baud Rate Mismatch. The NEO-6M defaults to 9600 baud. If you bought a pre-configured module flashed to 115200, change the
BAUD_RATEvariable in the Python script to match.
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.






