adafruit-circuitpython-gps 3.11.9__tar.gz → 4.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/PKG-INFO +1 -1
  2. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/adafruit_circuitpython_gps.egg-info/PKG-INFO +1 -1
  3. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/adafruit_gps.py +109 -42
  4. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/pyproject.toml +1 -1
  5. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/tests/adafruit_gps_test.py +4 -4
  6. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/.coveragerc +0 -0
  7. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/.gitattributes +0 -0
  8. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/.github/PULL_REQUEST_TEMPLATE/adafruit_circuitpython_pr.md +0 -0
  9. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/.github/workflows/build.yml +0 -0
  10. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/.github/workflows/failure-help-text.yml +0 -0
  11. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/.github/workflows/release_gh.yml +0 -0
  12. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/.github/workflows/release_pypi.yml +0 -0
  13. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/.gitignore +0 -0
  14. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/.pre-commit-config.yaml +0 -0
  15. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/.readthedocs.yaml +0 -0
  16. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/CODE_OF_CONDUCT.md +0 -0
  17. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/LICENSE +0 -0
  18. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/LICENSES/CC-BY-4.0.txt +0 -0
  19. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/LICENSES/MIT.txt +0 -0
  20. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/LICENSES/Unlicense.txt +0 -0
  21. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/README.rst +0 -0
  22. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/README.rst.license +0 -0
  23. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/adafruit_circuitpython_gps.egg-info/SOURCES.txt +0 -0
  24. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/adafruit_circuitpython_gps.egg-info/dependency_links.txt +0 -0
  25. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/adafruit_circuitpython_gps.egg-info/requires.txt +0 -0
  26. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/adafruit_circuitpython_gps.egg-info/top_level.txt +0 -0
  27. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/docs/_static/custom.css +0 -0
  28. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/docs/_static/favicon.ico +0 -0
  29. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/docs/_static/favicon.ico.license +0 -0
  30. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/docs/api.rst +0 -0
  31. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/docs/api.rst.license +0 -0
  32. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/docs/conf.py +0 -0
  33. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/docs/details.rst +0 -0
  34. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/docs/details.rst.license +0 -0
  35. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/docs/examples.rst +0 -0
  36. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/docs/examples.rst.license +0 -0
  37. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/docs/index.rst +0 -0
  38. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/docs/index.rst.license +0 -0
  39. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/docs/requirements.txt +0 -0
  40. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/examples/gps_datalogging.py +0 -0
  41. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/examples/gps_displayio_simpletest.py +0 -0
  42. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/examples/gps_echotest.py +0 -0
  43. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/examples/gps_satellitefix.py +0 -0
  44. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/examples/gps_simpletest.py +0 -0
  45. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/examples/gps_time_source.py +0 -0
  46. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/optional_requirements.txt +0 -0
  47. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/requirements.txt +0 -0
  48. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/ruff.toml +0 -0
  49. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/setup.cfg +0 -0
  50. {adafruit_circuitpython_gps-3.11.9 → adafruit_circuitpython_gps-4.0.0}/tests/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: adafruit-circuitpython-gps
3
- Version: 3.11.9
3
+ Version: 4.0.0
4
4
  Summary: CircuitPython library for GPS modules.
5
5
  Author-email: Adafruit Industries <circuitpython@adafruit.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: adafruit-circuitpython-gps
3
- Version: 3.11.9
3
+ Version: 4.0.0
4
4
  Summary: CircuitPython library for GPS modules.
5
5
  Author-email: Adafruit Industries <circuitpython@adafruit.com>
6
6
  License-Expression: MIT
@@ -40,7 +40,7 @@ try:
40
40
  except ImportError:
41
41
  pass
42
42
 
43
- __version__ = "3.11.9"
43
+ __version__ = "4.0.0"
44
44
  __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_GPS.git"
45
45
 
46
46
 
@@ -315,9 +315,14 @@ class GPS:
315
315
  | 3 - 3D fix
