Skip to content

Commit 1b80dea

Browse files
authored
Merge pull request #54 from lesamouraipourpre/parse-gsa-and-gsv
Implement GSV and GSA parsing
2 parents e6d7ba9 + 13b6cc9 commit 1b80dea

File tree

2 files changed

+191
-39
lines changed

2 files changed

+191
-39
lines changed

adafruit_gps.py

Lines changed: 86 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,8 @@ def __init__(self, uart, debug=False):
9090
self.height_geoid = None
9191
self.speed_knots = None
9292
self.track_angle_deg = None
93-
self.sats = None
93+
self._sats = None # Temporary holder for information from GSV messages
94+
self.sats = None # Completed information from GSV messages
9495
self.isactivedata = None
9596
self.true_track = None
9697
self.mag_track = None
@@ -121,16 +122,30 @@ def update(self):
121122
print(sentence)
122123
data_type, args = sentence
123124
data_type = bytes(data_type.upper(), "ascii")
124-
# return sentence
125-
if data_type in (
126-
b"GPGLL",
127-
b"GNGLL",
128-
): # GLL, Geographic Position – Latitude/Longitude
125+
(talker, sentence_type) = GPS._parse_talker(data_type)
126+
127+
# Check for all currently known GNSS talkers
128+
# GA - Galileo
129+
# GB - BeiDou Systems
130+
# GI - NavIC
131+
# GL - GLONASS
132+
# GP - GPS
133+
# GQ - QZSS
134+
# GN - GNSS / More than one of the above
135+
if talker not in (b"GA", b"GB", b"GI", b"GL", b"GP", b"GQ", b"GN"):
136+
# It's not a known GNSS source of data
137+
return True
138+
139+
if sentence_type == b"GLL": # Geographic position - Latitude/Longitude
129140
self._parse_gpgll(args)
130-
elif data_type in (b"GPRMC", b"GNRMC"): # RMC, minimum location info
141+
elif sentence_type == b"RMC": # Minimum location info
131142
self._parse_gprmc(args)
132-
elif data_type in (b"GPGGA", b"GNGGA"): # GGA, 3d location fix
143+
elif sentence_type == b"GGA": # 3D location fix
133144
self._parse_gpgga(args)
145+
elif sentence_type == b"GSV": # Satellites in view
146+
self._parse_gpgsv(talker, args)
147+
elif sentence_type == b"GSA": # GPS DOP and active satellites
148+
self._parse_gpgsa(talker, args)
134149
return True
135150

136151
def send_command(self, command, add_checksum=True):
@@ -241,6 +256,14 @@ def _parse_sentence(self):
241256
data_type = sentence[1:delimiter]
242257
return (data_type, sentence[delimiter + 1 :])
243258

259+
@staticmethod
260+
def _parse_talker(data_type):
261+
# Split the data_type into talker and sentence_type
262+
if data_type[0] == b"P": # Proprietary codes
263+
return (data_type[:1], data_type[1:])
264+
265+
return (data_type[:2], data_type[2:])
266+
244267
def _parse_gpgll(self, args):
245268
data = args.split(",")
246269
if data is None or data[0] is None or (data[0] == ""):
@@ -402,7 +425,8 @@ def _parse_gpgga(self, args):
402425
self.altitude_m = _parse_float(data[8])
403426
self.height_geoid = _parse_float(data[10])
404427

405-
def _parse_gpgsa(self, args):
428+
def _parse_gpgsa(self, talker, args):
429+
talker = talker.decode("ascii")
406430
data = args.split(",")
407431
if data is None or (data[0] == ""):
408432
return # Unexpected number of params
@@ -412,9 +436,9 @@ def _parse_gpgsa(self, args):
412436
# Parse 3d fix
413437
self.fix_quality_3d = _parse_int(data[1])
414438
satlist = list(filter(None, data[2:-4]))
415-
self.sat_prns = {}
416-
for i, sat in enumerate(satlist, 1):
417-
self.sat_prns["gps{}".format(i)] = _parse_int(sat)
439+
self.sat_prns = []
440+
for sat in satlist:
441+
self.sat_prns.append("{}{}".format(talker, _parse_int(sat)))
418442

