Skip to content

Commit 8786f1f

Browse files
Enable GSA (DOP and active satellites) parsing
Enable GSV (satellites in view) parsing. Rewrote the GSA and GSV parsing to handle each satellite system (talker) separately. - self.sats now uses keys based upon the talker and satellite number, eg. GL67 for GLONASS adafruit#67, GP7 for GPS adafruit#7 - When the end message of a GSV sequence is received, eg. 3 of 3, all previous records in self.sats matching that talker are removed before adding the updated ones. - self.sat_prns stores the last satellite IDs that were used for a fix and returned in the most recent GSA sentence. They will be from only one Satellite system and should have a record in self.sats .
1 parent 8629b4f commit 8786f1f

File tree

2 files changed

+154
-32
lines changed

2 files changed

+154
-32
lines changed

adafruit_gps.py

+51-32
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ 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
9394
self.sats = None
9495
self.isactivedata = None
9596
self.true_track = None
@@ -121,9 +122,9 @@ def update(self):
121122
print(sentence)
122123
data_type, args = sentence
123124
data_type = bytes(data_type.upper(), "ascii")
124-
(talker, sentence_type) = (data_type[:2], data_type[2:])
125+
(talker, sentence_type) = self._parse_talker(data_type)
125126

126-
# Check for all currently known talkers
127+
# Check for all currently known GNSS talkers
127128
# GA - Galileo
128129
# GB - BeiDou Systems
129130
# GI - NavIC
@@ -132,17 +133,19 @@ def update(self):
132133
# GQ - QZSS
133134
# GN - GNSS / More than one of the above
134135
if talker not in (b'GA', b'GB', b'GI', b'GL', b'GP', b'GQ', b'GN'):
135-
if self.debug:
136-
print(f" Unknown talker: {talker}")
137-
# We don't know the talker so it's not new data
138-
return False
136+
# It's not a known GNSS source of data
137+
return True
139138

140139
if sentence_type == b'GLL': # Geographic position - Latitude/Longitude
141140
self._parse_gpgll(args)
142141
elif sentence_type == b'RMC': # Minimum location info
143142
self._parse_gprmc(args)
144143
elif sentence_type == b'GGA': # 3D location fix
145144
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)
146149
return True
147150

148151
def send_command(self, command, add_checksum=True):
@@ -253,6 +256,13 @@ def _parse_sentence(self):
253256
data_type = sentence[1:delimiter]
254257
return (data_type, sentence[delimiter + 1 :])
255258

259+
def _parse_talker(self, data_type):
260+
# Split the data_type into talker and sentence_type
261+
if data_type[0] == b'P': # Proprietary codes
262+
return (data_type[:1], data_type[1:])
263+
else:
264+
return (data_type[:2], data_type[2:])
265+
256266
def _parse_gpgll(self, args):
257267
data = args.split(",")
258268
if data is None or data[0] is None or (data[0] == ""):
@@ -414,7 +424,8 @@ def _parse_gpgga(self, args):
414424
self.altitude_m = _parse_float(data[8])
415425
self.height_geoid = _parse_float(data[10])
416426

417-
def _parse_gpgsa(self, args):
427+
def _parse_gpgsa(self, talker, args):
428+
talker = talker.decode('ascii')
418429
data = args.split(",")
419430
if data is None or (data[0] == ""):
420431
return # Unexpected number of params
@@ -424,9 +435,9 @@ def _parse_gpgsa(self, args):
424435
# Parse 3d fix
425436
self.fix_quality_3d = _parse_int(data[1])
426437
satlist = list(filter(None, data[2:-4]))
427-
self.sat_prns = {}
428-
for i, sat in enumerate(satlist, 1):
429-
self.sat_prns["gps{}".format(i)] = _parse_int(sat)
438+
self.sat_prns = []
439+
for sat in satlist:
440+
self.sat_prns.append("{}{}".format(talker, _parse_int(sat)))
430441

431442
# Parse PDOP, dilution of precision
432443
self.pdop = _parse_float(data[-3])
@@ -435,9 +446,10 @@ def _parse_gpgsa(self, args):
435446
# Parse VDOP, vertical dilution of precision
436447
self.vdop = _parse_float(data[-1])
437448

438-
def _parse_gpgsv(self, args):
449+
def _parse_gpgsv(self, talker, args):
439450
# Parse the arguments (everything after data type) for NMEA GPGGA
440451
# 3D location fix sentence.
452+
talker = talker.decode('ascii')
441453
data = args.split(",")
442454
if data is None or (data[0] == ""):
443455
return # Unexpected number of params.
@@ -454,33 +466,40 @@ def _parse_gpgsv(self, args):
454466

455467
sat_tup = data[3:]
456468

457-
satdict = {}
458-
for i in range(len(sat_tup) / 4):
469+
satlist = []
470+
for i in range(len(sat_tup) // 4):
459471
j = i * 4
460-
key = "gps{}".format(i + (4 * (self.mess_num - 1)))
461-
satnum = _parse_int(sat_tup[0 + j]) # Satellite number
472+
satnum = "{}{}".format(talker, _parse_int(sat_tup[0 + j])) # Satellite number
462473
satdeg = _parse_int(sat_tup[1 + j]) # Elevation in degrees
463474
satazim = _parse_int(sat_tup[2 + j]) # Azimuth in degrees
464475
satsnr = _parse_int(sat_tup[3 + j]) # signal-to-noise ratio in dB
465476
value = (satnum, satdeg, satazim, satsnr)
466-
satdict[key] = value
477+
satlist.append(value)
478+
479+
if self._sats is None:
480+
self._sats = []
481+
for value in satlist:
482+
self._sats.append(value)
483+
484+
if self.mess_num == self.total_mess_num:
485+
# Last part of GSV message
486+
if len(self._sats) == self.satellites:
487+
# Transfer received satellites to self.sats
488+
if self.sats is None:
489+
self.sats = {}
490+
else:
491+
# Remove all old data from self.sats which
492+
# match the current talker
493+
old = []
494+
for i in self.sats:
495+
if i[0:2] == talker:
496+
old.append(i)
497+
for i in old:
498+
self.sats.pop(i)
499+
for s in self._sats:
500+
self.sats[s[0]] = s
501+
self._sats.clear()
467502

468-
if self.sats is None:
469-
self.sats = {}
470-
for satnum in satdict:
471-
self.sats[satnum] = satdict[satnum]
472-
473-
try:
474-
if self.satellites < self.satellites_prev:
475-
for i in self.sats:
476-
try:
477-
if int(i[-2]) >= self.satellites:
478-
del self.sats[i]
479-
except ValueError:
480-
if int(i[-1]) >= self.satellites:
481-
del self.sats[i]
482-
except TypeError:
483-
pass
484503
self.satellites_prev = self.satellites
485504

486505

examples/gps_satellitefix.py

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

0 commit comments

Comments
 (0)