Skip to content

Commit 42dd755

Browse files
committed
fix minute parsing
1 parent 9fd82a7 commit 42dd755

File tree

3 files changed

+96
-41
lines changed

3 files changed

+96
-41
lines changed

adafruit_gps.py

Lines changed: 61 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def _parse_degrees(nmea_data: str) -> int:
9595
degrees = int(raw[0]) // 100 * 1000000 # the ddd
9696
minutes = int(raw[0]) % 100 # the mm.
9797
minutes += int(f"{raw[1][:4]:0<4}") / 10000
98-
minutes = int(minutes / 60 * 1000000)
98+
minutes = int((minutes * 1000000) / 60)
9999
return degrees + minutes
100100

101101

@@ -125,12 +125,22 @@ def _read_degrees(data: List[float], index: int, neg: str) -> float:
125125
return x
126126

127127

128-
def _read_int_degrees(data: List[float], index: int, neg: str) -> Tuple[int, float]:
129-
deg = data[index] // 1000000
130-
minutes = data[index] % 1000000 / 10000
128+
def _read_deg_mins(data: List[str], index: int, neg: str) -> Tuple[int, float]:
129+
# the degrees come in different formats and vary between latitudes and
130+
# longitudes, which makes parsing tricky:
131+
# for latitudes: ddmm,mmmm (0 - 7 decimal places, not zero padded)
132+
# for longitudes: dddmm,mmmm (0 - 7 decimal places, not zero padded)
133+
int_part, _, minutes_decimal = data[index].partition(".")
134+
# we need to parse from right to left, minutes can only have 2 digits
135+
minutes_int = int_part[-2:]
136+
# the rest mus be degrees which are either 2 or 3 digits
137+
deg = int(int_part[:-2])
138+
# combine the parts of the minutes, this also works when there are no
139+
# decimal places specified in the sentence
140+
minutes = float(f"{minutes_int}.{minutes_decimal}")
131141
if data[index + 1].lower() == neg:
132142
deg *= -1
133-
return (deg, minutes)
143+
return deg, minutes
134144

135145

136146
def _parse_talker(data_type: bytes) -> Tuple[bytes, bytes]:
@@ -490,26 +500,30 @@ def _parse_gll(self, data: List[str]) -> bool:
490500

491501
if data is None or len(data) != 7:
492502
return False # Unexpected number of params.
493-
data = _parse_data(_GLL, data)
503+
parsed_data = _parse_data(_GLL, data)
494504
if data is None:
495505
return False # Params didn't parse
496506

497507
# Latitude
498-
self.latitude = _read_degrees(data, 0, "s")
499-
self.latitude_degrees, self.latitude_minutes = _read_int_degrees(data, 0, "s")
508+
self.latitude = _read_degrees(parsed_data, 0, "s")
509+
self.latitude_degrees, self.latitude_minutes = _read_deg_mins(
510+
data=data, index=0, neg="s"
511+
)
500512

501513
# Longitude
502-
self.longitude = _read_degrees(data, 2, "w")
503-
self.longitude_degrees, self.longitude_minutes = _read_int_degrees(data, 2, "w")
514+
self.longitude = _read_degrees(parsed_data, 2, "w")
515+
self.longitude_degrees, self.longitude_minutes = _read_deg_mins(
516+
data=data, index=2, neg="w"
517+
)
504518

505519
# UTC time of position
506-
self._update_timestamp_utc(data[4])
520+
self._update_timestamp_utc(parsed_data[4])
507521

508522
# Status Valid(A) or Invalid(V)
509-
self.isactivedata = data[5]
523+
self.isactivedata = parsed_data[5]
510524

511525
# Parse FAA mode indicator
512-
self._mode_indicator = data[6]
526+
self._mode_indicator = parsed_data[6]
513527

514528
return True
515529

@@ -518,44 +532,48 @@ def _parse_rmc(self, data: List[str]) -> bool:
518532

519533
if data is None or len(data) not in (12, 13):
520534
return False # Unexpected number of params.
521-
data = _parse_data({12: _RMC, 13: _RMC_4_1}[len(data)], data)
522-
if data is None:
535+
parsed_data = _parse_data({12: _RMC, 13: _RMC_4_1}[len(data)], data)
536+
if parsed_data is None:
523537
self.fix_quality = 0
524538
return False # Params didn't parse
525539

526540
# UTC time of position and date
527-
self._update_timestamp_utc(data[0], data[8])
541+
self._update_timestamp_utc(parsed_data[0], parsed_data[8])
528542