316
316
  """
317
317
  self.satellites = None
318
- """The number of satellites in use, 0 - 12"""
318
+ """The number of satellites in use (GGA), 0 - 12"""
319
319
  self.satellites_prev = None
320
320
  """The number of satellites in use from the previous data point, 0 - 12"""
321
+ self.satellites_in_view = None
322
+ """The number of satellites in view (GSV), 0 - 12"""
323
+ self.satellites_in_view_prev = None
324
+ """The number of satellites in view from the previous data point, 0 - 12"""
325
+
321
326
  self.horizontal_dilution = None
322
327
  """Horizontal dilution of precision (GGA)"""
323
328
  self.altitude_m = None
@@ -678,6 +683,9 @@ class GPS:
678
683
  # GPS quality indicator
679
684
  self.fix_quality = parsed_data[5]
680
685
 
686
+ # Capture previous value
687
+ self.satellites_prev = self.satellites
688
+
681
689
  # Number of satellites in use, 0 - 12
682
690
  self.satellites = parsed_data[6]
683
691
 
@@ -754,8 +762,12 @@ class GPS:
754
762
  self.total_mess_num = data[0]
755
763
  # Message number
756
764
  self.mess_num = data[1]
765
+
766
+ # Capture previous value
767
+ self.satellites_in_view_prev = self.satellites_in_view
768
+
757
769
  # Number of satellites in view
758
- self.satellites = data[2]
770
+ self.satellites_in_view = data[2]
759
771
 
760
772
  sat_tup = data[3:]
761
773
 
@@ -784,7 +796,7 @@ class GPS:
784
796
 
785
797
  if self.mess_num == self.total_mess_num:
786
798
  # Last part of GSV message
787
- if len(self._sats) == self.satellites:
799
+ if len(self._sats) == self.satellites_in_view:
788
800
  # Transfer received satellites to self.sats
789
801
  if self.sats is None:
790
802
  self.sats = {}
@@ -802,11 +814,16 @@ class GPS:
802
814
  self.sats[sat[0]] = sat
803
815
  self._sats.clear()
804
816
 
805
- self.satellites_prev = self.satellites
806
-
807
817
  return True
808
818
 
809
819
 
820
+ _GPSI2C_MAX_PENDING = const(128) # NMEA sentences are <= 82 bytes; an unterminated
821
+ # buffer past this means a terminator was lost, so
822
+ # resync rather than growing without bound (issue #123).
823
+ _GPSI2C_MIN_PACKET = const(11) # smallest viable sentence; matches the gate in
824
+ # _read_sentence() before it attempts a readline().
825
+
826
+
810
827
  class GPS_GtopI2C(GPS):
811
828
  """GTop-compatible I2C GPS parsing module. Can parse simple NMEA data
812
829
  sentences from an I2C-capable GPS module to read latitude, longitude, and more.
@@ -819,31 +836,70 @@ class GPS_GtopI2C(GPS):
819
836
  address: int = _GPSI2C_DEFAULT_ADDRESS,
820
837
  debug: bool = False,
821
838
  timeout: float = 5.0,
839
+ chunk_size: int = 32,
822
840
  ) -> None:
823
841
  from adafruit_bus_device import i2c_device # noqa: PLC0415
824
842
 
825
843
  super().__init__(None, debug) # init the parent with no UART
826
844
  self._i2c = i2c_device.I2CDevice(i2c_bus, address)
827
845
  self._lastbyte = None
828
- self._charbuff = bytearray(1)
829
- self._internalbuffer = []
846
+ self._chunk = bytearray(chunk_size)
847
+ self._pending = bytearray()
830
848
  self._timeout = timeout
831
849
 