419443
# Parse PDOP, dilution of precision
420444
self.pdop = _parse_float(data[-3])
@@ -423,9 +447,11 @@ def _parse_gpgsa(self, args):
423447
# Parse VDOP, vertical dilution of precision
424448
self.vdop = _parse_float(data[-1])
425449

426-
def _parse_gpgsv(self, args):
450+
def _parse_gpgsv(self, talker, args):
427451
# Parse the arguments (everything after data type) for NMEA GPGGA
452+
# pylint: disable=too-many-branches
428453
# 3D location fix sentence.
454+
talker = talker.decode("ascii")
429455
data = args.split(",")
430456
if data is None or (data[0] == ""):
431457
return # Unexpected number of params.
@@ -442,33 +468,54 @@ def _parse_gpgsv(self, args):
442468

443469
sat_tup = data[3:]
444470

445-
satdict = {}
446-
for i in range(len(sat_tup) / 4):
447-
j = i * 4
448-
key = "gps{}".format(i + (4 * (self.mess_num - 1)))
449-
satnum = _parse_int(sat_tup[0 + j]) # Satellite number
450-
satdeg = _parse_int(sat_tup[1 + j]) # Elevation in degrees
451-
satazim = _parse_int(sat_tup[2 + j]) # Azimuth in degrees
452-
satsnr = _parse_int(sat_tup[3 + j]) # signal-to-noise ratio in dB
453-
value = (satnum, satdeg, satazim, satsnr)
454-
satdict[key] = value
455-
456-
if self.sats is None:
457-
self.sats = {}
458-
for satnum in satdict:
459-
self.sats[satnum] = satdict[satnum]
471+
satlist = []
472+
timestamp = time.monotonic()
473+
for i in range(len(sat_tup) // 4):
474+
try:
475+
j = i * 4
476+
value = (
477+
# Satellite number
478+
"{}{}".format(talker, _parse_int(sat_tup[0 + j])),
479+
# Elevation in degrees
480+
_parse_int(sat_tup[1 + j]),
481+
# Azimuth in degrees
482+
_parse_int(sat_tup[2 + j]),
483+
# signal-to-noise ratio in dB
484+
_parse_int(sat_tup[3 + j]),
485+
# Timestamp
486+
timestamp,
487+
)
488+
satlist.append(value)
489+
except ValueError:
490+
# Something wasn't an int
491+
pass
492+
493+
if self._sats is None:
494+
self._sats = []
495+
for value in satlist:
496+
self._sats.append(value)
497+
498+
if self.mess_num == self.total_mess_num:
499+
# Last part of GSV message
500+
if len(self._sats) == self.satellites:
501+
# Transfer received satellites to self.sats
502+
if self.sats is None:
503+
self.sats = {}
504+
else:
505+
# Remove all satellites which haven't
506+
# been seen for 30 seconds
507+
timestamp = time.monotonic()
508+
old = []
509+
for i in self.sats:
510+
sat = self.sats[i]
511+
if (timestamp - sat[4]) > 30:
512+
old.append(i)
513+
for i in old:
514+
self.sats.pop(i)
515+
for sat in self._sats:
516+
self.sats[sat[0]] = sat
517+
self._sats.clear()
460518

461-
try:
462-
if self.satellites < self.satellites_prev:
463-
for i in self.sats:
464-
try:
465-
if int(i[-2]) >= self.satellites:
466-
del self.sats[i]
467-
except ValueError:
468-
if int(i[-1]) >= self.satellites:
469-
del self.sats[i]
470-
except TypeError:
471-
pass
472519
self.satellites_prev = self.satellites
473520

474521

examples/gps_satellitefix.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# SPDX-FileCopyrightText: 2021 lesamouraipourpre
2+
# SPDX-License-Identifier: MIT
3+
4+
import time
5+
import board
6+
7+
import adafruit_gps
8+
9+
# Create a serial connection for the GPS connection using default speed and
10+
# a slightly higher timeout (GPS modules typically update once a second).
11+
# These are the defaults you should use for the GPS FeatherWing.
12+
# For other boards set RX = GPS module TX, and TX = GPS module RX pins.
13+
# uart = busio.UART(board.TX, board.RX, baudrate=9600, timeout=10)
14+
15+
# for a computer, use the pyserial library for uart access
16+
# import serial
17+
# uart = serial.Serial("/dev/ttyUSB0", baudrate=9600, timeout=10)
18+
19+
# If using I2C, we'll create an I2C interface to talk to using default pins
20+
i2c = board.I2C()
21+
22+
# Create a GPS module instance.
23+
# gps = adafruit_gps.GPS(uart, debug=False) # Use UART/pyserial
24+
gps = adafruit_gps.GPS_GtopI2C(i2c, debug=False) # Use I2C interface
25+
26+
# Initialize the GPS module by changing what data it sends and at what rate.
27+
# These are NMEA extensions for PMTK_314_SET_NMEA_OUTPUT and
28+
# PMTK_220_SET_NMEA_UPDATERATE but you can send anything from here to adjust
29+
# the GPS module behavior:
30+
# https://cdn-shop.adafruit.com/datasheets/PMTK_A11.pdf
31+
32+
# Turn on everything (not all of it is parsed!)
33+
gps.send_command(b"PMTK314,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0")
34+
35+
# Set update rate to once a second (1hz) which is what you typically want.
36+
gps.send_command(b"PMTK220,1000")
37+
# Or decrease to once every two seconds by doubling the millisecond value.
38+
# Be sure to also increase your UART timeout above!
39+
# gps.send_command(b'PMTK220,2000')
40+
# You can also speed up the rate, but don't go too fast or else you can lose
41+
# data during parsing. This would be twice a second (2hz, 500ms delay):
42+
# gps.send_command(b'PMTK220,500')
43+
44+
45+
def format_dop(dop):
46+
# https://en.wikipedia.org/wiki/Dilution_of_precision_(navigation)
47+
if dop > 20:
48+
msg = "Poor"
49+
elif dop > 10:
50+
msg = "Fair"
51+
elif dop > 5:
52+
msg = "Moderate"
53+
elif dop > 2:
54+
msg = "Good"
55+
elif dop > 1:
56+
msg = "Excellent"
57+
else:
58+
msg = "Ideal"
59+
return f"{dop} - {msg}"
60+
61+
62+
talkers = {
63+
"GA": "Galileo",
64+
"GB": "BeiDou",
65+
"GI": "NavIC",
66+
"GL": "GLONASS",
67+
"GP": "GPS",
68+
"GQ": "QZSS",
69+
"GN": "GNSS",
70+
}
71+
72+
# Main loop runs forever printing the location, etc. every second.
73+
last_print = time.monotonic()
74+
while True:
75+
# Make sure to call gps.update() every loop iteration and at least twice
76+
# as fast as data comes from the GPS unit (usually every second).
77+
# This returns a bool that's true if it parsed new data (you can ignore it
78+
# though if you don't care and instead look at the has_fix property).
79+
if not gps.update() or not gps.has_fix:
80+
time.sleep(0.1)
81+
continue
82+
83+
if gps.nmea_sentence[3:6] == "GSA":
84+
print(f"{gps.latitude:.6f}, {gps.longitude:.6f} {gps.altitude_m}m")
85+
print(f"2D Fix: {gps.has_fix} 3D Fix: {gps.has_3d_fix}")
86+
print(f" PDOP (Position Dilution of Precision): {format_dop(gps.pdop)}")
87+
print(f" HDOP (Horizontal Dilution of Precision): {format_dop(gps.hdop)}")
88+
print(f" VDOP (Vertical Dilution of Precision): {format_dop(gps.vdop)}")
89+
print("Satellites used for fix:")
90+
for s in gps.sat_prns:
91+
talker = talkers[s[0:2]]
92+
number = s[2:]
93+
print(f" {talker}-{number} ", end="")
94+
if gps.sats is None:
95+
print("- no info")
96+
else:
97+
try:
98+
sat = gps.sats[s]
99+
if sat is None:
100+
print("- no info")
101+
else:
102+
print(f"Elevation:{sat[1]}* Azimuth:{sat[2]}* SNR:{sat[3]}dB")
103+
except KeyError:
104+
print("- no info")
105+
print()

0 commit comments

Comments
 (0)