529543
# Status Valid(A) or Invalid(V)
530-
self.isactivedata = data[1]
531-
if data[1].lower() == "a":
544+
self.isactivedata = parsed_data[1]
545+
if parsed_data[1].lower() == "a":
532546
if self.fix_quality == 0:
533547
self.fix_quality = 1
534548
else:
535549
self.fix_quality = 0
536550

537551
# Latitude
538-
self.latitude = _read_degrees(data, 2, "s")
539-
self.latitude_degrees, self.latitude_minutes = _read_int_degrees(data, 2, "s")
552+
self.latitude = _read_degrees(parsed_data, 2, "s")
553+
self.latitude_degrees, self.latitude_minutes = _read_deg_mins(
554+
data=data, index=2, neg="s"
555+
)
540556

541557
# Longitude
542-
self.longitude = _read_degrees(data, 4, "w")
543-
self.longitude_degrees, self.longitude_minutes = _read_int_degrees(data, 4, "w")
558+
self.longitude = _read_degrees(parsed_data, 4, "w")
559+
self.longitude_degrees, self.longitude_minutes = _read_deg_mins(
560+
data=data, index=4, neg="w"
561+
)
544562

545563
# Speed over ground, knots
546-
self.speed_knots = data[6]
564+
self.speed_knots = parsed_data[6]
547565

548566
# Track made good, degrees true
549-
self.track_angle_deg = data[7]
567+
self.track_angle_deg = parsed_data[7]
550568

551569
# Magnetic variation
552-
if data[9] is None or data[10] is None:
570+
if parsed_data[9] is None or parsed_data[10] is None:
553571
self._magnetic_variation = None
554572
else:
555-
self._magnetic_variation = _read_degrees(data, 9, "w")
573+
self._magnetic_variation = _read_degrees(parsed_data, 9, "w")
556574

557575
# Parse FAA mode indicator
558-
self._mode_indicator = data[11]
576+
self._mode_indicator = parsed_data[11]
559577

560578
return True
561579

@@ -564,37 +582,41 @@ def _parse_gga(self, data: List[str]) -> bool:
564582

565583
if data is None or len(data) != 14:
566584
return False # Unexpected number of params.
567-
data = _parse_data(_GGA, data)
568-
if data is None:
585+
parsed_data = _parse_data(_GGA, data)
586+
if parsed_data is None:
569587
self.fix_quality = 0
570588
return False # Params didn't parse
571589

572590
# UTC time of position
573-
self._update_timestamp_utc(data[0])
591+
self._update_timestamp_utc(parsed_data[0])
574592

575593
# Latitude
576-
self.latitude = _read_degrees(data, 1, "s")
577-
self.latitude_degrees, self.latitude_minutes = _read_int_degrees(data, 1, "s")
594+
self.latitude = _read_degrees(parsed_data, 1, "s")
595+
self.longitude_degrees, self.longitude_minutes = _read_deg_mins(
596+
data=data, index=3, neg="w"
597+
)
578598

579599
# Longitude
580-
self.longitude = _read_degrees(data, 3, "w")
581-
self.longitude_degrees, self.longitude_minutes = _read_int_degrees(data, 3, "w")
600+
self.longitude = _read_degrees(parsed_data, 3, "w")
601+
self.latitude_degrees, self.latitude_minutes = _read_deg_mins(
602+
data=data, index=1, neg="s"
603+
)
582604

583605
# GPS quality indicator
584-
self.fix_quality = data[5]
606+
self.fix_quality = parsed_data[5]
585607

586608
# Number of satellites in use, 0 - 12
587-
self.satellites = data[6]
609+
self.satellites = parsed_data[6]
588610

589611
# Horizontal dilution of precision
590-
self.horizontal_dilution = data[7]
612+
self.horizontal_dilution = parsed_data[7]
591613

592614
# Antenna altitude relative to mean sea level
593-
self.altitude_m = _parse_float(data[8])
615+
self.altitude_m = _parse_float(parsed_data[8])
594616
# data[9] - antenna altitude unit, always 'M' ???
595617

596618
# Geoidal separation relative to WGS 84
597-
self.height_geoid = _parse_float(data[10])
619+
self.height_geoid = _parse_float(parsed_data[10])
598620
# data[11] - geoidal separation unit, always 'M' ???
599621

600622
# data[12] - Age of differential GPS data, can be null