850
+ def _fill(self) -> int:
851
+ """Read one chunk from the module into the pending buffer.
852
+
853
+ The GTop I2C protocol has no way to report how much data is waiting: the
854
+ module returns 0x0A filler bytes when it has nothing to send. Those are
855
+ discarded here, so a return value of 0 means the module is idle and there
856
+ is no point asking again until more time has passed.
857
+
858
+ Returns the number of real bytes recovered.
859
+ """
860
+ with self._i2c as i2c:
861
+ i2c.readinto(self._chunk)
862
+ recovered = 0
863
+ for char in self._chunk:
864
+ if char == 0x0A and self._lastbyte != 0x0D:
865
+ continue # filler LF (a real LF only ever follows a CR)
866
+ self._pending.append(char)
867
+ self._lastbyte = char # last approved byte, for CR/LF across chunks
868
+ recovered += 1
869
+ return recovered
870
+
871
+ def _newline_index(self) -> int:
872
+ """Index of the first 0x0A in the pending buffer, or -1.
873
+
874
+ Written as an explicit scan rather than bytearray.find() so it does not
875
+ depend on which string methods a given CircuitPython build provides.
876
+ """
877
+ for index, char in enumerate(self._pending):
878
+ if char == 0x0A:
879
+ return index
880
+ return -1
881
+
882
+ def _resync(self) -> None:
883
+ """Discard the pending buffer after a lost terminator so it cannot grow
884
+ without bound. Clearing ``_lastbyte`` prevents a stale CR from wrongly
885
+ preserving the next filler LF."""
886
+ self._pending = bytearray()
887
+
888
+ self._lastbyte = None
889
+
832
890
  def read(self, num_bytes: int = 1) -> bytearray:
833
891
  """Read up to num_bytes of data from the GPS directly, without parsing.
834
- Returns a bytearray with up to num_bytes or None if nothing was read"""
835
- result = []
836
- for _ in range(num_bytes):
837
- with self._i2c as i2c:
838
- # we read one byte at a time, verify it isnt part of a string of
839
- # 'stuffed' newlines and then append to our result array for byteification
840
- i2c.readinto(self._charbuff)
841
- char = self._charbuff[0]
842
- if (char == 0x0A) and (self._lastbyte != 0x0D):
843
- continue # skip duplicate \n's!
844
- result.append(char)
845
- self._lastbyte = char # keep track of the last character approved
846
- return bytearray(result)
892
+ Returns a bytearray with up to num_bytes, which may be empty if the
893
+ module had nothing to send or the timeout expired."""
894
+ deadline = time.monotonic() + self._timeout
895
+ while len(self._pending) < num_bytes:
896
+ if self._fill() == 0:
897
+ break # module is idle; return what we have
898
+ if time.monotonic() > deadline:
899
+ break
900
+ result = self._pending[:num_bytes]
901
+ self._pending[:num_bytes] = b"" # in-place shrink
902
+ return result
847
903
 
848
904
  def write(self, bytestr: ReadableBuffer) -> None:
849
905
  """Write a bytestring data to the GPS directly, without parsing
@@ -852,26 +908,37 @@ class GPS_GtopI2C(GPS):
852
908
  i2c.write(bytestr)
853
909
 
854
910
  @property
855
- def in_waiting(self) -> Literal[16]:
856
- """Returns number of bytes available in UART read buffer, always 16
857
- since I2C does not have the ability to know how much data is available"""
858
- return 16
911
+ def in_waiting(self) -> int:
912
+ """Number of recovered bytes not yet consumed.
913
+
914
+ I2C cannot report a UART-style buffer level, so this pulls a chunk when
915
+ the pending buffer is below the threshold ``_read_sentence()`` needs
916
+ before it will attempt a ``readline()``.
917
+ """
918
+ while len(self._pending) < _GPSI2C_MIN_PACKET:
919
+ if self._fill() == 0:
920
+ break # module idle; report what we actually have
921
+ return len(self._pending)
859
922
 
860
923
  def readline(self) -> Optional[bytearray]:
