Squeezing Accuracy out of a Cheap GPS — Taking the Naive Approach

Share via

GPS is a really nice and quite cheap technology – unless you want accuracy in orders of decimeters or centimeters. Yes, there are high-accuracy GPS receivers which can achieve such an accuracy, however the price of the cheapest one is starting at $500. Not really a something a hobbyist can afford/is willing to pay for.

For many application there’s no need for absolute position, a relative position of 2 or more GPS receivers is sufficient. Take an autonomous robot moving in outside area as an example. You can setup a fixed ground station and navigate it relative to it. The intuition tells you that measuring relative position should be more accurate (when we’re talking about station less than 500 m away from each other) that measuring the absolute position int the WGS-84 system.

Why? The most of the GPS receiver’s accuracy depends on the atmospheric delay (the time the GPS signal travels through air) and it should be pretty much-less the same in a small ground area. So, having two the same GPS receivers, which uses the same antennas and algorithms for position measurement, should produce the same results, right? Well, kinda.

There are many papers out there about relative GPS position measurements. I’ve read several of them and none of them and I have to say they were bad. All of them were very shallow with lots of motivation and no results (don’t even think about public implementation!). I gave up reading (for now).

4$ u-blox NEO-6M GPS receiver
4$ u-blox NEO-6M GPS receiver

The Naive Approach

I wanted to test the basic, simple idea – the same receivers in the same conditions should produce the same results. So I got 2 u-blox NEO-6M GPS receiver modules with a patch antenna, which you can nowadays get for less than $4 on AliExpress (unbelievable).

Module Patch Antenna
Module patch antenna. Ruller units are centimeters (one part is a milimeter)

Getting these GPS modules running is not a big deal. They use the NMEA standard to communicate through serial line. After few minutes of googling I found out the modules are 5V tolerant. So I connected them to PC using FTDI USB to serial converter. I used the factory settings. U-blox provides u-center application for Windows, which can configure them through GUI. There are plenty of settings, however I can say no settings appears to change accuracy in a significant way (at least what I can tell after an afternoon spent on playing with various configurations).

Then I wrote a simple Python script, you can find at the end. This script reads and parses NMEA output of two modules every second and computes relative XY position of the obtained coordinates using a local projection. It produces a file of values, which can be easily plotted using gnuplot.

I oriented the antennas in the same way and attached them on a ruler so there is a 10 cm gap between them. I took this module outside, place it on ground and let the GPS receivers to “view the clear sky” for 3 minutes. I used 2 modes – the first considers every position as independent of the other. The second mode computes average of the last 20 positions.

Measurement setup
Measurement setup

Results

The results are really, really bad as you can see in the chart. Even the GPS modules measure in the same time intervals (they were powered-up simultaneously) and use the same antennas, the results are as noisy as an absolute position measurement. The error of the relative positions is up to 8 meters. So, the original hypothesis about same modules and conditions was not confirmed. This is not the way to go. If I want better accuracy, I should read more literature and probably look at the RTKLIB.

Results of the experiment
Results of the experiment – relative XY position in meters. Green line — average of 20 samples, purple one — independent samples

Python Script

#!/usr/bin/env python3

import sys
import time
import collections
from math import sqrt
import pynmea2
from pyproj import Proj, transform
from serial import Serial

class GPS:
	"""Interface for NMEA GPS device connected through serial line"""
	def __init__(self, port, baudrate=9600):
		self.port = Serial(port=port, baudrate=baudrate, timeout=0)
		self.buffer = ""
		samples = 20

		self.lon = collections.deque(maxlen=samples)
		self.lat = collections.deque(maxlen=samples)
		self.sat = None

	def update(self):
		"""Reads data from serial line and parses new sentence"""
		while True:
			read = self.port.read()
			if read is b'':
				return
			if read == b'\x00':
				continue
			self.buffer += read.decode('utf-8')
			if self.buffer[-1] == '\n':
				try:
					msg = pynmea2.parse(self.buffer[:-1])
					if hasattr(msg, "num_sats"):
						self.sat = msg.num_sats
					if hasattr(msg, "lon"):
						self.lon.append(msg.longitude)
					if hasattr(msg, "lat"):
						self.lat.append(msg.latitude)
				except pynmea2.nmea.ParseError:
					print("Parsing error")
				self.buffer = ""
				return True
		return False

	def fix(self):
		"""True if at least one position was obtained"""
		return len(self.lon) > 0 and len(self.lat) > 0

	def get_lat(self):
		return sum(self.lat) / len(self.lat)

	def get_lon(self):
		return sum(self.lon) / len(self.lon)

def print_gps(outfile, gps1, gps2):
	"""Prints GPS status"""
	# outfile.write("{0:.20f}\t{1:.20f}\t{2:.20f}\t{3:.20f}\n"
	#			   .format(gps1.get_lon(), gps1.get_lat(),
	#					   gps2.get_lon(), gps2.get_lat()))
	# outfile.flush()

	wgs84 = Proj('+proj=longlat +datum=WGS84 +no_defs')
	# Assign custom location for XX and YY
	nad27Blm15n = Proj('+proj=tmerc +lat_0=XX +lon_0=YY +k=0.9996 +x_0=500000.001016002 +y_0=0 +datum=NAD27 +no_defs') # http://epsg.io/32065 but in meters
	point1 = transform(wgs84, nad27Blm15n, gps1.get_lon(), gps1.get_lat())
	point2 = transform(wgs84, nad27Blm15n, gps2.get_lon(), gps2.get_lat())
	diff_x = point1[0] - point2[0]
	diff_y = point1[1] - point2[1]
	dist = sqrt(diff_x * diff_x + diff_y * diff_y)

	outfile.write("{0:.20f}\t{1:.20f}\t{2:.20f}\n".format(diff_x, diff_y, dist))
	outfile.flush()

	sys.stdout.write("{0:.20f}\t{1:.20f}\t{2:.20f}\t{3:.20f}\n"
					 .format(gps1.get_lon(), gps1.get_lat(),
							 gps2.get_lon(), gps2.get_lat()))
	sys.stdout.write("{0:.20f}\t{1:.20f}\t{2:.20f}\n".format(diff_x, diff_y, dist))

def main():
	"""Entry point"""
	if len(sys.argv) != 4:
		print("Wrong usage!")
		print("Please use: gps.py tty1 tty2 file")
		sys.exit(1)

	gps1 = GPS(sys.argv[1])
	gps2 = GPS(sys.argv[2])
	lines = 0

	with open(sys.argv[3], "w") as file:
		start_time = time.time()
		while True:
			gps1.update()
			gps2.update()

			tim = time.time()
			if tim - start_time > 1:
				print_gps(file, gps1, gps2)
				start_time = tim
				lines += 1
				print("Written {0} lines".format(lines))

			time.sleep(0.1)


if __name__ == "__main__":
	main()


Recent news: My open letter to the 3D-printing community

I love the 3D-printing community, but I think there is room for improvement. Let's get better in 2023! Read the full letter.

Support my work!

If you like my work (these blog posts, my software and CAD models) and you would like to see more posts on various topics coming, consider supporting me in various ways:

If you are interested in knowing what I am up to and recent sneak-peaks, consider following me on social media (Twitter, Instagram, Facebook).

My store offers

I launched new tank cleaning kits for Elegoo Saturn, Saturn S, Mars 1, and Mars 3. You can find them in my store.

Share via
Back To Top