tests/adafruit_gps_test.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from adafruit_gps import _read_degrees
1616
from adafruit_gps import _parse_talker
1717
from adafruit_gps import _parse_data
18+
from adafruit_gps import _read_deg_mins
1819
from adafruit_gps import GPS
1920

2021

@@ -177,6 +178,10 @@ def test_GPS_update_rmc_no_magnetic_variation():
177178
assert gps.timestamp_utc == exp_time
178179
assert gps.latitude == pytest.approx(12.57613)
179180
assert gps.longitude == pytest.approx(1.385391)
181+
assert gps.latitude_degrees == 12
182+
assert gps.longitude_degrees == 1
183+
assert gps.latitude_minutes == 34.5678
184+
assert gps.longitude_minutes == 23.12345
180185
assert gps.fix_quality == 1
181186
assert gps.fix_quality_3d == 0
182187
assert gps.speed_knots == 0.45
@@ -324,6 +329,10 @@ def test_GPS_update_from_GLL():
324329
assert gps.timestamp_utc == exp_time
325330
assert gps.latitude == pytest.approx(49.27417)
326331
assert gps.longitude == pytest.approx(-123.1853)
332+
assert gps.latitude_degrees == 49
333+
assert gps.longitude_degrees == -123
334+
assert gps.latitude_minutes == 16.45
335+
assert gps.longitude_minutes == 11.12
327336
assert gps.isactivedata == "A"
328337
assert gps._mode_indicator == "A"
329338
assert gps.fix_quality == 0
@@ -335,7 +344,7 @@ def test_GPS_update_from_GLL():
335344

336345

337346
def test_GPS_update_from_RMC():
338-
r = b"$GNRMC,001031.00,A,4404.13993,N,12118.86023,W,0.146,084.4,100117,,,A*5d\r\n"
347+
r = b"$GNRMC,001031.00,A,4404.1399,N,12118.8602,W,0.146,084.4,100117,,,A*5d\r\n"
339348
# TODO: length 13 and 14 version
340349
with mock.patch.object(GPS, "readline", return_value=r):
341350
gps = GPS(uart=UartMock())
@@ -349,6 +358,10 @@ def test_GPS_update_from_RMC():
349358
assert gps.has_3d_fix is False
350359
assert gps.latitude == pytest.approx(44.069)
351360
assert gps.longitude == pytest.approx(-121.3143)
361+
assert gps.latitude_degrees == 44
362+
assert gps.longitude_degrees == -121
363+
assert gps.latitude_minutes == 4.1399
364+
assert gps.longitude_minutes == 18.8602
352365
assert gps.speed_knots == 0.146
353366
assert gps.track_angle_deg == 84.4
354367
assert gps._magnetic_variation is None
@@ -364,6 +377,10 @@ def test_GPS_update_from_GGA():
364377
assert gps.timestamp_utc == exp_time
365378
assert gps.latitude == pytest.approx(48.1173)
366379
assert gps.longitude == pytest.approx(11.51667)
380+
assert gps.latitude_degrees == 48
381+
assert gps.longitude_degrees == 11
382+
assert gps.latitude_minutes == 7.038
383+
assert gps.longitude_minutes == 31.000
367384
assert gps.fix_quality == 1
368385
assert gps.fix_quality_3d == 0
369386
assert gps.satellites == 8
@@ -467,3 +484,19 @@ def test_GPS_update_from_GSV_both_parts_sats_are_removed():
467484
assert gps.update()
468485
assert gps.satellites == 2
469486
assert set(gps.sats.keys()) == {"GP12", "GP14", "GP13", "GP15"}
487+
488+
489+
@pytest.mark.parametrize(
490+
("input_str", "exp", "neg"),
491+
(
492+
(["3723.2475", "N"], (37, 23.2475), "S"),
493+
(["3723.2475", "S"], (-37, 23.2475), "s"),
494+
(["00123.1234", "E"], (1, 23.1234), "W"),
495+
(["00123", "E"], (1, 23), "W"),
496+
(["1234.5678", "E"], (12, 34.5678), "W"),
497+
(["3723.2475123", "N"], (37, 23.2475123), "S"),
498+
(["3723", "N"], (37, 23), "S"),
499+
),
500+
)
501+
def test_read_min_secs(input_str, exp, neg):
502+
assert _read_deg_mins(data=input_str, index=0, neg=neg) == exp

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
envlist = py37
66

77
[testenv]
8-
deps = -rrequirements-dev.txt
8+
deps = -roptional_requirements.txt
99

1010
commands =
1111
coverage erase

0 commit comments

Comments
 (0)