861
- """Returns a newline terminated bytearray, must have timeout set for
862
- the underlying UART or this will block forever!"""
863
- timeout = time.monotonic() + self._timeout
864
- while timeout > time.monotonic():
865
- # check if our internal buffer has a '\n' termination already
866
- if self._internalbuffer and (self._internalbuffer[-1] == 0x0A):
867
- break
868
- char = self.read(1)
869
- if not char:
870
- continue
871
- self._internalbuffer.append(char[0])
872
- # print(bytearray(self._internalbuffer))
873
- if self._internalbuffer and self._internalbuffer[-1] == 0x0A:
874
- ret = bytearray(self._internalbuffer)
875
- self._internalbuffer = [] # reset the buffer to empty
876
- return ret
877
- return None # no completed data yet
924
+ """Return a newline-terminated bytearray, or None if no complete
925
+ sentence is available yet.
926
+
927
+ Returns as soon as the module reports idle rather than spinning until
928
+ the timeout expires. A partial sentence is retained and completed on a
929
+ later call; a partial that grows past a legal NMEA length is discarded
930
+ as a lost terminator.
931
+ """
932
+ deadline = time.monotonic() + self._timeout
933
+ while True:
934
+ newline = self._newline_index()
935
+ if newline >= 0:
936
+ line = self._pending[: newline + 1]
937
+ self._pending[: newline + 1] = b"" # in-place shrink
938
+ return line
939
+ if len(self._pending) > _GPSI2C_MAX_PENDING:
940
+ self._resync() # unterminated and over-long => garbage
941
+ if self._fill() == 0:
942
+ return None # module idle; no completed sentence yet
943
+ if time.monotonic() > deadline:
944
+ return None
@@ -12,7 +12,7 @@ requires = [
12
12
  [project]
13
13
  name = "adafruit-circuitpython-gps"
14
14
  description = "CircuitPython library for GPS modules."
15
- version = "3.11.9"
15
+ version = "4.0.0"
16
16
  readme = "README.rst"
17
17
  authors = [
18
18
  {name = "Adafruit Industries", email = "circuitpython@adafruit.com"}
@@ -421,7 +421,7 @@ def test_GPS_update_from_GSV_first_part():
421
421
  assert gps.update()
422
422
  assert gps.total_mess_num == 2
423
423
  assert gps.mess_num == 1
424
- assert gps.satellites == 8
424
+ assert gps.satellites_in_view == 8
425
425
  # check two satellites, without timestamp, since it is dynamic
426
426
  sats = gps._sats
427
427
  assert sats[0][:-1] == ("GP1", 40, 83, 46)
@@ -449,7 +449,7 @@ def test_GPS_update_from_GSV_both_parts_sats_are_removed():
449
449
  assert gps.update()
450
450
  assert gps.total_mess_num == 2
451
451
  assert gps.mess_num == 1
452
- assert gps.satellites == 4
452
+ assert gps.satellites_in_view == 4
453
453
  # first time we received satellites, so this must be None
454
454
  assert gps.sats is None
455
455
  # some time has passed so the first two satellites will be too old, but
@@ -460,7 +460,7 @@ def test_GPS_update_from_GSV_both_parts_sats_are_removed():
460
460
  assert gps.update()
461
461
  assert gps.total_mess_num == 2
462
462
  assert gps.mess_num == 2
463
- assert gps.satellites == 4
463
+ assert gps.satellites_in_view == 4
464
464
  # we should now have 4 satellites from the two part request
465
465
  assert set(gps.sats.keys()) == {"GP1", "GP2", "GP12", "GP14"}
466
466
 
@@ -470,7 +470,7 @@ def test_GPS_update_from_GSV_both_parts_sats_are_removed():
470
470
  # a third, one part request
471
471
  m.return_value = b"$GPGSV,1,1,02,13,07,344,39,15,22,228,45*7a\r\n"
472
472
  assert gps.update()
473
- assert gps.satellites == 2
473
+ assert gps.satellites_in_view == 2
474
474
  assert set(gps.sats.keys()) == {"GP12", "GP14", "GP13", "GP15"}
475
475
 
476
476