Parsing and converting output from a GPS Dongle

Viewed 338

I bought a cheap ublox7 GPS dongle and stuck it on my raspberry pi 3. When I looked at the output and tried to stick it into a map program I got weird results. Here is some sample output from the device after parsing with a library called "pynmea2".

$GPGLL,3745.81303,N,12214.62049,W,175033.00,A,D*7C

I did some research about how to convert this output to something useful and I found a formula that involved splitting the number up and dividing it by .6.

Doing GPS Conversion – Degrees to Latitude Longitude and vice versa

so I wrote a python program to try to capture and convert all of this, and the output is off by like a mile. I am trying to figure out what I am doing wrong, how could I be this close yet still off by about one mile?

from time import sleep
import pynmea2
import serial
import re

degree_sign = u"\N{DEGREE SIGN}"

ser = serial.Serial('/dev/ttyACM0', 9600, timeout=1.0)
sio = io.TextIOWrapper(io.BufferedRWPair(ser, ser))
while True:
    line = sio.readline()
    msg = pynmea2.parse(line)
    msg = str(msg)
    if re.search("GPGLL", msg):
        raw_nums = re.findall(r'\b\d*\.\d*', msg)

        lat_whole = (raw_nums[0])
        lat_part1 = lat_whole[0:2]
        lat_part2 = lat_whole[2:4]
        lat_part2 = int(lat_part2)
        lat_part2 = lat_part2 / .6
        lat_part2 = int(lat_part2)
        lat_part2 = str(lat_part2)
        lat_part3 = lat_whole[5:9]
        lat_part3 = float(lat_part3)
        lat_part3 = lat_part3 / .6
        lat_part3 = round(lat_part3, 0)
        lat_part3 = int(lat_part3)
        lat_part3 = str(lat_part3)

        lon_whole = raw_nums[1]
        lon_part1 = lon_whole[0:3]
        lon_part1 = int(lon_part1)
        lon_part1 = -lon_part1
        lon_part1 = str(lon_part1)
        lon_part2 = lon_whole[3:5]
        lon_part2 = int(lon_part2)
        lon_part2 = lon_part2 / .6
        lon_part2 = str(lon_part2)
        lon_part2 = lon_part2[0:2]
        lon_part3 = lon_whole[6:10]
        lon_part3 = float(lon_part3)
        lon_part3 = lon_part3 / .6
        lon_part3 = round(lon_part3, 0)
        lon_part3 = int(lon_part3)
        lon_part3 = str(lon_part3)

        print(lat_part1 + "." + lat_part2 + lat_part3 +"," , lon_part1 + "." + lon_part2 + lon_part3)
        print(lat_part1+degree_sign+lat_part2+"'"+lat_part3+"\"" + "N", lon_part1 + degree_sign+ lon_part2 + "'" + lon_part3+"\"" + "W")
        sleep(1)

Here is the list that regex generated using the pynmea2 output:

['3745.81246', '12214.61512', '224329.00'] assigned to raw_nums.

Output from the script:

37.7513540, -122.2310268

37°75'13540"N -122°23'10268"W

Entering the first bit of output into google maps brings up a place near me but about a mile away, the second number doesn't work on Google maps for some reason - but it works on apple maps.

My questions:

  • I know there must be at least 100 better ways to write this code, do you have suggestions for getting there quicker?
  • Does the formula make sense? Am I applying it correctly?
  • Do you see a reason why this should return a result that is close but no cigar?
  • Do you know why the second line of output would not work as input into google maps?
  • What accuracy should I expect from a ublox 7 GPS dongle I got from Amazon for $12?

Thanks in advance, I really appreciate it.

Update: I looked up my address on gps coordinates conversion and the latitude they show for my address is 3745.50084 while my gps is reporting 3745.81246. So it just seems like I am starting with bad data...

2 Answers

If your parsed string from the GPS device is always of the form you specified, you can simply split the string on the commas like split_msg = msg.split(","). Then your lat will be split_msg[1] and your long split_msg[3]. With indexes 2 and 4 being the heading direction.

The lat is provided as DDmm.mm and long is provided as DDDmm.mm, which you seem to have captured above. So 3745.81246 would be 37 degrees and 45.81246 minutes. You can take the decimal portion of the minutes (i.e. 0.81246) and multiply times 60 to get seconds. So you would get 37 degrees, 45 minutes, and 48.75 seconds. As a sanity check, minutes and seconds should always be less than 60 as either of them being 60 would increment the next value (e.g. 60 minutes in a degree, 60 seconds in a minute).

To convert the minutes to a decimal degree number, simply divide the minutes number by 60 (45.81246/60=0.763541) then add that to your degrees. So 3745.81246 would become 37.763541.

So within the if statement:

split_msg = msg.split(",")
lat, lat_dir, long, long_dir = split_msg[1:5]
lat_d, lat_m = float(lat[:2]), float(lat[2:])
long_d, long_m = float(long[:3]), float(long[3:])
lat_dec = lat_d + lat_m/60
long_dec = long_d + long_m/60
lat_min = math.floor(lat_m)
lat_sec = 60*(lat_m - lat_min)
long_min = math.floor(long_m)
long_sec = 60*(long_m - long_min)
print(f"{lat_dec} {lat_dir}, {long_dec} {long_dir}")
print(f"{lat_d}{degree_sign} {lat_min}' {lat_sec}\" {lat_dir}, {long_d}{degree_sign} {long_min}' {long_sec}\" {long_dir}")

I have not tested the above code, but this is the general way I would approach this problem.

Since you're using the pynmea2 library you can make use of the object properties to ease the subsequent steps.

import pynmea2

line = "$GPGLL,3745.81303,N,12214.62049,W,175033.00,A,D*7C"
nmeaobj = pynmea2.parse(line)

coord = f'{nmeaobj.latitude} {nmeaobj.longitude}'
print(coord)
# 37.763551 -122.243675

The decimal degree difference between the script output and the library is around 0.012 which is similar to the precision length (1.1132 km) cited in the Degree precision versus length table. This would explain why you are seen a discrepancy of about a mile.

abs(-122.243675 - -122.2310268)
0.012648200000000998

abs(37.7635505 - 37.7513540)
0.01219650000000172

You could use the formula cited in the previous link to convert from decimal degree to DMS components, and this would yield a valid location. But notice that the directions of the coordinate (NS/WE) were left out of the final string formation.

def dd_to_dms(coord):
    d = int(coord)
    abs_d = abs(coord-d)
    m = int(60 * abs_d)
    s = 3600 * abs_d - 60 * m
    return d,m,s

lat = '''%02d°%02d'%07.4f"''' % dd_to_dms(nmeaobj.latitude)
lon = '''%02d°%02d'%07.4f"''' % dd_to_dms(nmeaobj.longitude)
print(f'{lat} {lon}')
# 37°45'48.7818" -122°14'37.2294"

The second line of output would not work as input into google maps because, as already mentioned, minutes and seconds should always be less than 60. Moreover, adding the directions of the coordinate to a negative degree could make you (depending on the algorithm used to parse the string) "walk" in the opposite direction of the desired location as a consequence of having the degree sign (or the coordinate direction NS/WE) ignored, or, in Google maps case, simply not understanding the coordinate.

37°45'48.7818" -122°14'37.2294"    # works
37°45'48.7818"N -122°14'37.2294"W  # don't work
37°45'48.7818"N 122°14'37.2294"W   # works
Related