├── .coveragerc ├── .gitattributes ├── .github ├── PULL_REQUEST_TEMPLATE │ └── adafruit_circuitpython_pr.md └── workflows │ ├── build.yml │ ├── failure-help-text.yml │ ├── release_gh.yml │ └── release_pypi.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── LICENSES ├── CC-BY-4.0.txt ├── MIT.txt └── Unlicense.txt ├── README.rst ├── README.rst.license ├── adafruit_gps.py ├── docs ├── _static │ ├── favicon.ico │ └── favicon.ico.license ├── api.rst ├── api.rst.license ├── conf.py ├── details.rst ├── details.rst.license ├── examples.rst ├── examples.rst.license ├── index.rst ├── index.rst.license └── requirements.txt ├── examples ├── gps_datalogging.py ├── gps_displayio_simpletest.py ├── gps_echotest.py ├── gps_satellitefix.py ├── gps_simpletest.py └── gps_time_source.py ├── optional_requirements.txt ├── pyproject.toml ├── requirements.txt ├── ruff.toml └── tests ├── __init__.py └── adafruit_gps_test.py /.coveragerc: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Jonas Kittner 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | [run] 5 | plugins = covdefaults 6 | 7 | [report] 8 | fail_under = 90 9 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Justin Myers for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | .py text eol=lf 6 | .rst text eol=lf 7 | .txt text eol=lf 8 | .yaml text eol=lf 9 | .toml text eol=lf 10 | .license text eol=lf 11 | .md text eol=lf 12 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/adafruit_circuitpython_pr.md: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | Thank you for contributing! Before you submit a pull request, please read the following. 6 | 7 | Make sure any changes you're submitting are in line with the CircuitPython Design Guide, available here: https://docs.circuitpython.org/en/latest/docs/design_guide.html 8 | 9 | If your changes are to documentation, please verify that the documentation builds locally by following the steps found here: https://adafru.it/build-docs 10 | 11 | Before submitting the pull request, make sure you've run Pylint and Black locally on your code. You can do this manually or using pre-commit. Instructions are available here: https://adafru.it/check-your-code 12 | 13 | Please remove all of this text before submitting. Include an explanation or list of changes included in your PR, as well as, if applicable, a link to any related issues. 14 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | name: Build CI 6 | 7 | on: [pull_request, push] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Run Build CI workflow 14 | uses: adafruit/workflows-circuitpython-libs/build@main 15 | -------------------------------------------------------------------------------- /.github/workflows/failure-help-text.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Scott Shawcroft for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | name: Failure help text 6 | 7 | on: 8 | workflow_run: 9 | workflows: ["Build CI"] 10 | types: 11 | - completed 12 | 13 | jobs: 14 | post-help: 15 | runs-on: ubuntu-latest 16 | if: ${{ github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.event == 'pull_request' }} 17 | steps: 18 | - name: Post comment to help 19 | uses: adafruit/circuitpython-action-library-ci-failed@v1 20 | -------------------------------------------------------------------------------- /.github/workflows/release_gh.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | name: GitHub Release Actions 6 | 7 | on: 8 | release: 9 | types: [published] 10 | 11 | jobs: 12 | upload-release-assets: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Run GitHub Release CI workflow 16 | uses: adafruit/workflows-circuitpython-libs/release-gh@main 17 | with: 18 | github-token: ${{ secrets.GITHUB_TOKEN }} 19 | upload-url: ${{ github.event.release.upload_url }} 20 | -------------------------------------------------------------------------------- /.github/workflows/release_pypi.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | name: PyPI Release Actions 6 | 7 | on: 8 | release: 9 | types: [published] 10 | 11 | jobs: 12 | upload-release-assets: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Run PyPI Release CI workflow 16 | uses: adafruit/workflows-circuitpython-libs/release-pypi@main 17 | with: 18 | pypi-username: ${{ secrets.pypi_username }} 19 | pypi-password: ${{ secrets.pypi_password }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Kattni Rembor, written for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | # Do not include files and directories created by your personal work environment, such as the IDE 6 | # you use, except for those already listed here. Pull requests including changes to this file will 7 | # not be accepted. 8 | 9 | # This .gitignore file contains rules for files generated by working with CircuitPython libraries, 10 | # including building Sphinx, testing with pip, and creating a virual environment, as well as the 11 | # MacOS and IDE-specific files generated by using MacOS in general, or the PyCharm or VSCode IDEs. 12 | 13 | # If you find that there are files being generated on your machine that should not be included in 14 | # your git commit, you should create a .gitignore_global file on your computer to include the 15 | # files created by your personal setup. To do so, follow the two steps below. 16 | 17 | # First, create a file called .gitignore_global somewhere convenient for you, and add rules for 18 | # the files you want to exclude from git commits. 19 | 20 | # Second, configure Git to use the exclude file for all Git repositories by running the 21 | # following via commandline, replacing "path/to/your/" with the actual path to your newly created 22 | # .gitignore_global file: 23 | # git config --global core.excludesfile path/to/your/.gitignore_global 24 | 25 | # CircuitPython-specific files 26 | *.mpy 27 | 28 | # Python-specific files 29 | __pycache__ 30 | *.pyc 31 | 32 | # Sphinx build-specific files 33 | _build 34 | 35 | # This file results from running `pip -e install .` in a local repository 36 | *.egg-info 37 | 38 | # Virtual environment-specific files 39 | .env 40 | .venv 41 | 42 | # MacOS-specific files 43 | *.DS_Store 44 | 45 | # IDE-specific files 46 | .idea 47 | .vscode 48 | *~ 49 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Justin Myers for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | repos: 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: v4.5.0 8 | hooks: 9 | - id: check-yaml 10 | - id: end-of-file-fixer 11 | - id: trailing-whitespace 12 | - repo: https://github.com/astral-sh/ruff-pre-commit 13 | rev: v0.3.4 14 | hooks: 15 | - id: ruff-format 16 | - id: ruff 17 | args: ["--fix"] 18 | - repo: https://github.com/fsfe/reuse-tool 19 | rev: v3.0.1 20 | hooks: 21 | - id: reuse 22 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | # Read the Docs configuration file 6 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 7 | 8 | # Required 9 | version: 2 10 | 11 | sphinx: 12 | configuration: docs/conf.py 13 | 14 | build: 15 | os: ubuntu-20.04 16 | tools: 17 | python: "3" 18 | 19 | python: 20 | install: 21 | - requirements: docs/requirements.txt 22 | - requirements: requirements.txt 23 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Adafruit Community Code of Conduct 8 | 9 | ## Our Pledge 10 | 11 | In the interest of fostering an open and welcoming environment, we as 12 | contributors and leaders pledge to making participation in our project and 13 | our community a harassment-free experience for everyone, regardless of age, body 14 | size, disability, ethnicity, gender identity and expression, level or type of 15 | experience, education, socio-economic status, nationality, personal appearance, 16 | race, religion, or sexual identity and orientation. 17 | 18 | ## Our Standards 19 | 20 | We are committed to providing a friendly, safe and welcoming environment for 21 | all. 22 | 23 | Examples of behavior that contributes to creating a positive environment 24 | include: 25 | 26 | * Be kind and courteous to others 27 | * Using welcoming and inclusive language 28 | * Being respectful of differing viewpoints and experiences 29 | * Collaborating with other community members 30 | * Gracefully accepting constructive criticism 31 | * Focusing on what is best for the community 32 | * Showing empathy towards other community members 33 | 34 | Examples of unacceptable behavior by participants include: 35 | 36 | * The use of sexualized language or imagery and sexual attention or advances 37 | * The use of inappropriate images, including in a community member's avatar 38 | * The use of inappropriate language, including in a community member's nickname 39 | * Any spamming, flaming, baiting or other attention-stealing behavior 40 | * Excessive or unwelcome helping; answering outside the scope of the question 41 | asked 42 | * Trolling, insulting/derogatory comments, and personal or political attacks 43 | * Promoting or spreading disinformation, lies, or conspiracy theories against 44 | a person, group, organisation, project, or community 45 | * Public or private harassment 46 | * Publishing others' private information, such as a physical or electronic 47 | address, without explicit permission 48 | * Other conduct which could reasonably be considered inappropriate 49 | 50 | The goal of the standards and moderation guidelines outlined here is to build 51 | and maintain a respectful community. We ask that you don’t just aim to be 52 | "technically unimpeachable", but rather try to be your best self. 53 | 54 | We value many things beyond technical expertise, including collaboration and 55 | supporting others within our community. Providing a positive experience for 56 | other community members can have a much more significant impact than simply 57 | providing the correct answer. 58 | 59 | ## Our Responsibilities 60 | 61 | Project leaders are responsible for clarifying the standards of acceptable 62 | behavior and are expected to take appropriate and fair corrective action in 63 | response to any instances of unacceptable behavior. 64 | 65 | Project leaders have the right and responsibility to remove, edit, or 66 | reject messages, comments, commits, code, issues, and other contributions 67 | that are not aligned to this Code of Conduct, or to ban temporarily or 68 | permanently any community member for other behaviors that they deem 69 | inappropriate, threatening, offensive, or harmful. 70 | 71 | ## Moderation 72 | 73 | Instances of behaviors that violate the Adafruit Community Code of Conduct 74 | may be reported by any member of the community. Community members are 75 | encouraged to report these situations, including situations they witness 76 | involving other community members. 77 | 78 | You may report in the following ways: 79 | 80 | In any situation, you may send an email to . 81 | 82 | On the Adafruit Discord, you may send an open message from any channel 83 | to all Community Moderators by tagging @community moderators. You may 84 | also send an open message from any channel, or a direct message to 85 | @kattni#1507, @tannewt#4653, @Dan Halbert#1614, @cater#2442, 86 | @sommersoft#0222, @Mr. Certainly#0472 or @Andon#8175. 87 | 88 | Email and direct message reports will be kept confidential. 89 | 90 | In situations on Discord where the issue is particularly egregious, possibly 91 | illegal, requires immediate action, or violates the Discord terms of service, 92 | you should also report the message directly to Discord. 93 | 94 | These are the steps for upholding our community’s standards of conduct. 95 | 96 | 1. Any member of the community may report any situation that violates the 97 | Adafruit Community Code of Conduct. All reports will be reviewed and 98 | investigated. 99 | 2. If the behavior is an egregious violation, the community member who 100 | committed the violation may be banned immediately, without warning. 101 | 3. Otherwise, moderators will first respond to such behavior with a warning. 102 | 4. Moderators follow a soft "three strikes" policy - the community member may 103 | be given another chance, if they are receptive to the warning and change their 104 | behavior. 105 | 5. If the community member is unreceptive or unreasonable when warned by a 106 | moderator, or the warning goes unheeded, they may be banned for a first or 107 | second offense. Repeated offenses will result in the community member being 108 | banned. 109 | 110 | ## Scope 111 | 112 | This Code of Conduct and the enforcement policies listed above apply to all 113 | Adafruit Community venues. This includes but is not limited to any community 114 | spaces (both public and private), the entire Adafruit Discord server, and 115 | Adafruit GitHub repositories. Examples of Adafruit Community spaces include 116 | but are not limited to meet-ups, audio chats on the Adafruit Discord, or 117 | interaction at a conference. 118 | 119 | This Code of Conduct applies both within project spaces and in public spaces 120 | when an individual is representing the project or its community. As a community 121 | member, you are representing our community, and are expected to behave 122 | accordingly. 123 | 124 | ## Attribution 125 | 126 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 127 | version 1.4, available at 128 | , 129 | and the [Rust Code of Conduct](https://www.rust-lang.org/en-US/conduct.html). 130 | 131 | For other projects adopting the Adafruit Community Code of 132 | Conduct, please contact the maintainers of those projects for enforcement. 133 | If you wish to use this code of conduct for your own project, consider 134 | explicitly mentioning your moderation policy or making a copy with your 135 | own moderation policy so as to avoid confusion. 136 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Tony DiCola for Adafruit Industries 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSES/CC-BY-4.0.txt: -------------------------------------------------------------------------------- 1 | Creative Commons Attribution 4.0 International Creative Commons Corporation 2 | ("Creative Commons") is not a law firm and does not provide legal services 3 | or legal advice. Distribution of Creative Commons public licenses does not 4 | create a lawyer-client or other relationship. Creative Commons makes its licenses 5 | and related information available on an "as-is" basis. Creative Commons gives 6 | no warranties regarding its licenses, any material licensed under their terms 7 | and conditions, or any related information. Creative Commons disclaims all 8 | liability for damages resulting from their use to the fullest extent possible. 9 | 10 | Using Creative Commons Public Licenses 11 | 12 | Creative Commons public licenses provide a standard set of terms and conditions 13 | that creators and other rights holders may use to share original works of 14 | authorship and other material subject to copyright and certain other rights 15 | specified in the public license below. The following considerations are for 16 | informational purposes only, are not exhaustive, and do not form part of our 17 | licenses. 18 | 19 | Considerations for licensors: Our public licenses are intended for use by 20 | those authorized to give the public permission to use material in ways otherwise 21 | restricted by copyright and certain other rights. Our licenses are irrevocable. 22 | Licensors should read and understand the terms and conditions of the license 23 | they choose before applying it. Licensors should also secure all rights necessary 24 | before applying our licenses so that the public can reuse the material as 25 | expected. Licensors should clearly mark any material not subject to the license. 26 | This includes other CC-licensed material, or material used under an exception 27 | or limitation to copyright. More considerations for licensors : wiki.creativecommons.org/Considerations_for_licensors 28 | 29 | Considerations for the public: By using one of our public licenses, a licensor 30 | grants the public permission to use the licensed material under specified 31 | terms and conditions. If the licensor's permission is not necessary for any 32 | reason–for example, because of any applicable exception or limitation to copyright–then 33 | that use is not regulated by the license. Our licenses grant only permissions 34 | under copyright and certain other rights that a licensor has authority to 35 | grant. Use of the licensed material may still be restricted for other reasons, 36 | including because others have copyright or other rights in the material. A 37 | licensor may make special requests, such as asking that all changes be marked 38 | or described. Although not required by our licenses, you are encouraged to 39 | respect those requests where reasonable. More considerations for the public 40 | : wiki.creativecommons.org/Considerations_for_licensees Creative Commons Attribution 41 | 4.0 International Public License 42 | 43 | By exercising the Licensed Rights (defined below), You accept and agree to 44 | be bound by the terms and conditions of this Creative Commons Attribution 45 | 4.0 International Public License ("Public License"). To the extent this Public 46 | License may be interpreted as a contract, You are granted the Licensed Rights 47 | in consideration of Your acceptance of these terms and conditions, and the 48 | Licensor grants You such rights in consideration of benefits the Licensor 49 | receives from making the Licensed Material available under these terms and 50 | conditions. 51 | 52 | Section 1 – Definitions. 53 | 54 | a. Adapted Material means material subject to Copyright and Similar Rights 55 | that is derived from or based upon the Licensed Material and in which the 56 | Licensed Material is translated, altered, arranged, transformed, or otherwise 57 | modified in a manner requiring permission under the Copyright and Similar 58 | Rights held by the Licensor. For purposes of this Public License, where the 59 | Licensed Material is a musical work, performance, or sound recording, Adapted 60 | Material is always produced where the Licensed Material is synched in timed 61 | relation with a moving image. 62 | 63 | b. Adapter's License means the license You apply to Your Copyright and Similar 64 | Rights in Your contributions to Adapted Material in accordance with the terms 65 | and conditions of this Public License. 66 | 67 | c. Copyright and Similar Rights means copyright and/or similar rights closely 68 | related to copyright including, without limitation, performance, broadcast, 69 | sound recording, and Sui Generis Database Rights, without regard to how the 70 | rights are labeled or categorized. For purposes of this Public License, the 71 | rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. 72 | 73 | d. Effective Technological Measures means those measures that, in the absence 74 | of proper authority, may not be circumvented under laws fulfilling obligations 75 | under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, 76 | and/or similar international agreements. 77 | 78 | e. Exceptions and Limitations means fair use, fair dealing, and/or any other 79 | exception or limitation to Copyright and Similar Rights that applies to Your 80 | use of the Licensed Material. 81 | 82 | f. Licensed Material means the artistic or literary work, database, or other 83 | material to which the Licensor applied this Public License. 84 | 85 | g. Licensed Rights means the rights granted to You subject to the terms and 86 | conditions of this Public License, which are limited to all Copyright and 87 | Similar Rights that apply to Your use of the Licensed Material and that the 88 | Licensor has authority to license. 89 | 90 | h. Licensor means the individual(s) or entity(ies) granting rights under this 91 | Public License. 92 | 93 | i. Share means to provide material to the public by any means or process that 94 | requires permission under the Licensed Rights, such as reproduction, public 95 | display, public performance, distribution, dissemination, communication, or 96 | importation, and to make material available to the public including in ways 97 | that members of the public may access the material from a place and at a time 98 | individually chosen by them. 99 | 100 | j. Sui Generis Database Rights means rights other than copyright resulting 101 | from Directive 96/9/EC of the European Parliament and of the Council of 11 102 | March 1996 on the legal protection of databases, as amended and/or succeeded, 103 | as well as other essentially equivalent rights anywhere in the world. 104 | 105 | k. You means the individual or entity exercising the Licensed Rights under 106 | this Public License. Your has a corresponding meaning. 107 | 108 | Section 2 – Scope. 109 | 110 | a. License grant. 111 | 112 | 1. Subject to the terms and conditions of this Public License, the Licensor 113 | hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, 114 | irrevocable license to exercise the Licensed Rights in the Licensed Material 115 | to: 116 | 117 | A. reproduce and Share the Licensed Material, in whole or in part; and 118 | 119 | B. produce, reproduce, and Share Adapted Material. 120 | 121 | 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions 122 | and Limitations apply to Your use, this Public License does not apply, and 123 | You do not need to comply with its terms and conditions. 124 | 125 | 3. Term. The term of this Public License is specified in Section 6(a). 126 | 127 | 4. Media and formats; technical modifications allowed. The Licensor authorizes 128 | You to exercise the Licensed Rights in all media and formats whether now known 129 | or hereafter created, and to make technical modifications necessary to do 130 | so. The Licensor waives and/or agrees not to assert any right or authority 131 | to forbid You from making technical modifications necessary to exercise the 132 | Licensed Rights, including technical modifications necessary to circumvent 133 | Effective Technological Measures. For purposes of this Public License, simply 134 | making modifications authorized by this Section 2(a)(4) never produces Adapted 135 | Material. 136 | 137 | 5. Downstream recipients. 138 | 139 | A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed 140 | Material automatically receives an offer from the Licensor to exercise the 141 | Licensed Rights under the terms and conditions of this Public License. 142 | 143 | B. No downstream restrictions. You may not offer or impose any additional 144 | or different terms or conditions on, or apply any Effective Technological 145 | Measures to, the Licensed Material if doing so restricts exercise of the Licensed 146 | Rights by any recipient of the Licensed Material. 147 | 148 | 6. No endorsement. Nothing in this Public License constitutes or may be construed 149 | as permission to assert or imply that You are, or that Your use of the Licensed 150 | Material is, connected with, or sponsored, endorsed, or granted official status 151 | by, the Licensor or others designated to receive attribution as provided in 152 | Section 3(a)(1)(A)(i). 153 | 154 | b. Other rights. 155 | 156 | 1. Moral rights, such as the right of integrity, are not licensed under this 157 | Public License, nor are publicity, privacy, and/or other similar personality 158 | rights; however, to the extent possible, the Licensor waives and/or agrees 159 | not to assert any such rights held by the Licensor to the limited extent necessary 160 | to allow You to exercise the Licensed Rights, but not otherwise. 161 | 162 | 2. Patent and trademark rights are not licensed under this Public License. 163 | 164 | 3. To the extent possible, the Licensor waives any right to collect royalties 165 | from You for the exercise of the Licensed Rights, whether directly or through 166 | a collecting society under any voluntary or waivable statutory or compulsory 167 | licensing scheme. In all other cases the Licensor expressly reserves any right 168 | to collect such royalties. 169 | 170 | Section 3 – License Conditions. 171 | 172 | Your exercise of the Licensed Rights is expressly made subject to the following 173 | conditions. 174 | 175 | a. Attribution. 176 | 177 | 1. If You Share the Licensed Material (including in modified form), You must: 178 | 179 | A. retain the following if it is supplied by the Licensor with the Licensed 180 | Material: 181 | 182 | i. identification of the creator(s) of the Licensed Material and any others 183 | designated to receive attribution, in any reasonable manner requested by the 184 | Licensor (including by pseudonym if designated); 185 | 186 | ii. a copyright notice; 187 | 188 | iii. a notice that refers to this Public License; 189 | 190 | iv. a notice that refers to the disclaimer of warranties; 191 | 192 | v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; 193 | 194 | B. indicate if You modified the Licensed Material and retain an indication 195 | of any previous modifications; and 196 | 197 | C. indicate the Licensed Material is licensed under this Public License, and 198 | include the text of, or the URI or hyperlink to, this Public License. 199 | 200 | 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner 201 | based on the medium, means, and context in which You Share the Licensed Material. 202 | For example, it may be reasonable to satisfy the conditions by providing a 203 | URI or hyperlink to a resource that includes the required information. 204 | 205 | 3. If requested by the Licensor, You must remove any of the information required 206 | by Section 3(a)(1)(A) to the extent reasonably practicable. 207 | 208 | 4. If You Share Adapted Material You produce, the Adapter's License You apply 209 | must not prevent recipients of the Adapted Material from complying with this 210 | Public License. 211 | 212 | Section 4 – Sui Generis Database Rights. 213 | 214 | Where the Licensed Rights include Sui Generis Database Rights that apply to 215 | Your use of the Licensed Material: 216 | 217 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, 218 | reuse, reproduce, and Share all or a substantial portion of the contents of 219 | the database; 220 | 221 | b. if You include all or a substantial portion of the database contents in 222 | a database in which You have Sui Generis Database Rights, then the database 223 | in which You have Sui Generis Database Rights (but not its individual contents) 224 | is Adapted Material; and 225 | 226 | c. You must comply with the conditions in Section 3(a) if You Share all or 227 | a substantial portion of the contents of the database. 228 | 229 | For the avoidance of doubt, this Section 4 supplements and does not replace 230 | Your obligations under this Public License where the Licensed Rights include 231 | other Copyright and Similar Rights. 232 | 233 | Section 5 – Disclaimer of Warranties and Limitation of Liability. 234 | 235 | a. Unless otherwise separately undertaken by the Licensor, to the extent possible, 236 | the Licensor offers the Licensed Material as-is and as-available, and makes 237 | no representations or warranties of any kind concerning the Licensed Material, 238 | whether express, implied, statutory, or other. This includes, without limitation, 239 | warranties of title, merchantability, fitness for a particular purpose, non-infringement, 240 | absence of latent or other defects, accuracy, or the presence or absence of 241 | errors, whether or not known or discoverable. Where disclaimers of warranties 242 | are not allowed in full or in part, this disclaimer may not apply to You. 243 | 244 | b. To the extent possible, in no event will the Licensor be liable to You 245 | on any legal theory (including, without limitation, negligence) or otherwise 246 | for any direct, special, indirect, incidental, consequential, punitive, exemplary, 247 | or other losses, costs, expenses, or damages arising out of this Public License 248 | or use of the Licensed Material, even if the Licensor has been advised of 249 | the possibility of such losses, costs, expenses, or damages. Where a limitation 250 | of liability is not allowed in full or in part, this limitation may not apply 251 | to You. 252 | 253 | c. The disclaimer of warranties and limitation of liability provided above 254 | shall be interpreted in a manner that, to the extent possible, most closely 255 | approximates an absolute disclaimer and waiver of all liability. 256 | 257 | Section 6 – Term and Termination. 258 | 259 | a. This Public License applies for the term of the Copyright and Similar Rights 260 | licensed here. However, if You fail to comply with this Public License, then 261 | Your rights under this Public License terminate automatically. 262 | 263 | b. Where Your right to use the Licensed Material has terminated under Section 264 | 6(a), it reinstates: 265 | 266 | 1. automatically as of the date the violation is cured, provided it is cured 267 | within 30 days of Your discovery of the violation; or 268 | 269 | 2. upon express reinstatement by the Licensor. 270 | 271 | c. For the avoidance of doubt, this Section 6(b) does not affect any right 272 | the Licensor may have to seek remedies for Your violations of this Public 273 | License. 274 | 275 | d. For the avoidance of doubt, the Licensor may also offer the Licensed Material 276 | under separate terms or conditions or stop distributing the Licensed Material 277 | at any time; however, doing so will not terminate this Public License. 278 | 279 | e. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. 280 | 281 | Section 7 – Other Terms and Conditions. 282 | 283 | a. The Licensor shall not be bound by any additional or different terms or 284 | conditions communicated by You unless expressly agreed. 285 | 286 | b. Any arrangements, understandings, or agreements regarding the Licensed 287 | Material not stated herein are separate from and independent of the terms 288 | and conditions of this Public License. 289 | 290 | Section 8 – Interpretation. 291 | 292 | a. For the avoidance of doubt, this Public License does not, and shall not 293 | be interpreted to, reduce, limit, restrict, or impose conditions on any use 294 | of the Licensed Material that could lawfully be made without permission under 295 | this Public License. 296 | 297 | b. To the extent possible, if any provision of this Public License is deemed 298 | unenforceable, it shall be automatically reformed to the minimum extent necessary 299 | to make it enforceable. If the provision cannot be reformed, it shall be severed 300 | from this Public License without affecting the enforceability of the remaining 301 | terms and conditions. 302 | 303 | c. No term or condition of this Public License will be waived and no failure 304 | to comply consented to unless expressly agreed to by the Licensor. 305 | 306 | d. Nothing in this Public License constitutes or may be interpreted as a limitation 307 | upon, or waiver of, any privileges and immunities that apply to the Licensor 308 | or You, including from the legal processes of any jurisdiction or authority. 309 | 310 | Creative Commons is not a party to its public licenses. Notwithstanding, Creative 311 | Commons may elect to apply one of its public licenses to material it publishes 312 | and in those instances will be considered the "Licensor." The text of the 313 | Creative Commons public licenses is dedicated to the public domain under the 314 | CC0 Public Domain Dedication. Except for the limited purpose of indicating 315 | that material is shared under a Creative Commons public license or as otherwise 316 | permitted by the Creative Commons policies published at creativecommons.org/policies, 317 | Creative Commons does not authorize the use of the trademark "Creative Commons" 318 | or any other trademark or logo of Creative Commons without its prior written 319 | consent including, without limitation, in connection with any unauthorized 320 | modifications to any of its public licenses or any other arrangements, understandings, 321 | or agreements concerning use of licensed material. For the avoidance of doubt, 322 | this paragraph does not form part of the public licenses. 323 | 324 | Creative Commons may be contacted at creativecommons.org. 325 | -------------------------------------------------------------------------------- /LICENSES/MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice (including the next 11 | paragraph) shall be included in all copies or substantial portions of the 12 | Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 17 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 18 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF 19 | OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /LICENSES/Unlicense.txt: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or distribute 4 | this software, either in source code form or as a compiled binary, for any 5 | purpose, commercial or non-commercial, and by any means. 6 | 7 | In jurisdictions that recognize copyright laws, the author or authors of this 8 | software dedicate any and all copyright interest in the software to the public 9 | domain. We make this dedication for the benefit of the public at large and 10 | to the detriment of our heirs and successors. We intend this dedication to 11 | be an overt act of relinquishment in perpetuity of all present and future 12 | rights to this software under copyright law. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 17 | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH 19 | THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. For more information, 20 | please refer to 21 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | .. image:: https://readthedocs.org/projects/adafruit-circuitpython-gps/badge/?version=latest 5 | :target: https://docs.circuitpython.org/projects/gps/en/latest/ 6 | :alt: Documentation Status 7 | 8 | .. image:: https://raw.githubusercontent.com/adafruit/Adafruit_CircuitPython_Bundle/main/badges/adafruit_discord.svg 9 | :target: https://adafru.it/discord 10 | :alt: Discord 11 | 12 | .. image:: https://github.com/adafruit/Adafruit_CircuitPython_GPS/workflows/Build%20CI/badge.svg 13 | :target: https://github.com/adafruit/Adafruit_CircuitPython_GPS/actions/ 14 | :alt: Build Status 15 | 16 | .. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json 17 | :target: https://github.com/astral-sh/ruff 18 | :alt: Code Style: Ruff 19 | 20 | GPS parsing module. Can send commands to, and parse simple NMEA data sentences 21 | from serial and I2C GPS modules to read latitude, longitude, and more. 22 | 23 | 24 | Dependencies 25 | ============= 26 | This driver depends on: 27 | 28 | * `Adafruit CircuitPython `_ 29 | 30 | Please ensure all dependencies are available on the CircuitPython filesystem. 31 | This is easily achieved by downloading 32 | `the Adafruit library and driver bundle `_. 33 | 34 | Installing from PyPI 35 | ==================== 36 | 37 | On supported GNU/Linux systems like the Raspberry Pi, you can install the driver locally `from 38 | PyPI `_. To install for current user: 39 | 40 | .. code-block:: shell 41 | 42 | pip3 install adafruit-circuitpython-gps 43 | 44 | To install system-wide (this may be required in some cases): 45 | 46 | .. code-block:: shell 47 | 48 | sudo pip3 install adafruit-circuitpython-gps 49 | 50 | To install in a virtual environment in your current project: 51 | 52 | .. code-block:: shell 53 | 54 | mkdir project-name && cd project-name 55 | python3 -m venv .venv 56 | source .venv/bin/activate 57 | pip3 install adafruit-circuitpython-gps 58 | 59 | Usage Example 60 | ============= 61 | 62 | See examples/gps_simpletest.py for a demonstration of parsing and printing GPS location. 63 | 64 | Important: 65 | Feather boards and many other circuitpython boards will round to two decimal places like this: 66 | 67 | .. code-block:: python 68 | 69 | >>> float('1234.5678') 70 | 1234.57 71 | 72 | This isn't ideal for GPS data as this lowers the accuracy from 0.1m to 11m. 73 | 74 | This can be fixed by using string formatting when the GPS data is output. 75 | 76 | An implementation of this can be found in examples/gps_simpletest.py 77 | 78 | .. code-block:: python 79 | 80 | import time 81 | import board 82 | import busio 83 | 84 | import adafruit_gps 85 | 86 | RX = board.RX 87 | TX = board.TX 88 | 89 | uart = busio.UART(TX, RX, baudrate=9600, timeout=30) 90 | 91 | gps = adafruit_gps.GPS(uart, debug=False) 92 | 93 | gps.send_command(b'PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0') 94 | 95 | gps.send_command(b'PMTK220,1000') 96 | 97 | last_print = time.monotonic() 98 | while True: 99 | 100 | gps.update() 101 | 102 | current = time.monotonic() 103 | if current - last_print >= 1.0: 104 | last_print = current 105 | if not gps.has_fix: 106 | print('Waiting for fix...') 107 | continue 108 | print('=' * 40) # Print a separator line. 109 | print('Latitude: {0:.6f} degrees'.format(gps.latitude)) 110 | print('Longitude: {0:.6f} degrees'.format(gps.longitude)) 111 | 112 | 113 | These two lines are the lines that actually solve the issue: 114 | 115 | .. code-block:: python 116 | 117 | print('Latitude: {0:.6f} degrees'.format(gps.latitude)) 118 | print('Longitude: {0:.6f} degrees'.format(gps.longitude)) 119 | 120 | 121 | Note: Sending multiple PMTK314 packets with ``gps.send_command()`` will not 122 | work unless there is a substantial amount of time in-between each time 123 | ``gps.send_command()`` is called. A ``time.sleep()`` of 1 second or more 124 | should fix this. 125 | 126 | Documentation 127 | ============= 128 | 129 | API documentation for this library can be found on `Read the Docs `_. 130 | 131 | For information on building library documentation, please check out `this guide `_. 132 | 133 | Contributing 134 | ============ 135 | 136 | Contributions are welcome! Please read our `Code of Conduct 137 | `_ 138 | before contributing to help this project stay welcoming. 139 | -------------------------------------------------------------------------------- /README.rst.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries 2 | 3 | SPDX-License-Identifier: MIT 4 | -------------------------------------------------------------------------------- /adafruit_gps.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2017 Tony DiCola for Adafruit Industries 2 | # SPDX-FileCopyrightText: 2021 James Carr 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | """ 7 | `adafruit_gps` 8 | ==================================================== 9 | 10 | GPS parsing module. Can parse simple NMEA data sentences from serial GPS 11 | modules to read latitude, longitude, and more. 12 | 13 | * Author(s): Tony DiCola, James Carr 14 | 15 | Implementation Notes 16 | -------------------- 17 | 18 | **Hardware:** 19 | 20 | * Adafruit `Ultimate GPS Breakout `_ 21 | * Adafruit `Ultimate GPS FeatherWing `_ 22 | 23 | **Software and Dependencies:** 24 | 25 | * Adafruit CircuitPython firmware for the ESP8622 and M0-based boards: 26 | https://github.com/adafruit/circuitpython/releases 27 | 28 | """ 29 | 30 | import time 31 | 32 | from micropython import const 33 | 34 | try: 35 | from typing import List, Optional, Tuple 36 | 37 | from busio import I2C, UART 38 | from circuitpython_typing import ReadableBuffer 39 | from typing_extensions import Literal 40 | except ImportError: 41 | pass 42 | 43 | __version__ = "0.0.0+auto.0" 44 | __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_GPS.git" 45 | 46 | 47 | _GPSI2C_DEFAULT_ADDRESS = const(0x10) 48 | 49 | _GLL = 0 50 | _RMC = 1 51 | _GGA = 2 52 | _GSA = 3 53 | _GSA_4_11 = 4 54 | _GSV7 = 5 55 | _GSV11 = 6 56 | _GSV15 = 7 57 | _GSV19 = 8 58 | _RMC_4_1 = 9 59 | _VTG = 10 60 | _ST_MIN = _GLL 61 | _ST_MAX = _VTG 62 | 63 | _SENTENCE_PARAMS = ( 64 | # 0 - _GLL 65 | "dcdcscC", 66 | # 1 - _RMC 67 | "scdcdcffsDCC", 68 | # 2 - _GGA 69 | "sdcdciiffsfsIS", 70 | # 3 - _GSA 71 | "ciIIIIIIIIIIIIfff", 72 | # 4 - _GSA_4_11 73 | "ciIIIIIIIIIIIIfffS", 74 | # 5 - _GSV7 75 | "iiiiiiI", 76 | # 6 - _GSV11 77 | "iiiiiiIiiiI", 78 | # 7 - _GSV15 79 | "iiiiiiIiiiIiiiI", 80 | # 8 - _GSV19 81 | "iiiiiiIiiiIiiiIiiiI", 82 | # 9 - _RMC_4_1 83 | "scdcdcffsDCCC", 84 | # 10 - _VTG 85 | "fcFCfcfcC", 86 | ) 87 | 88 | 89 | # Internal helper parsing functions. 90 | # These handle input that might be none or null and return none instead of 91 | # throwing errors. 92 | def _parse_degrees(nmea_data: str) -> int: 93 | # Parse a NMEA lat/long data pair 'dddmm.mmmm' into a pure degrees value. 94 | # Where ddd is the degrees, mm.mmmm is the minutes. 95 | if nmea_data is None or len(nmea_data) < 3: 96 | return None 97 | # To avoid losing precision handle degrees and minutes separately 98 | # Return the final value as an integer. Further functions can parse 99 | # this into a float or separate parts to retain the precision 100 | raw = nmea_data.split(".") 101 | degrees = int(raw[0]) // 100 * 1000000 # the ddd 102 | minutes = int(raw[0]) % 100 # the mm. 103 | minutes += int(f"{raw[1][:4]:0<4}") / 10000 104 | minutes = int((minutes * 1000000) / 60) 105 | return degrees + minutes 106 | 107 | 108 | def _parse_int(nmea_data: str) -> int: 109 | if nmea_data is None or not nmea_data: 110 | return None 111 | return int(nmea_data) 112 | 113 | 114 | def _parse_float(nmea_data: str) -> float: 115 | if nmea_data is None or not nmea_data: 116 | return None 117 | return float(nmea_data) 118 | 119 | 120 | def _parse_str(nmea_data: str) -> str: 121 | if nmea_data is None or not nmea_data: 122 | return None 123 | return str(nmea_data) 124 | 125 | 126 | def _read_degrees(data: List[float], index: int, neg: str) -> float: 127 | # This function loses precision with float32 128 | x = data[index] / 1000000 129 | if data[index + 1].lower() == neg: 130 | x *= -1.0 131 | return x 132 | 133 | 134 | def _read_deg_mins(data: List[str], index: int, neg: str) -> Tuple[int, float]: 135 | # the degrees come in different formats and vary between latitudes and 136 | # longitudes, which makes parsing tricky: 137 | # for latitudes: ddmm,mmmm (0 - 7 decimal places, not zero padded) 138 | # for longitudes: dddmm,mmmm (0 - 7 decimal places, not zero padded) 139 | if "." in data[index]: 140 | int_part, minutes_decimal = data[index].split(".") 141 | else: 142 | int_part, minutes_decimal = data[index], 0 143 | 144 | # we need to parse from right to left, minutes can only have 2 digits 145 | minutes_int = int_part[-2:] 146 | # the rest must be degrees which are either 2 or 3 digits 147 | deg = int(int_part[:-2]) 148 | # combine the parts of the minutes, this also works when there are no 149 | # decimal places specified in the sentence 150 | minutes = float(f"{minutes_int}.{minutes_decimal}") 151 | if data[index + 1].lower() == neg: 152 | deg *= -1 153 | return deg, minutes 154 | 155 | 156 | def _parse_talker(data_type: bytes) -> Tuple[bytes, bytes]: 157 | # Split the data_type into talker and sentence_type 158 | if data_type[:1] == b"P": # Proprietary codes 159 | return (data_type[:1], data_type[1:]) 160 | 161 | return (data_type[:2], data_type[2:]) 162 | 163 | 164 | def _parse_data(sentence_type: int, data: List[str]) -> Optional[List]: 165 | """Parse sentence data for the specified sentence type and 166 | return a list of parameters in the correct format, or return None. 167 | """ 168 | 169 | if not _ST_MIN <= sentence_type <= _ST_MAX: 170 | # The sentence_type is unknown 171 | return None 172 | 173 | param_types = _SENTENCE_PARAMS[sentence_type] 174 | 175 | if len(param_types) != len(data): 176 | # The expected number does not match the number of data items 177 | return None 178 | 179 | params = [] 180 | try: 181 | for i, dti in enumerate(data): 182 | pti = param_types[i] 183 | len_dti = len(dti) 184 | nothing = dti is None or len_dti == 0 185 | if pti == "c": 186 | # A single character 187 | if len_dti != 1: 188 | return None 189 | params.append(dti) 190 | elif pti == "C": 191 | # A single character or Nothing 192 | if nothing: 193 | params.append(None) 194 | elif len_dti != 1: 195 | return None 196 | else: 197 | params.append(dti) 198 | elif pti == "d": 199 | # A number parseable as degrees 200 | params.append(_parse_degrees(dti)) 201 | elif pti == "D": 202 | # A number parseable as degrees or Nothing 203 | if nothing: 204 | params.append(None) 205 | else: 206 | params.append(_parse_degrees(dti)) 207 | elif pti == "f": 208 | # A floating point number 209 | params.append(_parse_float(dti)) 210 | elif pti == "F": 211 | # A floating point number or Nothing 212 | if nothing: 213 | params.append(None) 214 | else: 215 | params.append(_parse_float(dti)) 216 | elif pti == "i": 217 | # An integer 218 | params.append(_parse_int(dti)) 219 | elif pti == "I": 220 | # An integer or Nothing 221 | if nothing: 222 | params.append(None) 223 | else: 224 | params.append(_parse_int(dti)) 225 | elif pti == "s": 226 | # A string 227 | params.append(dti) 228 | elif pti == "S": 229 | # A string or Nothing 230 | if nothing: 231 | params.append(None) 232 | else: 233 | params.append(dti) 234 | else: 235 | raise TypeError(f"GPS: Unexpected parameter type '{pti}'") 236 | except ValueError: 237 | # Something didn't parse, abort 238 | return None 239 | 240 | # Return the parsed data 241 | return params 242 | 243 | 244 | class GPS: 245 | """GPS parsing module. Can parse simple NMEA data sentences from serial 246 | GPS modules to read latitude, longitude, and more. 247 | """ 248 | 249 | def __init__(self, uart: UART, debug: bool = False) -> None: 250 | self._uart = uart 251 | # Initialize null starting values for GPS attributes. 252 | self.timestamp_utc = None 253 | """Timestamp in UTC""" 254 | self.latitude = None 255 | """Degrees latitude""" 256 | self.latitude_degrees = None 257 | """Degrees component of latitude measurement""" 258 | self.latitude_minutes = None # Use for full precision minutes 259 | """Minutes component of latitude measurement""" 260 | self.longitude = None 261 | """Degrees longitude""" 262 | self.longitude_degrees = None 263 | """Degrees component of longitude measurement""" 264 | self.longitude_minutes = None # Use for full precision minutes 265 | """Minutes component of longitude measurement""" 266 | self.fix_quality = 0 267 | """ 268 | GPS quality indicator 269 | 270 | | 0 - fix not available 271 | | 1 - GPS fix 272 | | 2 - Differential GPS fix (values above 2 are 2.3 features) 273 | | 3 - PPS fix 274 | | 4 - Real Time Kinematic 275 | | 5 - Float RTK 276 | | 6 - estimated (dead reckoning) 277 | | 7 - Manual input mode 278 | | 8 - Simulation mode 279 | """ 280 | self.fix_quality_3d = 0 281 | """ 282 | The type of fix for a reading 283 | 284 | | 1 - no fix 285 | | 2 - 2D fix 286 | | 3 - 3D fix 287 | """ 288 | self.satellites = None 289 | """The number of satellites in use, 0 - 12""" 290 | self.satellites_prev = None 291 | """The number of satellites in use from the previous data point, 0 - 12""" 292 | self.horizontal_dilution = None 293 | """Horizontal dilution of precision (GGA)""" 294 | self.altitude_m = None 295 | """Antenna altitude relative to mean sea level""" 296 | self.height_geoid = None 297 | """Geoidal separation relative to WGS 84""" 298 | self.speed_knots = None 299 | """Ground speed in knots""" 300 | self.speed_kmh = None 301 | """Ground speed in km/h""" 302 | self.track_angle_deg = None 303 | """Track angle in degrees""" 304 | self._sats = None # Temporary holder for information from GSV messages 305 | self.sats = None 306 | """Information from GSV messages""" 307 | self.isactivedata = None 308 | """Status Valid(A) or Invalid(V)""" 309 | self.sat_prns = None 310 | """Satellite pseudorandom noise code""" 311 | self.sel_mode = None 312 | """ 313 | Selection mode 314 | 315 | | 'M' - manual 316 | | 'A' - automatic 317 | """ 318 | self.pdop = None 319 | """Dilution of precision""" 320 | self.hdop = None 321 | """Horizontal dilution of precision (GSA)""" 322 | self.vdop = None 323 | """Vertical dilution of precision""" 324 | self.total_mess_num = None 325 | """Number of messages""" 326 | self.mess_num = None 327 | """Message number""" 328 | self._raw_sentence = None 329 | self._mode_indicator = None 330 | self._magnetic_variation = None 331 | self.debug = debug 332 | """Toggles debug mode. When True, prints the incoming data sentence to the console""" 333 | 334 | def update(self) -> bool: 335 | """Check for updated data from the GPS module and process it 336 | accordingly. Returns True if new data was processed, and False if 337 | nothing new was received. 338 | """ 339 | # Grab a sentence and check its data type to call the appropriate 340 | # parsing function. 341 | 342 | try: 343 | sentence = self._parse_sentence() 344 | except UnicodeError: 345 | return None 346 | if sentence is None: 347 | return False 348 | if self.debug: 349 | print(sentence) 350 | data_type, args = sentence 351 | if len(data_type) < 5: 352 | return False 353 | data_type = bytes(data_type.upper(), "ascii") 354 | (talker, sentence_type) = _parse_talker(data_type) 355 | 356 | # Check for all currently known GNSS talkers 357 | # GA - Galileo 358 | # GB - BeiDou Systems 359 | # GI - NavIC 360 | # GL - GLONASS 361 | # GP - GPS 362 | # GQ - QZSS 363 | # GN - GNSS / More than one of the above 364 | if talker not in {b"GA", b"GB", b"GI", b"GL", b"GP", b"GQ", b"GN"}: 365 | # It's not a known GNSS source of data 366 | # Assume it's a valid packet anyway 367 | return True 368 | 369 | result = True 370 | args = args.split(",") 371 | if sentence_type == b"GLL": # Geographic position - Latitude/Longitude 372 | result = self._parse_gll(args) 373 | elif sentence_type == b"RMC": # Minimum location info 374 | result = self._parse_rmc(args) 375 | elif sentence_type == b"GGA": # 3D location fix 376 | result = self._parse_gga(args) 377 | elif sentence_type == b"GSV": # Satellites in view 378 | result = self._parse_gsv(talker, args) 379 | elif sentence_type == b"GSA": # GPS DOP and active satellites 380 | result = self._parse_gsa(talker, args) 381 | elif sentence_type == b"VTG": # Ground speed 382 | result = self._parse_vtg(args) 383 | 384 | return result 385 | 386 | def send_command(self, command: bytes, add_checksum: bool = True) -> None: 387 | """Send a command string to the GPS. If add_checksum is True (the 388 | default) a NMEA checksum will automatically be computed and added. 389 | Note you should NOT add the leading $ and trailing * to the command 390 | as they will automatically be added! 391 | """ 392 | self.write(b"$") 393 | self.write(command) 394 | if add_checksum: 395 | checksum = 0 396 | for char in command: 397 | checksum ^= char 398 | self.write(b"*") 399 | self.write(bytes(f"{checksum:02x}".upper(), "ascii")) 400 | self.write(b"\r\n") 401 | 402 | @property 403 | def has_fix(self) -> bool: 404 | """True if a current fix for location information is available.""" 405 | return self.fix_quality is not None and self.fix_quality >= 1 406 | 407 | @property 408 | def has_3d_fix(self) -> bool: 409 | """Returns true if there is a 3d fix available. 410 | use has_fix to determine if a 2d fix is available, 411 | passing it the same data""" 412 | return self.fix_quality_3d is not None and self.fix_quality_3d >= 2 413 | 414 | @property 415 | def datetime(self) -> Optional[time.struct_time]: 416 | """Return struct_time object to feed rtc.set_time_source() function""" 417 | return self.timestamp_utc 418 | 419 | @property 420 | def nmea_sentence(self) -> Optional[str]: 421 | """Return raw_sentence which is the raw NMEA sentence read from the GPS""" 422 | return self._raw_sentence 423 | 424 | def read(self, num_bytes: Optional[int]) -> Optional[bytes]: 425 | """Read up to num_bytes of data from the GPS directly, without parsing. 426 | Returns a bytestring with up to num_bytes or None if nothing was read""" 427 | return self._uart.read(num_bytes) 428 | 429 | def write(self, bytestr: ReadableBuffer) -> Optional[int]: 430 | """Write a bytestring data to the GPS directly, without parsing 431 | or checksums""" 432 | return self._uart.write(bytestr) 433 | 434 | @property 435 | def in_waiting(self) -> int: 436 | """Returns number of bytes available in UART read buffer""" 437 | return self._uart.in_waiting 438 | 439 | def readline(self) -> Optional[bytes]: 440 | """Returns a newline terminated bytestring, must have timeout set for 441 | the underlying UART or this will block forever!""" 442 | return self._uart.readline() 443 | 444 | def _read_sentence(self) -> Optional[str]: 445 | # Parse any NMEA sentence that is available. 446 | # This needs to be refactored when it can be tested. 447 | 448 | # Only continue if we have at least 11 bytes in the input buffer 449 | if self.in_waiting < 11: 450 | return None 451 | 452 | sentence = self.readline() 453 | if sentence is None or sentence == b"" or len(sentence) < 1: 454 | return None 455 | try: 456 | sentence = str(sentence, "ascii").strip() 457 | except UnicodeError: 458 | return None 459 | # Look for a checksum and validate it if present. 460 | if len(sentence) > 7 and sentence[-3] == "*": 461 | # Get included checksum, then calculate it and compare. 462 | expected = int(sentence[-2:], 16) 463 | actual = 0 464 | for i in range(1, len(sentence) - 3): 465 | actual ^= ord(sentence[i]) 466 | if actual != expected: 467 | return None # Failed to validate checksum. 468 | 469 | # copy the raw sentence 470 | self._raw_sentence = sentence 471 | 472 | return sentence 473 | # At this point we don't have a valid sentence 474 | return None 475 | 476 | def _parse_sentence(self) -> Optional[Tuple[str, str]]: 477 | sentence = self._read_sentence() 478 | 479 | # sentence is a valid NMEA with a valid checksum 480 | if sentence is None: 481 | return None 482 | 483 | # Remove checksum once validated. 484 | sentence = sentence[:-3] 485 | # Parse out the type of sentence (first string after $ up to comma) 486 | # and then grab the rest as data within the sentence. 487 | delimiter = sentence.find(",") 488 | if delimiter == -1: 489 | return None # Invalid sentence, no comma after data type. 490 | data_type = sentence[1:delimiter] 491 | return (data_type, sentence[delimiter + 1 :]) 492 | 493 | def _update_timestamp_utc(self, time_utc: str, date: Optional[str] = None) -> None: 494 | hours = int(time_utc[0:2]) 495 | mins = int(time_utc[2:4]) 496 | secs = int(time_utc[4:6]) 497 | if date is None: 498 | if self.timestamp_utc is None: 499 | day, month, year = 0, 0, 0 500 | else: 501 | day = self.timestamp_utc.tm_mday 502 | month = self.timestamp_utc.tm_mon 503 | year = self.timestamp_utc.tm_year 504 | else: 505 | day = int(date[0:2]) 506 | month = int(date[2:4]) 507 | year = 2000 + int(date[4:6]) 508 | 509 | self.timestamp_utc = time.struct_time((year, month, day, hours, mins, secs, 0, 0, -1)) 510 | 511 | def _parse_vtg(self, data: List[str]) -> bool: 512 | # VTG - Course Over Ground and Ground Speed 513 | 514 | if data is None or len(data) != 9: 515 | return False # Unexpected number of params 516 | 517 | parsed_data = _parse_data(_VTG, data) 518 | if parsed_data is None: 519 | return False # Params didn't parse 520 | 521 | # Track made good, degrees true 522 | self.track_angle_deg = parsed_data[0] 523 | 524 | # Speed over ground, knots 525 | self.speed_knots = parsed_data[4] 526 | 527 | # Speed over ground, kilometers / hour 528 | self.speed_kmh = parsed_data[6] 529 | 530 | # Parse FAA mode indicator 531 | self._mode_indicator = parsed_data[8] 532 | 533 | return True 534 | 535 | def _parse_gll(self, data: List[str]) -> bool: 536 | # GLL - Geographic Position - Latitude/Longitude 537 | 538 | if data is None or len(data) != 7: 539 | return False # Unexpected number of params. 540 | parsed_data = _parse_data(_GLL, data) 541 | if parsed_data is None: 542 | return False # Params didn't parse 543 | 544 | # Latitude 545 | self.latitude = _read_degrees(parsed_data, 0, "s") 546 | self.latitude_degrees, self.latitude_minutes = _read_deg_mins(data=data, index=0, neg="s") 547 | 548 | # Longitude 549 | self.longitude = _read_degrees(parsed_data, 2, "w") 550 | self.longitude_degrees, self.longitude_minutes = _read_deg_mins(data=data, index=2, neg="w") 551 | 552 | # UTC time of position 553 | self._update_timestamp_utc(parsed_data[4]) 554 | 555 | # Status Valid(A) or Invalid(V) 556 | self.isactivedata = parsed_data[5] 557 | 558 | # Parse FAA mode indicator 559 | self._mode_indicator = parsed_data[6] 560 | 561 | return True 562 | 563 | def _parse_rmc(self, data: List[str]) -> bool: 564 | # RMC - Recommended Minimum Navigation Information 565 | 566 | if data is None or len(data) not in {12, 13}: 567 | return False # Unexpected number of params. 568 | parsed_data = _parse_data({12: _RMC, 13: _RMC_4_1}[len(data)], data) 569 | if parsed_data is None: 570 | self.fix_quality = 0 571 | return False # Params didn't parse 572 | 573 | # UTC time of position and date 574 | self._update_timestamp_utc(parsed_data[0], parsed_data[8]) 575 | 576 | # Status Valid(A) or Invalid(V) 577 | self.isactivedata = parsed_data[1] 578 | if parsed_data[1].lower() == "a": 579 | if self.fix_quality == 0: 580 | self.fix_quality = 1 581 | else: 582 | self.fix_quality = 0 583 | 584 | # Latitude 585 | self.latitude = _read_degrees(parsed_data, 2, "s") 586 | self.latitude_degrees, self.latitude_minutes = _read_deg_mins(data=data, index=2, neg="s") 587 | 588 | # Longitude 589 | self.longitude = _read_degrees(parsed_data, 4, "w") 590 | self.longitude_degrees, self.longitude_minutes = _read_deg_mins(data=data, index=4, neg="w") 591 | 592 | # Speed over ground, knots 593 | self.speed_knots = parsed_data[6] 594 | 595 | # Track made good, degrees true 596 | self.track_angle_deg = parsed_data[7] 597 | 598 | # Magnetic variation 599 | if parsed_data[9] is None or parsed_data[10] is None: 600 | self._magnetic_variation = None 601 | else: 602 | self._magnetic_variation = _read_degrees(parsed_data, 9, "w") 603 | 604 | # Parse FAA mode indicator 605 | self._mode_indicator = parsed_data[11] 606 | 607 | return True 608 | 609 | def _parse_gga(self, data: List[str]) -> bool: 610 | # GGA - Global Positioning System Fix Data 611 | 612 | if data is None or len(data) != 14: 613 | return False # Unexpected number of params. 614 | parsed_data = _parse_data(_GGA, data) 615 | if parsed_data is None: 616 | self.fix_quality = 0 617 | return False # Params didn't parse 618 | 619 | # UTC time of position 620 | self._update_timestamp_utc(parsed_data[0]) 621 | 622 | # Latitude 623 | self.latitude = _read_degrees(parsed_data, 1, "s") 624 | self.longitude_degrees, self.longitude_minutes = _read_deg_mins(data=data, index=3, neg="w") 625 | 626 | # Longitude 627 | self.longitude = _read_degrees(parsed_data, 3, "w") 628 | self.latitude_degrees, self.latitude_minutes = _read_deg_mins(data=data, index=1, neg="s") 629 | 630 | # GPS quality indicator 631 | self.fix_quality = parsed_data[5] 632 | 633 | # Number of satellites in use, 0 - 12 634 | self.satellites = parsed_data[6] 635 | 636 | # Horizontal dilution of precision 637 | self.horizontal_dilution = parsed_data[7] 638 | 639 | # Antenna altitude relative to mean sea level 640 | self.altitude_m = _parse_float(parsed_data[8]) 641 | # data[9] - antenna altitude unit, always 'M' ??? 642 | 643 | # Geoidal separation relative to WGS 84 644 | self.height_geoid = _parse_float(parsed_data[10]) 645 | # data[11] - geoidal separation unit, always 'M' ??? 646 | 647 | # data[12] - Age of differential GPS data, can be null 648 | # data[13] - Differential reference station ID, can be null 649 | 650 | return True 651 | 652 | def _parse_gsa(self, talker: bytes, data: List[str]) -> bool: 653 | # GSA - GPS DOP and active satellites 654 | 655 | if data is None or len(data) not in {17, 18}: 656 | return False # Unexpected number of params. 657 | if len(data) == 17: 658 | data = _parse_data(_GSA, data) 659 | else: 660 | data = _parse_data(_GSA_4_11, data) 661 | if data is None: 662 | self.fix_quality_3d = 0 663 | return False # Params didn't parse 664 | 665 | talker = str(talker, "ascii") 666 | 667 | # Selection mode: 'M' - manual, 'A' - automatic 668 | self.sel_mode = data[0] 669 | 670 | # Mode: 1 - no fix, 2 - 2D fix, 3 - 3D fix 671 | self.fix_quality_3d = data[1] 672 | 673 | satlist = list(filter(None, data[2:-4])) 674 | self.sat_prns = [] 675 | for sat in satlist: 676 | self.sat_prns.append(f"{talker}{sat}") 677 | 678 | # PDOP, dilution of precision 679 | self.pdop = _parse_float(data[14]) 680 | 681 | # HDOP, horizontal dilution of precision 682 | self.hdop = _parse_float(data[15]) 683 | 684 | # VDOP, vertical dilution of precision 685 | self.vdop = _parse_float(data[16]) 686 | 687 | # data[17] - System ID 688 | 689 | return True 690 | 691 | def _parse_gsv(self, talker: bytes, data: List[str]) -> bool: 692 | # GSV - Satellites in view 693 | 694 | if data is None or len(data) not in {7, 11, 15, 19}: 695 | return False # Unexpected number of params. 696 | data = _parse_data( 697 | {7: _GSV7, 11: _GSV11, 15: _GSV15, 19: _GSV19}[len(data)], 698 | data, 699 | ) 700 | if data is None: 701 | return False # Params didn't parse 702 | 703 | talker = str(talker, "ascii") 704 | 705 | # Number of messages 706 | self.total_mess_num = data[0] 707 | # Message number 708 | self.mess_num = data[1] 709 | # Number of satellites in view 710 | self.satellites = data[2] 711 | 712 | sat_tup = data[3:] 713 | 714 | satlist = [] 715 | timestamp = time.monotonic() 716 | for i in range(len(sat_tup) // 4): 717 | j = i * 4 718 | value = ( 719 | # Satellite number 720 | f"{talker}{sat_tup[0 + j]}", 721 | # Elevation in degrees 722 | sat_tup[1 + j], 723 | # Azimuth in degrees 724 | sat_tup[2 + j], 725 | # signal-to-noise ratio in dB 726 | sat_tup[3 + j], 727 | # Timestamp 728 | timestamp, 729 | ) 730 | satlist.append(value) 731 | 732 | if self._sats is None: 733 | self._sats = [] 734 | for value in satlist: 735 | self._sats.append(value) 736 | 737 | if self.mess_num == self.total_mess_num: 738 | # Last part of GSV message 739 | if len(self._sats) == self.satellites: 740 | # Transfer received satellites to self.sats 741 | if self.sats is None: 742 | self.sats = {} 743 | else: 744 | # Remove all satellites which haven't 745 | # been seen for 30 seconds 746 | timestamp = time.monotonic() 747 | old = [] 748 | for sat_id, sat_data in self.sats.items(): 749 | if (timestamp - sat_data[4]) > 30: 750 | old.append(sat_id) 751 | for i in old: 752 | self.sats.pop(i) 753 | for sat in self._sats: 754 | self.sats[sat[0]] = sat 755 | self._sats.clear() 756 | 757 | self.satellites_prev = self.satellites 758 | 759 | return True 760 | 761 | 762 | class GPS_GtopI2C(GPS): 763 | """GTop-compatible I2C GPS parsing module. Can parse simple NMEA data 764 | sentences from an I2C-capable GPS module to read latitude, longitude, and more. 765 | """ 766 | 767 | def __init__( 768 | self, 769 | i2c_bus: I2C, 770 | *, 771 | address: int = _GPSI2C_DEFAULT_ADDRESS, 772 | debug: bool = False, 773 | timeout: float = 5.0, 774 | ) -> None: 775 | from adafruit_bus_device import i2c_device # noqa: PLC0415 776 | 777 | super().__init__(None, debug) # init the parent with no UART 778 | self._i2c = i2c_device.I2CDevice(i2c_bus, address) 779 | self._lastbyte = None 780 | self._charbuff = bytearray(1) 781 | self._internalbuffer = [] 782 | self._timeout = timeout 783 | 784 | def read(self, num_bytes: int = 1) -> bytearray: 785 | """Read up to num_bytes of data from the GPS directly, without parsing. 786 | Returns a bytearray with up to num_bytes or None if nothing was read""" 787 | result = [] 788 | for _ in range(num_bytes): 789 | with self._i2c as i2c: 790 | # we read one byte at a time, verify it isnt part of a string of 791 | # 'stuffed' newlines and then append to our result array for byteification 792 | i2c.readinto(self._charbuff) 793 | char = self._charbuff[0] 794 | if (char == 0x0A) and (self._lastbyte != 0x0D): 795 | continue # skip duplicate \n's! 796 | result.append(char) 797 | self._lastbyte = char # keep track of the last character approved 798 | return bytearray(result) 799 | 800 | def write(self, bytestr: ReadableBuffer) -> None: 801 | """Write a bytestring data to the GPS directly, without parsing 802 | or checksums""" 803 | with self._i2c as i2c: 804 | i2c.write(bytestr) 805 | 806 | @property 807 | def in_waiting(self) -> Literal[16]: 808 | """Returns number of bytes available in UART read buffer, always 16 809 | since I2C does not have the ability to know how much data is available""" 810 | return 16 811 | 812 | def readline(self) -> Optional[bytearray]: 813 | """Returns a newline terminated bytearray, must have timeout set for 814 | the underlying UART or this will block forever!""" 815 | timeout = time.monotonic() + self._timeout 816 | while timeout > time.monotonic(): 817 | # check if our internal buffer has a '\n' termination already 818 | if self._internalbuffer and (self._internalbuffer[-1] == 0x0A): 819 | break 820 | char = self.read(1) 821 | if not char: 822 | continue 823 | self._internalbuffer.append(char[0]) 824 | # print(bytearray(self._internalbuffer)) 825 | if self._internalbuffer and self._internalbuffer[-1] == 0x0A: 826 | ret = bytearray(self._internalbuffer) 827 | self._internalbuffer = [] # reset the buffer to empty 828 | return ret 829 | return None # no completed data yet 830 | -------------------------------------------------------------------------------- /docs/_static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/Adafruit_CircuitPython_GPS/ee938efc5638da10cf7e475f9a6e336d402e5e2a/docs/_static/favicon.ico -------------------------------------------------------------------------------- /docs/_static/favicon.ico.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2018 Phillip Torrone for Adafruit Industries 2 | 3 | SPDX-License-Identifier: CC-BY-4.0 4 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | 2 | .. If you created a package, create one automodule per module in the package. 3 | 4 | .. If your library file(s) are nested in a directory (e.g. /adafruit_foo/foo.py) 5 | .. use this format as the module name: "adafruit_foo.foo" 6 | 7 | API Reference 8 | ############# 9 | 10 | .. automodule:: adafruit_gps 11 | :members: 12 | -------------------------------------------------------------------------------- /docs/api.rst.license: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 ladyada for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | import datetime 6 | import os 7 | import sys 8 | 9 | sys.path.insert(0, os.path.abspath("..")) 10 | 11 | # -- General configuration ------------------------------------------------ 12 | 13 | # Add any Sphinx extension module names here, as strings. They can be 14 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 15 | # ones. 16 | extensions = [ 17 | "sphinx.ext.autodoc", 18 | "sphinxcontrib.jquery", 19 | "sphinx.ext.intersphinx", 20 | "sphinx.ext.viewcode", 21 | ] 22 | 23 | intersphinx_mapping = { 24 | "python": ("https://docs.python.org/3", None), 25 | "CircuitPython": ("https://docs.circuitpython.org/en/latest/", None), 26 | } 27 | 28 | # Add any paths that contain templates here, relative to this directory. 29 | templates_path = ["_templates"] 30 | 31 | source_suffix = ".rst" 32 | 33 | # The master toctree document. 34 | master_doc = "index" 35 | 36 | # General information about the project. 37 | project = "Adafruit GPS Library" 38 | creation_year = "2017" 39 | current_year = str(datetime.datetime.now().year) 40 | year_duration = ( 41 | current_year if current_year == creation_year else creation_year + " - " + current_year 42 | ) 43 | copyright = year_duration + " Tony DiCola, James Carr" 44 | author = "Tony DiCola, James Carr" 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = "1.0" 52 | # The full version, including alpha/beta/rc tags. 53 | release = "1.0" 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | # 58 | # This is also used if you do content translation via gettext catalogs. 59 | # Usually you set "language" from the command line for these cases. 60 | language = "en" 61 | 62 | # List of patterns, relative to source directory, that match files and 63 | # directories to ignore when looking for source files. 64 | # This patterns also effect to html_static_path and html_extra_path 65 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", ".env", "CODE_OF_CONDUCT.md"] 66 | 67 | # The reST default role (used for this markup: `text`) to use for all 68 | # documents. 69 | # 70 | default_role = "any" 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | # 74 | add_function_parentheses = True 75 | 76 | # The name of the Pygments (syntax highlighting) style to use. 77 | pygments_style = "sphinx" 78 | 79 | # If true, `todo` and `todoList` produce output, else they produce nothing. 80 | todo_include_todos = False 81 | 82 | # If this is True, todo emits a warning for each TODO entries. The default is False. 83 | todo_emit_warnings = True 84 | 85 | 86 | # -- Options for HTML output ---------------------------------------------- 87 | 88 | # The theme to use for HTML and HTML Help pages. See the documentation for 89 | # a list of builtin themes. 90 | # 91 | import sphinx_rtd_theme 92 | 93 | html_theme = "sphinx_rtd_theme" 94 | 95 | # Add any paths that contain custom static files (such as style sheets) here, 96 | # relative to this directory. They are copied after the builtin static files, 97 | # so a file named "default.css" will overwrite the builtin "default.css". 98 | html_static_path = ["_static"] 99 | 100 | # The name of an image file (relative to this directory) to use as a favicon of 101 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 102 | # pixels large. 103 | # 104 | html_favicon = "_static/favicon.ico" 105 | 106 | # Output file base name for HTML help builder. 107 | htmlhelp_basename = "AdafruitGPSLibrarydoc" 108 | 109 | # -- Options for LaTeX output --------------------------------------------- 110 | 111 | latex_elements = { 112 | # The paper size ('letterpaper' or 'a4paper'). 113 | # 114 | # 'papersize': 'letterpaper', 115 | # The font size ('10pt', '11pt' or '12pt'). 116 | # 117 | # 'pointsize': '10pt', 118 | # Additional stuff for the LaTeX preamble. 119 | # 120 | # 'preamble': '', 121 | # Latex figure (float) alignment 122 | # 123 | # 'figure_align': 'htbp', 124 | } 125 | 126 | # Grouping the document tree into LaTeX files. List of tuples 127 | # (source start file, target name, title, 128 | # author, documentclass [howto, manual, or own class]). 129 | latex_documents = [ 130 | ( 131 | master_doc, 132 | "AdafruitGPSLibrary.tex", 133 | "Adafruit GPS Library Documentation", 134 | author, 135 | "manual", 136 | ), 137 | ] 138 | 139 | # -- Options for manual page output --------------------------------------- 140 | 141 | # One entry per manual page. List of tuples 142 | # (source start file, name, description, authors, manual section). 143 | man_pages = [ 144 | ( 145 | master_doc, 146 | "adafruitGPSlibrary", 147 | "Adafruit GPS Library Documentation", 148 | [author], 149 | 1, 150 | ) 151 | ] 152 | 153 | # -- Options for Texinfo output ------------------------------------------- 154 | 155 | # Grouping the document tree into Texinfo files. List of tuples 156 | # (source start file, target name, title, author, 157 | # dir menu entry, description, category) 158 | texinfo_documents = [ 159 | ( 160 | master_doc, 161 | "AdafruitGPSLibrary", 162 | "Adafruit GPS Library Documentation", 163 | author, 164 | "AdafruitGPSLibrary", 165 | "One line description of project.", 166 | "Miscellaneous", 167 | ), 168 | ] 169 | -------------------------------------------------------------------------------- /docs/details.rst: -------------------------------------------------------------------------------- 1 | Communicating with the GPS 2 | ========================== 3 | 4 | The code communicates with the GPS by sending and receiving specially formatted 5 | sentences. The format used is the NMEA 0183 protocol specified by the National 6 | Marine Electronics Association. This was designed for boat navigation and 7 | control systems and is widely used by GPSs. 8 | 9 | In general, you configure the device to send the sentences that you want at the 10 | frequency you need and then receive a flow of GPS update messages. 11 | 12 | Sentences received from the GPS module use the same format, irrespective of the 13 | manufacturer. Sentences sent to the GPS module to control it, and answers to 14 | these commands, are proprietary to the manufacturer. 15 | 16 | **NOTE:** All of the example commands used in this documentation, and 17 | the examples folder, are for the MediaTek 333X GPS chips used in Adafruit 18 | products. Make sure to check the datasheet for your GPS chip if it is different. 19 | 20 | Sentence format 21 | --------------- 22 | 23 | $TAG[,DATA[,DATA...]]*hh 24 | 25 | * '$' is the opening delimiter 26 | * TAG is the tag describing the type of message. 27 | 28 | * The tag for a proprietary (chipset specific) message is composed of 29 | 30 | * 'P' for proprietary. 31 | * 'ABC', a 3 letter code for the manufacturer, eg. 'MTK' for MediaTek. 32 | * 'CODE', a manufacturer specified code for the command or answer. 33 | *Note: This can be made up of letters and numbers and there is no 34 | required length.* 35 | 36 | 'PMTK220' is the Mediatek command for setting the update rate. 37 | 38 | *Note: not all commands have an answer counterpart* 39 | 40 | * The tag for a received data sentence is of the form TTDDD, where: 41 | 42 | * TT is the talker sending the data. The list of talkers is large but we 43 | are only interested in ones starting with a 'G': 44 | 45 | * GA - Galileo (Europe) 46 | * GB - BeiDou (China) 47 | * GI - NavIC (India) 48 | * GL - GLONASS (Russia) 49 | * GP - GPS (US) 50 | * GQ - QZSS (Japan) 51 | * GN - GNSS, a combination of the above 52 | 53 | * DDD is the data type of the sentence, this determines how to decode it. 54 | Again, the list of data types is long but we are only interested in a 55 | few: 56 | 57 | * RMC - Recommended Minimum Navigation Information 58 | * GLL - Geographic Position - Latitude/Longitude 59 | * GGA - Global Positioning System Fix Data 60 | * VTG - Track made good and Ground speed *(not currently parsed)* 61 | * ZDA - Time & Date - UTC, day, month, year and local time zone *(not 62 | currently parsed)* 63 | * GSA - GPS `DOP 64 | `_ 65 | and active satellites 66 | * GSV - Satellites in view 67 | * GRS - GPS Range Residuals *(not currently parsed)* 68 | * GST - GPS Pseudorange Noise Statistics *(not currently parsed)* 69 | 70 | * DATA is separated from the TAG by a comma and is a comma separated list of 71 | data. Proprietary commands, and answers, will specify on their datasheet what 72 | the list of data is. The normal sentences generated by GPS modules are 73 | specified by NMEA. An unofficial list is `here 74 | `_. 75 | * '*' is the end of data delimiter. 76 | * hh is the 1-byte checksum of all characters between '$' and '*' in 77 | hexadecimal. 78 | * is the mandatory sentence terminator 79 | 80 | Checksums 81 | --------- 82 | 83 | When sending commands with the `send_command()` method it will add the 84 | necessary delimiters and calculate the checksum for you, eg. 85 | 86 | .. code-block:: python 87 | 88 | gps.send_command(b'PMTK220,1000') 89 | 90 | When receiving answers or data from the GPS module, if you use the `update()` 91 | method to poll the device it will reject any sentences with an invalid 92 | checksum and then try to parse the data. However, you can choose to manually 93 | pull data with the :py:meth:`~adafruit_gps.GPS.read` or 94 | :py:meth:`~adafruit_gps.GPS.readline` which will do no parsing or checksum 95 | validation. 96 | 97 | 98 | Initial Configuration 99 | --------------------- 100 | 101 | .. code-block:: python 102 | 103 | import board 104 | import busio 105 | import adafruit_gps 106 | 107 | USE_UART = True # Change this to False to connect via I2C 108 | 109 | if USE_UART: 110 | # Create a serial connection for the GPS connection. 111 | uart = busio.UART(board.TX, board.RX, baudrate=9600, timeout=10) 112 | 113 | # for a computer, use the pyserial library for uart access 114 | # import serial 115 | # uart = serial.Serial("/dev/ttyUSB0", baudrate=9600, timeout=10) 116 | 117 | # Create a GPS module instance. 118 | gps = adafruit_gps.GPS(uart, debug=False) # Use UART/pyserial 119 | else: 120 | # If using I2C, we'll create an I2C interface to talk to using default pins 121 | i2c = board.I2C() 122 | 123 | # Create a GPS module instance. 124 | gps = adafruit_gps.GPS_GtopI2C(i2c, debug=False) # Use I2C interface 125 | 126 | Configuring the GPS 127 | ------------------- 128 | 129 | .. code-block:: python 130 | 131 | # Set update rate to 1000 milliseconds (1Hz) 132 | gps.send_command(b"PMTK220,1000") 133 | 134 | # Ask for specific data to be sent. 135 | # A B C D E F G H I 136 | gps.send_command(b'PMTK314,1,1,5,1,1,1,0,0,0,0,0,0,0,0,0,0,0,1,0') 137 | 138 | # A - send GLL sentences 139 | # B - send RMC sentences 140 | # C - send VTG sentences 141 | # D - send GGA sentences 142 | # E - send GSA sentences 143 | # F - send GSV sentences 144 | # G - send GRS sentences 145 | # H - send GST sentences 146 | # I - send ZDA sentences 147 | 148 | # The number is how often to send the sentence compared to the update frequency. 149 | # If the update frequency is 500ms and the number is 5, it will send that message 150 | # every 2.5 seconds. 151 | 152 | **Note:** Be aware that some data types send multiple sentences per update. So 153 | if you ask for 5 different types of data at 1Hz, you need to be able to handle 154 | at least 10 sentences per second. If the data is not read fast enough, the 155 | internal buffer and backlog behaviour is not specified. 156 | 157 | Poll for data 158 | ------------- 159 | 160 | .. code-block:: python 161 | 162 | while True: 163 | if gps.update(): 164 | # A valid sentence was received - do something 165 | if gps.has_fix: 166 | print(f"{gps.latitude:.6f},{gps.longitude:.6f}") 167 | else: 168 | print("Waiting for a fix...") 169 | else: 170 | # No valid sentence was received, wait a moment. 171 | time.sleep(100) 172 | 173 | The `update()` call takes care of reading data from the device and parsing it 174 | into usable data. This can then be accessed using the property accessors, eg. 175 | `has_fix`, `datetime`, latitude, longitude etc. 176 | 177 | Selected Data Types 178 | =================== 179 | 180 | RMC - Recommended Minimum Navigation Information 181 | ------------------------------------------------ 182 | :: 183 | 184 | 1 2 3 4 5 6 7 8 9 10 11 12 185 | | | | | | | | | | | | | 186 | $--RMC,hhmmss.ss,A,llll.ll,a,yyyyy.yy,a,x.x,x.x,xxxx,x.x,a*hh 187 | $GNRMC,001031.00,A,4404.13993,N,12118.86023,W,0.146,,100117,,,A*7B 188 | 189 | 1. Time (UTC) 190 | 2. Status, A = Valid, V = Warning 191 | 3. Latitude 192 | 4. N or S 193 | 5. Longitude 194 | 6. E or W 195 | 7. Speed over ground, knots 196 | 8. Track made good, degrees true 197 | 9. Date, ddmmyy 198 | 10. Magnetic Variation, degrees 199 | 11. E or W 200 | 12. FAA mode indicator (NMEA 2.3 and later) 201 | 13. Checksum 202 | 203 | GGA - Global Positioning System Fix Data 204 | ---------------------------------------- 205 | :: 206 | 207 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 208 | | | | | | | | | | | | | | | | 209 | $--GGA,hhmmss.ss,llll.ll,a,yyyyy.yy,a,x,xx,x.x,x.x,M,x.x,M,x.x,xxxx*hh 210 | $GNGGA,001043.00,4404.14036,N,12118.85961,W,1,12,0.98,1113.0,M,-21.3,M,,*47 211 | 212 | 1. Time (UTC) 213 | 2. Latitude 214 | 3. N or S (North or South) 215 | 4. Longitude 216 | 5. E or W (East or West) 217 | 6. GPS Quality Indicator: 218 | 219 | 0. Fix not available 220 | 1. GPS fix 221 | 2. Differential GPS fix 222 | 3. PPS fix (values above 2 are NMEA 0183 v2.3 features) 223 | 4. Real Time Kinematic 224 | 5. Float RTK 225 | 6. Estimated (dead reckoning) 226 | 7. Manual input mode 227 | 8. Simulation mode 228 | 229 | 7. Number of satellites in view, 00 - 12 230 | 8. Horizontal dilution of precision 231 | 9. Antenna altitude above/below mean-sea-level (geoid) 232 | 10. Units of antenna altitude, meters 233 | 11. Geoidal separation, the difference between the WGS-84 earth ellipsoid and mean-sea-level (geoid), "-" means mean-sea-level below ellipsoid 234 | 12. Units of geoidal separation, meters 235 | 13. Age of differential GPS data, time in seconds since last SC104 type 1 or 9 update, empty field when DGPS is not used 236 | 14. Differential reference station ID, 0000-1023 237 | 15. Checksum 238 | 239 | Info about NMEA taken from `here (2001) 240 | `_. 241 | and `here (2021) 242 | `_ 243 | -------------------------------------------------------------------------------- /docs/details.rst.license: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 lesamouraipourpre 2 | # 3 | # SPDX-License-Identifier: MIT 4 | -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | Simple test 2 | ------------ 3 | 4 | Ensure your device works with this simple test. 5 | 6 | .. literalinclude:: ../examples/gps_simpletest.py 7 | :caption: examples/gps_simpletest.py 8 | :linenos: 9 | 10 | Echo test 11 | --------- 12 | 13 | Simple GPS module demonstration. This will print NMEA sentences received from 14 | the GPS, great for testing connection. This uses the GPS to send some 15 | commands, then reads directly from the GPS. 16 | 17 | .. literalinclude:: ../examples/gps_echotest.py 18 | :caption: examples/gps_echotest.py 19 | :linenos: 20 | 21 | Time source 22 | ----------- 23 | 24 | Simple script using GPS timestamps as RTC time source. The GPS timestamps are 25 | available without a full location fix if a single satellite can be seen. The 26 | GPS unit will keep the track of time while there is power source (i.e. a coin 27 | cell battery.) 28 | 29 | .. literalinclude:: ../examples/gps_time_source.py 30 | :caption: examples/gps_time_source.py 31 | :linenos: 32 | 33 | Data logging 34 | ------------ 35 | 36 | Simple GPS datalogging demonstration. This example uses the GPS library and to 37 | read raw NMEA sentences over I2C or UART from the GPS unit and dumps them to a 38 | file on an SD card (recommended), microcontroller internal storage (be careful 39 | as only a few kilobytes are available), or to a filesystem. 40 | 41 | If you are using a microcontroller, before writing to internal storage you 42 | MUST carefully follow the steps in this guide to enable writes to the internal 43 | filesystem: 44 | `Writing to the filesystem 45 | `_ 46 | 47 | .. literalinclude:: ../examples/gps_datalogging.py 48 | :caption: examples/gps_datalogging.py 49 | :linenos: 50 | 51 | Satellite fix 52 | ------------- 53 | 54 | This example uses GSA and GSV sentences from the GPS device to report on the 55 | quality of the received data from the satellites. 56 | 57 | * GSA - DOP(Dilution of Precision) and active satellites 58 | * GSV - Satellites in view 59 | 60 | .. literalinclude:: ../examples/gps_satellitefix.py 61 | :caption: examples/gps_satellitefix.py 62 | :linenos: 63 | -------------------------------------------------------------------------------- /docs/examples.rst.license: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 ladyada for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | 3 | Table of Contents 4 | ================= 5 | 6 | .. toctree:: 7 | :maxdepth: 4 8 | :hidden: 9 | 10 | self 11 | details 12 | 13 | .. toctree:: 14 | :caption: Examples 15 | 16 | examples 17 | 18 | .. toctree:: 19 | :caption: API Reference 20 | :maxdepth: 3 21 | 22 | api 23 | 24 | .. toctree:: 25 | :caption: Tutorials 26 | 27 | Adafruit Ultimate GPS 28 | Adafruit Ultimate GPS featherwing 29 | Adafruit Mini GPS module 30 | GPS Tour Guide 31 | 32 | .. toctree:: 33 | :caption: Related Products 34 | 35 | Adafruit Ultimate GPS Breakout 36 | Adafruit Ultimate GPS FeatherWing 37 | Adafruit Mini GPS module 38 | 39 | .. toctree:: 40 | :caption: Other Links 41 | 42 | Download from GitHub 43 | Download Library Bundle 44 | CircuitPython Reference Documentation 45 | CircuitPython Support Forum 46 | Discord Chat 47 | Adafruit Learning System 48 | Adafruit Blog 49 | Adafruit Store 50 | 51 | Indices and tables 52 | ================== 53 | 54 | * :ref:`genindex` 55 | * :ref:`modindex` 56 | * :ref:`search` 57 | -------------------------------------------------------------------------------- /docs/index.rst.license: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 ladyada for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Kattni Rembor for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | sphinx 6 | sphinxcontrib-jquery 7 | sphinx-rtd-theme 8 | -------------------------------------------------------------------------------- /examples/gps_datalogging.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # SPDX-License-Identifier: MIT 3 | 4 | # Simple GPS datalogging demonstration. 5 | # This example uses the GPS library and to read raw NMEA sentences 6 | # over I2C or UART from the GPS unit and dumps them to a file on an SD card 7 | # (recommended), microcontroller internal storage (be careful as only a few 8 | # kilobytes are available), or to a filesystem. 9 | # If you are using a microcontroller, before writing to internal storage you 10 | # MUST carefully follow the steps in this guide to enable writes to the 11 | # internal filesystem: 12 | # https://learn.adafruit.com/adafruit-ultimate-gps-featherwing/circuitpython-library 13 | import sys 14 | 15 | import board 16 | import busio 17 | 18 | import adafruit_gps 19 | 20 | # Path to the file to log GPS data. By default this will be appended to 21 | # which means new lines are added at the end and all old data is kept. 22 | # Change this path to point at internal storage (like '/gps.txt') or SD 23 | # card mounted storage ('/sd/gps.txt') as desired. 24 | LOG_FILE = "gps.txt" # Example for writing to internal path gps.txt 25 | 26 | # File more for opening the log file. Mode 'ab' means append or add new lines 27 | # to the end of the file rather than erasing it and starting over. If you'd 28 | # like to erase the file and start clean each time use the value 'wb' instead. 29 | LOG_MODE = "ab" 30 | 31 | # sdcardio and adafruit_sdcard are NOT supported on blinka. If you are using a 32 | # Raspberry Pi or other single-board linux computer, the code will save the 33 | # output to the path defined in LOG_FILE above. 34 | if sys.platform != "linux": 35 | import storage 36 | 37 | SD_CS_PIN = board.D10 # CS for SD card using Adalogger Featherwing 38 | try: 39 | import sdcardio 40 | 41 | sdcard = sdcardio.SDCard(board.SPI, SD_CS_PIN) 42 | except ImportError: 43 | import adafruit_sdcard 44 | import digitalio 45 | 46 | sdcard = adafruit_sdcard.SDCard( 47 | board.SPI(), 48 | digitalio.DigitalInOut(SD_CS_PIN), 49 | ) 50 | 51 | vfs = storage.VfsFat(sdcard) 52 | storage.mount(vfs, "/sd") # Mount SD card under '/sd' path in filesystem. 53 | LOG_FILE = "/sd/gps.txt" # Example for writing to SD card path /sd/gps.txt 54 | 55 | # Create a serial connection for the GPS connection using default speed and 56 | # a slightly higher timeout (GPS modules typically update once a second). 57 | # These are the defaults you should use for the GPS FeatherWing. 58 | # For other boards set RX = GPS module TX, and TX = GPS module RX pins. 59 | uart = busio.UART(board.TX, board.RX, baudrate=9600, timeout=10) 60 | 61 | # If using a USB/Serial converter, use pyserial and update the serial 62 | # port name to match the serial connection for the GPS! 63 | # import serial 64 | # uart = serial.Serial("/dev/ttyUSB0", baudrate=9600, timeout=10) 65 | 66 | # If using I2C, we'll create an I2C interface to talk to using default pins 67 | # i2c = board.I2C() # uses board.SCL and board.SDA 68 | # i2c = board.STEMMA_I2C() # For using the built-in STEMMA QT connector on a microcontroller 69 | 70 | # Create a GPS module instance. 71 | gps = adafruit_gps.GPS(uart) # Use UART/pyserial 72 | # gps = adafruit_gps.GPS_GtopI2C(i2c) # Use I2C interface 73 | 74 | # Main loop just reads data from the GPS module and writes it back out to 75 | # the output file while also printing to serial output. 76 | with open(LOG_FILE, LOG_MODE) as outfile: 77 | while True: 78 | sentence = gps.readline() 79 | if not sentence: 80 | continue 81 | print(str(sentence, "ascii").strip()) 82 | outfile.write(sentence) 83 | outfile.flush() 84 | -------------------------------------------------------------------------------- /examples/gps_displayio_simpletest.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 2 | # SPDX-License-Identifier: MIT 3 | 4 | import time 5 | 6 | import board 7 | from adafruit_display_text.label import Label 8 | from displayio import Group 9 | from terminalio import FONT 10 | 11 | import adafruit_gps 12 | 13 | # import busio 14 | # uart = busio.UART(board.TX, board.RX, baudrate=9600, timeout=10) 15 | i2c = board.I2C() # uses board.SCL and board.SDA 16 | # i2c = board.STEMMA_I2C() # For using the built-in STEMMA QT connector 17 | 18 | # Create a GPS module instance. 19 | # gps = adafruit_gps.GPS(uart, debug=False) # Use UART 20 | gps = adafruit_gps.GPS_GtopI2C(i2c, debug=False) # Use I2C interface 21 | 22 | # Turn on the basic GGA and RMC info (what you typically want) 23 | gps.send_command(b"PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0") 24 | 25 | # Set update rate to once a second 1hz (what you typically want) 26 | gps.send_command(b"PMTK220,1000") 27 | 28 | 29 | # Example written for boards with built-in displays 30 | display = board.DISPLAY 31 | 32 | # Create a main_group to hold anything we want to show on the display. 33 | main_group = Group() 34 | 35 | # Create a Label to show the readings. If you have a very small 36 | # display you may need to change to scale=1. 37 | display_output_label = Label(FONT, text="", scale=2) 38 | 39 | # Place the label near the top left corner with anchored positioning 40 | display_output_label.anchor_point = (0, 0) 41 | display_output_label.anchored_position = (4, 4) 42 | 43 | # Add the label to the main_group 44 | main_group.append(display_output_label) 45 | 46 | # Set the main_group as the root_group of the display 47 | display.root_group = main_group 48 | 49 | 50 | last_print = time.monotonic() 51 | 52 | # Begin main loop 53 | while True: 54 | gps.update() 55 | 56 | current = time.monotonic() 57 | # Update display data every second 58 | if current - last_print >= 1.0: 59 | last_print = current 60 | if not gps.has_fix: 61 | # Try again if we don't have a fix yet. 62 | display_output_label.text = "Waiting for fix..." 63 | continue 64 | # We have a fix! (gps.has_fix is true) 65 | t = gps.timestamp_utc 66 | 67 | # Update the label.text property to change the text on the display 68 | display_output_label.text = f"Timestamp (UTC): \ 69 | \n{t.tm_mday}/{t.tm_mon}/{t.tm_year} {t.tm_hour}:{t.tm_min:02}:{t.tm_sec:02}\ 70 | \nLat: {gps.latitude:.6f}\ 71 | \nLong: {gps.longitude:.6f}" 72 | -------------------------------------------------------------------------------- /examples/gps_echotest.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # SPDX-License-Identifier: MIT 3 | 4 | # Simple GPS module demonstration. 5 | # Will print NMEA sentences received from the GPS, great for testing connection 6 | # Uses the GPS to send some commands, then reads directly from the GPS 7 | import time 8 | 9 | import board 10 | import busio 11 | 12 | import adafruit_gps 13 | 14 | # Create a serial connection for the GPS connection using default speed and 15 | # a slightly higher timeout (GPS modules typically update once a second). 16 | # These are the defaults you should use for the GPS FeatherWing. 17 | # For other boards set RX = GPS module TX, and TX = GPS module RX pins. 18 | uart = busio.UART(board.TX, board.RX, baudrate=9600, timeout=10) 19 | 20 | # for a computer, use the pyserial library for uart access 21 | # import serial 22 | # uart = serial.Serial("/dev/ttyUSB0", baudrate=9600, timeout=10) 23 | 24 | # If using I2C, we'll create an I2C interface to talk to using default pins 25 | # i2c = board.I2C() # uses board.SCL and board.SDA 26 | # i2c = board.STEMMA_I2C() # For using the built-in STEMMA QT connector on a microcontroller 27 | 28 | # Create a GPS module instance. 29 | gps = adafruit_gps.GPS(uart) # Use UART/pyserial 30 | # gps = adafruit_gps.GPS_GtopI2C(i2c) # Use I2C interface 31 | 32 | # Initialize the GPS module by changing what data it sends and at what rate. 33 | # These are NMEA extensions for PMTK_314_SET_NMEA_OUTPUT and 34 | # PMTK_220_SET_NMEA_UPDATERATE but you can send anything from here to adjust 35 | # the GPS module behavior: 36 | # https://cdn-shop.adafruit.com/datasheets/PMTK_A11.pdf 37 | 38 | # Turn on the basic GGA and RMC info (what you typically want) 39 | gps.send_command(b"PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0") 40 | # Turn on just minimum info (RMC only, location): 41 | # gps.send_command(b'PMTK314,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0') 42 | # Turn off everything: 43 | # gps.send_command(b'PMTK314,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0') 44 | # Tuen on everything (not all of it is parsed!) 45 | # gps.send_command(b'PMTK314,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0') 46 | 47 | # Set update rate to once a second (1hz) which is what you typically want. 48 | gps.send_command(b"PMTK220,1000") 49 | # Or decrease to once every two seconds by doubling the millisecond value. 50 | # Be sure to also increase your UART timeout above! 51 | # gps.send_command(b'PMTK220,2000') 52 | # You can also speed up the rate, but don't go too fast or else you can lose 53 | # data during parsing. This would be twice a second (2hz, 500ms delay): 54 | # gps.send_command(b'PMTK220,500') 55 | 56 | # Main loop runs forever printing data as it comes in 57 | timestamp = time.monotonic() 58 | while True: 59 | data = gps.read(32) # read up to 32 bytes 60 | # print(data) # this is a bytearray type 61 | 62 | if data is not None: 63 | # convert bytearray to string 64 | data_string = "".join([chr(b) for b in data]) 65 | print(data_string, end="") 66 | 67 | if time.monotonic() - timestamp > 5: 68 | # every 5 seconds... 69 | gps.send_command(b"PMTK605") # request firmware version 70 | timestamp = time.monotonic() 71 | -------------------------------------------------------------------------------- /examples/gps_satellitefix.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 lesamouraipourpre 2 | # SPDX-License-Identifier: MIT 3 | 4 | # This example uses GSA and GSV sentences from the GPS device to report on the 5 | # quality of the received data from the satellites. 6 | # * GSA - DOP(Dilution of Precision) and active satellites 7 | # * GSV - Satellites in view 8 | 9 | import time 10 | 11 | import board 12 | 13 | import adafruit_gps 14 | 15 | # Create a serial connection for the GPS connection using default speed and 16 | # a slightly higher timeout (GPS modules typically update once a second). 17 | # These are the defaults you should use for the GPS FeatherWing. 18 | # For other boards set RX = GPS module TX, and TX = GPS module RX pins. 19 | # import busio 20 | # uart = busio.UART(board.TX, board.RX, baudrate=9600, timeout=10) 21 | 22 | # for a computer, use the pyserial library for uart access 23 | # import serial 24 | # uart = serial.Serial("/dev/ttyUSB0", baudrate=9600, timeout=10) 25 | 26 | # If using I2C, we'll create an I2C interface to talk to using default pins 27 | i2c = board.I2C() # uses board.SCL and board.SDA 28 | # i2c = board.STEMMA_I2C() # For using the built-in STEMMA QT connector on a microcontroller 29 | 30 | # Create a GPS module instance. 31 | # gps = adafruit_gps.GPS(uart, debug=False) # Use UART/pyserial 32 | gps = adafruit_gps.GPS_GtopI2C(i2c, debug=False) # Use I2C interface 33 | 34 | # Initialize the GPS module by changing what data it sends and at what rate. 35 | # These are NMEA extensions for PMTK_314_SET_NMEA_OUTPUT and 36 | # PMTK_220_SET_NMEA_UPDATERATE but you can send anything from here to adjust 37 | # the GPS module behavior: 38 | # https://cdn-shop.adafruit.com/datasheets/PMTK_A11.pdf 39 | 40 | # Turn on everything (not all of it is parsed!) 41 | gps.send_command(b"PMTK314,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0") 42 | 43 | # Set update rate to once a second (1hz) which is what you typically want. 44 | gps.send_command(b"PMTK220,1000") 45 | # Or decrease to once every two seconds by doubling the millisecond value. 46 | # Be sure to also increase your UART timeout above! 47 | # gps.send_command(b'PMTK220,2000') 48 | # You can also speed up the rate, but don't go too fast or else you can lose 49 | # data during parsing. This would be twice a second (2hz, 500ms delay): 50 | # gps.send_command(b'PMTK220,500') 51 | 52 | 53 | def format_dop(dop): 54 | # https://en.wikipedia.org/wiki/Dilution_of_precision_(navigation) 55 | if dop > 20: 56 | msg = "Poor" 57 | elif dop > 10: 58 | msg = "Fair" 59 | elif dop > 5: 60 | msg = "Moderate" 61 | elif dop > 2: 62 | msg = "Good" 63 | elif dop > 1: 64 | msg = "Excellent" 65 | else: 66 | msg = "Ideal" 67 | return f"{dop} - {msg}" 68 | 69 | 70 | talkers = { 71 | "GA": "Galileo", 72 | "GB": "BeiDou", 73 | "GI": "NavIC", 74 | "GL": "GLONASS", 75 | "GP": "GPS", 76 | "GQ": "QZSS", 77 | "GN": "GNSS", 78 | } 79 | 80 | # Main loop runs forever printing the location, etc. every second. 81 | last_print = time.monotonic() 82 | while True: 83 | # Make sure to call gps.update() every loop iteration and at least twice 84 | # as fast as data comes from the GPS unit (usually every second). 85 | # This returns a bool that's true if it parsed new data (you can ignore it 86 | # though if you don't care and instead look at the has_fix property). 87 | if not gps.update() or not gps.has_fix: 88 | time.sleep(0.1) 89 | continue 90 | 91 | if gps.nmea_sentence[3:6] == "GSA": 92 | print(f"{gps.latitude:.6f}, {gps.longitude:.6f} {gps.altitude_m}m") 93 | print(f"2D Fix: {gps.has_fix} 3D Fix: {gps.has_3d_fix}") 94 | print(f" PDOP (Position Dilution of Precision): {format_dop(gps.pdop)}") 95 | print(f" HDOP (Horizontal Dilution of Precision): {format_dop(gps.hdop)}") 96 | print(f" VDOP (Vertical Dilution of Precision): {format_dop(gps.vdop)}") 97 | print("Satellites used for fix:") 98 | for s in gps.sat_prns: 99 | talker = talkers[s[0:2]] 100 | number = s[2:] 101 | print(f" {talker}-{number} ", end="") 102 | if gps.sats is None: 103 | print("- no info") 104 | else: 105 | try: 106 | sat = gps.sats[s] 107 | if sat is None: 108 | print("- no info") 109 | else: 110 | print(f"Elevation:{sat[1]}* Azimuth:{sat[2]}* SNR:{sat[3]}dB") 111 | except KeyError: 112 | print("- no info") 113 | print() 114 | -------------------------------------------------------------------------------- /examples/gps_simpletest.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # SPDX-License-Identifier: MIT 3 | 4 | # Simple GPS module demonstration. 5 | # Will wait for a fix and print a message every second with the current location 6 | # and other details. 7 | import time 8 | 9 | import board 10 | import busio 11 | 12 | import adafruit_gps 13 | 14 | # Create a serial connection for the GPS connection using default speed and 15 | # a slightly higher timeout (GPS modules typically update once a second). 16 | # These are the defaults you should use for the GPS FeatherWing. 17 | # For other boards set RX = GPS module TX, and TX = GPS module RX pins. 18 | uart = busio.UART(board.TX, board.RX, baudrate=9600, timeout=10) 19 | 20 | # for a computer, use the pyserial library for uart access 21 | # import serial 22 | # uart = serial.Serial("/dev/ttyUSB0", baudrate=9600, timeout=10) 23 | 24 | # If using I2C, we'll create an I2C interface to talk to using default pins 25 | # i2c = board.I2C() # uses board.SCL and board.SDA 26 | # i2c = board.STEMMA_I2C() # For using the built-in STEMMA QT connector on a microcontroller 27 | 28 | # Create a GPS module instance. 29 | gps = adafruit_gps.GPS(uart, debug=False) # Use UART/pyserial 30 | # gps = adafruit_gps.GPS_GtopI2C(i2c, debug=False) # Use I2C interface 31 | 32 | # Initialize the GPS module by changing what data it sends and at what rate. 33 | # These are NMEA extensions for PMTK_314_SET_NMEA_OUTPUT and 34 | # PMTK_220_SET_NMEA_UPDATERATE but you can send anything from here to adjust 35 | # the GPS module behavior: 36 | # https://cdn-shop.adafruit.com/datasheets/PMTK_A11.pdf 37 | 38 | # Turn on the basic GGA and RMC info (what you typically want) 39 | gps.send_command(b"PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0") 40 | # Turn on the basic GGA and RMC info + VTG for speed in km/h 41 | # gps.send_command(b"PMTK314,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0") 42 | # Turn on just minimum info (RMC only, location): 43 | # gps.send_command(b'PMTK314,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0') 44 | # Turn off everything: 45 | # gps.send_command(b'PMTK314,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0') 46 | # Turn on everything (not all of it is parsed!) 47 | # gps.send_command(b'PMTK314,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0') 48 | 49 | # Set update rate to once a second (1hz) which is what you typically want. 50 | gps.send_command(b"PMTK220,1000") 51 | # Or decrease to once every two seconds by doubling the millisecond value. 52 | # Be sure to also increase your UART timeout above! 53 | # gps.send_command(b'PMTK220,2000') 54 | # You can also speed up the rate, but don't go too fast or else you can lose 55 | # data during parsing. This would be twice a second (2hz, 500ms delay): 56 | # gps.send_command(b'PMTK220,500') 57 | 58 | # Main loop runs forever printing the location, etc. every second. 59 | last_print = time.monotonic() 60 | while True: 61 | # Make sure to call gps.update() every loop iteration and at least twice 62 | # as fast as data comes from the GPS unit (usually every second). 63 | # This returns a bool that's true if it parsed new data (you can ignore it 64 | # though if you don't care and instead look at the has_fix property). 65 | gps.update() 66 | # Every second print out current location details if there's a fix. 67 | current = time.monotonic() 68 | if current - last_print >= 1.0: 69 | last_print = current 70 | if not gps.has_fix: 71 | # Try again if we don't have a fix yet. 72 | print("Waiting for fix...") 73 | continue 74 | # We have a fix! (gps.has_fix is true) 75 | # Print out details about the fix like location, date, etc. 76 | print("=" * 40) # Print a separator line. 77 | print( 78 | "Fix timestamp: {}/{}/{} {:02}:{:02}:{:02}".format( # noqa: UP032 79 | gps.timestamp_utc.tm_mon, # Grab parts of the time from the 80 | gps.timestamp_utc.tm_mday, # struct_time object that holds 81 | gps.timestamp_utc.tm_year, # the fix time. Note you might 82 | gps.timestamp_utc.tm_hour, # not get all data like year, day, 83 | gps.timestamp_utc.tm_min, # month! 84 | gps.timestamp_utc.tm_sec, 85 | ) 86 | ) 87 | print(f"Latitude: {gps.latitude:.6f} degrees") 88 | print(f"Longitude: {gps.longitude:.6f} degrees") 89 | print(f"Precise Latitude: {gps.latitude_degrees} degs, {gps.latitude_minutes:2.4f} mins") 90 | print(f"Precise Longitude: {gps.longitude_degrees} degs, {gps.longitude_minutes:2.4f} mins") 91 | print(f"Fix quality: {gps.fix_quality}") 92 | # Some attributes beyond latitude, longitude and timestamp are optional 93 | # and might not be present. Check if they're None before trying to use! 94 | if gps.satellites is not None: 95 | print(f"# satellites: {gps.satellites}") 96 | if gps.altitude_m is not None: 97 | print(f"Altitude: {gps.altitude_m} meters") 98 | if gps.speed_knots is not None: 99 | print(f"Speed: {gps.speed_knots} knots") 100 | if gps.speed_kmh is not None: 101 | print(f"Speed: {gps.speed_kmh} km/h") 102 | if gps.track_angle_deg is not None: 103 | print(f"Track angle: {gps.track_angle_deg} degrees") 104 | if gps.horizontal_dilution is not None: 105 | print(f"Horizontal dilution: {gps.horizontal_dilution}") 106 | if gps.height_geoid is not None: 107 | print(f"Height geoid: {gps.height_geoid} meters") 108 | -------------------------------------------------------------------------------- /examples/gps_time_source.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # SPDX-License-Identifier: MIT 3 | 4 | # Simple script using GPS timestamps as RTC time source 5 | # The GPS timestamps are available without a fix and keep the track of 6 | # time while there is powersource (ie coin cell battery) 7 | 8 | import time 9 | 10 | import board 11 | import busio 12 | import rtc 13 | 14 | import adafruit_gps 15 | 16 | uart = busio.UART(board.TX, board.RX, baudrate=9600, timeout=10) 17 | # i2c = busio.I2C(board.SCL, board.SDA) 18 | 19 | gps = adafruit_gps.GPS(uart, debug=False) 20 | # gps = adafruit_gps.GPS_GtopI2C(i2c, debug=False) # Use I2C interface 21 | 22 | gps.send_command(b"PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0") 23 | gps.send_command(b"PMTK220,1000") 24 | 25 | print("Set GPS as time source") 26 | rtc.set_time_source(gps) 27 | the_rtc = rtc.RTC() 28 | 29 | 30 | def _format_datetime(datetime): 31 | date_part = f"{datetime.tm_mon:02}/{datetime.tm_mday:02}/{datetime.tm_year}" 32 | time_part = f"{datetime.tm_hour:02}:{datetime.tm_min:02}:{datetime.tm_sec:02}" 33 | return f"{date_part} {time_part}" 34 | 35 | 36 | last_print = time.monotonic() 37 | while True: 38 | gps.update() 39 | # Every second print out current time from GPS, RTC and time.localtime() 40 | current = time.monotonic() 41 | if current - last_print >= 1.0: 42 | last_print = current 43 | if not gps.timestamp_utc: 44 | print("No time data from GPS yet") 45 | continue 46 | # Time & date from GPS informations 47 | print(f"Fix timestamp: {_format_datetime(gps.timestamp_utc)}") 48 | 49 | # Time & date from internal RTC 50 | print(f"RTC timestamp: {_format_datetime(the_rtc.datetime)}") 51 | 52 | # Time & date from time.localtime() function 53 | local_time = time.localtime() 54 | 55 | print(f"Local time: {_format_datetime(local_time)}") 56 | -------------------------------------------------------------------------------- /optional_requirements.txt: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Jonas Kittner 2 | # SPDX-FileCopyrightText: 2022 Alec Delaney, for Adafruit Industries 3 | # 4 | # SPDX-License-Identifier: Unlicense 5 | 6 | adafruit-circuitpython-sd 7 | covdefaults 8 | coverage 9 | freezegun 10 | pytest 11 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Alec Delaney for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | [build-system] 6 | requires = [ 7 | "setuptools", 8 | "wheel", 9 | "setuptools-scm", 10 | ] 11 | 12 | [project] 13 | name = "adafruit-circuitpython-gps" 14 | description = "CircuitPython library for GPS modules." 15 | version = "0.0.0+auto.0" 16 | readme = "README.rst" 17 | authors = [ 18 | {name = "Adafruit Industries", email = "circuitpython@adafruit.com"} 19 | ] 20 | urls = {Homepage = "https://github.com/adafruit/Adafruit_CircuitPython_GPS"} 21 | keywords = [ 22 | "adafruit", 23 | "gps", 24 | "module", 25 | "latitude", 26 | "longitude", 27 | "breakout", 28 | "hardware", 29 | "micropython", 30 | "circuitpython", 31 | ] 32 | license = {text = "MIT"} 33 | classifiers = [ 34 | "Intended Audience :: Developers", 35 | "Topic :: Software Development :: Libraries", 36 | "Topic :: Software Development :: Embedded Systems", 37 | "Topic :: System :: Hardware", 38 | "License :: OSI Approved :: MIT License", 39 | "Programming Language :: Python :: 3", 40 | ] 41 | dynamic = ["dependencies", "optional-dependencies"] 42 | 43 | [tool.setuptools] 44 | py-modules = ["adafruit_gps"] 45 | 46 | [tool.setuptools.dynamic] 47 | dependencies = {file = ["requirements.txt"]} 48 | optional-dependencies = {optional = {file = ["optional_requirements.txt"]}} 49 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Alec Delaney, for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | Adafruit-Blinka 6 | adafruit-circuitpython-busdevice 7 | pyserial 8 | adafruit-circuitpython-typing 9 | typing-extensions~=4.0 10 | -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | target-version = "py38" 6 | line-length = 100 7 | 8 | [lint] 9 | preview = true 10 | select = ["I", "PL", "UP"] 11 | 12 | extend-select = [ 13 | "D419", # empty-docstring 14 | "E501", # line-too-long 15 | "W291", # trailing-whitespace 16 | "PLC0414", # useless-import-alias 17 | "PLC2401", # non-ascii-name 18 | "PLC2801", # unnecessary-dunder-call 19 | "PLC3002", # unnecessary-direct-lambda-call 20 | "E999", # syntax-error 21 | "PLE0101", # return-in-init 22 | "F706", # return-outside-function 23 | "F704", # yield-outside-function 24 | "PLE0116", # continue-in-finally 25 | "PLE0117", # nonlocal-without-binding 26 | "PLE0241", # duplicate-bases 27 | "PLE0302", # unexpected-special-method-signature 28 | "PLE0604", # invalid-all-object 29 | "PLE0605", # invalid-all-format 30 | "PLE0643", # potential-index-error 31 | "PLE0704", # misplaced-bare-raise 32 | "PLE1141", # dict-iter-missing-items 33 | "PLE1142", # await-outside-async 34 | "PLE1205", # logging-too-many-args 35 | "PLE1206", # logging-too-few-args 36 | "PLE1307", # bad-string-format-type 37 | "PLE1310", # bad-str-strip-call 38 | "PLE1507", # invalid-envvar-value 39 | "PLE2502", # bidirectional-unicode 40 | "PLE2510", # invalid-character-backspace 41 | "PLE2512", # invalid-character-sub 42 | "PLE2513", # invalid-character-esc 43 | "PLE2514", # invalid-character-nul 44 | "PLE2515", # invalid-character-zero-width-space 45 | "PLR0124", # comparison-with-itself 46 | "PLR0202", # no-classmethod-decorator 47 | "PLR0203", # no-staticmethod-decorator 48 | "UP004", # useless-object-inheritance 49 | "PLR0206", # property-with-parameters 50 | "PLR0904", # too-many-public-methods 51 | "PLR0911", # too-many-return-statements 52 | "PLR0912", # too-many-branches 53 | "PLR0913", # too-many-arguments 54 | "PLR0914", # too-many-locals 55 | "PLR0915", # too-many-statements 56 | "PLR0916", # too-many-boolean-expressions 57 | "PLR1702", # too-many-nested-blocks 58 | "PLR1704", # redefined-argument-from-local 59 | "PLR1711", # useless-return 60 | "C416", # unnecessary-comprehension 61 | "PLR1733", # unnecessary-dict-index-lookup 62 | "PLR1736", # unnecessary-list-index-lookup 63 | 64 | # ruff reports this rule is unstable 65 | #"PLR6301", # no-self-use 66 | 67 | "PLW0108", # unnecessary-lambda 68 | "PLW0120", # useless-else-on-loop 69 | "PLW0127", # self-assigning-variable 70 | "PLW0129", # assert-on-string-literal 71 | "B033", # duplicate-value 72 | "PLW0131", # named-expr-without-context 73 | "PLW0245", # super-without-brackets 74 | "PLW0406", # import-self 75 | "PLW0602", # global-variable-not-assigned 76 | "PLW0603", # global-statement 77 | "PLW0604", # global-at-module-level 78 | 79 | # fails on the try: import typing used by libraries 80 | #"F401", # unused-import 81 | 82 | "F841", # unused-variable 83 | "E722", # bare-except 84 | "PLW0711", # binary-op-exception 85 | "PLW1501", # bad-open-mode 86 | "PLW1508", # invalid-envvar-default 87 | "PLW1509", # subprocess-popen-preexec-fn 88 | "PLW2101", # useless-with-lock 89 | "PLW3301", # nested-min-max 90 | ] 91 | 92 | ignore = [ 93 | "PLR2004", # magic-value-comparison 94 | "UP030", # format literals 95 | "PLW1514", # unspecified-encoding 96 | "PLR0913", # too-many-arguments 97 | "PLR0915", # too-many-statements 98 | "PLR0917", # too-many-positional-arguments 99 | "PLR0904", # too-many-public-methods 100 | "PLR0912", # too-many-branches 101 | "PLR0916", # too-many-boolean-expressions 102 | ] 103 | 104 | [lint.per-file-ignores] 105 | "tests/adafruit_gps_test.py" = ["PLC2701"] 106 | 107 | [format] 108 | line-ending = "lf" 109 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/Adafruit_CircuitPython_GPS/ee938efc5638da10cf7e475f9a6e336d402e5e2a/tests/__init__.py -------------------------------------------------------------------------------- /tests/adafruit_gps_test.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Jonas Kittner 2 | # 3 | # SPDX-License-Identifier: MIT 4 | import time 5 | from unittest import mock 6 | 7 | import pytest 8 | from freezegun import freeze_time 9 | 10 | from adafruit_gps import ( 11 | GPS, 12 | _parse_data, 13 | _parse_degrees, 14 | _parse_float, 15 | _parse_int, 16 | _parse_str, 17 | _parse_talker, 18 | _read_deg_mins, 19 | _read_degrees, 20 | ) 21 | 22 | 23 | @pytest.mark.parametrize( 24 | ("val", "exp"), 25 | ( 26 | pytest.param("0023.456", 390933, id="leading zero"), 27 | pytest.param("6413.9369", 64232281, id="regular value"), 28 | pytest.param("2747.416122087989", 27790268, id="long value"), 29 | ), 30 | ) 31 | def test_parse_degrees(val, exp): 32 | assert _parse_degrees(val) == pytest.approx(exp) 33 | 34 | 35 | def test_parse_degrees_too_short(): 36 | assert _parse_degrees("12") is None 37 | 38 | 39 | def test_parse_int(): 40 | assert _parse_int("456") == 456 41 | 42 | 43 | @pytest.mark.parametrize( 44 | "val", 45 | (None, ""), 46 | ) 47 | def test_parse_int_invalid(val): 48 | assert _parse_int(val) is None 49 | 50 | 51 | def test_parse_float(): 52 | assert _parse_float("456") == 456 53 | 54 | 55 | @pytest.mark.parametrize( 56 | "val", 57 | (None, ""), 58 | ) 59 | def test_parse_float_invalid(val): 60 | assert _parse_float(val) is None 61 | 62 | 63 | @pytest.mark.parametrize( 64 | ("data", "neg", "exp"), 65 | ( 66 | pytest.param([27790270, "S"], "s", -27.79027, id="south negative"), 67 | pytest.param([64232280, "N"], "s", 64.23228, id="north not negative"), 68 | pytest.param([123456700, "W"], "w", -123.4567, id="west negative"), 69 | pytest.param([10789100, "E"], "w", 10.7891, id="east not negative"), 70 | ), 71 | ) 72 | def test_read_degrees(data, neg, exp): 73 | assert _read_degrees(data, 0, neg) == exp 74 | 75 | 76 | @pytest.mark.parametrize( 77 | "val", 78 | (None, ""), 79 | ) 80 | def test_parse_str_invalid(val): 81 | assert _parse_str(val) is None 82 | 83 | 84 | def test_parse_str_valid(): 85 | assert _parse_str(13) == "13" 86 | 87 | 88 | def test_parse_talker_prop_code(): 89 | assert _parse_talker(b"PMTK001") == (b"P", b"MTK001") 90 | 91 | 92 | def test_parse_talker_regular(): 93 | assert _parse_talker(b"GPRMC") == (b"GP", b"RMC") 94 | 95 | 96 | @pytest.mark.parametrize( 97 | "sentence_type", 98 | (-1, 10), 99 | ) 100 | def test_parse_data_unknown_sentence_type(sentence_type): 101 | assert _parse_data(sentence_type, data=[]) is None 102 | 103 | 104 | def test_param_types_does_not_match_data_items(): 105 | assert _parse_data(sentence_type=1, data=["too", "few", "items"]) is None 106 | 107 | 108 | def test_parse_data_unexpected_parameter_type(): 109 | with mock.patch("adafruit_gps._SENTENCE_PARAMS", ("xyz",)): 110 | with pytest.raises(TypeError) as exc_info: 111 | _parse_data(sentence_type=0, data=["a", "b", "c"]) 112 | 113 | assert exc_info.value.args[0] == "GPS: Unexpected parameter type 'x'" 114 | 115 | 116 | class UartMock: 117 | """mocking the UART connection an its methods""" 118 | 119 | def write(self, bytestr): # noqa: PLR6301 120 | print(bytestr, end="") 121 | 122 | @property 123 | def in_waiting(self): 124 | return 100 125 | 126 | 127 | def test_read_sentence_too_few_in_waiting(): 128 | with mock.patch.object(GPS, "readline", return_value="x"): 129 | 130 | class UartMockWaiting(UartMock): 131 | @property 132 | def in_waiting(self): 133 | # overwrite the in_waiting property to perform the test 134 | return 3 135 | 136 | gps = GPS(uart=UartMockWaiting()) 137 | assert not gps.update() 138 | 139 | 140 | def test_GPS_update_timestamp_UTC_date_None(): 141 | gps = GPS(uart=UartMock()) 142 | assert gps.datetime is None 143 | assert gps.timestamp_utc is None 144 | exp_struct = time.struct_time((0, 0, 0, 22, 14, 11, 0, 0, -1)) 145 | gps._update_timestamp_utc(time_utc="221411") 146 | assert gps.timestamp_utc == exp_struct 147 | 148 | 149 | def test_GPS_update_timestamp_UTC_date_not_None(): 150 | gps = GPS(uart=UartMock()) 151 | exp_struct = time.struct_time((2021, 10, 2, 22, 14, 11, 0, 0, -1)) 152 | gps._update_timestamp_utc(time_utc="221411", date="021021") 153 | assert gps.timestamp_utc == exp_struct 154 | 155 | 156 | def test_GPS_update_timestamp_timestamp_utc_was_not_none_new_date_none(): 157 | gps = GPS(uart=UartMock()) 158 | # set this to a value 159 | gps.timestamp_utc = time.struct_time((2021, 10, 2, 22, 10, 11, 0, 0, -1)) 160 | exp_struct = time.struct_time((2021, 10, 2, 22, 14, 11, 0, 0, -1)) 161 | # update the timestamp 162 | gps._update_timestamp_utc(time_utc="221411") 163 | assert gps.timestamp_utc == exp_struct 164 | 165 | 166 | def test_GPS_update_with_unknown_talker(): 167 | r = b"$XYRMC,215032.086,A,1234.5678,N,00123.12345,E,0.45,56.35,021021,,,A*7c\r\n" 168 | with mock.patch.object(GPS, "readline", return_value=r): 169 | gps = GPS(uart=UartMock()) 170 | assert gps.update() 171 | 172 | 173 | def test_GPS_update_rmc_no_magnetic_variation(): 174 | r = b"$GPRMC,215032.086,A,1234.5678,N,00123.12345,E,0.45,56.35,021021,,,A*6A\r\n" 175 | with mock.patch.object(GPS, "readline", return_value=r): 176 | gps = GPS(uart=UartMock()) 177 | assert gps.update() 178 | exp_time = time.struct_time((2021, 10, 2, 21, 50, 32, 0, 0, -1)) 179 | assert gps.timestamp_utc == exp_time 180 | assert gps.latitude == pytest.approx(12.57613) 181 | assert gps.longitude == pytest.approx(1.385391) 182 | assert gps.latitude_degrees == 12 183 | assert gps.longitude_degrees == 1 184 | assert gps.latitude_minutes == 34.5678 185 | assert gps.longitude_minutes == 23.12345 186 | assert gps.fix_quality == 1 187 | assert gps.fix_quality_3d == 0 188 | assert gps.speed_knots == 0.45 189 | assert gps.track_angle_deg == 56.35 190 | assert gps._magnetic_variation is None 191 | assert gps._mode_indicator == "A" 192 | assert gps.has_fix is True 193 | assert gps.has_3d_fix is False 194 | assert gps.datetime == exp_time 195 | assert ( 196 | gps._raw_sentence 197 | == "$GPRMC,215032.086,A,1234.5678,N,00123.12345,E,0.45,56.35,021021,,,A*6A" 198 | ) 199 | assert ( 200 | gps.nmea_sentence 201 | == "$GPRMC,215032.086,A,1234.5678,N,00123.12345,E,0.45,56.35,021021,,,A*6A" 202 | ) 203 | 204 | 205 | def test_GPS_update_rmc_fix_is_set(): 206 | r_valid = b"$GPRMC,215032.086,A,1234.5678,N,00123.12345,E,0.45,56.35,021021,,,A*6A\r\n" 207 | r_invalid = b"$GPRMC,215032.086,V,1234.5678,N,00123.12345,E,0.45,56.35,021021,,,A*7D\r\n" 208 | with mock.patch.object(GPS, "readline", return_value=r_valid): 209 | gps = GPS(uart=UartMock()) 210 | assert gps.update() 211 | assert gps.fix_quality == 1 212 | assert gps.has_fix is True 213 | 214 | with mock.patch.object(gps, "readline", return_value=r_invalid): 215 | assert gps.update() 216 | assert gps.fix_quality == 0 217 | assert gps.has_fix is False 218 | 219 | 220 | def test_GPS_update_rmc_fix_is_set_new(): 221 | r_valid = b"$GPRMC,215032.086,A,1234.5678,N,00123.12345,E,0.45,56.35,021021,,,A*6A\r\n" 222 | r_invalid = b"$GPRMC,215032.086,V,ABC,N,00123.12345,E,0.45,56.35,021021,,,A*1B\r\n" 223 | with mock.patch.object(GPS, "readline", return_value=r_valid): 224 | gps = GPS(uart=UartMock()) 225 | assert gps.update() 226 | assert gps.fix_quality == 1 227 | assert gps.has_fix is True 228 | # now get an invalid response --> set fix_quality to 0 229 | with mock.patch.object(gps, "readline", return_value=r_invalid): 230 | assert not gps.update() 231 | assert gps.fix_quality == 0 232 | assert gps.has_fix is False 233 | 234 | 235 | def test_GPS_update_rmc_invalid_checksum(): 236 | r = b"$GPRMC,215032.086,A,1234.5678,N,00123.12345,E,0.45,56.35,021021,,,A*5C\r\n" 237 | with mock.patch.object(GPS, "readline", return_value=r): 238 | gps = GPS(uart=UartMock()) 239 | assert not gps.update() 240 | 241 | 242 | def test_GPS_update_empty_sentence(): 243 | with mock.patch.object(GPS, "readline", return_value=b""): 244 | gps = GPS(uart=UartMock()) 245 | assert not gps.update() 246 | 247 | 248 | @pytest.mark.parametrize( 249 | ("r", "exp"), 250 | ( 251 | pytest.param( 252 | b"$GPRMC,215032.086,A,1234.5678,N,00123.12345,E,0.45,56.35,021021,1234.56,W,A*14\r\n", 253 | -12.576, 254 | id="W", 255 | ), 256 | pytest.param( 257 | b"$GPRMC,215032.086,A,1234.5678,N,00123.12345,E,0.45,56.35,021021,1234.56,E,A*06\r\n", 258 | 12.576, 259 | id="E", 260 | ), 261 | ), 262 | ) 263 | def test_GPS_update_rmc_has_magnetic_variation(r, exp): 264 | with mock.patch.object(GPS, "readline", return_value=r): 265 | gps = GPS(uart=UartMock()) 266 | assert gps.update() 267 | assert gps._magnetic_variation == pytest.approx(exp) 268 | 269 | 270 | def test_parse_sentence_invalid_delimiter(): 271 | with mock.patch.object(GPS, "readline", return_value=b"a;b;c;d;12*66"): 272 | gps = GPS(uart=UartMock()) 273 | assert gps._parse_sentence() is None 274 | 275 | 276 | def test_GPS_update_sentence_is_None(): 277 | with mock.patch.object(GPS, "_parse_sentence", return_value=None): 278 | gps = GPS(uart=UartMock()) 279 | assert not gps.update() 280 | 281 | 282 | def test_GPS_update_rmc_debug_shows_sentence(capsys): 283 | r = b"$GPRMC,215032.086,A,1234.5678,N,00123.12345,E,0.45,56.35,021021,,,A*6A\r\n" 284 | with mock.patch.object(GPS, "readline", return_value=r): 285 | gps = GPS(uart=UartMock(), debug=True) 286 | assert gps.update() 287 | out, err = capsys.readouterr() 288 | assert not err 289 | assert out == "('GPRMC', '215032.086,A,1234.5678,N,00123.12345,E,0.45,56.35,021021,,,A')\n" 290 | 291 | 292 | def test_GPS_update_data_type_too_short(): 293 | r = ("GPRM", "x,y,z") 294 | with mock.patch.object(GPS, "_parse_sentence", return_value=r): 295 | gps = GPS(uart=UartMock(), debug=True) 296 | assert not gps.update() 297 | 298 | 299 | def test_GPS_send_command_with_checksum(capsys): 300 | gps = GPS(uart=UartMock()) 301 | gps.send_command(command=b"$PMTK001,314,3\r\n", add_checksum=True) 302 | out, err = capsys.readouterr() 303 | assert not err 304 | assert out == ("b'$'" "b'$PMTK001,314,3\\r\\n'" "b'*'" "b'15'" "b'\\r\\n'") 305 | 306 | 307 | def test_GPS_send_command_without_checksum(capsys): 308 | gps = GPS(uart=UartMock()) 309 | gps.send_command(command=b"$PMTK001,314,3\r\n", add_checksum=False) 310 | out, err = capsys.readouterr() 311 | assert not err 312 | assert out == ("b'$'" "b'$PMTK001,314,3\\r\\n'" "b'\\r\\n'") 313 | 314 | 315 | def test_GPS_update_from_GLL(): 316 | r = b"$GPGLL,4916.45,N,12311.12,W,225444,A,A*5c\r\n" 317 | with mock.patch.object(GPS, "readline", return_value=r): 318 | gps = GPS(uart=UartMock()) 319 | assert gps.update() 320 | exp_time = time.struct_time((0, 0, 0, 22, 54, 44, 0, 0, -1)) 321 | assert gps.timestamp_utc == exp_time 322 | assert gps.latitude == pytest.approx(49.27417) 323 | assert gps.longitude == pytest.approx(-123.1853) 324 | assert gps.latitude_degrees == 49 325 | assert gps.longitude_degrees == -123 326 | assert gps.latitude_minutes == 16.45 327 | assert gps.longitude_minutes == 11.12 328 | assert gps.isactivedata == "A" 329 | assert gps._mode_indicator == "A" 330 | assert gps.fix_quality == 0 331 | assert gps.fix_quality_3d == 0 332 | assert gps.has_fix is False 333 | assert gps.has_3d_fix is False 334 | assert gps._raw_sentence == "$GPGLL,4916.45,N,12311.12,W,225444,A,A*5c" 335 | assert gps.nmea_sentence == "$GPGLL,4916.45,N,12311.12,W,225444,A,A*5c" 336 | 337 | 338 | def test_GPS_update_from_RMC(): 339 | r = b"$GNRMC,001031.00,A,4404.1399,N,12118.8602,W,0.146,084.4,100117,,,A*5d\r\n" 340 | # TODO: length 13 and 14 version 341 | with mock.patch.object(GPS, "readline", return_value=r): 342 | gps = GPS(uart=UartMock()) 343 | assert gps.update() 344 | exp_time = time.struct_time((2017, 1, 10, 0, 10, 31, 0, 0, -1)) 345 | assert gps.timestamp_utc == exp_time 346 | assert gps.datetime == exp_time 347 | assert gps.isactivedata == "A" 348 | assert gps.fix_quality == 1 349 | assert gps.has_fix is True 350 | assert gps.has_3d_fix is False 351 | assert gps.latitude == pytest.approx(44.069) 352 | assert gps.longitude == pytest.approx(-121.3143) 353 | assert gps.latitude_degrees == 44 354 | assert gps.longitude_degrees == -121 355 | assert gps.latitude_minutes == 4.1399 356 | assert gps.longitude_minutes == 18.8602 357 | assert gps.speed_knots == 0.146 358 | assert gps.track_angle_deg == 84.4 359 | assert gps._magnetic_variation is None 360 | assert gps._mode_indicator == "A" 361 | 362 | 363 | def test_GPS_update_from_GGA(): 364 | r = b"$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n" 365 | with mock.patch.object(GPS, "readline", return_value=r): 366 | gps = GPS(uart=UartMock()) 367 | assert gps.update() 368 | exp_time = time.struct_time((0, 0, 0, 12, 35, 19, 0, 0, -1)) 369 | assert gps.timestamp_utc == exp_time 370 | assert gps.latitude == pytest.approx(48.1173) 371 | assert gps.longitude == pytest.approx(11.51667) 372 | assert gps.latitude_degrees == 48 373 | assert gps.longitude_degrees == 11 374 | assert gps.latitude_minutes == 7.038 375 | assert gps.longitude_minutes == 31.000 376 | assert gps.fix_quality == 1 377 | assert gps.fix_quality_3d == 0 378 | assert gps.satellites == 8 379 | assert gps.horizontal_dilution == 0.9 380 | assert gps.altitude_m == 545.4 381 | assert gps.height_geoid == 46.9 382 | assert gps.has_fix is True 383 | assert gps.has_3d_fix is False 384 | assert gps.datetime == exp_time 385 | assert ( 386 | gps._raw_sentence == "$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47" 387 | ) 388 | assert ( 389 | gps.nmea_sentence == "$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47" 390 | ) 391 | 392 | 393 | @pytest.mark.parametrize( 394 | "r", 395 | ( 396 | pytest.param(b"$GPGSA,A,3,15,18,14,,,31,,,23,,,,04.5,02.1,04.0*0f\r\n", id="smaller v4.1"), 397 | pytest.param( 398 | b"$GPGSA,A,3,15,18,14,,,31,,,23,,,,04.5,02.1,04.0,3*10\r\n", 399 | id="greater v4.1", 400 | ), 401 | ), 402 | ) 403 | def test_GPS_update_from_GSA(r): 404 | with mock.patch.object(GPS, "readline", return_value=r): 405 | gps = GPS(uart=UartMock()) 406 | assert gps.update() 407 | assert gps.sel_mode == "A" 408 | assert gps.fix_quality_3d == 3 409 | # assert gps.has_fix is True # TODO: shouldn't this be True? 410 | assert gps.has_3d_fix is True 411 | assert gps.sat_prns == ["GP15", "GP18", "GP14", "GP31", "GP23"] 412 | assert gps.pdop == 4.5 413 | assert gps.hdop == 2.1 414 | assert gps.vdop == 4.0 415 | 416 | 417 | def test_GPS_update_from_GSV_first_part(): 418 | r = b"$GPGSV,2,1,08,01,40,083,46,02,17,308,41,12,07,344,39,14,22,228,45*75\r\n" 419 | with mock.patch.object(GPS, "readline", return_value=r): 420 | gps = GPS(uart=UartMock()) 421 | assert gps.update() 422 | assert gps.total_mess_num == 2 423 | assert gps.mess_num == 1 424 | assert gps.satellites == 8 425 | # check two satellites, without timestamp, since it is dynamic 426 | sats = gps._sats 427 | assert sats[0][:-1] == ("GP1", 40, 83, 46) 428 | assert sats[-1][:-1] == ("GP14", 22, 228, 45) 429 | 430 | # check at least that timestamp is there 431 | assert isinstance(sats[0][4], float) 432 | assert isinstance(sats[-1][4], float) 433 | assert ( 434 | gps._raw_sentence 435 | == "$GPGSV,2,1,08,01,40,083,46,02,17,308,41,12,07,344,39,14,22,228,45*75" 436 | ) 437 | assert ( 438 | gps.nmea_sentence 439 | == "$GPGSV,2,1,08,01,40,083,46,02,17,308,41,12,07,344,39,14,22,228,45*75" 440 | ) 441 | 442 | 443 | def test_GPS_update_from_GSV_both_parts_sats_are_removed(): 444 | gps = GPS(uart=UartMock()) 445 | with mock.patch.object(GPS, "readline") as m: 446 | with freeze_time("2021-10-20 19:00:00"): 447 | # first part of the request 448 | m.return_value = b"$GPGSV,2,1,04,01,40,083,46,02,17,308,41*78\r\n" 449 | assert gps.update() 450 | assert gps.total_mess_num == 2 451 | assert gps.mess_num == 1 452 | assert gps.satellites == 4 453 | # first time we received satellites, so this must be None 454 | assert gps.sats is None 455 | # some time has passed so the first two satellites will be too old, but 456 | # this one not 457 | with freeze_time("2021-10-20 19:00:20"): 458 | # second part of the request 459 | m.return_value = b"$GPGSV,2,2,04,12,07,344,39,14,22,228,45*7c\r\n" 460 | assert gps.update() 461 | assert gps.total_mess_num == 2 462 | assert gps.mess_num == 2 463 | assert gps.satellites == 4 464 | # we should now have 4 satellites from the two part request 465 | assert set(gps.sats.keys()) == {"GP1", "GP2", "GP12", "GP14"} 466 | 467 | # time passed (more than 30 seconds) and the next request does not 468 | # contain the previously seen satellites but two new ones 469 | with freeze_time("2021-10-20 19:00:31"): 470 | # a third, one part request 471 | m.return_value = b"$GPGSV,1,1,02,13,07,344,39,15,22,228,45*7a\r\n" 472 | assert gps.update() 473 | assert gps.satellites == 2 474 | assert set(gps.sats.keys()) == {"GP12", "GP14", "GP13", "GP15"} 475 | 476 | 477 | @pytest.mark.parametrize( 478 | ("input_str", "exp", "neg"), 479 | ( 480 | (["3723.2475", "n"], (37, 23.2475), "s"), 481 | (["3723.2475", "s"], (-37, 23.2475), "s"), 482 | (["00123.1234", "e"], (1, 23.1234), "w"), 483 | (["00123", "e"], (1, 23), "w"), 484 | (["1234.5678", "e"], (12, 34.5678), "w"), 485 | (["3723.2475123", "n"], (37, 23.2475123), "s"), 486 | (["3723", "n"], (37, 23), "s"), 487 | ), 488 | ) 489 | def test_read_min_secs(input_str, exp, neg): 490 | assert _read_deg_mins(data=input_str, index=0, neg=neg) == exp 491 | --------------------------------------------------------------------------------