pax_global_header00006660000000000000000000000064152364035420014516gustar00rootroot0000000000000052 comment=4623f7c8c588701d629cdf8d6de24c1e6b973ef4 pynmeagps-1.1.7/000077500000000000000000000000001523640354200135275ustar00rootroot00000000000000pynmeagps-1.1.7/.github/000077500000000000000000000000001523640354200150675ustar00rootroot00000000000000pynmeagps-1.1.7/.github/FUNDING.yml000066400000000000000000000000401523640354200166760ustar00rootroot00000000000000buy_me_a_coffee: semuconsulting pynmeagps-1.1.7/.github/ISSUE_TEMPLATE/000077500000000000000000000000001523640354200172525ustar00rootroot00000000000000pynmeagps-1.1.7/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000027571523640354200217570ustar00rootroot00000000000000--- name: pynmeagps Bug Report about: Create a report to help us improve title: '' labels: '' assignees: semuadmin --- **Describe the bug** A clear and concise description of what the bug is. Please include: 1. The pynmeagps version (`>>> pynmeagps.version`) 2. The **complete** Python script. Embed your code here (please do *NOT* attach *.py, *.zip, *.tgz or other executable / zipped files) ... ```python your code here ``` 3. The error message and full traceback. 4. A binary / hexadecimal dump of the NMEA/UBX data stream. **To Reproduce** Steps to reproduce the behaviour: 1. Any relevant device configuration (if other than factory defaults). 2. Any causal UBX command input(s). **Expected Behaviour** A clear and concise description of what you expected to happen. **Desktop (please complete the following information):** - The operating system you're using [e.g. Windows 11, MacOS Sequoia, Ubuntu Noble]. - The type of serial connection [e.g. USB, UART1, I2C]. **GNSS/GPS Device (please complete the following information as best you can):** - Device Model/Generation: [e.g. u-blox NEO-9M]. - Firmware Version: [e.g. SPG 4.03]. - Protocol: [e.g. 32.00]. This information is typically output by the device at startup via a series of NMEA TXT messages. It can also be found by polling the device with a UBX MON-VER message. If you're using the PyGPSClient GUI, a screenshot of the UBXConfig window should suffice. **Additional context** Add any other context about the problem here. pynmeagps-1.1.7/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000003031523640354200212360ustar00rootroot00000000000000blank_issues_enabled: false contact_links: - name: pynmeagps Community Support url: https://github.com/semuconsulting/pynmeagps/discussions about: Please ask and answer questions here. pynmeagps-1.1.7/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000014671523640354200230070ustar00rootroot00000000000000--- name: pynmeagps Feature request about: Suggest an idea for this project title: '' labels: '' assignees: semuadmin --- # pynmeagps Feature Request Template **NB**: Please raise any feature suggestions or queries in the [pynmeagps Discussions Channels](https://github.com/semuconsulting/pynmeagps/discussions) in the first instance. **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is e.g. I'd like to be able to do this [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context about the feature request here. pynmeagps-1.1.7/.github/pull_request_template.md000066400000000000000000000030531523640354200220310ustar00rootroot00000000000000# pynmeagps Pull Request Template ## Description Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. Fixes # (issue) ## Testing Please test all changes, however trivial, against the supplied pytest suite `tests/test_*.py`. Please describe any test cases you have amended or added to this suite to maintain >= 99% code coverage. - [ ] Test A - [ ] Test B ## Checklist: - [ ] I agree to abide by the code of conduct (see [CODE_OF_CONDUCT.md](https://github.com/semuconsulting/pynmeagps/blob/master/CODE_OF_CONDUCT.md)). - [ ] My code follows the style guidelines of this project (see [CONTRIBUTING.md](https://github.com/semuconsulting/pynmeagps/blob/master/CONTRIBUTING.md)). - [ ] I have performed a self-review of my own code. - [ ] I have commented my code, particularly in hard-to-understand areas. - [ ] I have made corresponding changes to the documentation. - [ ] (*if appropriate*) I have added test cases to the `tests/test_*.py` unittest suite to maintain >= 99% code coverage. - [ ] I have tested my code against the full `tests/test_*.py` unittest suite. - [ ] My changes generate no new warnings. - [ ] Any dependent changes have been merged and published in downstream modules. - [ ] I have [signed](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits) my commits. - [ ] I understand and acknowledge that the code will be published under a BSD 3-Clause license. pynmeagps-1.1.7/.github/workflows/000077500000000000000000000000001523640354200171245ustar00rootroot00000000000000pynmeagps-1.1.7/.github/workflows/checkpr.yml000066400000000000000000000013541523640354200212710ustar00rootroot00000000000000name: checkpr on: pull_request: jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: [ "3.10", "3.11", "3.12", "3.13", "3.14" ] steps: - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install deploy dependencies run: pip install --group deploy - name: Install code dependencies run: pip install . - name: Lint with pylint run: pylint -E src - name: Scan security vulnerabilities with bandit run: bandit -c pyproject.toml -r . - name: Generate coverage report run: pytest pynmeagps-1.1.7/.github/workflows/deploy.yml000066400000000000000000000021261523640354200211440ustar00rootroot00000000000000name: deploy on: release: # when new release created types: [published] permissions: contents: read jobs: release-build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 with: python-version: 3.13 - name: Build release distributions run: | python -m pip install build python -m build - name: Upload distributions # upload build artifacts uses: actions/upload-artifact@v7 with: name: release-dists path: dist/ release-publish: needs: - release-build name: Upload release to PyPI runs-on: ubuntu-latest environment: name: pypi permissions: id-token: write steps: - name: Download all the dists # download build artifacts saved in previous job uses: actions/download-artifact@v8 with: name: release-dists path: dist/ - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 pynmeagps-1.1.7/.github/workflows/main.yml000066400000000000000000000014031523640354200205710ustar00rootroot00000000000000name: pynmeagps on: push: branches: - master jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install deploy dependencies run: pip install --group deploy - name: Install code dependencies run: pip install . - name: Lint with pylint run: pylint -E src - name: Security vulnerability analysis with bandit run: bandit -c pyproject.toml -r . - name: Generate coverage report run: pytest pynmeagps-1.1.7/.gitignore000066400000000000000000000004661523640354200155250ustar00rootroot00000000000000.DS_Store .dropbox .project .coverage .pydevproject *.code-workspace Pipfile*.* /.settings /htmlcov /build /dist /docs/_build /references /pynmeagps_* /pynmeagps-* src/pynmeagps.egg* /__pycache__ /.dbeaver /pynmeagps/__pycache__ *.pyc /examples/temp*.* /.pytest_cache/ pylint_report.txt .venv /conda setup.cfg pynmeagps-1.1.7/.vscode/000077500000000000000000000000001523640354200150705ustar00rootroot00000000000000pynmeagps-1.1.7/.vscode/settings.json000066400000000000000000000026371523640354200176330ustar00rootroot00000000000000{ // These test, build and deploy tasks are targeted at a venv // called 'pygpsclient' located in the user's home directory. // Set your workspace default VSCode Python interpreter to // this venv, i.e. // "${userHome}/pygpsclient/bin/python3" on linux/macos // "${userHome}/pygpsclient/Scripts/python" on windows "python.testing.pytestEnabled": false, "python.terminal.activateEnvironment": true, "editor.formatOnSave": true, "modulename": "${workspaceFolderBasename}", "distname": "${workspaceFolderBasename}", "venv": "${userHome}/pygpsclient", "python.testing.pytestArgs": [ "tests" ], "C_Cpp.copilotHover": "disabled", "chat.agent.enabled": false, "chat.commandCenter.enabled": false, "chat.notifyWindowOnConfirmation": false, "telemetry.feedback.enabled": false, "python.analysis.addHoverSummaries": false, "python-envs.defaultEnvManager": "ms-python.python:venv", "python-envs.pythonProjects": [ { "path": ".", "envManager": "ms-python.python:venv", "packageManager": "ms-python.python:pip" } ], "python.defaultInterpreterPath": "${userHome}/pygpsclient/bin/python3", "python.testing.unittestEnabled": true, "python.testing.unittestArgs": [ "-v", "-s", "./tests", "-p", "test_*.py" ] }pynmeagps-1.1.7/.vscode/tasks.json000066400000000000000000000203341523640354200171120ustar00rootroot00000000000000{ // pynmeagps VSCode workflow. // // These build, install and test tasks are targeted at a venv // called 'pygpsclient' located in the user's home directory. // Set your workspace default VSCode Python interpreter to // this venv, i.e. // "${userHome}/pygpsclient/bin/python3" on Linux/MacOS // "C:/Users/user/pygpsclient/Scripts/python.exe" on Windows // // The "Test" task will initialise the venv and execute all // necessary build and installation precursors. // "version": "2.0.0", "tasks": [ { "label": "Delete Venv", "type": "shell", "command": "rm", "args": [ "-rf", "${config:venv}", ], "problemMatcher": [] }, { "label": "Create Venv", "type": "shell", "command": "${config:python.systemPath}", "args": [ "-m", "venv", "${config:venv}", ], "problemMatcher": [], "dependsOrder": "sequence", "dependsOn": [ "Delete Venv", ], }, { "label": "Activate Venv", "type": "shell", "command": "source", "args": [ "~/pygpsclient/bin/activate", ], "problemMatcher": [], }, { "label": "Install Deploy Dependencies", "type": "shell", "command": "${config:python.defaultInterpreterPath}", "args": [ "-m", "pip", "install", "--upgrade", "--group", "deploy" ], "problemMatcher": [], }, { "label": "Install Optional Dependencies", "type": "shell", "command": "${config:python.defaultInterpreterPath}", "args": [ "-m", "pip", "install", "--upgrade", "--group", "optional" ], "problemMatcher": [], }, { "label": "Clean", "type": "shell", "command": "rm", "args": [ "-rfvd", "build", "dist", "htmlcov", "docs/_build", "${config:modulename}.egg-info", ], "windows": { "command": "Remove-Item", "args": [ "-Recurse", "-Force", "-ErrorAction", "SilentlyContinue", // doesn't work! - stops on exit code 1 anyway "-Path", "'build','dist','htmlcov','docs/_build','src/${config:modulename}.egg-info'", ] }, "options": { "cwd": "${workspaceFolder}" }, "problemMatcher": [] }, { "label": "Sort Imports", "type": "shell", "command": "${config:python.defaultInterpreterPath}", "args": [ "-m", "isort", "src", "--jobs", "-1" ], "problemMatcher": [] }, { "label": "Format", "type": "shell", "command": "${config:python.defaultInterpreterPath}", "args": [ "-m", "black", "src" ], "problemMatcher": [] }, { "label": "Pylint", "type": "process", "command": "${config:python.defaultInterpreterPath}", "args": [ "-m", "pylint", "src" ], "problemMatcher": [] }, { "label": "Security", "type": "process", "command": "bandit", "args": [ "-c", "pyproject.toml", "-r", "src/pygpsclient" ], "problemMatcher": [] }, { "label": "Build", "type": "shell", "command": "${config:python.defaultInterpreterPath}", "args": [ "-m", "build", ".", "--wheel", "--sdist", ], "problemMatcher": [], "dependsOrder": "sequence", "dependsOn": [ //"Create Venv", "Install Deploy Dependencies", "Install Optional Dependencies", "Clean", "Sort Imports", "Format", ], "group": { "kind": "build", "isDefault": true } }, { "label": "Install", // into Virtual Environment "type": "shell", "command": "${config:python.defaultInterpreterPath}", "args": [ "-m", "pip", "install", "--force-reinstall", // wildcard only works on Posix platforms "${workspaceFolder}/dist/pynmeagps-*-py3-none-any.whl", ], "windows": { "command": "${config:python.defaultInterpreterPath}", "args": [ "-m", "pip", "install", "--force-reinstall", "${workspaceFolder}/dist/pyneamgps-1.1.1-py3-none-any.whl", ] }, "problemMatcher": [], }, { "label": "Test", "type": "shell", "command": "${config:python.defaultInterpreterPath}", "args": [ "-m", "pytest" ], "problemMatcher": [], "dependsOrder": "sequence", "dependsOn": [ "Activate Venv", "Build", "Install", // have to install before running pylint "Pylint", "Security", "Sphinx HTML", ], "group": { "kind": "test", "isDefault": true } }, { "label": "Sphinx", "type": "shell", "command": "sphinx-apidoc", "args": [ "--ext-autodoc", "--ext-viewcode", "--templatedir=docs", "-f", "-o", "docs", "src/${config:modulename}" ], "problemMatcher": [] }, { "label": "Sphinx HTML", "type": "shell", "command": "/usr/bin/make", "windows": { "command": "${workspaceFolder}/docs/make.bat" }, "args": [ "html" ], "options": { "cwd": "${workspaceFolder}/docs" }, "dependsOrder": "sequence", "dependsOn": [ "Sphinx" ], "problemMatcher": [] }, { "label": "Sphinx Deploy", // needs AWS S3 credentials "type": "shell", "command": "aws", "args": [ "s3", "cp", "${workspaceFolder}/docs/_build/html", "s3://www.semuconsulting.com/${config:modulename}/", "--recursive" ], "dependsOrder": "sequence", "dependsOn": [ "Sphinx HTML" ], "problemMatcher": [] }, { "label": "Run from Source", "type": "shell", "command": "python3", "args": [ "-m", "pygpsclient", //"--verbosity", //"3" ], "options": { "cwd": "${workspaceFolder}/src" }, "problemMatcher": [] }, ] }pynmeagps-1.1.7/CITATION.bib000066400000000000000000000005071523640354200154210ustar00rootroot00000000000000@Misc{pynmeagps, author = {{SEMU Consulting}}, howpublished = {GitHub repository}, note = {Viewed last: xxxx:xx:xx}, title = {Python library for parsing and generating NMEA 0183 v4 protocol GNSS messages.}, year = {2021}, url = {https://github.com/semuconsulting/pynmeagps}, } pynmeagps-1.1.7/CODE_OF_CONDUCT.md000066400000000000000000000064401523640354200163320ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at semuadmin@semuconsulting.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq pynmeagps-1.1.7/CONTRIBUTING.md000066400000000000000000000065171523640354200157710ustar00rootroot00000000000000# pynmeagps How to contribute **pynmeagps** is a volunteer project and we appreciate any contribution, from fixing a grammar mistake in a comment to extending device test coverage or implementing new functionality. Please read this section if you are contributing your work. If you're intending to make significant changes, please raise them in the [Discussions Channel](https://github.com/semuconsulting/pynmeagps/discussions/categories/ideas) beforehand. Being one of our contributors, you agree and confirm that: * The work is all your own. For the avoidance of doubt, this means **no AI coding agents such as Copilot**. * Your work will be distributed under a BSD 3-Clause License once your pull request is merged. * You submitted work fulfils or mostly fulfils our coding conventions, styles and standards. Please help us keep our issue list small by adding fixes: #{$ISSUE_NO} to the commit message of pull requests that resolve open issues. GitHub will use this tag to auto close the issue when the PR is merged. ## Coding conventions * This is open source software. Code should be as simple and transparent as possible. Favour clarity over brevity. * The code should be compatible with Python >= 3.10. * The core code should be as generic and reusable as possible. We endeavour to limit the amount of processing dedicated to specific UBX message types, though this is sometimes unavoidable. * Avoid external library dependencies unless there's a compelling reason not to. * We use and recommend [Visual Studio Code](https://code.visualstudio.com/) with the [Python Extension](https://marketplace.visualstudio.com/items?itemName=ms-python.python) for development and testing. * Code should be documented in accordance with [Sphinx](https://www.sphinx-doc.org/en/master/) docstring conventions. * Code should formatted using [black](https://pypi.org/project/black/). * We use and recommend [pylint](https://pypi.org/project/pylint/) for code analysis. * We use and recommend [bandit](https://pypi.org/project/bandit/) for security vulnerability analysis. * Commits must be [signed](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits). ## Testing We use Python's native unittest framework for local unit testing, complemented by the GitHub Actions automated build and testing workflow. We endeavour to have >98% code coverage. Please write unittest examples for new code you create and add them to the `/tests` folder following the naming convention `test_*.py`. ## Submitting changes Please send a [GitHub Pull Request to pynmeagps](https://github.com/semuconsulting/pynmeagps/pulls) with a clear list of what you've done (read more about [pull requests](https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/about-pull-requests)). Please follow our coding conventions (above) and make sure all of your commits are atomic (one feature per commit). Please sign all commits - see [Signing GitHub Commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits) for instructions. Always write a clear log message for your commits. One-line messages are fine for small changes, but bigger changes should look like this: $ git commit -m "A brief summary of the commit > > A paragraph describing what changed and its impact." Thanks, semuadminpynmeagps-1.1.7/LICENSE000066400000000000000000000031251523640354200145350ustar00rootroot00000000000000BSD 3-Clause License ("BSD License 2.0", "Revised BSD License", "New BSD License", or "Modified BSD License") Copyright (c) 2020, semuadmin (Steve Smith) All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. pynmeagps-1.1.7/README.md000066400000000000000000000500111523640354200150030ustar00rootroot00000000000000pynmeagps ========= [Current Status](#currentstatus) | [Installation](#installation) | [Reading](#reading) | [Parsing](#parsing) | [Generating](#generating) | [Serializing](#serializing) | [Utilities](#utilities) | [Examples](#examples) | [Extensibility](#extensibility) | [Command Line Utility](#cli) | [Graphical Client](#gui) | [Author & License](#author) `pynmeagps` is an original Python 3 parser aimed *primarily* at the subset of the NMEA 0183 © v4 protocol relevant to GNSS/GPS receivers. The intention is to make it as easy as possible to read, parse and utilise NMEA GNSS/GPS messages in Python applications. The `pynmeagps` homepage is located at [https://github.com/semuconsulting/pynmeagps](https://github.com/semuconsulting/pynmeagps). Companion libraries are available which handle UBX © and RTCM3 © messages: - [pyubx2](http://github.com/semuconsulting/pyubx2) (installing `pyubx2` via pip also installs `pynmeagps` and `pyrtcm`) - [pyrtcm](http:/github.com/semuconsulting/pyrtcm) --- ## Current Status ![Status](https://img.shields.io/pypi/status/pynmeagps) ![Release](https://img.shields.io/github/v/release/semuconsulting/pynmeagps?include_prereleases) ![Build](https://img.shields.io/github/actions/workflow/status/semuconsulting/pynmeagps/main.yml?branch=master) ![Coverage](https://github.com/semuconsulting/pynmeagps/blob/master/images/coverage.svg?raw=true) ![Release Date](https://img.shields.io/github/release-date-pre/semuconsulting/pynmeagps) ![Last Commit](https://img.shields.io/github/last-commit/semuconsulting/pynmeagps) ![Contributors](https://img.shields.io/github/contributors/semuconsulting/pynmeagps.svg) ![Open Issues](https://img.shields.io/github/issues-raw/semuconsulting/pynmeagps) The library implements a comprehensive set of outbound (GET) and inbound (SET/POLL) GNSS NMEA messages relating to GNSS/GPS and Maritime devices, but is readily [extensible](#extensibility). Refer to [`NMEA_MSGIDS`](https://github.com/semuconsulting/pynmeagps/blob/master/src/pynmeagps/nmeatypes_core.py#L224) and [`NMEA_MSGIDS_PROP`](https://github.com/semuconsulting/pynmeagps/blob/master/src/pynmeagps/nmeatypes_core.py#L367) for the complete dictionary of standard and proprietary messages currently supported. The library also supports [user-defined NMEA message definitions](#userdef) (e.g. for product development). While the [NMEA 0183 protocol itself is proprietary](https://www.nmea.org/nmea-0183.html), the definitions here have been collated from public domain sources. Sphinx API Documentation in HTML format is available at [https://www.semuconsulting.com/pynmeagps/](https://www.semuconsulting.com/pynmeagps/). Contributions _**from human beings**_ welcome - please refer to [CONTRIBUTING.MD](https://github.com/semuconsulting/pynmeagps/blob/master/CONTRIBUTING.md). [Bug reports](https://github.com/semuconsulting/pynmeagps/blob/master/.github/ISSUE_TEMPLATE/bug_report.md) and [Feature requests](https://github.com/semuconsulting/pynmeagps/blob/master/.github/ISSUE_TEMPLATE/feature_request.md) - please use the templates provided. For general queries and advice, post a message to one of the [pynmeagps Discussions](https://github.com/semuconsulting/pynmeagps/discussions) channels. ![No Copilot](https://github.com/semuconsulting/PyGPSClient/blob/master/images/nocopilot100.png?raw=true) --- ## Installation ![Python version](https://img.shields.io/pypi/pyversions/pynmeagps.svg?style=flat) [![PyPI version](https://img.shields.io/pypi/v/pynmeagps.svg?style=flat)](https://pypi.org/project/pynmeagps/) [![PyPI downloads](https://github.com/semuconsulting/pynmeagps/blob/master/images/clickpy_top01.svg?raw=true)](https://clickpy.clickhouse.com/dashboard/pynmeagps) `pynmeagps` is compatible with Python>=3.10. In the following, `python3` & `pip` refer to the Python 3 executables. You may need to substitute `python` for `python3`, depending on your particular environment (*on Windows it's generally `python`*). The recommended way to install the latest version of `pynmeagps` is with [pip](http://pypi.python.org/pypi/pip/): ```shell python3 -m pip install --upgrade pynmeagps ``` If required, `pynmeagps` can also be installed into a virtual environment, e.g.: ```shell python3 -m venv env source env/bin/activate # (or env\Scripts\activate on Windows) python3 -m pip install --upgrade pynmeagps ``` For [Conda](https://docs.conda.io/en/latest/) users, `pynmeagps` is also available from [conda forge](https://github.com/conda-forge/pynmeagps-feedstock): [![Anaconda-Server Badge](https://anaconda.org/conda-forge/pynmeagps/badges/version.svg)](https://anaconda.org/conda-forge/pynmeagps) [![Anaconda-Server Badge](https://img.shields.io/conda/dn/conda-forge/pynmeagps)](https://anaconda.org/conda-forge/pynmeagps) ```shell conda install -c conda-forge pynmeagps ``` --- ## Reading (Streaming) ``` class pynmeagps.nmeareader.NMEAReader(stream, **kwargs) ``` You can create an `NMEAReader` object by calling the constructor with an active stream object. The stream object can be any data stream which supports a `read(n) -> bytes` method (e.g. File or Serial, with or without a buffer wrapper). `pynmeagps` implements an internal `SocketWrapper` class to allow sockets to be read in the same way as other streams (see example below). Individual input NMEA messages can then be read using the `NMEAReader.read()` function, which returns both the raw data (as bytes) and the parsed data (as an `NMEAMessage` object, via the `parse()` method). The function is thread-safe in so far as the incoming data stream object is thread-safe. `NMEAReader` also implements an iterator. The constructor accepts the following optional keyword arguments: * `msgmode`: 0 = GET (default, i.e. output _from_ receiver), 1 = SET (i.e. input _to_ receiver), 2 = POLL (i.e. query _to_ receiver in anticipation of response back) * `nmeaonly`: True = raise error if stream contains non-NMEA data, False = ignore non-NMEA data (default) * `validate`: validation flags `VALCKSUM` (0x01) = validate checksum (default), `VALMSGID` (0x02) = validate msgId (i.e. raise error if unknown NMEA message is received) * `quitonerror`: `ERR_IGNORE` (0) = ignore errors, `ERR_LOG` (1) = log continue, `ERR_RAISE` (2) = (re)raise (1) * `userdefined`: An optional user-defined payload definition dictionary, supplementing the existing `NMEA_PAYLOADS_GET` and `NMEA_PAYLOADS_GET_PROP` dictionaries (None). * `encoding`: optional encoding for socket stream input, 0 = none, 1 = chunk, 2 = gzip, 4 = compress, 8 = deflate (can be OR'd) (0) Examples: * Serial input - this example will ignore any non-NMEA data. ```python from serial import Serial from pynmeagps import NMEAReader with Serial('/dev/tty.usbmodem14101', 9600, timeout=3) as stream: nmr = NMEAReader(stream) raw_data, parsed_data = nmr.read() if parsed_data is not None: print(parsed_data) ``` * File input (using iterator) - this example will produce a `NMEAStreamError` if non-NMEA data is encountered. ```python from pynmeagps import NMEAReader with open('nmeadata.log', 'rb') as stream: nmr = NMEAReader(stream, nmeaonly=True) for raw_data, parsed_data in nmr: print(parsed_data) ``` * Socket input (using iterator): ```python import socket from pynmeagps import NMEAReader with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as stream: stream.connect(("localhost", 50007)) nmr = NMEAReader(stream) for raw_data, parsed_data in nmr: print(parsed_data) ``` * User-defined NMEA message definition dictionary: ```python from pynmeagps import NMEAReader, DE, CH NMEA_PROD_DEVEL = { "XX1": { "roll": DE, "pitch": DE, "yaw": DE, "status": CH, } } with open('testfile.log', 'rb') as stream: nmr = NMEAReader(stream, userdefined=NMEA_PROD_DEVEL) for raw_data, parsed_data in nmr: print(parsed_data) ``` ``` ``` --- ## Parsing You can parse individual NMEA messages using the static `NMEAReader.parse(message)` function, which takes a string or bytes containing an NMEA message and returns an `NMEAMessage` object. Note that latitude and longitude are parsed as signed decimal values for ease of use. Helper methods `latlon2dms` and `latlon2dmm` are available to convert decimal degrees to d°m′s.s″ or d°m.m′ display format. Attributes within repeating groups are parsed with a two-digit suffix (svid_01, svid_02, etc.). The `parse()` function accepts the following optional keyword arguments: * `msgmode`: 0 = GET (default), 1 = SET, 2 = POLL * `validate`: validation flags `VALCKSUM` (0x01) = validate checksum (default), `VALMSGID` (0x02) = validate msgId (i.e. raise error if unknown NMEA message is received) * `quitonerror`: `ERR_IGNORE` (0) = ignore errors, `ERR_LOG` (1) = log continue, `ERR_RAISE` (2) = (re)raise (1) * `userdefined`: An optional user-defined payload definition dictionary, supplementing the existing `NMEA_PAYLOADS_GET` and `NMEA_PAYLOADS_GET_PROP` dictionaries (None). Example: ```python from pynmeagps import NMEAReader msg = NMEAReader.parse('$GNGLL,5327.04319,S,00214.41396,E,223232.00,A,A*68\r\n') print(msg) ``` ``` ``` The `NMEAMessage` object exposes different public attributes depending on its message ID, e.g. the `RMC` message has the following attributes: ```python from pynmeagps import latlon2dms, latlon2dmm print(msg) print(msg.msgID) print(msg.lat, msg.lon) print(msg.spd) print(latlon2dms((msg.lat, msg.lon))) print(latlon2dmm((msg.lat, msg.lon))) ``` ``` 'RMC' (52.62063, -2.16012) 37.84 ('52°37′14.268″N', '2°9′36.432″W') ('52°37.2378′N', '2°9.6072′W') ``` If the NMEA sentence type is unrecognised or not yet implemented (*e.g. due to definition not yet being in the public domain*) and the `VALMSGID` validation flag is *NOT* set, `NMEAMessage` will parse the message to a NOMINAL structure e.g.: ```python from pynmeagps import NMEAReader, VALCKSUM msg = NMEAReader.parse('$GNACN,103607.00,ECN,E,A,W,A,test,C*67\r\n', validate=VALCKSUM) print(msg) ``` ``` ``` --- ## Generating ``` class pynmeagps.nmeamessage.NMEAMessage(talker: str, msgID: str, msgmode: int, **kwargs) ``` You can create an `NMEAMessage` object by calling the constructor with the following parameters: 1. talker (must be a valid talker from `pynmeagps.NMEA_TALKERS`) 1. message id (must be a valid id from `pynmeagps.NMEA_MSGIDS` or `pynmeagps.NMEA_MSGIDS_PROP`) 1. msgmode (0=GET, 1=SET, 2=POLL) 1. hpnmeamode - boolean flag to signify high-precision NMEA mode (7 dp rather than 5) (False) 1. validate - integer flag for checksum and/or message type validation (0=VALNONE, 1=VALCKSUM, 2=VALMSGID) (1) 1. userdefined - an optional user-defined payload definition dictionary (None) 1. (optional) a series of keyword parameters representing the message payload The 'msgmode' parameter signifies whether the message payload refers to a: * GET message (i.e. output from the receiver - NB these would normally be generated via the NMEAReader.read() or NMEAReader.parse() methods but can also be created manually) * SET message (i.e. command input to the receiver) * POLL message (i.e. query input to the receiver in anticipation of a response back) The message payload can be defined via keyword arguments in one of two ways: 1. A single keyword parameter of `payload` containing the full payload as a list of string values (any other keyword parameters will be ignored). 2. One or more keyword parameters corresponding to individual message attributes. Any attributes not explicitly provided as keyword parameters will be set to a nominal value according to their type. For position messages, the `NS` or `EW` values will be derived from the sign of the `lat` or `lon` values and need not be provided, e.g. if `lat` = -32.4, `NS` will be "S", if `lon` = -1.34, `EW` will be "W" (_any provided `NS` or `EW` values will be overridden accordingly_). e.g. Create a GLL message, passing the entire payload as a list of strings in native NMEA format: ```python from pynmeagps import NMEAMessage, GET pyld=['4330.00000','N','00245.000000','W','120425.234','A','A'] msg = NMEAMessage('GN', 'GLL', GET, payload=pyld) print(msg) ``` ``` ``` e.g. Create GLL (GET) and GNQ (POLL) message, passing individual typed values as keywords, with any omitted keywords defaulting to nominal values (in the GLL example, the 'time' parameter has been omitted and has defaulted to the current time): ```python from pynmeagps import NMEAMessage, GET msg = NMEAMessage('GN', 'GLL', GET, lat=43.5, lon=-2.75, status='A', posMode='A') print(msg) ``` ``` ``` ```python from pynmeagps import NMEAMessage, POLL msg = NMEAMessage('EI', 'GNQ', POLL, msgId='RMC') print(msg) ``` ``` ``` By default, NMEA position message payloads store lat/lon to 5dp of minutes (i.e. (d)ddmm.mmmmm). An optional boolean keyword argument `hpnmeamode` increases this to 7dp (i.e. (d)ddmm.mmmmmmm) when set to True, e.g. ```python from pynmeagps import NMEAMessage, GET msgsp = NMEAMessage('GN', 'GLL', GET, lat=43.123456789, lon=-2.987654321, status='A', posMode='A', hpnmeamode=0) # standard precision print(msgsp) msghp = NMEAMessage('GN', 'GLL', GET, lat=-43.123456789, lon=2.987654321, status='A', posMode='A', hpnmeamode=1) # high precision print(msghp) ``` ``` NMEAMessage('GN','GLL', 0, payload=['4307.40741', 'N', '00259.25926', 'W', '095045.78', 'A', 'A']) NMEAMessage('GN','GLL', 0, payload=['4307.4074073', 'S', '00259.2592593', 'E', '094824.88', 'A', 'A']) ``` **NB:** Once instantiated, an `NMEAMessage` object is immutable. See [pynmeagps._usage.py](https://github.com/semuconsulting/pynmeagps/blob/master/examples/pynmeagps_usage.py) for further examples. --- ## Serializing The `NMEAMessage` class implements a `serialize()` method to convert an `NMEAMessage` object to a bytes array suitable for writing to an output stream. ```python from serial import Serial from pynmeagps import NMEAMessage, POLL stream = Serial('COM6', 38400, timeout=3) msg = NMEAMessage('EI','GNQ', POLL, msgId='RMC') print(msg.serialize()) stream.write(msg.serialize()) ``` ``` b'$EIGNQ,RMC*24\r\n' ``` --- ## Utility Methods `pynmeagps` provides the following utility methods: - `latlon2dms` - converts decimal lat/lon to degrees, minutes, decimal seconds format e.g. "53°20′45.6″N", "2°32′46.68″W" - `latlon2dmm` - converts decimal lat/lon to degrees, decimal minutes format e.g. "53°20.76′N", "2°32.778′W" - `dms2deg` - converts lat/lon in d.m(.s) string format to signed decimal degrees e.g. "51°20′45.6″S" -> -51.346 - `llh2iso6709` - converts lat/lon and altitude (hMSL) to ISO6709 format e.g. "+27.5916+086.5640+8850CRSWGS_84/" - `ecef2llh` - converts ECEF (X, Y, Z) coordinates to geodetic (lat, lon, ellipsoidal height) coordinates - `llh2ecef` - converts geodetic (lat, lon, ellipsoidal height) coordinates to ECEF (X, Y, Z) coordinates - `haversine` - finds great circle distance in km between two sets of (lat, lon) coordinates - `planar` - finds planar distance in m between two sets of (lat, lon) coordinates - `bearing` - finds bearing in degrees between two sets of (lat, lon) coordinates - `area` - finds spherical area bounded by two sets of (lat, lon) coordinates - `leapsecond` - find UTC leapsecond offset for a given effective date (epoch) and GNSS time system (n/a for GLONASS) - `utc2wnotow` - converts UTC datetime to WNO (week number), TOW (time of week in milliseconds) and Leapsecond offset for various GNSS time systems (n/a for GLONASS). - `wnotow2utc` - converts WNO (week number), TOW (time of week in milliseconds) and Leapsecond offset to UTC datetime for various GNSS time systems (n/a for GLONASS). See [Sphinx documentation](https://www.semuconsulting.com/pynmeagps/pynmeagps.html#module-pynmeagaps.nmeahelpers) for details. --- ## Examples The following command line examples can be found in the `/examples` folder: 1. `nmeapoller.py` illustrates how to read, write and display NMEA messages 'concurrently' using threads and queues. This represents a useful generic pattern for many end user applications. 1. `nmeafile.py` illustrates how to implement an NMEA datalog file reader using `pynmeagps.NMEAReader` iterator functionality. 1. `nmeasocket.py` illustrates how to implement a TCP Socket reader for NMEA messages using NMEAReader iterator functionality. 1. `gpxtracker.py` illustrates a simple utility to convert an NMEA datalog file to a `*.gpx` track file using `pynmeagps.NMEAReader`. 1. `/webserver/nmeaserver.py` illustrates a simple HTTP web server wrapper around `pynmeagps.NMEAReader`; it presents data from selected NMEA messages as a web page http://localhost:8080 or a RESTful API http://localhost:8080/gps. 1. `utilities.py` illustrates how to use various `pynmeagps` utility methods. --- ## Extensibility The NMEA protocol is principally defined in the modules `nmeatypes_*.py` as a series of dictionaries. Additional message types can be readily added to the appropriate dictionary. Message payload definitions must conform to the following rules: ``` 1. attribute names must be unique within each message class 2. avoid reserved names 'msgID', 'talker', 'payload', 'checksum'. 3. attribute types must be one of the valid types (IN, DE, CH, etc.) 4. repeating groups must be defined as a tuple ('numr', {dict}), where: 'numr' is either: a. an integer representing a fixed number of repeats e.g. 32 b. a string representing the name of a preceding attribute containing the number of repeats e.g. 'numSv' c. 'None' for an indeterminate repeating group. Only one such group is permitted per payload and it must be at the end. {dict} is the nested dictionary of repeating items ``` --- ## Command Line Utility A command line utility `gnssstreamer` is available via the `pygnssutils` package. This is capable of reading and parsing NMEA, UBX and RTCM3 data from a variety of input sources (e.g. serial, socket and file) and outputting to a variety of media in a variety of formats. See https://github.com/semuconsulting/pygnssutils for further details. To install `pygnssutils`: ``` python3 -m pip install --upgrade pygnssutils ``` For help with the `gnssstreamer` utility, type: ``` gnssstreamer -h ``` --- ## Graphical Client A python/tkinter graphical GPS client which supports NMEA, UBX and RTCM3 protocols is available at: [https://github.com/semuconsulting/PyGPSClient](https://github.com/semuconsulting/PyGPSClient) --- ## Author & License Information semuadmin@semuconsulting.com ![License](https://img.shields.io/github/license/semuconsulting/pynmeagps.svg) `pynmeagps` is maintained entirely by unpaid volunteers. It receives no funding from advertising or corporate sponsorship. If you find the utility useful, please consider sponsoring the project with the price of a coffee... [![Sponsor](https://github.com/semuconsulting/pyubx2/blob/master/images/sponsor.png?raw=true)](https://buymeacoffee.com/semuconsulting) [![Freedom for Ukraine](https://github.com/semuadmin/sandpit/blob/main/src/semuadmin_sandpit/resources/ukraine200.jpg?raw=true)](https://u24.gov.ua/) pynmeagps-1.1.7/RELEASE_NOTES.md000066400000000000000000000524331523640354200161100ustar00rootroot00000000000000# pynmeagps Release Notes ### RELEASE 1.1.7 1. Fix typo in release version (v1.1.6 rather than V1.1.6) for those using pkgsrc to deploy. ### RELEASE 1.1.6 1. Add fix for ALC message definition - thanks to @SoerenDittman for contribution #96. ### RELEASE 1.1.5 1. Add support for additional proprietary Quectel PQTM configuration messages from LG290/LG580 firmware version 1.3. - PQTMAIDINI: Inputs aiding position and time data - PQTMAIDPOS: Inputs aiding position data - PQTMAIDTIME: Inputs aiding time data - PQTMCFGANTENNA: Sets/gets GNSS antenna power supply - PQTMCFGCNRTHD: Sets/gets the CNR threshold for position engine - PQTMCFGEVENT: Sets/gets external interrupt event - PQTMCFGPINALT: Sets/gets the function of module's pin 14 - PQTMCFGPPP: Sets/gets the PPP feature - PQTMCFGPPS2: Sets/gets the PPS extend feature - PQTMCFGRTKRL: Sets/gets the RTK reliability level - PQTMCFGSTANDALONE: Sets/gets the Standalone mode - PQTMCLRMSG: Disables serial message output. (2 variants) - PQTMENV: Outputs the environment information. - PQTMLSTMSG: Queries message output of serial port (2 variants) - PQTMPPPNAV: Outputs the PPP navigation information - PQTMRTCMIS: Outputs the RTCM input status ### RELEASE 1.1.4 1. Add `modwno` boolean argument to wnotow2utc and utc2wnotow helper functions - True => modular week number, False => continuous week number. The default is True (modular week no). ### RELEASE 1.1.3 1. Update wnotow2utc, utc2wnotow and leapsecond helper functions to accommodate all GNSS time systems. wnotow2utc method also adds an 'autoroll' argument which, if True, will automatically roll forward modular GNSS week numbers to the latest date less than the current date - see API docs for details. ### RELEASE 1.1.2 1. Add workaround for Unicore UM9* firmware error, which creates malformed NMEA GLL sentences (-ve latitude). ### RELEASE 1.1.1 1. Add helper methods `utc2wnotow` and `wnotow2utc` to convert between UTC datetime and GPS week number, time of week (in milliseconds) and leapsecond offset. 1. Helper methods `leapsecond` and `get_gpswnotow` will now accept timezone-aware or timezone-naive datetimes. If naive, UTC will be inferred. ### RELEASE 1.1.0 1. Add support for Unicore extended NMEA sentences: Secondary Antenna Data: - "GGAH": "Global Positioning System Fix Data (Secondary Antenna)", - "GLLH": "Geographic Position (Secondary Antenna)", - "GNSH": "GNSS Fix Data (Secondary Antenna)", - "GRSH": "GNSS Range Residuals (Secondary Antenna)", - "GSAH": "GNSS DOP and Active Satellites (Secondary Antenna)", - "GSTH": "GNSS Pseudorange Error Statistics (Secondary Antenna)", - "GSVH": "GNSS Satellites in View (Secondary Antenna)", - "RMCH": "Recommended Minimum Specific GNSS Data (Secondary Antenna)", - "VTGH": "Course over Ground and Ground Speed (Secondary Antenna)", Attitude Data: - "THS2": "True Heading and Status", - "HPR": "Attitude Parameters", - "HPR2": "Attitude Parameters", - "TRA2": "Heading, Pitch & Roll Information", - "ROT2": "Rate of Turn", - "HPD": "Positioning and Heading Information", ### RELEASE 1.0.57 1. Fix HDM definition - Fixes #89 1. Add NMEAReader encoding argument for chunked-encoded socket streams. 1. Type hints and docstrings updated 1. VSCode actions updated ### RELEASE 1.0.56 1. Cosmetic fix to GSV parsing - `elv` now rendered as `int` rather than `float`. 1. Add helper method `groupsize` to fix issue which prevented manual generation of messages with variable length groups (e.g. GSV, RTE) ### RELEASE 1.0.55 ENHANCEMENTS: 1. Add additional proprietary NMEA PQTM message definitions for Quectel LG290P / LG580P series. 1. Address what appear to be minor bugs in LG580P firmware (LG580P03AANR01A04S_SH 2025/06/06-10:08:42) relating to proprietary NMEA PQTM message output (*outputs differ from documented firmware specifications for PQTMSN and PQTMCFGRTKSRCTYPE*). 1. Add additional proprietary NMEA PSTM message definitions for Quectel LG69T (AA,AD,AF,AI,AJ,AR) series. CHANGES: 1. Drop active support for Python 3.9 (EOL as at 31 October 2025) ### RELEASE 1.0.54 FIXES: 1. Fix PUBX040 payload definition (`id` = str, not int) ENHANCEMENTS: 1. Add further Quectel proprietary PQTM message definitions for LC29H (BA,CA,DA,EA) DR series. 1. Add `get_leapseconds()` helper method to retrieve GPS leapsecond offset effective at given date. ### RELEASE 1.0.53 FIXES: 1. Fix PUBX040 message definition (id = str rather than int). ### RELEASE 1.0.53 FIXES: 1. Fix typo in Quectel PAIR650 GET message definition. 1. Allow string type (*as well as datetime.date/time type*) for NMEA DT, DTL, DM and TM attribute constructors. TM strings must be in format "hhmmss" (or "hh:mm:ss"). DT/DTL/DM strings must be "yyyymmdd" (or "yyyy-mm-dd"). See examples below: ```python from datetime import datetime from pynmeagps import SET, NMEAMessage # NOTE THAT LAD/NS ("N"/"S") and LND/EW ("E"/"W") attributes do not need to be explicitly # provided - these values will be derived from the sign of the decimal lat/lon values. # NMEA Date (DM, DT, DTL) and Time (TM) attributes can be populated in any of the following ways: # A) use formatted string types for TM and DT attributes msg1 = NMEAMessage( "P", "GRMI", SET, lat=-115.81513, lon=37.23345, date="2025-09-12", # "-" delimiters are optional time="12:15:34", # ":" delimiters are optional rcvr_cmd="D", ) # # b'$PGRMI,11548.90780,S,03714.00700,E,120925,121534,D*3B\r\n' print(msg1) print(msg1.serialize()) # B) use datetime.date() and datetime.time() types for DT and TM attributes msg2 = NMEAMessage( "P", "GRMI", SET, lat=-115.81513, lon=37.23345, date=datetime(2025, 9, 12).date(), time=datetime(2025, 9, 12, 12, 15, 34).time(), rcvr_cmd="D", ) # # b'$PGRMI,11548.90780,S,03714.00700,E,120925,121534.00,D*15\r\n' print(msg2) print(msg2.serialize()) # C) use default values msg3 = NMEAMessage( "P", "GRMI", SET, lat=-115.81513, lon=37.23345, rcvr_cmd="D", ) # # b'$PGRMI,11548.90780,S,03714.00700,E,180925,143720.37,D*18\r\n' print(msg3) print(msg3.serialize()) ``` ### RELEASE 1.0.52 ENHANCEMENTS 1. Add support for proprietary Quectel $PAIR message types (as used by Quectel LC29H and LC79H receivers). 1. Add support for additional proprietary Quectel $PQTM message types (as used by Quectel LC29H and LC79H receivers). 1. BSD 3-Clause license attribution clarified in all modules. ### RELEASE 1.0.51 ENHANCEMENTS 1. Add Python 3.14rc2 to workflow. No functional changes. ### RELEASE 1.0.50 ENHANCEMENTS 1. Add support for Feyman IM19 IMU NMEA sentence GPFMI. ### RELEASE 1.0.49 FIXES: 1. Minor updates to proprietary Quectel NMEA definitions to improve attribute name clarity - affects QTMCFGGEOFENCE and QTMCFGPPS. 1. SocketWrapper class updated to accommodate chunk and/or zip encoded socket streams. ### RELEASE 1.0.48 FIXES: 1. Fix for Conda build issue (`license = { file = "LICENSE" }` deprecated in latest Python build but still mandated by Conda). 1. No other functional changes. ### RELEASE 1.0.47 ENHANCEMENTS: 1. Add support for proprietary Quectel LG290P PQTM* NMEA sentences, as documented in https://quectel.com/content/uploads/2024/09/Quectel_LG290P03_GNSS_Protocol_Specification_V1.0.pdf. See /examples/quecteldemo.py for example usage. ### RELEASE 1.0.46 ENHANCEMENTS: 1. Add support for proprietary Septentrio X5 NMEA sentences. ### RELEASE 1.0.45 FIXES: 1. Fix NAK message talker attribute name - fixes #71 ### RELEASE 1.0.44 ENHANCEMENTS: 1. dms2deg helper method added to convert d.m.s or d.m to d.dd format. ### RELEASE 1.0.43 ENHANCEMENTS: 1. Add provision for user-defined payload definition dictionary, supplementing the existing `NMEA_PAYLOADS_GET` and `NMEA_PAYLOADS_GET_PROP` dictionaries. Format should mirror that used in `NMEA_PAYLOADS_GET`. Can be used for proprietary product development purposes or for standard payload definitions which are not yet in the public domain. 1. Drop active support for Python 3.8 - now End of Life as at October 2024. ### RELEASE 1.0.42 ENHANCEMENTS: 1. Add additional maritime talker IDs and NMEA sentence definitions. 1. Add `DTL` date format ddmmyyyy. ### RELEASE 1.0.41 ENHANCEMENTS: 1. Enhance NMEAMessage to parse unrecognised* NMEA sentence types to a nominal `` message structure if `VALMSGID` validation flag is *not* set, rather than raise a `NMEAParseMessage` error e.g.: A. with the `VALMSGID` flag *not* set (*the new default behaviour*): ```shell from pynmeagps import NMEAReader msg = NMEAReader.parse("$GNACN,103607.00,ECN,E,A,W,A,test,C*67\r\n") print(msg) ``` ``` ``` B. with the `VALMSGID flag` set: ```shell from pynmeagps import NMEAReader, VALMSGID msg = NMEAReader.parse("$GNACN,103607.00,ECN,E,A,W,A,test,C*67\r\n", validate=VALMSGID) print(msg) ``` ``` pynmeagps.exceptions.NMEAParseError: Unknown msgID GNACN, msgmode GET. ``` \* unrecognised message types include those with unknown or invalid NMEA msgIDs (*but valid payloads and checksums*), or valid NMEA sentences whose payload definitions are not yet in the public domain (e.g. those currently commented-out in [`NMEA_MSGIDS`](https://github.com/semuconsulting/pynmeagps/blob/master/src/pynmeagps/nmeatypes_core.py#L207)). 1. Add NMEA ALF sentence definition. 1. Add `validate` argument to `NMEAMessage` and carry forward from `NMEAReader` 1. Add logger to `NMEAMessage`. ### RELEASE 1.0.40 ENHANCEMENTS: 1. Add area() helper method to calculate spherical area of bounding box. 1. Sphinx documentation and docstrings enhanced to include global constants and decodes. 1. `socket_stream.SocketStream` class renamed to `socket_wrapper.SocketWrapper` class for clarity. ### RELEASE 1.0.39 ENHANCEMENTS: 1. Add support for NMEA streams with lower case hex checksums ### RELEASE 1.0.38 ENHANCEMENTS: 1. Add Locosys proprietary NMEA GET and SET messages: - $PINVCRES: Clear the NVM data - $PINVCSTR: Start session - $PINVMATTIT: ATTIT information - $PINVMIMU: MEMS RAW-DATA message information - $PINVMINR: Calibration status - $PINVMSTR: Session Status - $PINVMSLOPE: SLOPE information - $PLSC: Set status/poll version - $PLSR: Set status response - $PLSVD: Attitude yaw, pitch, roll NB: $PMTKnnn: proprietary command message sets not yet implemented ### RELEASE 1.0.37 ENHANCEMENTS: 1. Correct `planar()` helper function. 1. Internal logging & exception handling enhancements. ### RELEASE 1.0.36 ENHANCEMENTS: 1. Add further proprietary message definitions. ### RELEASE 1.0.35 FIXES: 1. Fixes & simplifies the derivation of NMEA `NS` and `EW` values when creating `NMEAMessage` objects (e.g. GNGLL) using individual keyword arguments. `NS` and `EW` will always be derived from the sign of the provided signed decimal `lat` and `lon` values and need not be provided explicitly. When creating an `NMEAMessage` using a single payload argument (_which is effectively what happens when parsing incoming serial data streams_), the value of `NS` and `EW` in the payload will determine the sign of the decimal `lat`/`lon` values in the `NMEAMessage` object. ### RELEASE 1.0.34 FIXES: 1. Add missing Trimble PASHR Proprietary Pitch and Roll sentence Fixes [#52](https://github.com/semuconsulting/pynmeagps/issues/52). ### RELEASE 1.0.33 ENHANCEMENTS: 1. Add planar formula in `nmeahelpers.py` to calculate approximate planar distance between two sets of coordinates. Complements the haversine formula at smaller scales. ### RELEASE 1.0.32 ENHANCEMENTS: 1. Cater for NMEA streams with LF (b"\x0a") rather than CRLF (b"\x0d\x0a") message terminators. ### RELEASE 1.0.31 ENHANCEMENTS: 1. Extend `NMEA_HDR` to include *all* known Talker IDs (*not just those relevant to GNSS*). 2. Minor Internal streamlining. ### RELEASE 1.0.30 ENHANCEMENTS: 1. Cater for 'IN' (Integrated Navigation) Talker ID. ### RELEASE 1.0.29 ENHANCEMENTS: 1. Cater for legacy "BD" (Beidou) NMEA Talker ID. ### RELEASE 1.0.28 ENHANCEMENTS: 1. Add 'NAVIC' to list of gnssId enumerations - reference only, no functional change. ### RELEASE 1.0.27 CHANGES: 1. Add write capability to socket_stream wrapper, allowing clients to write to NMEAReader socket stream (NMEAReader.datastream) as well as read from it. 1. Update constructor arguments and docstrings to clarify API (no functional changes). ### RELEASE 1.0.26 CHANGES: 1. Deprecated `NMEAReader.iterate()` method removed - use the standard iterator instead e.g. `nmr = NMEAReader(stream, **wkargs): for (raw,parse) in nmr: ...`, passing any `quitonerror` or `errorhandler` kwargs to the NMEAReader constructor. 1. Internal streamlining of helper methods. ### RELEASE 1.0.25 FIXES: 1. Add support for non-standard 3-digit degree latitude value, e.g. `'$GNGLL,02348.3822990,S,15313.5862807,E,040856.82,A,D*5F'` 1. NB: footprint of `dmm2ddd` helper method has changed - `att` argument no longer required. 1. Fixes #37 ### RELEASE 1.0.24 1. Remove Python 3.7 from workflows. 1. Add proprietary POLL messages to nmeatypes_poll.py ### RELEASE 1.0.23 ENHANCEMENTS: 1. Add proprietary Trimble message definitions. - GMP GNSS Map Projection Fix Data - LLQ Leica local position and quality - ROT Rate of turn - PASHR,ALR Alarms - PASHR,ARA True Heading - PASHR,ARR Vector & Accuracy - PASHR,ATT True Heading - PASHR,BTS Bluetooth Status - PASHR,CAP Parameters of Antenna Used at Received Base - PASHR,CPA Height of Antenna Used at Received Base - PASHR,CPO Position of Received Base - PASHR,DDM Differential Decoder Message - PASHR,DDS Differential Decoder Status - PASHR,HPR True Heading - PASHRHR Proprietary Roll and Pitch - PASHR,LTN Latency - PASHR,MDM Modem State and Parameter - PASHR,PBN Position and Velocity Information - PASHR,POS Position - PASHR,PTT PPS Time Tag - PASHR,PWR Power Status - PASHR,RCS Recording Status - PASHR,SBD BEIDOU Satellites Status - PASHR,SGA GALILEO Satellites Status (E1,E5a,E5b) - PASHR,SGL GLONASS Satellites Status - PASHR,SGO GALILEO Satellites Status (E1,E5a,E5b,E6) - PASHR,SGP GPS Satellites Status - PASHR,SIR IRNSS Satellites Status - PASHR,SLB L-Band Satellites Status - PASHR,SQZ QZSS Satellites Status - PASHR,SSB SBAS Satellites Status - PASHR,TEM Receiver Temperature - PASHR,THS True Heading and Status - PASHR,TTT Event Marker - PASHR,VCR Vector and Accuracy - PASHR,VCT Vector and Accuracy - PASHR,VEL Velocity - FUGDP Fugro Dynamic Positioning - GPPADV,110 Position and satellite information for RTK network operations 110 - GPPADV,120 Position and satellite information for RTK network operations 120 - PTNL,AVR Time, yaw, tilt, range, mode, PDOP, and number of SVs for Moving Baseline RTK - PTNL,BPQ Base station position and position quality indicator - PTNL,DG L-band corrections and beacon signal strength and related information - PTNL,EVT Event marker data - PTNL,GGK Time, position, position type, and DOP values - PTNL,PJK Time, position, position type, and DOP values - PTNL,PJT Projection type - PTNL,REX Rover Extended Output - PTNL,VGK Time, locator vector, type, and DOP values - PTNL,VHD Heading Information ### RELEASE 1.0.22 FIXES: 1. Rounding removed from `haversine` helper method. ### RELEASE 1.0.21 ENHANCEMENTS: 1. Add `bearing` helper method. ### RELEASE 1.0.20 CHANGES: 1. `quitonerror` and `errorhandler` kwargs added to NMEAReader constructor - see Sphinx documentation for details. 2. `NMEAReader.iterate()` method deprecated - use the standard iterator instead e.g. `nmr = NMEAReader(stream, **wkargs): for (raw,parse) in nmr: ...`, passing any `quitonerror` or `errorhandler` kwargs to the NMEAReader constructor. ### RELEASE 1.0.19 FIXES: 1. Fix typo in PUBX00 payload definition - PDOP is now TDOP. Thanks to @dbstf for issue report. 1. Fix handling of VALMSGID flag in NMEAReader. Thanks to @nmichaels-qualinx for issue report. ### RELEASE 1.0.18 ENHANCEMENTS: 1. Following utility methods added to nmeahelpers.py: - `latlon2dms` - converts decimal lat/lon to degrees, minutes, decimal seconds format e.g. "53°20′45.6″N", "2°32′46.68″W" - `latlon2dmm` - converts decimal lat/lon to degrees, decimal minutes format e.g. "53°20.76′N", "2°32.778′W" - `llh2iso6709` - converts lat/lon and altitude (hMSL) to ISO6709 format e.g. "+27.5916+086.5640+8850CRSWGS_84/" - `ecef2llh` - converts ECEF (X, Y, Z) coordinates to geodetic (lat, lon, ellipsoidal height) coordinates - `llh2ecef` - converts geodetic (lat, lon, ellipsoidal height) coordinates to ECEF (X, Y, Z) coordinates - `haversine` - finds spherical distance in km between two sets of (lat, lon) coordinates ### RELEASE 1.0.17 CHANGES: 1. shields.io build status badge URL updated. No other functional changes. ### RELEASE 1.0.16 ENHANCEMENTS: 1. `time2utc` helper method enhanced to cater for proprietary NMEA messages containing timestamps in format hh.mm.ss rather than hh.mm.ss.ss (i.e. missing decimal seconds). Addresses PR #19. ### RELEASE 1.0.15 ENHANCEMENTS: 1. Enhancement to NMEAMessage constructor - will now automatically derive value of `NS` or `EW` attributes from provided lat/lon e.g. `lon` < 0 => `EW` = "W" 2. Enhancement to NMEAMessage constructor - optional keyword argument `hpnmeamode` added which allows NMEA position message payloads to be constructed to 7dp decimal minutes rather than the standard 5dp (i.e. (d)ddmm.mmmmmmm rather than (d)ddmm.mmmm). **NB:** this is primarily for manually constructed messages. Messages parsed from a GNSS receiver data stream retain whatever level of precision is output by the receiver to a maximum 10dp of decimal degrees. ### RELEASE 1.0.14 CHANGES: 1. `nmeadump` CLI utility has been removed and replaced by the `gnssdump` utility available via the `pygnssutils` PyPi package. 2. Python 3.11 classifier added to setup. ### RELEASE 1.0.13. FIXES: 1. When manually creating NMEA Messages, the nominal (default) value of time and date fields is now set to UTC time rather than local time. This is relevant, for example, when constructing NMEA GGA messages to send to NTRIP casters. ### RELEASE 1.0.12 CHANGES: 1. Add support for THS message type. 1. Extend test coverage for socket handler. ### RELEASE 1.0.11 ENHANCEMENTS: 1. Add capability to read from TCP/UDP socket as well as serial stream. Utilises a SocketStream utility class to allow sockets to be read using standard stream-like read(bytes) and readline() methods. ### RELEASE 1.0.10 FIXES: 1. GBS and GSA message definitions updated - `systemId` and `signalId` now correctly defined as HX rather than IN. ### RELEASE 1.0.9 FIXES: 1. GRS message definition updated - `systemId` and `signalId` now correctly defined as HX rather than IN. ### RELEASE 1.0.8 ENHANCEMENTS: 1. `identity` property added to NMEAMessage for consistency with companion `pyubx2` library - identity = (talker+msgID) 2. internal refactoring of error handling in `NMEAReader.read()` method to make it more consistent with `pyubx2` when processing corrupted data streams. ### RELEASE 1.0.7 FIXES: 1. HX attribute type processing corrected - will now parse HX values as hex strings rather than convert to/from integers. 2. GSV payload corrected - SignalId is now hex. ### RELEASE 1.0.6 ENHANCEMENTS: 1. Python 3.10 compatibility added 2. Minor pylint code tweaks ### RELEASE 1.0.5 ENHANCEMENTS: 1. Filter added to `nmeadump` cli utility to limit output to specified NMEA msgIDs. See README for usage. 2. Update `dmm2ddd()` helper method to increase conversion accuracy from 6 to 8 decimal places - thanks for Doradx for the contribution. ### RELEASE 1.0.4 ENHANCEMENTS: 1. The nmeadump.py example has been moved into the pynmeagpscli module and configured as a setup entry point. It is now available as a simple command line utility. See README for usage. ### RELEASE 1.0.3 FIXES: 1. Fixed diffAge field type in GGA payload definition. ### RELEASE 1.0.2 FIXES: 1. Fixed typo in VTG payload definition - `cogT` is now `cogt`. Test script updated. ### RELEASE 1.0.1 FIXES: 1. Fixed typo in GBS payload definition - `effLon` is now `errLon`. Test script updated. 2. Fixed cosmetic typo in nmeafile.py example - functionality not affected. ### RELEASE 1.0.0 CHANGES: 1. Marked to v1.0.0 Production/Stable. No other functional changes. pynmeagps-1.1.7/SECURITY.md000066400000000000000000000006431523640354200153230ustar00rootroot00000000000000# pynmeagps Security Policy ## Supported Versions The following versions are currently being supported with security updates. ![Release](https://img.shields.io/github/v/release/semuconsulting/pynmeagps) ## Reporting a Vulnerability Please report any suspected security vulnerabilities via the supplied [Issue Template](https://github.com/semuconsulting/pynmeagps/blob/master/.github/ISSUE_TEMPLATE/bug_report.md). pynmeagps-1.1.7/docs/000077500000000000000000000000001523640354200144575ustar00rootroot00000000000000pynmeagps-1.1.7/docs/Makefile000066400000000000000000000011721523640354200161200ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) pynmeagps-1.1.7/docs/conf.py000066400000000000000000000060761523640354200157670ustar00rootroot00000000000000# Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import os import sys # get path to site-packages (source) folder within venv pypath = ( f"{os.path.expanduser("~")}/pygpsclient/lib/python" f"{sys.version_info.major}.{sys.version_info.minor}/site-packages" ) print(f"\n\033[1mUsing absolute path:\033[0m \033[95m{pypath}\033[0m\n") sys.path.insert(0, os.path.abspath(pypath)) from pynmeagps import version as VERSION # -- Project information ----------------------------------------------------- project = "pynmeagps" copyright = "2021 semuadmin (Steve Smith)" author = "semuadmin (Steve Smith)" # The full version, including alpha/beta/rc tags release = VERSION version = VERSION # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode"] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "sphinx_rtd_theme" html_title = " v documentation." # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". # html_static_path = ["_static"] html_last_updated_fmt = "%b %d %Y" html_theme_options = { "logo_only": False, "prev_next_buttons_location": "bottom", "style_external_links": False, "vcs_pageview_mode": "", "style_nav_header_background": "white", "flyout_display": "hidden", "version_selector": True, "language_selector": True, # Toc options "collapse_navigation": True, "sticky_navigation": True, "navigation_depth": 4, "includehidden": True, "titles_only": False, } autodoc_default_options = { "members": True, "member-order": "bysource", "special-members": "__init__", "undoc-members": True, "exclude-members": "__weakref__", } pynmeagps-1.1.7/docs/index.rst000066400000000000000000000007161523640354200163240ustar00rootroot00000000000000.. pynmeagps documentation master file, created by sphinx-quickstart on Thu Mar 4 11:32:55 2021. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to pynmeagps's documentation! ===================================== .. toctree:: :maxdepth: 2 :caption: Contents: Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` pynmeagps-1.1.7/docs/make.bat000066400000000000000000000014331523640354200160650ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd pynmeagps-1.1.7/docs/modules.rst000066400000000000000000000001071523640354200166570ustar00rootroot00000000000000pynmeagps ========= .. toctree:: :maxdepth: 4 pynmeagps pynmeagps-1.1.7/docs/pynmeagps.rst000066400000000000000000000043551523640354200172230ustar00rootroot00000000000000pynmeagps package ================= Submodules ---------- pynmeagps.exceptions module --------------------------- .. automodule:: pynmeagps.exceptions :members: :undoc-members: :show-inheritance: pynmeagps.nmeahelpers module ---------------------------- .. automodule:: pynmeagps.nmeahelpers :members: :undoc-members: :show-inheritance: pynmeagps.nmeamessage module ---------------------------- .. automodule:: pynmeagps.nmeamessage :members: :undoc-members: :show-inheritance: pynmeagps.nmeareader module --------------------------- .. automodule:: pynmeagps.nmeareader :members: :undoc-members: :show-inheritance: pynmeagps.nmeatypes\_core module -------------------------------- .. automodule:: pynmeagps.nmeatypes_core :members: :undoc-members: :show-inheritance: pynmeagps.nmeatypes\_decodes module ----------------------------------- .. automodule:: pynmeagps.nmeatypes_decodes :members: :undoc-members: :show-inheritance: pynmeagps.nmeatypes\_get module ------------------------------- .. automodule:: pynmeagps.nmeatypes_get :members: :undoc-members: :show-inheritance: pynmeagps.nmeatypes\_get\_prop module ------------------------------------- .. automodule:: pynmeagps.nmeatypes_get_prop :members: :undoc-members: :show-inheritance: pynmeagps.nmeatypes\_poll module -------------------------------- .. automodule:: pynmeagps.nmeatypes_poll :members: :undoc-members: :show-inheritance: pynmeagps.nmeatypes\_poll\_prop module -------------------------------------- .. automodule:: pynmeagps.nmeatypes_poll_prop :members: :undoc-members: :show-inheritance: pynmeagps.nmeatypes\_set module ------------------------------- .. automodule:: pynmeagps.nmeatypes_set :members: :undoc-members: :show-inheritance: pynmeagps.nmeatypes\_set\_prop module ------------------------------------- .. automodule:: pynmeagps.nmeatypes_set_prop :members: :undoc-members: :show-inheritance: pynmeagps.socketwrapper module ------------------------------ .. automodule:: pynmeagps.socketwrapper :members: :undoc-members: :show-inheritance: Module contents --------------- .. automodule:: pynmeagps :members: :undoc-members: :show-inheritance: pynmeagps-1.1.7/examples/000077500000000000000000000000001523640354200153455ustar00rootroot00000000000000pynmeagps-1.1.7/examples/__init__.py000066400000000000000000000001651523640354200174600ustar00rootroot00000000000000""" Created on 4 Mar 2021 :author: semuadmin (Steve Smith) :copyright: semuadmin © 2020 :license: BSD 3-Clause """ pynmeagps-1.1.7/examples/benchmark.py000066400000000000000000000104571523640354200176600ustar00rootroot00000000000000""" pynmeagps Performance benchmarking utility Usage (kwargs optional): python3 benchmark.py cycles=10000 Created on 5 Nov 2021 :author: semuadmin (Steve Smith) :copyright: semuadmin © 2021 :license: BSD 3-Clause """ # pylint: disable=line-too-long from sys import argv from time import process_time_ns from platform import version as osver, python_version from pynmeagps.nmeareader import NMEAReader from pynmeagps._version import __version__ as nmeaver NMEAMESSAGES = [ b"$GNDTM,W84,,0.0,N,0.0,E,0.0,W84*71", b"$GNRMC,103607.00,A,5327.03942,N,10214.42462,W,0.046,,060321,,,A,V*0E", b"$GPRTE,2,1,c,0,PBRCPK,PBRTO,PTELGR,PPLAND,PYAMBU,PPFAIR,PWARRN,PMORTL,PLISMR*73", b"$GNRLM,00000078A9FBAD5,083559.00,3,C45B*06", b"$GNVTG,,T,,M,0.046,N,0.085,K,A*32", b"$GNGNS,103607.00,5327.03942,N,00214.42462,W,AANN,06,5.88,56.0,48.5,,,V*34", b"$GNGGA,103607.00,5327.03942,N,00214.42462,W,1,06,5.88,56.0,M,48.5,M,,*64", b"$GNGSA,A,3,23,24,20,12,,,,,,,,,9.62,5.88,7.62,1*0C", b"$GNGSA,A,3,66,76,,,,,,,,,,,9.62,5.88,7.62,2*08", b"$GNGSA,A,3,,,,,,,,,,,,,9.62,5.88,7.62,3*08", b"$GNGSA,A,3,,,,,,,,,,,,,9.62,5.88,7.62,4*0F", b"$GPGSV,3,1,11,01,06,014,08,12,43,207,28,14,06,049,,15,44,171,23,1*6B", b"$GPGSV,3,2,11,17,32,064,16,19,33,094,,20,20,251,31,21,04,354,,1*63", b"$GPGSV,3,3,11,23,27,251,31,24,89,268,26,25,05,223,,1*5A", b"$GLGSV,3,1,10,65,07,176,,66,57,223,35,67,42,315,23,68,00,341,29,1*7A", b"$GLGSV,3,2,10,75,37,057,,76,78,303,18,77,27,253,21,84,19,018,,B*07", b"$GLGSV,3,3,10,85,22,078,,86,01,121,,1*76", b"$GAGSV,1,1,00,7*73", b"$GBGSV,1,1,02,21,,,15,25,,,28,1*7E", b"$GNGLL,5327.03942,N,00214.42462,W,103607.00,A,A*68", b"$GNGRS,103607.00,1,-2.1,0.2,2.7,-0.4,,,,,,,,,1,1*53", b"$GNGRS,103607.00,1,0.6,5.1,,,,,,,,,,,2,1*52", b"$GNGRS,103607.00,1,,,,,,,,,,,,,3,7*57", b"$GNGRS,103607.00,1,,,,,,,,,,,,,4,1*56", b"$GNGST,103607.00,38,60,38,89,15,24,31*63", b"$GNZDA,103607.00,06,03,2021,00,00*7F", b"$GNGBS,103607.00,15.1,24.2,31.0,,,,,,*6F", b"$GNVLW,,N,,N,0.000,N,0.000,N*44", b"$PUBX,00,103607.00,5327.03942,N,00214.42462,W,104.461,G3,29,31,0.085,39.63,-0.007,,5.88,7.62,8.09,6,0,0*69", b"$PUBX,03,23,1,-,014,06,08,000,12,U,207,43,28,009,14,-,049,06,,000,15,-,171,44,23,000,17,-,064,32,16,000,19,-,094,33,,000,20,U,251,20,31,038,21,-,354,04,,000,23,U,251,27,31,064,24,U,268,89,26,000,25,-,223,05,,000,48,-,,,15,000,52,-,,,28,013,65,-,176,07,,000,66,U,223,57,35,064,67,-,315,42,23,000,68,-,341,00,29,000,75,-,057,37,,000,76,U,303,78,18,000,77,-,253,27,21,000,84,-,018,19,,000,85,-,078,22,,000,86,-,121,01,,000*02", ] def progbar(i: int, lim: int, inc: int = 20): """ Display progress bar on console. :param int i: iteration :param int lim: max iterations :param int inc: bar increments (20) """ i = min(i, lim) pct = int(i * inc / lim) if not i % int(lim / inc): print( f"{int(pct*100/inc):02}% " + "\u2593" * pct + "\u2591" * (inc - pct), end="\r", ) def benchmark(**kwargs) -> float: """ Performance benchmark test. :param int cycles: (kwarg) number of test cycles (10,000) :returns: benchmark as transactions/second :rtype: float :raises: UBXStreamError """ cyc = int(kwargs.get("cycles", 10000)) txnc = len(NMEAMESSAGES) txnt = txnc * cyc print( f"\nOperating system: {osver()}", f"\nPython version: {python_version()}", f"\npynmeagps version: {nmeaver}", f"\nTest cycles: {cyc:,}", f"\nTxn per cycle: {txnc:,}", ) msglen = 0 start = process_time_ns() print(f"\nBenchmark test started at {start}") for i in range(cyc): progbar(i, cyc) for msg in NMEAMESSAGES: msglen += len(msg) _ = NMEAReader.parse(msg) end = process_time_ns() print(f"Benchmark test ended at {end}.") duration = end - start txs = round(txnt * 1e9 / duration, 2) kbs = round(msglen * 1e9 / duration / 2**10, 2) print( f"\n{txnt:,} messages processed in {duration/1e9:,.3f} seconds = {txs:,.2f} txns/second, {kbs:,.2f} kB/second.\n" ) return txs, kbs def main(): """ CLI Entry point. args as benchmark() method """ benchmark(**dict(arg.split("=") for arg in argv[1:])) if __name__ == "__main__": main() pynmeagps-1.1.7/examples/datums.py000066400000000000000000000525451523640354200172270ustar00rootroot00000000000000""" List of geodetic datums with delta values relative to WGS84. epd = ellipsoid. a = semi-major axis. f = flattening. dx, dy, dz parameters are WGS-84 X, Y, Z parameters minus the specified datum X, Y, Z in meters. Source: http://ncgia.ucsb.edu/units/u015/tables/table03.html Created on 16 Jan 2023 :author: semuadmin (Steve Smith) """ # pylint: disable=too-many-lines DATUMS = { "Adindan": { "epd": "Clarke_1880", "a": 6378024.855, "f": 293.464999995854, "dx": -166, "dy": -15, "dz": 204, }, "Afgooye": { "epd": "Krassovsky", "a": 6378029, "f": 298.299999956546, "dx": -43, "dy": -163, "dz": 45, }, "Ain_El_Abd_1970": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -150, "dy": -251, "dz": -2, }, "Alaska_(NAD-27)": { "epd": "Clarke_1866", "a": 6378067.6, "f": 294.978698229674, "dx": -5, "dy": 135, "dz": 172, }, "Alaska/Canada_NAD-27": { "epd": "Clarke_1866", "a": 6378067.6, "f": 294.978698229674, "dx": -9, "dy": 151, "dz": 185, }, "Anna_1_Astro_1965": { "epd": "Australian_National", "a": 6378114, "f": 298.250000043564, "dx": -491, "dy": -22, "dz": 435, }, "ARC-1950_mean": { "epd": "Clarke_1880", "a": 6378024.855, "f": 293.464999995854, "dx": -143, "dy": -90, "dz": -294, }, "ARC-1960_mean": { "epd": "Clarke_1880", "a": 6378024.855, "f": 293.464999995854, "dx": -160, "dy": -8, "dz": -300, }, "Ascension_Island_58": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -207, "dy": 107, "dz": 52, }, "Astro_B4_Sor.Atoll": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": 114, "dy": -116, "dz": -333, }, 'Astro_Beacon_"E"': { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": 145, "dy": 75, "dz": -272, }, "Astro_Pos_71/4": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -320, "dy": 550, "dz": -494, }, "Astronomic_Stn._52": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": 124, "dy": -234, "dz": -25, }, "Australian_Geodetic_1984": { "epd": "Australian_National", "a": 6378114, "f": 298.250000043564, "dx": -134, "dy": -48, "dz": 149, }, "Bahamas_(NAD-27)": { "epd": "Clarke_1866", "a": 6378067.6, "f": 294.978698229674, "dx": -4, "dy": 154, "dz": 178, }, "Bellevue_(IGN)": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -127, "dy": -769, "dz": 472, }, "Bermuda_1957": { "epd": "Clarke_1866", "a": 6378067.6, "f": 294.978698229674, "dx": -73, "dy": 213, "dz": 296, }, "Bogota_Observatory": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": 307, "dy": 304, "dz": -318, }, "Bukit_Rimpah": { "epd": "Bessel_1841", "a": 6378876.845, "f": 299.152812838242, "dx": -384, "dy": 664, "dz": -48, }, "Camp_Area_Astro": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -104, "dy": -129, "dz": 239, }, "Campo_Inchauspe": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -148, "dy": 136, "dz": 90, }, "Canada_Mean_(NAD27)": { "epd": "Clarke_1866", "a": 6378067.6, "f": 294.978698229674, "dx": -10, "dy": 158, "dz": 187, }, "Canal_Zone_(NAD27)": { "epd": "Clarke_1866", "a": 6378067.6, "f": 294.978698229674, "dx": 0, "dy": 125, "dz": 201, }, "Canton_Island_1966": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": 298, "dy": -304, "dz": -375, }, "Cape": { "epd": "Clarke_1880", "a": 6378024.855, "f": 293.464999995854, "dx": -136, "dy": -108, "dz": -292, }, "Cape_Canaveral_mean": { "epd": "Clarke_1866", "a": 6378067.6, "f": 294.978698229674, "dx": -2, "dy": 150, "dz": 181, }, "Carribean_(NAD27)": { "epd": "Clarke_1866", "a": 6378067.6, "f": 294.978698229674, "dx": -7, "dy": 152, "dz": 178, }, "Carthage": { "epd": "Clarke_1880", "a": 6378024.855, "f": 293.464999995854, "dx": -263, "dy": 6, "dz": 431, }, "Central_America_(NAD27)": { "epd": "Clarke_1866", "a": 6378067.6, "f": 294.978698229674, "dx": 0, "dy": 125, "dz": 194, }, "Chatham_1971": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": 175, "dy": -38, "dz": 113, }, "Chua_Astro": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -134, "dy": 229, "dz": -29, }, "Corrego_Alegre": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -206, "dy": 172, "dz": -6, }, "Corrego_Alegre_(Provisional)": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -206, "dy": 172, "dz": -6, }, "Cuba_(NAD27)": { "epd": "Clarke_1866", "a": 6378067.6, "f": 294.978698229674, "dx": -9, "dy": 152, "dz": 178, }, "Cyprus": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -104, "dy": -101, "dz": -140, }, "Djakarta(Batavia)": { "epd": "Bessel_1841", "a": 6378876.845, "f": 299.152812838242, "dx": -377, "dy": 681, "dz": -50, }, "DOS_1968": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": 230, "dy": -199, "dz": -752, }, "Easter_lsland_1967": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": 211, "dy": 147, "dz": 111, }, "Egypt": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -130, "dy": -117, "dz": -151, }, "European_1950": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -87, "dy": -96, "dz": -120, }, "European_1950_mean": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -87, "dy": -98, "dz": -121, }, "European_1979_mean": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -86, "dy": -98, "dz": -119, }, "Finnish_Nautical_Chart": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -78, "dy": -231, "dz": -97, }, "Gandajika_Base": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -133, "dy": -321, "dz": 50, }, "Geodetic_Datum_49": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": 84, "dy": -22, "dz": 209, }, "Ghana": { "epd": "WGS-84", "a": 6378137, "f": 298.257223563, "dx": 0, "dy": 0, "dz": 0, }, "Greenland_(NAD27)": { "epd": "Clarke_1866", "a": 6378067.6, "f": 294.978698229674, "dx": 11, "dy": 114, "dz": 195, }, "Guam_1963": { "epd": "Clarke_1866", "a": 6378067.6, "f": 294.978698229674, "dx": -100, "dy": -248, "dz": 259, }, "Gunung_Segara": { "epd": "Bessel_1841", "a": 6378876.845, "f": 299.152812838242, "dx": -403, "dy": 684, "dz": 41, }, "Gunung_Serindung_1962": { "epd": "WGS-84", "a": 6378137, "f": 298.257223563, "dx": 0, "dy": 0, "dz": 0, }, "GUX_1_Astro": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": 252, "dy": -209, "dz": -751, }, "Herat_North": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -333, "dy": -222, "dz": 114, }, "Hjorsey_1955": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -73, "dy": 46, "dz": -86, }, "Hong_Kong_1963": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -156, "dy": -271, "dz": -189, }, "Hu-Tzu-Shan": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -634, "dy": -549, "dz": -201, }, "Indian": { "epd": "Everest", "a": 6378997.655, "f": 300.801699992346, "dx": 289, "dy": 734, "dz": 257, }, "Iran": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -117, "dy": -132, "dz": -164, }, "Ireland_1965": { "epd": "Modified_Airy", "a": 6378933.811, "f": 299.324964577565, "dx": 506, "dy": -122, "dz": 611, }, "ISTS_073_Astro_69": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": 208, "dy": -435, "dz": -229, }, "Johnston_Island_61": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": 191, "dy": -77, "dz": -204, }, "Kandawala": { "epd": "Everest", "a": 6378997.655, "f": 300.801699992346, "dx": -97, "dy": 787, "dz": 86, }, "Kerguelen_Island": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": 145, "dy": -187, "dz": 103, }, "Kertau_48": { "epd": "Modified_Everest", "a": 6378969.937, "f": 300.801699992346, "dx": -11, "dy": 851, "dz": 5, }, "L.C._5_Astro": { "epd": "Clarke_1866", "a": 6378067.6, "f": 294.978698229674, "dx": 42, "dy": 124, "dz": 147, }, "La_Reunion": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": 94, "dy": -948, "dz": -1262, }, "Liberia_1964": { "epd": "Clarke_1880", "a": 6378024.855, "f": 293.464999995854, "dx": -90, "dy": 40, "dz": 88, }, "Luzon": { "epd": "Clarke_1866", "a": 6378067.6, "f": 294.978698229674, "dx": -133, "dy": -77, "dz": -51, }, "Mahe_1971": { "epd": "Clarke_1880", "a": 6378024.855, "f": 293.464999995854, "dx": 41, "dy": -220, "dz": -134, }, "Marco_Astro": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -289, "dy": -124, "dz": 60, }, "Masirah_Is._(Nahrwan)": { "epd": "Clarke_1880", "a": 6378024.855, "f": 293.464999995854, "dx": -247, "dy": -148, "dz": 369, }, "Massawa": { "epd": "Bessel_1841", "a": 6378876.845, "f": 299.152812838242, "dx": 639, "dy": 405, "dz": 60, }, "Merchich": { "epd": "Clarke_1880", "a": 6378024.855, "f": 293.464999995854, "dx": 31, "dy": 146, "dz": 47, }, "Mexico_(NAD27)": { "epd": "Clarke_1866", "a": 6378067.6, "f": 294.978698229674, "dx": -12, "dy": 130, "dz": 190, }, "Midway_Astro_61": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": 912, "dy": -58, "dz": 1227, }, "Mindanao": { "epd": "Clarke_1866", "a": 6378067.6, "f": 294.978698229674, "dx": -133, "dy": -79, "dz": -72, }, "Minna": { "epd": "Clarke_1880", "a": 6378024.855, "f": 293.464999995854, "dx": -92, "dy": -93, "dz": 122, }, "Montjong_Lowe": { "epd": "WGS-84", "a": 6378137, "f": 298.257223563, "dx": 0, "dy": 0, "dz": 0, }, "Nahrwan": { "epd": "Clarke_1880", "a": 6378024.855, "f": 293.464999995854, "dx": -231, "dy": -196, "dz": 482, }, "Naparima_BWI": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -2, "dy": 374, "dz": 172, }, "North_America_83": { "epd": "GRS_80", "a": 6378137, "f": 298.257222139682, "dx": 0, "dy": 0, "dz": 0, }, "North_America_1927_mean": { "epd": "Clarke_1866", "a": 6378067.6, "f": 294.978698229674, "dx": -8, "dy": 160, "dz": 176, }, "Observatorio_1966": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -425, "dy": -169, "dz": 81, }, "Old_Egyptian": { "epd": "Helmert_1906", "a": 6378074, "f": 298.299999956546, "dx": -130, "dy": 110, "dz": -13, }, "Old_Hawaiian_mean": { "epd": "Clarke_1866", "a": 6378067.6, "f": 294.978698229674, "dx": 89, "dy": -279, "dz": -183, }, "Old_Hawaiian_Kauai": { "epd": "Clarke_1866", "a": 6378067.6, "f": 294.978698229674, "dx": 45, "dy": -290, "dz": -172, }, "Old_Hawaiian_Maui": { "epd": "Clarke_1866", "a": 6378067.6, "f": 294.978698229674, "dx": 65, "dy": -290, "dz": -190, }, "Old_Hawaiian_Oahu": { "epd": "Clarke_1866", "a": 6378067.6, "f": 294.978698229674, "dx": 56, "dy": -284, "dz": -181, }, "Oman": { "epd": "Clarke_1880", "a": 6378024.855, "f": 293.464999995854, "dx": -346, "dy": -1, "dz": 224, }, "Ordnance_Survey_of_Great_Britain_36": { "epd": "Airy", "a": 6378710.604, "f": 299.324964577565, "dx": 375, "dy": -111, "dz": 431, }, "Pico_De_Las_Nieves": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -307, "dy": -92, "dz": 127, }, "Pitcairn_Astro_67": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": 185, "dy": 165, "dz": 42, }, "Potsdam_Rauenberg_DHDN": { "epd": "Bessel_1841", "a": 6378876.845, "f": 299.152812838242, "dx": 606, "dy": 23, "dz": 413, }, "Provisional_South_American_1956_mean": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -288, "dy": 175, "dz": -376, }, "Provisional_South_Chilean_1963": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": 16, "dy": 196, "dz": 93, }, "Puerto_Rico": { "epd": "Clarke_1866", "a": 6378067.6, "f": 294.978698229674, "dx": 11, "dy": 72, "dz": -101, }, "Pulkovo_1942": { "epd": "Krassovsky", "a": 6378029, "f": 298.299999956546, "dx": 28, "dy": -130, "dz": -95, }, "Qornoq": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": 164, "dy": 138, "dz": -189, }, "Quatar_National": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -128, "dy": -283, "dz": 22, }, "Rome_1940": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -225, "dy": -65, "dz": 9, }, "S_42": { "epd": "Krassovsky", "a": 6378029, "f": 298.299999956546, "dx": 28, "dy": -121, "dz": -77, }, "S.E.Asia_(Indian)": { "epd": "Everest", "a": 6378997.655, "f": 300.801699992346, "dx": 173, "dy": 750, "dz": 264, }, "SAD-69/Brazil": { "epd": "South_American_1969", "a": 6378114, "f": 298.250000043564, "dx": -60, "dy": -2, "dz": -41, }, "Santa_Braz": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -203, "dy": 141, "dz": 53, }, "Santo_(DOS)": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": 170, "dy": 42, "dz": 84, }, "Sapper_Hill_43": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -355, "dy": 16, "dz": 74, }, "Schwarzeck": { "epd": "Bessel_1841_(Namibia)", "a": 6378790.135, "f": 299.152812838242, "dx": 616, "dy": 97, "dz": -251, }, "Sicily": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -97, "dy": -88, "dz": -135, }, "Sierra_Leone_1960": { "epd": "WGS-84", "a": 6378137, "f": 298.257223563, "dx": 0, "dy": 0, "dz": 0, }, "South_American_1969_mean": { "epd": "South_American_1969", "a": 6378114, "f": 298.250000043564, "dx": -57, "dy": 1, "dz": -41, }, "South_Asia": { "epd": "Modified_Fischer_1960", "a": 6378119, "f": 298.299999956546, "dx": 7, "dy": -10, "dz": -26, }, "Southeast_Base": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -499, "dy": -249, "dz": 314, }, "Southwest_Base": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -104, "dy": 167, "dz": -38, }, "Tananarive_Observatory_25": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -189, "dy": -242, "dz": -91, }, "Thai/Viet_(Indian)": { "epd": "Everest", "a": 6378997.655, "f": 300.801699992346, "dx": 214, "dy": 836, "dz": 303, }, "Timbalai_1948": { "epd": "Everest", "a": 6378997.655, "f": 300.801699992346, "dx": -689, "dy": 691, "dz": -45, }, "Tokyo_mean": { "epd": "Bessel_1841", "a": 6378876.845, "f": 299.152812838242, "dx": -128, "dy": 481, "dz": 664, }, "Tristan_Astro_1968": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -632, "dy": 438, "dz": -609, }, "Unites_Arab_Emirates_(Nahrwan)": { "epd": "Clarke_1880", "a": 6378024.855, "f": 293.464999995854, "dx": -249, "dy": -156, "dz": 381, }, "Viti_Levu_1916": { "epd": "Clarke_1880", "a": 6378024.855, "f": 293.464999995854, "dx": 51, "dy": 391, "dz": -36, }, "Wake-Eniwetok_60": { "epd": "Hough", "a": 6378004, "f": 297.000000022571, "dx": 101, "dy": 52, "dz": -39, }, "WGS-72": { "epd": "WGS-72", "a": 6378139, "f": 298.259999999113, "dx": 0, "dy": 0, "dz": 5, }, "WGS-84": { "epd": "WGS-84", "a": 6378137, "f": 298.257223563, "dx": 0, "dy": 0, "dz": 0, }, "Yacare": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -155, "dy": 171, "dz": 37, }, "Zanderij": { "epd": "International", "a": 6377886, "f": 297.000000022571, "dx": -265, "dy": 120, "dz": -358, }, } pynmeagps-1.1.7/examples/gpxtracker.py000066400000000000000000000135251523640354200200770ustar00rootroot00000000000000""" Simple CLI utility which creates a GPX track file from a binary NMEA dump. Dump must contain NMEA GGA messages. Usage: python3 gpxtracker.py infile="pygpsdata.log" outdir="." There are a number of free online GPX viewers e.g. https://gpx-viewer.com/view Could have used minidom for XML but didn't seem worth it. Created on 7 Mar 2021 @author: semuadmin """ # pylint: disable=consider-using-with import os from datetime import datetime from sys import argv from time import strftime import pynmeagps.exceptions as nme from pynmeagps.nmeareader import NMEAReader XML_HDR = '' GPX_NS = " ".join( ( 'xmlns="http://www.topografix.com/GPX/1/1"', 'creator="pynmeagps" version="1.1"', 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"', 'xsi:schemaLocation="http://www.topografix.com/GPX/1/1', 'http://www.topografix.com/GPX/1/1/gpx.xsd"', ) ) GITHUB_LINK = "https://github.com/semuconsulting/pynmeagps" class NMEATracker: """ NMEATracker class. """ def __init__(self, infile, outdir): """ Constructor. """ self._filename = infile self._outdir = outdir self._infile = None self._trkfname = None self._trkfile = None self._nmeareader = None self._connected = False def open(self): """ Open datalog file. """ self._infile = open(self._filename, "rb") self._connected = True def close(self): """ Close datalog file. """ if self._connected and self._infile: self._infile.close() def reader(self, validate=False): """ Reads and parses UBX message data from stream using UBXReader iterator method """ i = 0 self._nmeareader = NMEAReader(self._infile, validate=validate) self.write_gpx_hdr() for _, msg in self._nmeareader: # invokes iterator method try: if msg.msgID == "GGA": dat = datetime.now() tim = msg.time dat = ( dat.replace( year=dat.year, month=dat.month, day=dat.day, hour=tim.hour, minute=tim.minute, second=tim.second, ).isoformat() + "Z" ) if msg.quality == 1: fix = "3d" elif msg.quality == 2: fix = "2d" else: fix = "none" self.write_gpx_trkpnt( msg.lat, msg.lon, ele=msg.alt, time=dat, fix=fix, hdop=msg.HDOP, ) i += 1 except (nme.NMEAMessageError, nme.NMEATypeError, nme.NMEAParseError) as err: print(f"Something went wrong {err}") continue self.write_gpx_tlr() print(f"\n{i} GGA message{'' if i == 1 else 's'} read from {self._filename}") print(f"{i} trackpoint{'' if i == 1 else 's'} written to {self._trkfname}") def write_gpx_hdr(self): """ Open gpx file and write GPX track header tags """ timestamp = strftime("%Y%m%d%H%M%S") self._trkfname = os.path.join(self._outdir, f"gpxtrack-{timestamp}.gpx") self._trkfile = open(self._trkfname, "a", encoding="utf-8") date = datetime.now().isoformat() + "Z" gpxtrack = ( XML_HDR + "" f"" f'pynmeagps' "" "GPX track from NMEA datalog" ) self._trkfile.write(gpxtrack) def write_gpx_trkpnt(self, lat: float, lon: float, **kwargs): """ Write GPX track point from NAV-PVT message content """ trkpnt = f'' # these are the permissible elements in the GPX schema for wptType # http://www.topografix.com/GPX/1/1/#type_wptType for tag in ( "ele", "time", "magvar", "geoidheight", "name", "cmt", "desc", "src", "link", "sym", "type", "fix", "sat", "hdop", "vdop", "pdop", "ageofdgpsdata", "dgpsid", "extensions", ): if tag in kwargs: val = kwargs[tag] trkpnt += f"<{tag}>{val}" trkpnt += "" self._trkfile.write(trkpnt) def write_gpx_tlr(self): """ Write GPX track trailer tags and close file """ gpxtrack = "" self._trkfile.write(gpxtrack) self._trkfile.close() def main(**kwargs): """ Main routine. """ infile = kwargs.get("infile", "pygpsdata-test.log") outdir = kwargs.get("outdir", ".") print("NMEA datalog to GPX file converter") tkr = NMEATracker(infile, outdir) print(f"\nProcessing file {infile}...") tkr.open() tkr.reader() tkr.close() print("\nOperation Complete") if __name__ == "__main__": main(**dict(arg.split("=") for arg in argv[1:])) pynmeagps-1.1.7/examples/nmeafile.py000066400000000000000000000020631523640354200175000ustar00rootroot00000000000000""" nmeafile.py This example illustrates a simple implementation of a NMEAMessage logfile reader using the NMEAReader iterator functions and an external error handler. Usage: python3 nmeafile.py filename="nmeadata.log" Created on 7 Mar 2021 @author: semuadmin """ from sys import argv from pynmeagps.nmeareader import NMEAReader def errhandler(err): """ Handles errors output by iterator. """ print(f"\nERROR: {err}\n") def main(**kwargs): """ Main routine. """ filename = kwargs.get("filename", "nmeadata.log") count = 0 print(f"\nOpening file {filename}...\n") with open(filename, "rb") as stream: nmr = NMEAReader( stream, nmeaonly=False, quitonerror=False, errorhandler=errhandler ) for raw, parsed_data in nmr: print(parsed_data) count += 1 print(f"\n{count} messages read.\n") print("\nProcessing Complete") if __name__ == "__main__": main(**dict(arg.split("=") for arg in argv[1:])) pynmeagps-1.1.7/examples/nmeapoller.py000066400000000000000000000102321523640354200200530ustar00rootroot00000000000000""" nmeapoller.py This example illustrates how to read, write and display NMEA messages "concurrently" using threads and queues. This represents a useful generic pattern for many end user applications. Usage: python3 nmeapoller.py port=/dev/ttyACM0 baudrate=38400 timeout=3 It implements two threads which run concurrently: 1) an I/O thread which continuously reads NMEA data from the receiver and sends any queued outbound command or poll messages. 2) a process thread which processes parsed NMEA data - in this example it simply prints the parsed data to the terminal. NMEA data is passed between threads using queues. Press CTRL-C to terminate. FYI: Since Python implements a Global Interpreter Lock (GIL), threads are not strictly concurrent, though this is of minor practical consequence here. Created on 07 Aug 2021 :author: semuadmin (Steve Smith) :copyright: semuadmin © 2021 :license: BSD 3-Clause """ from queue import Queue from sys import argv from threading import Event, Thread from time import sleep from serial import Serial from pynmeagps import NMEA_MSGIDS, POLL, NMEAMessage, NMEAReader def io_data( nmr: NMEAReader, readqueue: Queue, sendqueue: Queue, stop: Event, ): """ THREADED Read and parse inbound NMEA data and place raw and parsed data on queue. Send any queued outbound messages to receiver. """ # pylint: disable=broad-exception-caught while not stop.is_set(): try: (raw_data, parsed_data) = nmr.read() if parsed_data: readqueue.put((raw_data, parsed_data)) while not sendqueue.empty(): data = sendqueue.get(False) if data is not None: nmr.datastream.write(data.serialize()) sendqueue.task_done() except Exception as err: print(f"\n\nSomething went wrong {err}\n\n") continue def process_data(queue: Queue, stop: Event): """ THREADED Get NMEA data from queue and display. """ while not stop.is_set(): if queue.empty() is False: (_, parsed) = queue.get() print(parsed) queue.task_done() def main(**kwargs): """ Main routine. """ port = kwargs.get("port", "/dev/ttyACM0") baudrate = int(kwargs.get("baudrate", 38400)) timeout = float(kwargs.get("timeout", 3)) with Serial(port, baudrate, timeout=timeout) as serial_stream: nmeareader = NMEAReader(serial_stream) read_queue = Queue() send_queue = Queue() stop_event = Event() io_thread = Thread( target=io_data, args=( nmeareader, read_queue, send_queue, stop_event, ), ) process_thread = Thread( target=process_data, args=( read_queue, stop_event, ), ) print("\nStarting handler threads. Press Ctrl-C to terminate...") io_thread.start() process_thread.start() # loop until user presses Ctrl-C while not stop_event.is_set(): try: # DO STUFF IN THE BACKGROUND... # Poll for each NMEA sentence type. # NB: Your receiver may not support all types. It will return a # GNTXT "NMEA unknown msg" response for any types it doesn't support. for msgid in NMEA_MSGIDS: print( f"\nSending a GNQ message to poll for an {msgid} response...\n" ) msg = NMEAMessage("EI", "GNQ", POLL, msgId=msgid) send_queue.put(msg) sleep(1) sleep(3) stop_event.set() except KeyboardInterrupt: # capture Ctrl-C print("\n\nTerminated by user.") stop_event.set() print("\nStop signal set. Waiting for threads to complete...") io_thread.join() process_thread.join() print("\nProcessing complete") if __name__ == "__main__": main(**dict(arg.split("=") for arg in argv[1:])) pynmeagps-1.1.7/examples/nmeasocket.py000066400000000000000000000024721523640354200200550ustar00rootroot00000000000000""" nmeasocket.py A simple example implementation of a GNSS socket reader using the pynmeagps.NMEAReader iterator functions. Usage: python3 nmeasocket.py ipaddress=127.0.0.1 ipport=50012 Created on 05 May 2022 @author: semuadmin """ import socket from sys import argv from datetime import datetime from pynmeagps.nmeareader import NMEAReader def main(**kwargs): """ Reads and parses NMEA message from socket stream. """ count = 0 start = datetime.now() ipaddress = kwargs.get("ipaddress", "localhost") ipport = int(kwargs.get("ipport", 50012)) try: print(f"Opening socket {ipaddress}:{ipport}...") with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as stream: stream.connect((ipaddress, ipport)) print("Starting NMEA reader...") nmr = NMEAReader(stream) for _, parsed_data in nmr: print(parsed_data) count += 1 except KeyboardInterrupt: dur = datetime.now() - start secs = dur.seconds + dur.microseconds / 1e6 print("Session terminated by user") print( f"{count:,d} messages read in {secs:.2f} seconds:", f"{count/secs:.2f} msgs per second", ) if __name__ == "__main__": main(**dict(arg.split("=") for arg in argv[1:])) pynmeagps-1.1.7/examples/pynmeagps_usage.py000066400000000000000000000034361523640354200211140ustar00rootroot00000000000000""" pynmeagps_usage.py Examples of pynmeagps.NMEAMessage constructors. Created on 19 Aug 2024 :author: semuadmin (Steve Smith) :copyright: semuadmin © 2021 :license: BSD 3-Clause """ from datetime import datetime from pynmeagps import SET, NMEAMessage # NOTE THAT LAD ("N"/"S") and LND ("E"/"W") attributes do not need to be explicitly # provided - these values will be derived from the sign of the decimal lat/lon values. # NMEA Date (DM, DT, DTL) and Time (TM) attributes can be populated in any of the following ways: # A) use formatted string types for TM and DT attributes msg1 = NMEAMessage( "P", "GRMI", SET, lat=37.23345, lon=-115.81513, date="2025-09-12", # "-" delimiters are optional time="12:15:34", # ":" delimiters are optional rcvr_cmd="D", ) # # b'$PGRMI,3714.00700,N,11548.90780,W,120925,121534,D*04\r\n' print(msg1) print(msg1.serialize()) # B) use datetime.date() and datetime.time() types for DT and TM attributes msg2 = NMEAMessage( "P", "GRMI", SET, lat=37.23345, lon=-115.81513, date=datetime(2025, 9, 12).date(), time=datetime(2025, 9, 12, 12, 15, 34).time(), rcvr_cmd="D", ) # # b'$PGRMI,3714.00700,N,11548.90780,W,120925,121534.00,D*2A\r\n' print(msg2) print(msg2.serialize()) # C) use default values msg3 = NMEAMessage( "P", "GRMI", SET, lat=37.23345, lon=-115.81513, rcvr_cmd="D", ) # # b'$PGRMI,3714.00700,N,11548.90780,W,180925,151931.24,D*28\r\n' print(msg3) print(msg3.serialize()) pynmeagps-1.1.7/examples/quecteldemo.py000066400000000000000000000310731523640354200202320ustar00rootroot00000000000000""" quecteldemo.py A simple demonstrator for Quectel LG290P configuration and monitoring support in pynmeagps. Usage: python3 quecteldemo.py port=/dev/ttyACM0 baudrate=38400 timeout=3 qtmonly=1 The 'qtmonly' boolean argument can be used to limit displayed output to proprietary PQTM messages only. It implements two threads which run concurrently: 1) an I/O thread which continuously reads NMEA data from the receiver and sends any queued outbound command or poll messages. 2) a process thread which processes parsed NMEA data - in this example it simply prints the parsed data to the terminal. NMEA data is passed between threads using queues. NB: Some LG290P commands require a hot reset (PQTMHOT) or a parameter save (PQTMSAVEPAR) and a full system reset (PQTMSRR) before taking effect. Press CTRL-C to terminate. Created on 19 Aug 2024 :author: semuadmin (Steve Smith) :copyright: semuadmin © 2021 :license: BSD 3-Clause """ from queue import Queue from sys import argv from threading import Event, Thread from time import sleep from serial import Serial from pynmeagps import POLL, SET, NMEAMessage, NMEAReader, hex2str # standard NMEA messages which can be set using PQTMCFGMSGRATE # NB don't need msgver with these message types LG290P_STANDARD_MESSAGES = [ "GGA", "GLL", "GSA", "GSV", "RMC", "VTG", ] # proprietary NMEA messages which can be set using PQTMCFGMSGRATE # NB need msgver with these message types LG290P_PROPRIETARY_MESSAGES = [ ("PQTMDOP", 1), # Outputs Dilution of Precision ("PQTMEPE", 2), # Output Estimated Position Error ("PQTMGEOFENCESTATUS", 1), # Outputs Geofence Status ("PQTMODO", 1), # Outputs Odometer Information ("PQTMPL", 1), # Outputs Protection Level Information ("PQTMPVT", 1), # Outputs PVT (GNSS) Result ("PQTMSVINSTATUS", 1), # Outputs Survey-In Status ("PQTMTXT", 1), # Outputs Short Text Message ("PQTMVEL", 1), # Output Velocity Information ] def io_data( nmr: NMEAReader, readqueue: Queue, sendqueue: Queue, stop: Event, ): """ THREADED Read and parse inbound NMEA data and place raw and parsed data on queue. Send any queued outbound messages to receiver. """ # pylint: disable=broad-exception-caught while not stop.is_set(): try: (raw_data, parsed_data) = nmr.read() if parsed_data: readqueue.put((raw_data, parsed_data)) while not sendqueue.empty(): data = sendqueue.get() if data is not None: nmr.datastream.write(data.serialize()) sendqueue.task_done() except Exception as err: print(f"\n\nSomething went wrong {err}\n\n") continue def process_data(queue: Queue, stop: Event, qtmonly: bool): """ THREADED Get NMEA data from queue and display. """ while not stop.is_set(): if queue.empty() is False: (_, parsed) = queue.get() if not (qtmonly and "QTM" not in parsed.identity): print(f"Receiving {parsed}") queue.task_done() def send(queue: Queue, msg: NMEAMessage, delay: float = 0.1): """ Place message on send queue. """ print(f"Sending {msg} ...") queue.put(msg) sleep(delay) def main(**kwargs): """ Main routine. """ port = kwargs.get("port", "/dev/ttyAMA0") baudrate = int(kwargs.get("baudrate", 460800)) timeout = float(kwargs.get("timeout", 3)) qtmonly = int(kwargs.get("qtmonly", 0)) with Serial(port, baudrate, timeout=timeout) as serial_stream: nmeareader = NMEAReader(serial_stream) read_queue = Queue() send_queue = Queue() stop_event = Event() io_thread = Thread( target=io_data, args=( nmeareader, read_queue, send_queue, stop_event, ), ) process_thread = Thread( target=process_data, args=( read_queue, stop_event, qtmonly, ), ) print("\nStarting handler threads. Press Ctrl-C to terminate...") io_thread.start() process_thread.start() try: # ******************************************************************* # COMMENT/UNCOMMMENT LG290P COMMANDS (SET) OR QUERIES (POLL) HERE... # # Refer to testConstructors_QUECTEL() in tests\test_quectel.py # for a complete set of example LG290P message constructors # ******************************************************************* # query version number msg = NMEAMessage("P", "QTMVERNO", POLL) send(send_queue, msg) # do factory reset (NB only takes effect after restart) msg = NMEAMessage("P", "QTMRESTOREPAR", SET) send(send_queue, msg) # save settings to non-volatile memory # msg = NMEAMessage("P", "QTMSAVEPAR", SET) # send(send_queue, msg) # turn debug mode on # msg = NMEAMessage("P", "QTMDEBUGON", SET) # send(send_queue, msg) # turn debug mode off # msg = NMEAMessage("P", "QTMDEBUGOFF", SET) # send(send_queue, msg) # check receiver mode (1 = Rover) msg = NMEAMessage("P", "QTMCFGRCVRMODE", POLL) send(send_queue, msg) # set fix rate to 1Hz (NB requires hot restart PQTMHOT to take effect) msg = NMEAMessage("P", "QTMCFGFIXRATE", SET, fixinterval=1000) send(send_queue, msg) # check to see if fix rate has been updated msg = NMEAMessage("P", "QTMCFGFIXRATE", POLL) send(send_queue, msg) # ************************************************* # NB! PQTMCFGUART command has several permutations: # ************************************************* # set baud rate on current UART, leaving other parameters unchanged NMEAMessage("P", "QTMCFGUART", SET, baudrate=460800) send(send_queue, msg) # set baud rate on UART2 interface (portid=2), leaving other parameters unchanged NMEAMessage("P", "QTMCFGUART", SET, portid=2, baudrate=115200) send(send_queue, msg) # configure all parameters on current UART NMEAMessage( "P", "QTMCFGUART", SET, baudrate=460800, databit=8, parity=0, stopbit=1, flowctrl=0, ) send(send_queue, msg) # configure all parameters on UART2 interface (portid=2) NMEAMessage( "P", "QTMCFGUART", SET, portid=2, baudrate=115200, databit=8, parity=0, stopbit=1, flowctrl=0, ) send(send_queue, msg) # check UART config for all 3 available ports for portid in range(1, 4): msg = NMEAMessage("P", "QTMCFGUART", POLL, portid=portid) send(send_queue, msg) msg = NMEAMessage("P", "QTMCFGPROT", POLL, porttype=1, portid=portid) send(send_queue, msg) # turn gnss engine off # msg = NMEAMessage("P", "QTMGNSSSTOP", SET) # send(send_queue, msg) # turn gnss engine on # msg = NMEAMessage("P", "QTMGNSSSTART", SET) # send(send_queue, msg) # do cold start # msg = NMEAMessage("P", "QTMCOLD", SET) # send(send_queue, msg) # do warm start # msg = NMEAMessage("P", "QTMWARM", SET) # send(send_queue, msg) # set satellite mask (see also PQTMCFGCNST) NMEAMessage( "P", "QTMCFGSAT", SET, systemid=1, signalid=1, masklow=hex2str(0xFFFFFFFF, 8), # hex as padded string ) send(send_queue, msg) # set signal masks NMEAMessage( "P", "QTMCFGSIGNAL", SET, gpssig=hex2str(0x07), # hex as unpadded string glonasssig=hex2str(0x03), galileosig=hex2str(0x0F), beidousig=hex2str(0x3F), qzsssig=hex2str(0x07), navicsig=hex2str(0x01), ) send(send_queue, msg) # disable all standard NMEA messages for msgname in LG290P_STANDARD_MESSAGES: msg = NMEAMessage("P", "QTMCFGMSGRATE", SET, msgname=msgname, rate=0) send(send_queue, msg) # enable all proprietary NMEA messages for msgname, msgver in LG290P_PROPRIETARY_MESSAGES: msg = NMEAMessage( "P", "QTMCFGMSGRATE", SET, msgname=msgname, rate=1, msgver=msgver ) send(send_queue, msg) # ********************************************************************* # NB! Base and Rover configurations require 3 or 4 commands in sequence # ********************************************************************* # Set Base Station, Survey-In Mode - requires 4 commands in sequence: msg1 = NMEAMessage("P", "QTMCFGRCVRMODE", SET, rcvrmode=2) msg2 = NMEAMessage( "P", "QTMCFGSVIN", SET, svinmode=1, cfgcnt=60, acclimit=3000 ) msg3 = NMEAMessage( "P", "QTMSAVEPAR", SET, ) msg4 = NMEAMessage( "P", "QTMSRR", SET, ) msgs = [msg1, msg2, msg3, msg4] for msg in msgs: send(send_queue, msg) # Set Base Station, Fixed Mode - requires 4 commands in sequence: # If required, use the pynmeagps.llh2ecef() helper function to # convert LLH coordinates to ECEF e.g. # ecefx, ecefy, ecefz = llh2ecef(37.233333, -115.808333, 0) # # msg1 = NMEAMessage("P", "QTMCFGRCVRMODE", SET, rcvrmode=2) # msg2 = NMEAMessage( # "P", # "QTMCFGSVIN", # SET, # svinmode=2, # cfgcnt=0, # acclimit=0, # ecefx=-2213540.321087019, # ecefy=-4577229.071167925, # ecefz=3838042.2419518335, # ) # msg3 = NMEAMessage( # "P", # "QTMSAVEPAR", # SET, # ) # msg4 = NMEAMessage( # "P", # "QTMSRR", # SET, # ) # msgs = [msg1, msg2, msg3, msg4] # for msg in msgs: # send(send_queue, msg) # Reset to Normal (Rover) Mode - requires 3 commands in sequence: msg1 = NMEAMessage("P", "QTMCFGRCVRMODE", SET, rcvrmode=1) msg2 = NMEAMessage( "P", "QTMSAVEPAR", SET, ) msg3 = NMEAMessage( "P", "QTMSRR", SET, ) msgs = [msg1, msg2, msg3] for msg in msgs: send(send_queue, msg) # check geofence config # for index in range(0, 4): # msg = NMEAMessage("P", "QTMCFGGEOFENCE", POLL, index=index) # send(send_queue, msg) # # check satellite config (signal 1 only in this example) # for systemid in range(1, 7): # msg = NMEAMessage("P", "QTMCFGSAT", POLL, systemid=systemid, signalid=1) # send(send_queue, msg) # do hot start (required after setting fix rate or satellite config) msg = NMEAMessage("P", "QTMHOT", SET) send(send_queue, msg, 1) while not stop_event.is_set(): # loop until user presses Ctrl-C sleep(1) except KeyboardInterrupt: # capture Ctrl-C print("\n\nTerminated by user.") stop_event.set() print("\nStop signal set. Waiting for threads to complete...") io_thread.join() process_thread.join() print("\nProcessing complete") if __name__ == "__main__": main(**dict(arg.split("=") for arg in argv[1:])) pynmeagps-1.1.7/examples/utilities.py000066400000000000000000000056311523640354200177370ustar00rootroot00000000000000""" utilities.py Illustration of the various utility methods available in pynmeagps Created on 14 Jan 2023 @author: semuadmin :copyright: semuadmin © 2022 :license: BSD 3-Clause """ from datums import DATUMS # assumes this is in same folder from pynmeagps import ( area, bearing, ecef2llh, haversine, latlon2dmm, latlon2dms, llh2ecef, llh2iso6709, planar, ) LAT1, LON1, ALT1 = 53.24, -2.16, 143.23 LAT2, LON2 = 53.32, -2.08 LAT3, LON3 = 53.2401, -2.1601 print(f"\nConvert {LAT1}, {LON1} to degrees, minutes, seconds ...") lat, lon = latlon2dms(LAT1, LON1) print(f"D.M.S = {lat}, {lon}") print(f"\nConvert {LAT2}, {LON2} to degrees, minutes ...") lat, lon = latlon2dmm(LAT2, LON2) print(f"D.MM = {lat}, {lon}") print(f"\nConvert {LAT1}, {LON1}, {ALT1}m to ISO6709 format ...") iso6709 = llh2iso6709(LAT1, LON1, ALT1) print(f"ISO6709 = {iso6709}") print( f"\nFind great circle distance between {LAT1}, {LON1} and", f" {LAT2}, {LON2} using default WGS84 datum...", ) dist = haversine(LAT1, LON1, LAT2, LON2) print(f"Distance: {dist:.4f} km") print( f"\nFind planar distance between {LAT1}, {LON1} and", f" {LAT3}, {LON3} using default WGS84 datum...", ) dist = planar(LAT1, LON1, LAT3, LON3) print(f"Distance: {dist:.4f} m") print( f"\nFind bearing between {LAT1}, {LON1} and", f" {LAT2}, {LON2} using default WGS84 datum...", ) brng = bearing(LAT1, LON1, LAT2, LON2) print(f"Bearing: {brng:.4f} degrees") X, Y, Z = 3822566.3113, -144427.5123, 5086857.1208 print(f"\nConvert ECEF X: {X}, Y: {Y}, Z: {Z} to geodetic using default WGS84 datum...") lat, lon, height = ecef2llh(X, Y, Z) print(f"Geodetic lat: {lat}, lon: {lon}, height: {height}") print(f"\nConvert geodetic {lat}, {lon}, {height} back to ECEF ...") x, y, z = llh2ecef(lat, lon, height) print(f"ECEF X: {x}, Y: {y}, Z: {z}") # Refer to DATUMS.py in the /examples folder for list of common # international datums with semi-major axis, flattening and # delta_x,y,z reference values DATUM = "North_America_83" datum_dict = DATUMS[DATUM] ellipsoid, a, f, delta_x, delta_y, delta_z = datum_dict.values() print( f"\nFind great circle distance between {LAT1}, {LON1} and", f"{LAT2}, {LON2} using alternate {DATUM} ({ellipsoid}) datum...", ) dist = haversine(LAT1, LON1, LAT2, LON2, a / 1000) print(f"Distance: {dist:.4f} km") print( f"\nFind spherical area bound by {LAT1}, {LON1} and", f"{LAT2}, {LON2}...", ) bbarea = area(LAT1, LON1, LAT2, LON2) print(f"Area: {bbarea:.4f} km²") print( f"\nConvert ECEF X: {X}, Y: {Y}, Z: {Z} to", f"geodetic using alternate {DATUM} ({ellipsoid}) datum ...", ) lat, lon, height = ecef2llh(X - delta_x, Y - delta_y, Z - delta_z, a, f) print(f"Geodetic lat: {lat}, lon: {lon}, height: {height}") print(f"\nConvert geodetic {lat}, {lon}, {height} back to ECEF ...") x, y, z = llh2ecef(lat, lon, height, a, f) print(f"ECEF X: {x + delta_x}, Y: {y + delta_y}, Z: {z + delta_z}") pynmeagps-1.1.7/examples/webserver/000077500000000000000000000000001523640354200173515ustar00rootroot00000000000000pynmeagps-1.1.7/examples/webserver/favicon.ico000066400000000000000000000021761523640354200215000ustar00rootroot00000000000000 h(  Û"Û�Û×ÛûÛûÛ×Û�Û"ÙÛrÛùÛÇÛlÛBÛBÛlÛÇÛùÛrÙÛƒÛõÛX))ßÛÛ¿ÛZÛZÛõÛƒÛ@ÛÿÛ™ÛXÛÿÛûÛ8ÛHÛýÛ@ÛÃÛÿÛ×jjÊÛ`ÛÿÛÿÛ¥Û�ÛÃÛÛÿÛÿÛ½iiÌÛÛ ÛÍÛÿÛÿÛxÛ2ÛÿÛÛHÛÿÛéÛ ÛVÛÿÛÿÛÿÛÿÛÙÛÛ ÛÛ÷ÛHÛTÛÿÛjÛ�ÛÿÛÿÛÿÛÿÛFÛ2ÛÑÛÛ›ÛTÛ>ÛÿÛÛ<ÛïÛÿÛÿÛ§ ÛÛÉÛÿÛtÛÛ.Û Û÷ÛNÛHÛ»ÛjÛ`ÛýÛ«ÛóÛÛ�ÛÉÛÛÛ—Û`ÛÛãÛ›Û ÛåÛ“ÛÛíÛ�ÛVVÚÛ@ÛÛXÛûÛÛnÛõÛÛ>ÛóÛùÛ:ÛLÛ"Û�ÛÁÛvÛñÛBÛÿÛHÛ*ÛÇÛýÛãÛûÛ ÛŸÛÇÛvÛñÛJÛÿÛHÝÛ:ÛƒÛ§Û&Û@ÛÿÛŸÛ`ÛÏÛßÛ ÛXÛÝÛûÛ½Û&ðÀÃ�‡ AŒ„À€àðÿÁpynmeagps-1.1.7/examples/webserver/gpshttpserver.py000066400000000000000000000063071523640354200226510ustar00rootroot00000000000000""" This is a simple HTTP Server utilising the native Python 3 http.server library. It implements a REST API /gps to retrieve GPS data from the designated GPSClass object. A dummy GPSDataStub object is provided to simulate the output from a GPS device. NB: Must be executed from the root folder i.e. /examples/webserver/. Press CTRL-C to terminate. The web page can be accessed at http://localhost:8080. The dummy parsed data can also be accessed directly via the REST API http://localhost:8080/gps. Created on 17 May 2021 :author: semuadmin (Steve Smith) :license: (c) SEMU Consulting 2021 - BSD 3-Clause License """ from http.server import SimpleHTTPRequestHandler, HTTPServer import json import random from datetime import datetime ADDRESS = "localhost" TCPPORT = 8080 HTML = "/index.html" CSS = "/styles.css" JS = "/scripts.js" ICON = "/favicon.ico" class GPSDataStub: """ Stub data class to simulate GPS data. """ def get_data(self): """ Simulated REST API /gps response. """ now = datetime.now() dic = { "date": now.strftime("%Y-%m-%d"), "time": now.strftime("%H:%M:%S"), "latitude": round(random.uniform(-90.0, 90.0), 5), "longitude": round(random.uniform(-180.0, 180.0), 5), "elevation": round(random.uniform(-50.0, 100.0), 2), "speed": round(random.uniform(0, 100.0), 2), "track": round(random.uniform(0, 360.0), 2), "siv": random.randrange(0, 33), "pdop": round(random.uniform(0, 99.0), 2), "hdop": round(random.uniform(0, 99.0), 2), "vdop": round(random.uniform(0, 99.0), 2), "fix": random.randrange(1, 4), } return json.dumps(dic) class GPSHTTPServer(HTTPServer): """ HTTPServer subclass incorporating reference to GPSClass object which must implement a get_data() method in support of the /gps REST API. """ def __init__(self, server_address, RequestHandlerClass, GPSClass): """ Constructor. """ self.gps = GPSClass super().__init__(server_address, RequestHandlerClass) class GPSHTTPHandler(SimpleHTTPRequestHandler): """ HTTP Request Handler subclass. """ def do_GET(self): """ Handle GET request. """ if self.path == "/": self.path = HTML mimetype = self.guess_type(self.path) rc = 200 if self.path in (HTML, JS, CSS, ICON): res = open(self.path[1:]).read() elif self.path == "/gps": # invoke GPS REST API res = self.server.gps.get_data() mimetype = "application/json" else: res = "Unknown Request" rc = 501 self.send_response(rc) self.send_header("Content-type", mimetype) self.end_headers() self.wfile.write(res.encode()) if __name__ == "__main__": gps = GPSDataStub() print("\nStarting HTTP Server on http://" + ADDRESS + ":" + str(TCPPORT) + " ...") httpd = GPSHTTPServer((ADDRESS, TCPPORT), GPSHTTPHandler, gps) try: httpd.serve_forever() except KeyboardInterrupt: httpd.shutdown() print("\nHTTP Server stopped.") pynmeagps-1.1.7/examples/webserver/index.html000066400000000000000000000045221523640354200213510ustar00rootroot00000000000000 GPS Server Demo

GPS Server Demo

UTC Date 0000-00-00
UTC Time 00:00:00
Latitude 00.00000
Longitude 00.00000
Elevation 000.00
Speed 000.00
Track 000.00
Fix No Fix
SIV 0
PDOP 00.00
HDOP 00.00
VDOP 00.00
pynmeagps-1.1.7/examples/webserver/nmeaserver.py000066400000000000000000000161571523640354200221040ustar00rootroot00000000000000""" nmeaserver.py This illustrates a simple HTTP wrapper around the pynmneagps NMEAStreamer streaming and parsing example. Usage: python3 nmeaserver.py ipaddress=localhost ipport=8080 serport=/dev/ttyACM1 baudrate=38400 timeout=0.1 It displays selected GPS data on a dynamically updated web page using the native Python 3 http.server library and a RESTful API implemented by the pynmeagps streaming and parsing service. NB: Must be executed from the root folder i.e. /examples/webserver/. Press CTRL-C to terminate. The web page can be accessed at http://localhost:8080. The parsed data can also be accessed directly via the REST API http://localhost:8080/gps. Created on 17 May 2021 :author: semuadmin (Steve Smith) :license: (c) SEMU Consulting 2021 - BSD 3-Clause License """ from sys import argv from io import BufferedReader from threading import Thread, Event from time import sleep import json from gpshttpserver import GPSHTTPServer, GPSHTTPHandler from serial import Serial, SerialException, SerialTimeoutException from pynmeagps import NMEAReader, GET import pynmeagps.exceptions as nme class GNSSStreamer: """ GNSSStreamer class. """ def __init__(self, port, baudrate, timeout, nmea_only=0, validate=1): """ Constructor. """ self._serial_object = None self._serial_thread = None self._nmeareader = None self._connected = False self._port = port self._baudrate = baudrate self._timeout = timeout self._nmea_only = nmea_only self._validate = validate self._stopevent = Event() self.gpsdata = { "date": "1900-01-01", "time": "00.00.00", "latitude": 0.0, "longitude": 0.0, "elevation": 0.0, "speed": 0.0, "track": 0, "siv": 0, "pdop": 99, "hdop": 99, "vdop": 99, "fix": 0, } def __del__(self): """ Destructor. """ self.stop_read_thread() self.disconnect() def connect(self): """ Open serial connection. """ self._connected = False try: print(f"Connecting to serial port {self._port} at {self._baudrate} baud...") self._serial_object = Serial( self._port, self._baudrate, timeout=self._timeout ) self._nmeareader = NMEAReader( BufferedReader(self._serial_object), nmeaonly=self._nmea_only, validate=self._validate, msgmode=GET, ) self._connected = True except (SerialException, SerialTimeoutException) as err: print(f"Error connecting to serial port {err}") return self._connected def disconnect(self): """ Close serial connection. """ if self._connected and self._serial_object: print("Disconnecting from serial port...") try: self._serial_object.close() except (SerialException, SerialTimeoutException) as err: print(f"Error disconnecting from serial port {err}") self._connected = False return self._connected def start_read_thread(self): """ Start the serial reader thread. """ if self._connected: print("\nStarting reader thread...\n") self._serial_thread = Thread( target=self._read_thread, args=(self._stopevent,) ) self._serial_thread.start() def stop_read_thread(self): """ Stop the serial reader thread. """ if self._serial_thread is not None: print("\nStopping web server thread...") self._stopevent.set() def _read_thread(self, stopevent): """ THREADED PROCESS Reads and parses NMEA message data from stream """ while not stopevent.is_set(): if self._serial_object.in_waiting: try: (raw_data, parsed_data) = self._nmeareader.read() if parsed_data: self.set_data(parsed_data) except ( nme.NMEAStreamError, nme.NMEAMessageError, nme.NMEATypeError, nme.NMEAParseError, ) as err: print(f"Something went wrong {err}") continue def set_data(self, parsed_data): """ Set GPS data dictionary from NMEA RMC, GGA and GSA sentences. """ # print(parsed_data) if parsed_data.msgID == "RMC": self.gpsdata["date"] = str(parsed_data.date) self.gpsdata["time"] = str(parsed_data.time) self.gpsdata["latitude"] = parsed_data.lat self.gpsdata["longitude"] = parsed_data.lon self.gpsdata["speed"] = parsed_data.spd self.gpsdata["track"] = parsed_data.cog elif parsed_data.msgID == "GGA": self.gpsdata["time"] = str(parsed_data.time) self.gpsdata["latitude"] = parsed_data.lat self.gpsdata["longitude"] = parsed_data.lon self.gpsdata["elevation"] = parsed_data.alt self.gpsdata["siv"] = parsed_data.numSV self.gpsdata["hdop"] = parsed_data.HDOP elif parsed_data.msgID == "GSA": self.gpsdata["fix"] = parsed_data.navMode self.gpsdata["pdop"] = parsed_data.PDOP self.gpsdata["hdop"] = parsed_data.HDOP self.gpsdata["vdop"] = parsed_data.VDOP def get_data(self): """ Return GPS data in JSON format. This is used by the REST API /gps implemented in the GPSHTTPServer class. """ return json.dumps(self.gpsdata) def main(**kwargs): """ Main routine. """ ipaddress = kwargs.get("ipaddress", "localhost") ipport = int(kwargs.get("ipport", 8080)) serport = kwargs.get("serport", "/dev/ttyACM0") baudrate = int(kwargs.get("baudrate", 38400)) timeout = float(kwargs.get("timeout", 0.1)) gps = GNSSStreamer(serport, baudrate, timeout) httpd = GPSHTTPServer((ipaddress, ipport), GPSHTTPHandler, gps) if gps.connect(): gps.start_read_thread() print( f"\nStarting HTTP Server on http://{ipaddress}:{ipport}...\nPress Ctrl-C to terminate.\n" ) httpd_thread = Thread(target=httpd.serve_forever, daemon=True) httpd_thread.start() try: while True: pass except KeyboardInterrupt: print("\n\nTerminated by user\n\n") httpd.shutdown() gps.stop_read_thread() sleep(2) # wait for shutdown gps.disconnect() print("\nProcessing Complete") if __name__ == "__main__": main(**dict(arg.split("=") for arg in argv[1:])) pynmeagps-1.1.7/examples/webserver/scripts.js000066400000000000000000000031741523640354200214030ustar00rootroot00000000000000/*! JavaScript for GPS Server Demo */ var gInterval = 5000; // 5 seconds var gTimer = 0; var FIXDESC = ["No Fix", "2D", "3D"]; // Refresh web page function refreshPage(obj) { "use strict"; document.getElementById('date').innerHTML = obj.date; document.getElementById('time').innerHTML = obj.time; document.getElementById('latitude').innerHTML = obj.latitude; document.getElementById('longitude').innerHTML = obj.longitude; document.getElementById('elevation').innerHTML = obj.elevation; document.getElementById('speed').innerHTML = obj.speed; document.getElementById('track').innerHTML = obj.track; document.getElementById('siv').innerHTML = obj.siv; document.getElementById('pdop').innerHTML = obj.pdop; document.getElementById('hdop').innerHTML = obj.hdop; document.getElementById('vdop').innerHTML = obj.vdop; document.getElementById('fix').innerHTML = FIXDESC[obj.fix - 1]; } // Execute REST GET request to retrieve latest gps data function getGPS() { "use strict"; var obj; var xhr = new XMLHttpRequest(); xhr.open('GET', '/gps'); xhr.onload = function () { if (xhr.status === 200) { obj = JSON.parse(xhr.responseText); refreshPage(obj); } else { alert('Request failed. Returned status of ' + xhr.status); } }; xhr.send(); } // Set interval timer for GET request function setTimer() { "use strict"; clearInterval(gTimer); gTimer = setInterval("getGPS()", gInterval); } // Functions to call when body first loaded function start() { "use strict"; getGPS(); setTimer(); } pynmeagps-1.1.7/examples/webserver/styles.css000066400000000000000000000016151523640354200214110ustar00rootroot00000000000000/* Stylesheet for GPS Server Demo */ @charset "UTF-8"; html,body { height:100%; } html { -ms-text-size-adjust:100%; -webkit-text-size-adjust:100%; } body { font-family:Arial,Helvetica,sans-serif; } div.main-container { display:block; position:relative; min-height:95vh; background-color:white; overflow:auto; margin:0 auto; padding:0; padding-bottom:40px; width:50%; } header { margin:0 auto; text-align:center; } footer { position:absolute; margin:0 auto; width:100%; bottom:0; height:40px; padding-top: 5px; padding-bottom: 5px; text-align:center; } h1 { color: #444444; font-size:3.0em; } table { margin:0 auto; font-size:1.5em; padding:10px; border-collapse: collapse; } td { color: #444444; padding-left:20px; padding-right:20px; border-style: solid; border-width: 2px; border-color: lightgrey; } td.right-align { text-align:right; } pynmeagps-1.1.7/images/000077500000000000000000000000001523640354200147745ustar00rootroot00000000000000pynmeagps-1.1.7/images/clickpy_top01.svg000066400000000000000000000170141523640354200202010ustar00rootroot00000000000000 downloads: ClickPydownloadsTOP 1%downloads: ClickPy pynmeagps-1.1.7/images/coverage.svg000066400000000000000000000103271523640354200173130ustar00rootroot00000000000000 coverage: 98% coverage 99% 99% pynmeagps-1.1.7/pyproject.toml000066400000000000000000000053721523640354200164520ustar00rootroot00000000000000[build-system] requires = ["setuptools>=75.0.0", "wheel"] build-backend = "setuptools.build_meta" [project] name = "pynmeagps" dynamic = ["version"] authors = [{ name = "Steve Smith", email = "semuadmin@semuconsulting.com" }] maintainers = [{ name = "Steve Smith", email = "semuadmin@semuconsulting.com" }] description = "NMEA protocol parser and generator" license = "BSD-3-Clause" license-files = ["LICENSE"] readme = "README.md" requires-python = ">=3.10" classifiers = [ "Operating System :: OS Independent", "Development Status :: 5 - Production/Stable", "Environment :: MacOS X", "Environment :: Win32 (MS Windows)", "Environment :: X11 Applications", "Environment :: Console", "Intended Audience :: Developers", "Intended Audience :: Science/Research", "Intended Audience :: End Users/Desktop", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: Utilities", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Scientific/Engineering :: GIS", ] dependencies = [] [project.urls] homepage = "https://github.com/semuconsulting/pynmeagps" documentation = "https://www.semuconsulting.com/pynmeagps/" repository = "https://github.com/semuconsulting/pynmeagps" changelog = "https://github.com/semuconsulting/pynmeagps/blob/master/RELEASE_NOTES.md" [dependency-groups] build = [ "build", "packaging>=24.2", "pip", "setuptools>=75.0.0", "twine>=6.1.0", "wheel", ] test = [ "bandit", "black", "certifi", "isort", "pylint", "pytest", "pytest-cov", "Sphinx", "sphinx-rtd-theme", ] optional = [] deploy = [{ include-group = "build" }, { include-group = "test" }] [tool.setuptools.dynamic] version = { attr = "pynmeagps._version.__version__" } [tool.black] target-version = ['py310'] [tool.isort] py_version = 310 profile = "black" [tool.bandit] exclude_dirs = ["docs", "examples", "references", "tests"] skips = [] [tool.pylint] jobs = 0 recursive = "y" reports = "y" py-version = "3.10" fail-under = "9.8" fail-on = "E,F" clear-cache-post-run = "y" disable = """ raw-checker-failed, bad-inline-option, locally-disabled, file-ignored, suppressed-message, useless-suppression, deprecated-pragma, use-symbolic-message-instead, """ [tool.pytest.ini_options] minversion = "7.0" addopts = "--cov --cov-report html --cov-fail-under 98" pythonpath = ["src"] [tool.coverage.run] source = ["src"] [tool.coverage.paths] source = ["src"] [tool.coverage.report] fail_under = 98 [tool.coverage.html] directory = "htmlcov" pynmeagps-1.1.7/src/000077500000000000000000000000001523640354200143165ustar00rootroot00000000000000pynmeagps-1.1.7/src/pynmeagps/000077500000000000000000000000001523640354200163215ustar00rootroot00000000000000pynmeagps-1.1.7/src/pynmeagps/__init__.py000066400000000000000000000015341523640354200204350ustar00rootroot00000000000000""" Created on 4 Mar 2021 :author: semuadmin (Steve Smith) :copyright: semuadmin © 2020 :license: BSD 3-Clause """ # pylint: disable=wrong-import-position, invalid-name from pynmeagps._version import __version__ from pynmeagps.exceptions import ( NMEAMessageError, NMEAParseError, NMEAStreamError, NMEATypeError, ) from pynmeagps.nmeahelpers import * from pynmeagps.nmeamessage import NMEAMessage from pynmeagps.nmeareader import NMEAReader from pynmeagps.nmeatypes_core import * from pynmeagps.nmeatypes_decodes import * from pynmeagps.nmeatypes_get import * from pynmeagps.nmeatypes_get_prop import * from pynmeagps.nmeatypes_poll import * from pynmeagps.nmeatypes_poll_prop import * from pynmeagps.nmeatypes_set import * from pynmeagps.nmeatypes_set_prop import * from pynmeagps.socketwrapper import SocketWrapper version = __version__ pynmeagps-1.1.7/src/pynmeagps/_version.py000066400000000000000000000002511523640354200205150ustar00rootroot00000000000000""" Release Version. Created on 4 Mar 2021 :author: semuadmin (Steve Smith) :copyright: semuadmin © 2020 :license: BSD 3-Clause """ __version__ = "1.1.7" pynmeagps-1.1.7/src/pynmeagps/exceptions.py000066400000000000000000000011711523640354200210540ustar00rootroot00000000000000""" NMEA Custom Exception Types Created on 04 Mar 2021 :author: semuadmin (Steve Smith) :copyright: semuadmin © 2020 :license: BSD 3-Clause """ class NMEAParseError(Exception): """ NMEA Parsing error. """ class NMEAStreamError(Exception): """ NMEA Streaming error. """ class NMEAMessageError(Exception): """ NMEA Undefined message class/id. Essentially a prompt to add missing payload types to UBX_PAYLOADS. """ class NMEATypeError(Exception): """ NMEA Undefined payload attribute type. Essentially a prompt to fix incorrect payload definitions to UBX_PAYLOADS. """ pynmeagps-1.1.7/src/pynmeagps/nmeahelpers.py000066400000000000000000000570701523640354200212070ustar00rootroot00000000000000""" Collection of NMEA helper methods which can be used outside the NMEAMessage or NMEAReader classes Created on 04 Mar 2021 :author: semuadmin (Steve Smith) :copyright: semuadmin © 2021 :license: BSD 3-Clause """ # pylint: disable=invalid-name import re from datetime import datetime, timedelta, timezone from math import acos, asin, atan2, cos, floor, pi, sin, sqrt from types import NoneType from typing import Literal import pynmeagps.exceptions as nme from pynmeagps.nmeatypes_core import ( BDS, DM, DT, DTL, EPOCH0_BEIDOU, EPOCH0_GAL, EPOCH0_GPS, EPOCH0_IRN, GAL, GLO, GPS, IRN, LA, LN, NMEA_MSGIDS, NMEA_MSGIDS_PROP, WGS84, WGS84_FLATTENING, WGS84_SMAJ_AXIS, ) KNOTSCONV = {"MS": 0.5144447324, "FS": 1.68781084, "MPH": 1.15078, "KMPH": 1.852001} LEAPS0 = datetime(1900, 1, 1, 0, 0, 0, tzinfo=timezone.utc) LEAPSECONDS = [ (2272060800, 10), # 1 Jan 1972 (2287785600, 11), # 1 Jul 1972 (2303683200, 12), # 1 Jan 1973 (2335219200, 13), # 1 Jan 1974 (2366755200, 14), # 1 Jan 1975 (2398291200, 15), # 1 Jan 1976 (2429913600, 16), # 1 Jan 1977 (2461449600, 17), # 1 Jan 1978 (2492985600, 18), # 1 Jan 1979 (2524521600, 19), # 1 Jan 1980 (2571782400, 20), # 1 Jul 1981 (2603318400, 21), # 1 Jul 1982 (2634854400, 22), # 1 Jul 1983 (2698012800, 23), # 1 Jul 1985 (2776982400, 24), # 1 Jan 1988 (2840140800, 25), # 1 Jan 1990 (2871676800, 26), # 1 Jan 1991 (2918937600, 27), # 1 Jul 1992 (2950473600, 28), # 1 Jul 1993 (2982009600, 29), # 1 Jul 1994 (3029443200, 30), # 1 Jan 1996 (3076704000, 31), # 1 Jul 1997 (3124137600, 32), # 1 Jan 1999 (3345062400, 33), # 1 Jan 2006 (3439756800, 34), # 1 Jan 2009 (3550089600, 35), # 1 Jul 2012 (3644697600, 36), # 1 Jul 2015 (3692217600, 37), # 1 Jan 2017 ] """ Leapsecond reference table from NTC - Updated through IERS Bulletin C69 https://hpiers.obspm.fr/iers/bul/bulc/ntp/leap-seconds.list File expires on: 28 December 2026 """ def area( lat1: float, lon1: float, lat2: float, lon2: float, radius: float = WGS84_SMAJ_AXIS / 1000, ) -> float: """ Calculate spherical area bounded by two coordinates. :param float lat1: lat1 :param float lon1: lon1 :param float lat2: lat2 :param float lon2: lon2 :param float radius: radius in km (Earth = 6378.137 km) :return: area in km² :rtype: float """ phi1, phi2 = [c * pi / 180 for c in (lat1, lat2)] return pow(radius, 2) * pi * abs(sin(phi1) - sin(phi2)) * abs(lon1 - lon2) / 180 def bearing(lat1: float, lon1: float, lat2: float, lon2: float) -> float: """ Calculate bearing between two coordinates. :param float lat1: lat1 :param float lon1: lon1 :param float lat2: lat2 :param float lon2: lon2 :return: bearing in degrees :rtype: float """ phi1, lambda1, phi2, lambda2 = [c * pi / 180 for c in (lat1, lon1, lat2, lon2)] y = sin(lambda2 - lambda1) * cos(phi2) x = cos(phi1) * sin(phi2) - sin(phi1) * cos(phi2) * cos(lambda2 - lambda1) theta = atan2(y, x) brng = (theta * 180 / pi + 360) % 360 return brng def calc_checksum(content: str) -> str: """ Calculate checksum for raw NMEA message. :param str content: NMEA message content (everything except checksum) :return: checksum as hex string :rtype: str """ cksum = 0 for sub in content: cksum ^= ord(sub) return f"{cksum:02X}" def date2str(dat: datetime.date, form: Literal["DT", "DTL", "DM"] = DT) -> str: """ Convert datetime.date to NMEA formatted string. :param datetime.date dat: date :param Literal["DT","DTL","DM"] form: date format DT = ddmmyy, DTL = ddmmyyyy, DM = mmddyy (DT) :return: NMEA formatted date string :rtype: str """ try: if form == DM: dform = "%m%d%y" elif form == DTL: dform = "%d%m%Y" else: dform = "%d%m%y" return dat.strftime(dform) except (AttributeError, TypeError, ValueError): return "" def date2utc(dates: str, form: Literal["DT", "DTL", "DM"] = DT) -> datetime.date: """ Convert NMEA Date to UTC datetime. :param str dates: NMEA date :param Literal["DT","DTL","DM"] form: date format DT = ddmmyy, DTL = ddmmyyyy, DM = mmddyy (DT) :return: UTC date YYyy:mm:dd :rtype: datetime.date """ try: if form == "DM": dform = "%m%d%y" elif form == "DTL": dform = "%d%m%Y" else: dform = "%d%m%y" utc = datetime.strptime(dates, dform) return utc.date() except (TypeError, ValueError): return "" def ddd2dmm(degrees: float, att: Literal["LA", "LN"], hpmode: bool = False) -> str: """ Convert decimal degrees to native NMEA degrees decimal minutes string (NB: standard NMEA only supports 5dp minutes precision - a high precision mode offers 7dp precision but this may not be accepted by all NMEA parsers). :param float degrees: degrees :param Literal["LA","LN"] att: 'LA' (lat) or 'LN' (lon) :param bool hpmode: high precision mode (7dp rather than 5dp) :return: degrees as (d)ddmm.mmmmm(mm) formatted string :rtype: str """ try: degrees = abs(degrees) degrees, minutes = divmod(degrees * 60, 60) degrees = int(degrees * 100) if hpmode: if att == LA: dmm = f"{degrees + minutes:.7f}".zfill(12) else: # LN dmm = f"{degrees + minutes:.7f}".zfill(13) else: if att == LA: dmm = f"{degrees + minutes:.5f}".zfill(10) else: # LN dmm = f"{degrees + minutes:.5f}".zfill(11) return dmm except (TypeError, ValueError): return "" def deg2dmm(degrees: float, att: Literal["LA", "LN"]) -> str: """ Convert decimal degrees to degrees decimal minutes string e.g. '51°20.76′S'. :param float degrees: degrees :param Literal["LA","LN"] att: 'LA' (lat) or 'LN' (lon) :return: degrees as dm.m formatted string :rtype: str """ try: negative = degrees < 0 degrees = abs(degrees) degrees, minutes = divmod(degrees * 60, 60) if negative: sfx = "S" if att == LA else "W" else: sfx = "N" if att == LA else "E" return f"{int(degrees)}\u00b0{round(minutes,7)}\u2032{sfx}" except (TypeError, ValueError): return "" def deg2dms(degrees: float, att: Literal["LA", "LN"]) -> str: """ Convert decimal degrees to degrees minutes seconds string e.g. '51°20′45.6″N' :param float degrees: degrees :param Literal["LA","LN"] att: 'LA' (lat) or 'LN' (lon) :return: degrees as d.m.s formatted string :rtype: str """ try: negative = degrees < 0 degrees = abs(degrees) minutes, seconds = divmod(degrees * 3600, 60) degrees, minutes = divmod(minutes, 60) if negative: sfx = "S" if att == LA else "W" else: sfx = "N" if att == LA else "E" return f"{int(degrees)}\u00b0{int(minutes)}\u2032{round(seconds,5)}\u2033{sfx}" except (TypeError, ValueError): return "" def dmm2ddd(pos: str) -> float | str: """ Convert NMEA lat/lon string to (unsigned) decimal degrees. :param str pos: (d)ddmm.mmmmm :return: pos as decimal degrees, or "" if invalid :rtype: float | str """ try: dp = pos.find(".") if dp < 4: raise ValueError() posdeg = abs(float(pos[0 : dp - 2])) posmin = float(pos[dp - 2 :]) return round((posdeg + posmin / 60), 10) except (TypeError, ValueError): return "" def dms2deg( dmsstr: str, rnd: int = 7, ) -> float: """ Convert degrees, minutes, seconds string to decimal degrees. Expects degrees and (optional) minutes and seconds to be delimited by non-numeric char, e.g. '51°20′45.6″N' -> 51.346 '51°30.0′' -> 51.5 :param str dmsstr: d,m,s string :param int rnd: decimal places (7) :return: degrees as d.dd :rtype: float :raises: ValueError if invalid dms string format """ dmsstr = dmsstr.replace(" ", "") if ( re.match( r"^([\d.]+[^\d.]{1}){1}([\d.]+[^\d.]{1})?([\d.]+[^\d.]{1})?[NSEW]?$", dmsstr ) is None ): raise ValueError(f"Invalid dms string format {dmsstr}") sign = -1 if dmsstr.find("S") >= 0 or dmsstr.find("W") >= 0 else 1 dms = re.split(r"[^\d.]", dmsstr, maxsplit=3) ddd = 0.0 for x in range(len(dms) - 1): ddd += 0.0 if dms[x] == "" else float(dms[x]) / pow(60, x) return round(ddd * sign, rnd) def ecef2llh( x: float, y: float, z: float, a: float = WGS84_SMAJ_AXIS, f: float = WGS84_FLATTENING, ) -> tuple: """ Convert ECEF coordinates to geodetic (LLH) using Olson algorithm. Olson, D. K. (1996). Converting Earth-Centered, Earth-Fixed Coordinates to Geodetic Coordinates. IEEE Transactions on Aerospace and Electronic Systems, 32(1), 473-476. https://doi.org/10.1109/7.481290 :param float x: X coordinate :param float y: Y coordinate :param float z: Z coordinate :param float a: semi-major axis (6378137.0 for WGS84) :param float f: flattening (298.257223563 for WGS84) :return: tuple of (lat, lon, ellipsoidal height in m) as floats :rtype: tuple """ # pylint: disable=too-many-locals # commented default values are for WGS84 spheroid f = 1 / f e2 = f * (2 - f) # 6.6943799901377997e-3 a1 = a * e2 # 4.2697672707157535e4 a2 = a1 * a1 # 1.8230912546075455e9 a3 = a1 * e2 / 2 # 1.8230912546075455e9 a4 = 2.5 * a2 # 4.5577281365188637e9 a5 = a1 + a3 # 4.2840589930055659e4 a6 = 1 - e2 # 9.9330562000986220e-1 zp = abs(z) w2 = x * x + y * y w = sqrt(w2) z2 = z * z r2 = w2 + z2 r = sqrt(r2) # algorithm inaccurate near Earth's core # so nominal value returned if r < 100000.0: return 0.0, 0.0, -1.0e7 lon = atan2(y, x) s2 = z2 / r2 c2 = w2 / r2 u = a2 / r v = a3 - a4 / r if c2 > 0.3: s = (zp / r) * (1.0 + c2 * (a1 + u + s2 * v) / r) lat = asin(s) ss = s * s c = sqrt(1.0 - ss) else: c = (w / r) * (1.0 - s2 * (a5 - u - c2 * v) / r) lat = acos(c) ss = 1.0 - c * c s = sqrt(ss) g = 1.0 - e2 * ss rg = a / sqrt(g) rf = a6 * rg u = w - rg * c v = zp - rf * s f = c * u + s * v m = c * v - s * u p = m / (rf / g + f) lat = lat + p height = f + m * p / 2.0 if z < 0.0: lat = -lat lat, lon = [c * 180 / pi for c in (lat, lon)] return lat, lon, height def generate_checksum(talker: str, msgID: str, payload: list) -> str: """ Generate checksum for new NMEA message. :param str talker: talker e.g. "GN" :param str msgID: msgID e.g. "GLL" :param list payload: payload as list :return: checksum as hex string :rtype: str """ content = talker + msgID for field in payload: content += f",{field}" return calc_checksum(content) def get_parts(message: object) -> tuple: """ Get content, talker, msgid, payload and checksum of raw NMEA message. :param object message: entire message as bytes or string :return: tuple of (content, talker, msgID, payload as list, checksum) :rtype: tuple :raises: NMEAMessageError (if message is badly formed) """ try: if isinstance(message, bytes): message = message.decode("utf-8") content, cksum = message.strip("$\r\n").split("*", 1) hdr, *payload = content.split(",") s = 1 if hdr[:1] == "P" else 2 talker, msgid = hdr[:s], hdr[s:] return content, talker, msgid, payload, cksum except Exception as err: raise nme.NMEAMessageError(f"Badly formed message {message}") from err def groupsize(**kwargs) -> int: """ Get number of grouped items in generated payload arguments. e.g. {svid_01=7, svid_02=8, svid_03=24} -> 3 :param dict kwargs: payload arguments :return: group size :rtype: int """ idx = 0 for a in kwargs: ai = a.split("_", 1) if len(ai) > 1: idx = max(idx, int(ai[1])) return idx def haversine( lat1: float, lon1: float, lat2: float, lon2: float, radius: float = WGS84_SMAJ_AXIS / 1000, ) -> float: """ Calculate spherical distance in km between two coordinates using haversine formula. NB suitable for coordinates greater than around 50m apart. For smaller separations, use the planar approximation formula. :param float lat1: lat1 :param float lon1: lon1 :param float lat2: lat2 :param float lon2: lon2 :param float radius: radius in km (Earth = 6378.137 km) :return: spherical distance in km :rtype: float """ phi1, lambda1, phi2, lambda2 = [c * pi / 180 for c in (lat1, lon1, lat2, lon2)] dist = radius * acos( cos(phi2 - phi1) - cos(phi1) * cos(phi2) * (1 - cos(lambda2 - lambda1)) ) return dist def hex2str(num: int, padding: int = 0) -> str: """ Convert hex integer to padded or unpadded string format, as used by some proprietary NMEA message types. :param int num: hexadecimal integer :param padding: no of padded digits (0) :return: integer as string :rtype: str """ pad = f"{padding:02d}" if padding else "" return f"{num:{pad}x}".upper() def knots2spd(knots: float, unit: str = "MS") -> float: """ Convert speed in knots to speed in specified units. :param float knots: knots :param unit str: 'MS' (default), 'FS', MPH', 'KMPH' :return: speed in m/s, feet/s, mph or kmph :rtype: float """ try: return knots * KNOTSCONV[unit.upper()] except KeyError as err: raise KeyError( f"Invalid conversion unit {unit.upper()} - must be in {list(KNOTSCONV.keys())}." ) from err except TypeError as err: raise TypeError( f"Invalid knots value {knots} - must be float or integer." ) from err def latlon2dmm(lat: float, lon: float) -> tuple[str, str]: """ Converts decimal lat/lon tuple to degrees decimal minutes. :param float lat: lat :param float lon: lon :return: (lat,lon) in d.mm.m format :rtype: tuple[str, str] """ latd = deg2dmm(lat, LA) lond = deg2dmm(lon, LN) return latd, lond def latlon2dms(lat: float, lon: float) -> tuple[str, str]: """ Converts decimal lat/lon tuple to degrees minutes seconds. :param float lat: lat :param float lon: lon :return: (lat,lon) in d.m.s. format :rtype: tuple[str, str] """ latd = deg2dms(lat, LA) lond = deg2dms(lon, LN) return latd, lond def leapsecond(dat: datetime, gnss: Literal["G", "E", "C", "J", "I"] = GPS) -> int: """ Get leapsecond offset effective at given GNSS epoch and time system. If supplied datetime is timezone naive, UTC will be inferred. G = GPS, E = Galileo, C = Beidou, J = QZSS, I = IRNSS (NavIC) Note: GLONASS is a non-continuous time system which already takes into account UTC leapseconds. Refer to LEAPSECONDS reference table. :param datetime.datetime dat: effective date :param Literal["G","E","C","J","I"] gnss: GNSS time system e.g. "G" :return: leapsecond offset :rtype: int """ def totsec(dt) -> int: return (dt - LEAPS0).total_seconds() if dat.tzinfo is None: dat = dat.replace(tzinfo=timezone.utc) ep0 = EPOCH0_BEIDOU if gnss == BDS else EPOCH0_GPS if totsec(dat) < LEAPSECONDS[0][0]: return 0 dts = [] for dt in (dat, ep0): dts.append(max((val for key, val in LEAPSECONDS if key <= totsec(dt)))) return dts[0] - dts[1] def llh2ecef( lat: float, lon: float, height: float, a: float = WGS84_SMAJ_AXIS, f: float = WGS84_FLATTENING, ) -> tuple: """ Convert geodetic coordinates (LLH) to ECEF. :param float lat: lat in degrees :param float lon: lon on degrees :param float height: ellipsoidal height in metres :param float a: semi-major axis (6378137.0 for WGS84) :param float f: flattening (298.257223563 for WGS84) :return: tuple of ECEF (X, Y, Z) as floats :rtype: tuple """ lat, lon = [c * pi / 180 for c in (lat, lon)] f = 1 / f e2 = f * (2 - f) a2 = a**2 b2 = a2 * (1 - e2) N = a / sqrt(1 - e2 * sin(lat) ** 2) x = (N + height) * cos(lat) * cos(lon) y = (N + height) * cos(lat) * sin(lon) z = ((b2 / a2) * N + height) * sin(lat) return x, y, z def llh2iso6709(lat: float, lon: float, alt: float, crs: str = WGS84) -> str: """ Convert decimal degrees and alt to ISO6709 format e.g. "+27.5916+086.5640+8850CRSWGS_84/". :param float lat: latitude :param float lon: longitude :param float alt: altitude (hMSL) :param float crs: coordinate reference system (default = WGS_84) :return: position in ISO6709 format :rtype: str """ lati, loni, alti = ["-" if c < 0 else "+" for c in (lat, lon, alt)] return f"{lati}{abs(lat)}{loni}{abs(lon)}{alti}{alt}CRS{crs}/" def msgdesc(msgID: str) -> str: """ Return descriptive string for NMEA msgId. :param msgID str: message ID e.g. 'GGA' :return: description of message :rtype: str """ # pylint: disable=invalid-name if msgID in NMEA_MSGIDS: return NMEA_MSGIDS[msgID] if msgID in NMEA_MSGIDS_PROP: return NMEA_MSGIDS_PROP[msgID] return f"Unknown msgID {msgID}" def planar( lat1: float, lon1: float, lat2: float, lon2: float, radius: float = WGS84_SMAJ_AXIS, ) -> float: """ Calculate planar distance between two coordinates using planar approximation formula. NB suitable for coordinates less than around 50m apart. For larger separations, use the haversine great circle formula. :param float lat1: lat1 :param float lon1: lon1 :param float lat2: lat2 :param float lon2: lon2 :param float radius: radius in m (Earth = 6378137 m) :return: planar distance in m :rtype: float """ phi1, lambda1, phi2, lambda2 = [c * pi / 180 for c in (lat1, lon1, lat2, lon2)] dlambda = (lambda2 - lambda1) * cos(phi1) dphi = phi2 - phi1 dist = radius * sqrt(dlambda * dlambda + dphi * dphi) return dist def time2str(tim: datetime) -> str: """ Convert datetime.time to NMEA formatted string. :param datetime.time tim: time :return: NMEA formatted time string hhmmss.ss or "" if invalid :rtype: str """ try: return tim.strftime("%H%M%S.%f")[0:9] except (AttributeError, TypeError, ValueError): return "" def time2utc(times: str) -> datetime | str: """ Convert NMEA Time to UTC datetime. :param str times: NMEA time hhmmss.ss :return: UTC time hh:mm:ss.ss or "" if invalid :rtype: datetime.time | str """ try: if len(times) == 6: # decimal seconds is omitted times = times + ".00" utc = datetime.strptime(times, "%H%M%S.%f") return utc.time() except (TypeError, ValueError): return "" def utc2wnotow( utc: datetime | NoneType = None, gnss: Literal["G", "E", "C", "J", "I"] = GPS, modwno: bool = True, ) -> tuple[int, int, int]: """ Get Week number (wno), Time of Week (tow) in milliseconds and leapsecond offset for given GNSS UTC epoch and time system. G = GPS, E = Galileo, C = Beidou, J = QZSS, I = IRNSS (NavIC) Note: GLONASS does not use GPS-style wno or tow. If `utc` is None, will default to current UTC datetime. :param datetime | NoneType utc: UTC epoch :param Literal["G","E","C","J","I"] = GPS) gnss: \ GNSS time system (GPS) :param bool modwno: True = modular wno, False = continuous wno :return: wno, tow, leapsecond :rtype: tuple[int, int, int] """ if utc is None: utc = datetime.now(tz=timezone.utc) if utc.tzinfo is None: utc = utc.replace(tzinfo=timezone.utc) if gnss == BDS: ep0 = EPOCH0_BEIDOU rollover = 8192 elif gnss == GAL: ep0 = EPOCH0_GAL rollover = 4096 elif gnss == IRN: ep0 = EPOCH0_IRN rollover = 1024 else: ep0 = EPOCH0_GPS rollover = 1024 ls = 0 if gnss == GLO else leapsecond(utc, gnss) ts = ((utc - ep0).total_seconds() + ls) * 1000 wno = floor((utc - ep0).days / 7) wnom = wno % rollover tow = int(ts - wno * 604800000) return wnom if modwno else wno, tow, ls def wnotow2utc( wno: int, tow: int, ls: int | NoneType = None, gnss: Literal["G", "E", "C", "J", "I"] = GPS, autoroll: bool = False, modwno: bool = True, ) -> datetime: """ Convert week number and seconds of week (expressed as milliseconds) for given GNSS epoch to UTC time. G = GPS, E = Galileo, C = Beidou, J = QZSS, I = IRNSS (NavIC) Note: GLONASS does not use GPS-style wno or tow. If `ls` is None (the default), leapsecond offset will be automatically derived from NTC reference table. If `autoroll` is True, rolls over modular wno to LATEST date less than current date. e.g. if current date is 2026-04-17: wno=366, tow=381600000, gnss=GPS gives... - autoroll on: 2026-04-16 09:59:42+00:00 - autoroll off: 1987-01-15 09:59:56+00:00 wno=368, tow=610000000, gnss=GPS gives... - autoroll on: 2006-09-17 01:26:26+00:00 - autoroll off: 1987-02-01 01:26:36+00:00 :param int wno: week number (modular or mon-modular) :param int tow: time of week in milliseconds (mod 604,800,000) :param int | NoneType ls: leapsecond offset (will be derived if None) (None) :param Literal["G","E","C","J","I"] = GPS) gnss: GNSS time system (GPS) :param bool autoroll: automatic rollover (False) :param bool modwno: True = modular wno, False = continuous wno :return: GNSS epoch as UTC datetime :rtype: datetime """ if gnss == BDS: ep0 = EPOCH0_BEIDOU rollover = 8192 elif gnss == GAL: ep0 = EPOCH0_GAL rollover = 4096 elif gnss == IRN: ep0 = EPOCH0_IRN rollover = 1024 else: ep0 = EPOCH0_GPS rollover = 1024 wno = wno % rollover if modwno else wno tow %= 604800000 current = datetime.now(timezone.utc) i = 0 while True: utc = ep0 + timedelta(days=(wno + (i * rollover)) * 7, milliseconds=tow) if gnss != GLO: # apply leapsecond offset lps = leapsecond(utc, gnss) if ls is None else ls utc -= timedelta(seconds=lps) if not autoroll: break if utc + timedelta(days=rollover * 7) > current: break i += 1 return utc pynmeagps-1.1.7/src/pynmeagps/nmeamessage.py000066400000000000000000000733371523640354200211750ustar00rootroot00000000000000""" Main NMEA GNSS/GPS Message Protocol Class. Created on 04 Mar 2021 :author: semuadmin (Steve Smith) :copyright: semuadmin © 2021 :license: BSD 3-Clause """ # pylint: disable=invalid-name, too-many-instance-attributes, too-many-positional-arguments import struct from datetime import datetime, timezone from logging import getLogger from types import NoneType from typing import Any, Literal import pynmeagps.exceptions as nme import pynmeagps.nmeatypes_core as nmt import pynmeagps.nmeatypes_get as nmg import pynmeagps.nmeatypes_get_prop as nmgp import pynmeagps.nmeatypes_poll as nmp import pynmeagps.nmeatypes_poll_prop as nmpp import pynmeagps.nmeatypes_set as nms import pynmeagps.nmeatypes_set_prop as nmsp from pynmeagps.nmeahelpers import ( date2str, date2utc, ddd2dmm, dmm2ddd, generate_checksum, groupsize, time2str, time2utc, ) class NMEAMessage: """NMEA GNSS/GPS Message Class.""" def __init__( self, talker: str, msgID: str, msgmode: Literal[0, 1, 2], hpnmeamode: bool = False, validate: int = nmt.VALCKSUM, userdefined: dict | NoneType = None, checksum: str | NoneType = None, **kwargs, ): """Constructor. If 'payload' is passed as a keyword arg, this is taken to contain the entire message content as a list of string values; any other keyword args are ignored. Otherwise, any individual attributes passed as keyword args will be set to the value provided, all others will be assigned a nominal value according to type. :param str talker: message talker e.g. "GP" or "P" :param str msgID: message ID e.g. "GGA" :param int msgmode: mode (0=GET, 1=SET, 2=POLL) :param bool hpnmeamode: high precision lat/lon mode (7dp rather than 5dp) (False) :param int validate: VALNONE (0), VALCKSUM (1), VALMSGID (2), (can be OR'd) (1) :param dict userdefined: user-defined payload definition dictionary (None) :param str | NoneType checksum: checksum from incoming message or None if creating new message (None) :param kwargs: keyword arg(s) representing all or some payload attributes :raises: NMEAMessageError """ # object is mutable during initialisation only super().__setattr__("_immutable", False) self._logger = getLogger(__name__) self._validate = validate self._userdefined = {} if userdefined is None else userdefined if msgmode not in (0, 1, 2): raise nme.NMEAMessageError( f"Invalid msgmode {msgmode} - must be 0, 1 or 2." ) if talker not in nmt.NMEA_TALKERS: if self._validate & nmt.VALMSGID: raise nme.NMEAMessageError(f"Unknown talker {talker}.") if msgID in nmt.NMEA_MSGIDS: self._defsource = nmt.DEF_STND # standard elif msgID in nmt.NMEA_MSGIDS_PROP or msgID in nmt.NMEA_PREFIX_PROP: self._defsource = nmt.DEF_PROP # proprietary elif msgID in self._userdefined: self._defsource = nmt.DEF_USER # user-defined else: self._defsource = nmt.DEF_UNKN # unrecognised if self._validate & nmt.VALMSGID: raise nme.NMEAMessageError( f"Unknown msgID {talker}{msgID}, msgmode {('GET','SET','POLL')[msgmode]}." ) self._mode = msgmode # high precision NMEA mode returns NMEA lat/lon to 7dp rather than 5dp self._hpnmeamode = hpnmeamode self._talker = talker self._msgID = msgID self._checksum = checksum # flag to show message is being streamed rather than generated self._streaming = "payload" in kwargs self._do_attributes(**kwargs) self._immutable = True # once initialised, object is immutable def _do_attributes(self, **kwargs): """ Populate NMEAMessage from named attribute keywords. Where a named attribute is absent, set to a nominal value (zeros or blanks). :param kwargs: optional content key/value pairs :raises: UBXTypeError """ pindex = 0 # payload index gindex = [] # (nested) grouped attribute indices key = "" try: self._payload = kwargs.get("payload", []) # self._checksum = kwargs.get("checksum", None) pdict = self._get_dict(**kwargs) # get payload definition dict if pdict is None: # definition not yet implemented if "payload" in kwargs: self._set_attribute_nominal(kwargs["payload"]) return for key in pdict.keys(): # process each attribute in dict pindex, gindex = self._set_attribute( pindex, pdict, key, gindex, **kwargs ) # generate checksum for newly-created message if self._checksum is None: self._checksum = generate_checksum( self._talker, self._msgID, self._payload ) except ( AttributeError, OverflowError, struct.error, TypeError, ValueError, ) as err: raise nme.NMEATypeError( f"Incorrect type for attribute {key} in msgID {self._msgID}." ) from err def _set_attribute( self, pindex: int, pdict: dict, key: str, gindex: list, **kwargs ) -> tuple: """ Recursive routine to set individual or grouped payload attributes. :param int pindex: payload index :param dict pdict: dict representing payload definition :param str key: attribute keyword :param list gindex: repeating group index array :param kwargs: optional payload key/value pairs :return: (pindex, gindex[]) :rtype: tuple """ att = pdict[key] # get attribute type if isinstance(att, tuple): # repeating group of attributes pindex, gindex = self._set_attribute_group(att, pindex, gindex, **kwargs) else: # single attribute pindex = self._set_attribute_single(att, pindex, key, gindex, **kwargs) return (pindex, gindex) def _set_attribute_group( self, att: tuple[int, dict], pindex: int, gindex: list, **kwargs ) -> tuple: """ Process (nested) group of attributes. :param tuple att: attribute group - tuple of (num repeats, attribute dict) :param int pindex: payload index :param list gindex: repeating group index array :param kwargs: optional payload key/value pairs :return: (pindex, gindex[]) :rtype: tuple """ gindex.append(0) # add a (nested) group index numr, attd = att # number of repeats, group dictionary # derive or retrieve number of items in group if isinstance(numr, int): # fixed number of repeats rng = numr elif numr == "None": # indeterminate number of repeats pindexend = 0 # may need tweaking if self._streaming: rng = self._calc_num_repeats(attd, self._payload, pindex, pindexend) else: rng = groupsize(**kwargs) else: # number of repeats is defined in named attribute rng = getattr(self, numr) # recursively process each group attribute, # incrementing the payload index and group index as we go for i in range(rng): gindex[-1] = i + 1 for key1 in attd.keys(): pindex, gindex = self._set_attribute( pindex, attd, key1, gindex, **kwargs ) gindex.pop() # remove this (nested) group index return (pindex, gindex) def _set_attribute_single( self, att: str, pindex: int, key: str, gindex: list, **kwargs ) -> int: """ Set individual attribute value. :param str att: attribute type e.g. 'NU' :param int pindex: payload index :param str key: attribute keyword :param list gindex: repeating group index array :param kwargs: optional payload key/value pairs :return: pindex :rtype: int """ # pylint: disable=no-member, access-member-before-definition, attribute-defined-outside-init # if attribute is part of a (nested) repeating group, suffix name with group index keyr = key for i in gindex: # one index for each nested level if i > 0: keyr += f"_{i:02d}" try: # all attribute values have been provided if self._streaming: # remove group delimiters in proprietary PSSNSNC message if self.identity == "PSSNSNC": self._payload[pindex] = ( self._payload[pindex].replace("[", "").replace("]", "") ) val = self._payload[pindex] val = self.str2val(val, att) # some attribute values have been provided, # the rest will be set to a nominal value else: if att == nmt.LND and hasattr(self, "lon"): if isinstance(self.lon, (int, float)): val = "W" if self.lon < 0 else "E" else: # pragma: no cover val = "E" elif att == nmt.LAD and hasattr(self, "lat"): if isinstance(self.lat, (int, float)): val = "S" if self.lat < 0 else "N" else: # pragma: no cover val = "N" else: val = kwargs.get(keyr, self.nomval(att, self.msgmode)) vals = self.val2str(val, att, self._hpnmeamode) self._payload.append(vals) except ( IndexError ): # probably just an older device missing NMEA <=4.10 dict attributes return pindex setattr(self, keyr, val) # add attribute to NMEAMessage object if self._streaming: # override sign of lat/lon according to NS and EW values if att == nmt.LND and hasattr(self, "lon"): if isinstance(self.lon, (int, float)): self.lon = -abs(self.lon) if val == "W" else abs(self.lon) elif att == nmt.LAD and hasattr(self, "lat"): if isinstance(self.lat, (int, float)): self.lat = -abs(self.lat) if val == "S" else abs(self.lat) pindex += 1 # move on to next attribute in payload definition return pindex def _set_attribute_nominal(self, payload: list): """ Set nominal attributes for unrecognised NMEA sentence types. :param list payload: payload as list """ for i, fld in enumerate(payload): setattr(self, f"field_{i+1:02d}", fld) def _get_dict(self, **kwargs) -> dict | NoneType: """ Get payload dictionary. :return: dictionary representing payload definition :rtype: dict | NoneType """ dic = None key = self.msgID try: if key in nmt.NMEA_PREFIX_PROP: # proprietary, first element is msgId if self._streaming: if key == "ASHR" and self._payload[0][1].isdigit(): pass # exception for PASHR pitch and roll sentence without msgId else: key += self._payload[0] elif "msgId" in kwargs: key += kwargs["msgId"] else: raise nme.NMEAMessageError( f"P{key} message definitions must " "include payload or msgId keyword arguments." ) key = key.upper() if self._defsource == nmt.DEF_PROP: # proprietary dic = self._get_dict_prop(key, **kwargs) elif self._defsource == nmt.DEF_USER: # user defined dic = self._userdefined[key] else: # standard if self._mode == nmt.POLL: dic = nmp.NMEA_PAYLOADS_POLL[key] elif self._mode == nmt.SET: # pragma: no cover dic = nms.NMEA_PAYLOADS_SET[key] else: dic = nmg.NMEA_PAYLOADS_GET[key] except KeyError as err: # unknown msgid erm = f"Unknown msgID {key} msgmode {('GET', 'SET', 'POLL')[self._mode]}." if self._validate & nmt.VALMSGID: raise nme.NMEAMessageError(erm) from err return dic def _get_dict_prop(self, key: str, **kwargs) -> dict: """ Get payload dictionary for proprietary message types. :param str key: msgid :return: dictionary representing payload definition :rtype: dict """ if key == "QTMCFGGEOFENCE": key = self._get_dict_qtmcfggeofence(key, self._mode, **kwargs) elif key == "QTMCFGMSGRATE": key = self._get_dict_qtmcfgmsgrate(key, self._mode, **kwargs) elif key == "QTMCFGPPS": key = self._get_dict_qtmcfgpps(key, self._mode, **kwargs) elif key == "QTMCFGSAT": key = self._get_dict_qtmcfgsat(key, self._mode, **kwargs) elif key == "QTMCFGUART": key = self._get_dict_qtmcfguart(key, self._mode, **kwargs) elif key == "QTMCLRMSG": key = self._get_dict_qtmclrmsg(key, self._mode, **kwargs) elif key == "QTMLSTMSG": key = self._get_dict_qtmlstmsg(key, self._mode, **kwargs) elif key == "QTMSN": key = self._get_dict_qtmsn(key, self._mode, **kwargs) elif key == "STMDRSENMSG": key = self._get_dict_stmdrsenmsg(key, self._mode, **kwargs) elif key[0:3] == "QTM": key = self._get_dict_qtmacknak(key, self._mode) if self._mode == nmt.POLL: return nmpp.NMEA_PAYLOADS_POLL_PROP[key] if self._mode == nmt.SET: return nmsp.NMEA_PAYLOADS_SET_PROP[key] return nmgp.NMEA_PAYLOADS_GET_PROP[key] def _get_dict_qtmcfguart(self, key: str, mode: int, **kwargs) -> str: """ Get payload dictionary for proprietary Quectel QTMCFGUART command and query variants. :param str key: msgid :param int mode: msgmode 1/2 :return: key of payload definition :rtype: str """ lp = len(self._payload) py = self._streaming pt = "portid" in kwargs if mode == nmt.SET: bd = "baudrate" in kwargs db = "databit" in kwargs if (py and lp == 2) or (not py and not pt and bd and not db): key += "_CURRBAUD" elif (py and lp == 3) or (not py and pt and bd and not db): key += "_BAUD" elif (py and lp == 6) or (not py and not pt and bd and db): key += "_CURR" elif mode == nmt.POLL: if (py and lp == 1) or (not py and not pt): key += "_CURR" return key def _get_dict_qtmclrmsg(self, key: str, mode: int, **kwargs) -> str: """ Get payload dictionary for proprietary Quectel QTMCLRMSG command and query variants. :param str key: msgid :param int mode: msgmode 1/2 :return: key of payload definition :rtype: str """ lp = len(self._payload) py = self._streaming pt = "porttype" in kwargs pi = "portid" in kwargs if mode == nmt.SET: if (py and lp == 0) or (not py and not pt and not pi): key += "_CURR" return key def _get_dict_qtmlstmsg(self, key: str, mode: int, **kwargs) -> str: """ Get payload dictionary for proprietary Quectel QTMLSTMSG command and query variants. :param str key: msgid :param int mode: msgmode 1/2 :return: key of payload definition :rtype: str """ lp = len(self._payload) py = self._streaming pt = "porttype" in kwargs pi = "portid" in kwargs if mode == nmt.POLL: if (py and lp == 0) or (not py and not pi and not pt): key += "_CURR" return key def _get_dict_qtmcfgmsgrate(self, key: str, mode: int, **kwargs) -> str: """ Get payload dictionary for proprietary Quectel QTMCFGMSGRATE command and query variants. :param str key: msgid :param int mode: msgmode 1/2 :return: key of payload definition :rtype: str """ lp = len(self._payload) py = self._streaming mv = "msgver" in kwargs pt = "porttype" in kwargs if mode in (nmt.SET, nmt.GET): if (py and lp == 3) or (not py and not pt and not mv): key += "_NOVER" elif (py and lp == 5) or (not py and pt and not mv): key += "_INTFNOVER" elif (py and lp == 6) or (not py and pt and mv): key += "_INTF" elif mode == nmt.POLL: if (py and lp == 2) or (not py and not pt and not mv): key += "_NOVER" elif (py and lp == 4) or (not py and pt and not mv): key += "_INTFNOVER" elif (py and lp == 5) or (not py and pt and mv): key += "_INTF" return key def _get_dict_qtmcfgpps(self, key: str, mode: int, **kwargs) -> str: """ Get payload dictionary for proprietary Quectel QTMCFGPPS command and query variants. :param str key: msgid :param int mode: msgmode 1/2 :return: key of payload definition :rtype: str """ lp = len(self._payload) py = self._streaming if mode == nmt.SET: if (py and lp == 3) or (not py and kwargs.get("enable", 1) == 0): key += "_DIS" return key def _get_dict_qtmcfgsat(self, key: str, mode: int, **kwargs) -> str: """ Get payload dictionary for proprietary Quectel QTMCFGSAT command and query variants. :param str key: msgid :param int mode: msgmode 1/2 :return: key of payload definition :rtype: str """ lp = len(self._payload) py = self._streaming mh = "maskhigh" in kwargs if mode == nmt.SET: if (py and lp == 4) or (not py and not mh): key += "_LOW" elif mode == nmt.GET: if (py and lp == 4) or (not py and not mh): key += "_LOW" elif lp in (1, 2): key = self._get_dict_qtmacknak(key, mode) return key def _get_dict_qtmcfggeofence(self, key: str, mode: int, **kwargs) -> str: """ Get payload dictionary for proprietary Quectel QTMCFGGEOFENCE command and query variants. :param str key: msgid :param int mode: msgmode 1/2 :return: key of payload definition :rtype: str """ lp = len(self._payload) py = self._streaming l1 = "lon1" in kwargs if mode == nmt.SET: if (py and lp == 13) or (not py and l1): key += "_POLY" elif (py and lp == 3) or (not py and kwargs.get("geofencemode", 1) == 0): key += "_DIS" elif mode == nmt.GET: if (py and lp == 13) or (not py and l1): key += "_POLY" elif lp in (1, 2): key = self._get_dict_qtmacknak(key, mode) return key def _get_dict_qtmsn(self, key: str, mode: int, **kwargs) -> str: """ Get payload dictionary for proprietary Quectel QTMSN. (bug in LG580P firmware - seems to transpose status field?) :param str key: msgid :param int mode: msgmode 1/2 :return: key of payload definition :rtype: str """ py = self._streaming if mode == nmt.GET: if py and kwargs["payload"][0].isnumeric(): key += "_ALT" return key def _get_dict_stmdrsenmsg( self, key: str, mode: int, **kwargs ) -> str: # pylint: disable=unused-argument """ Get payload dictionary for proprietary Quectel PSTMDRSENMSG variants. :param str key: msgid :param int mode: msgmode 1/2 :return: key of payload definition :rtype: str """ py = self._streaming mt = "msgtype" in kwargs msgtype = "" if py: msgtype = self._payload[0] elif not py and mt: msgtype = kwargs["msgtype"] key += f"_{msgtype}" return key def _get_dict_qtmacknak(self, key: str, mode: int) -> str: """ Get payload dictionary for proprietary Quectel command response variants. :param str key: msgid :param int mode: msgmode 1/2 :return: key of payload definition :rtype: str """ lp = len(self._payload) if mode == nmt.GET: if lp == 1 and self._payload[0] == "OK": key = "QTMACK" elif lp == 2 and self._payload[0] == "ERROR": key = "QTMNAK" return key def _calc_num_repeats( self, attd: dict, payload: list, pindex: int, pindexend: int = 0 ) -> int: """ Deduce number of items in repeating group. :param dict attd: grouped attribute dictionary :param list payload : content as list :param int pindex: number of payload attributes before group :param int pindexend: number of payload attributes after group :return: number of repeats :rtype: int """ lenpayload = len(payload) - pindex - pindexend lengroup = len(attd) return int(lenpayload / lengroup) def __str__(self) -> str: """ Human readable representation. :return: human readable representation :rtype: str """ stg = f" str: """ Machine readable representation. eval(repr(obj)) = obj :return: machine readable representation :rtype: str """ return ( f"NMEAMessage('{self._talker}','{self._msgID}', " f"{self._mode}, payload={self._payload})" ) def __setattr__(self, name, value): """ Override setattr to make object immutable after instantiation. :param str name: attribute name :param object value: attribute value :raises: NMEAMessageError """ if self._immutable: raise nme.NMEAMessageError( f"Object is immutable. Updates to {name} not permitted after initialisation." ) super().__setattr__(name, value) def serialize(self) -> bytes: """ Serialize message. :return: serialized output :rtype: bytes """ output = f"${self._talker}{self._msgID}" for att in self._payload: output += "," + att output += f"*{self._checksum}\r\n" return output.encode("utf-8") # convert str to bytes @property def identity(self) -> str: """ Identity getter. :return: message identity e.g. GNGSA :rtype: str """ # pylint: disable=no-member if ( self._talker == "P" and self._msgID in nmt.NMEA_PREFIX_PROP and hasattr(self, "msgId") ): return self._talker + self._msgID + self.msgId return self._talker + self._msgID @property def talker(self) -> str: """ Talker getter. :return: talker e.g. GN :rtype: str """ return self._talker @property def msgID(self) -> str: """ Message id getter. :return: message id e.g. GSA :rtype: str """ return self._msgID @property def msgmode(self) -> int: """ Message mode getter. :return: message mode :rtype: int """ return self._mode @property def payload(self) -> list: """ Payload getter. :return: raw payload as list of strings :rtype: list """ return self._payload @property def checksum(self) -> str | NoneType: """ Checksum getter. :return: checksum as hex string :rtype: str | NoneType """ return self._checksum @staticmethod def str2val(vals: str, att: str) -> Any: """ Convert NMEA string to typed value (this is the format that will be available to end users). :param str vals: attribute value in NMEA protocol format :param str att: attribute type e.g. 'DE' :return: attribute value :rtype: Any :raises: MMEATypeError """ val = vals if att in (nmt.CH, nmt.ST, nmt.LAD, nmt.LND, nmt.QS): pass elif att == nmt.HX: val = vals elif att == nmt.DE: # decimal if vals != "": val = float(vals) elif att in (nmt.DT, nmt.DM): # date val = date2utc(vals, att) elif att == nmt.IN: # integer if vals != "": val = int(vals) elif att in (nmt.LA, nmt.LN): # lat/lon (d)ddmm.mmmmm(mm) val = dmm2ddd(vals) elif att == nmt.TM: # time hhmmss.ss val = time2utc(vals) else: raise nme.NMEATypeError(f"Unknown attribute type {att}.") return val @staticmethod def val2str(val, att: str, hpmode: bool = False) -> str: """ Convert typed value to NMEA string (this is the format used internally by the NMEA protocol). :param object val: typed attribute value :param str att: attribute type e.g. 'IN' :param bool hpmode: high precision lat/lon mode (7dp rather than 5dp) :return: attribute value in NMEA protocol format :rtype: str :raises: NMEATypeError """ if att in (nmt.CH, nmt.ST, nmt.LAD, nmt.LND, nmt.QS): vals = str(val) elif att == nmt.HX: vals = str(val) elif att == nmt.DE: vals = str(val) elif att == nmt.IN: vals = str(val) elif att in (nmt.LA, nmt.LN): vals = ddd2dmm(val, att, hpmode) elif att == nmt.TM: if isinstance(val, str): vals = str(val.replace(":", "")) else: # time vals = time2str(val) elif att == nmt.DT: if isinstance(val, str): # yyyymmdd -> ddmmyy val = val.replace("-", "") vals = f"{val[6:8]}{val[4:6]}{val[2:4]}" else: # datetime vals = date2str(val, att) elif att == nmt.DTL: # yyyymmdd -> ddmmyyyy if isinstance(val, str): val = val.replace("-", "") vals = f"{val[6:8]}{val[4:6]}{val[0:4]}" else: # datetime vals = date2str(val, att) elif att == nmt.DM: # yyyymmdd -> mmddyy if isinstance(val, str): val = val.replace("-", "") vals = f"{val[4:6]}{val[6:8]}{val[2:4]}" else: # datetime vals = date2str(val, att) else: raise nme.NMEATypeError(f"Unknown attribute type {att}.") return vals @staticmethod def nomval(att: str, msgmode: int = nmt.GET) -> Any: """ Return nominal value for specified attribute type :param str att: attribute type e.g. 'DE' :param int msgmode: message mode GET/SET/POLL :return: nominal value for type :rtype: Any :raises: NMEATypeError """ if att in (nmt.CH, nmt.ST, nmt.LA, nmt.LN): val = "" elif att == nmt.LAD: # pragma: no cover val = "N" elif att == nmt.LND: # pragma: no cover val = "E" elif att == nmt.HX: val = "0" elif att == nmt.DE: val = 0.0 elif att == nmt.IN: val = 0 elif att == nmt.TM: val = datetime.now(timezone.utc).time() elif att in (nmt.DT, nmt.DTL, nmt.DM): val = datetime.now(timezone.utc).date() elif att == nmt.QS: # Quectel status attribute if msgmode == nmt.POLL: val = "R" # read elif msgmode == nmt.SET: val = "W" # write else: val = "OK" # acknowledgement else: raise nme.NMEATypeError(f"Unknown attribute type {att}.") return val pynmeagps-1.1.7/src/pynmeagps/nmeareader.py000066400000000000000000000224051523640354200210010ustar00rootroot00000000000000""" NMEAReader class. Reads and parses individual NMEA GNSS/GPS messages from any stream which supports a read(n) -> bytes method. Can also read from socket via SocketStream wrapper. Returns both the raw binary data (as bytes) and the parsed data (as an NMEAMessage object). Implements an iterator: `for raw, parsed in NMEAReader(stream):` If the 'nmeaonly' kwarg is set to 'True', the reader will raise a NMEAParseError if it encounters any non-NMEA data. Otherwise, it will ignore the non-NMEA data and attempt to carry on. Created on 4 Mar 2021 :author: semuadmin (Steve Smith) :copyright: semuadmin © 2021 :license: BSD 3-Clause """ # pylint: disable=too-many-positional-arguments from logging import getLogger from socket import socket from types import FunctionType, NoneType from typing import Literal import pynmeagps.exceptions as nme from pynmeagps.nmeahelpers import calc_checksum, get_parts from pynmeagps.nmeamessage import NMEAMessage from pynmeagps.nmeatypes_core import ( DEFAULT_BUFSIZE, ENCODE_NONE, ERR_LOG, ERR_RAISE, GET, NMEA_HDR, VALCKSUM, VALMSGID, ) from pynmeagps.socketwrapper import SocketWrapper class NMEAReader: """ NMEAReader class. """ def __init__( self, stream, msgmode: Literal[0, 1, 2] = GET, validate: int = VALCKSUM, nmeaonly: bool = False, quitonerror: Literal[0, 1, 2] = ERR_LOG, bufsize: int = DEFAULT_BUFSIZE, errorhandler: FunctionType | NoneType = None, userdefined: dict | NoneType = None, encoding: int = ENCODE_NONE, ): """Constructor. :param stream stream: input data stream (e.g. Serial or binary File) :param Literal[0,1,2] msgmode: 0=GET, 1=SET, 2=POLL (0) :param int validate: VALNONE (0), VALCKSUM (1), VALMSGID (2), (can be OR'd) (1) :param bool nmeaonly: True = error on non-NMEA data, False = ignore non-NMEA data :param Literal[0,1,2] quitonerror: ERR_IGNORE (0) = ignore errors, ERR_LOG (1) = log continue, ERR_RAISE (2) = (re)raise (1) :param int bufsize: socket recv buffer size (4096) :param FunctionType | NoneType errorhandler: error handling callback function (None) :param dict | NoneType userdefined: user-defined payload definition dictionary (None) :param int encoding: encoding for socket stream \ (0 = none, 1 = chunk, 2 = gzip, 4 = compress, 8 = deflate (can be OR'd)) (0) :raises: NMEAParseError (if mode is invalid) """ # pylint: disable=too-many-arguments if isinstance(stream, socket): self._stream = SocketWrapper(stream, encoding=encoding, bufsize=bufsize) else: self._stream = stream if msgmode not in (0, 1, 2): raise nme.NMEAParseError( f"Invalid stream mode {msgmode} - must be 0, 1 or 2." ) self._quitonerror = quitonerror self._errorhandler = errorhandler self._nmea_only = nmeaonly self._validate = validate self._mode = msgmode self._userdefined = userdefined self._logger = getLogger(__name__) def __iter__(self): """Iterator.""" return self def __next__(self) -> tuple[bytes | NoneType, NMEAMessage | NoneType]: """ Return next item in iteration. :return: tuple of (raw_data as bytes, parsed_data as NMEAMessage) :rtype: tuple[bytes | NoneType, NMEAMessage | NoneType] :raises: StopIteration """ raw_data, parsed_data = self.read() if raw_data is None and parsed_data is None: raise StopIteration return (raw_data, parsed_data) def read(self) -> tuple[bytes | NoneType, NMEAMessage | NoneType]: """ Read the binary data from the stream buffer. :return: tuple of (raw_data as bytes, parsed_data as NMEAMessage) :rtype: tuple[bytes | NoneType, NMEAMessage | NoneType] :raises: NMEAStreamError (if nmeaonly=True and stream includes non-NMEA data) """ parsing = True raw_data = None parsed_data = None while parsing: # loop until end of valid NMEA message or EOF try: byte1 = self._read_bytes(1) # read 1st byte if byte1 != b"\x24": # not NMEA, discard and continue continue byte2 = self._read_bytes(1) # read 2nd byte to confirm protocol bytehdr = byte1 + byte2 if bytehdr in NMEA_HDR: # it's a NMEA message byten = self._read_line() # NMEA protocol is CRLF terminated raw_data = bytehdr + byten parsed_data = self.parse( raw_data, msgmode=self._mode, validate=self._validate, userdefined=self._userdefined, ) parsing = False else: # it's not a NMEA message (UBX or something else) if self._nmea_only: # raise error and quit raise nme.NMEAParseError(f"Unknown protocol header {bytehdr}.") except EOFError: return (None, None) except ( nme.NMEAMessageError, nme.NMEATypeError, nme.NMEAParseError, nme.NMEAStreamError, ) as err: if self._quitonerror: self._do_error(err) continue return (raw_data, parsed_data) def _read_bytes(self, size: int) -> bytes: """ Read a specified number of bytes from stream. :param int size: number of bytes to read :return: bytes :rtype: bytes :raises: EOFError if stream ends prematurely """ data = self._stream.read(size) if len(data) == 0: # EOF raise EOFError() # pragma: no cover if 0 < len(data) < size: # truncated stream raise nme.NMEAStreamError( # pragma: no cover "Serial stream terminated unexpectedly. " f"{size} bytes requested, {len(data)} bytes returned." ) return data def _read_line(self) -> bytes: """ Read bytes until LF (0x0a) terminator. :return: bytes :rtype: bytes :raises: EOFError if stream ends prematurely """ data = self._stream.readline() # NMEA protocol is CRLF-terminated if len(data) == 0: # EOF raise EOFError() # pragma: no cover if data[-1:] != b"\x0a": # truncated stream raise nme.NMEAStreamError( # pragma: no cover "Serial stream terminated unexpectedly. " f"Line requested, {len(data)} bytes returned." ) return data def _do_error(self, err: Exception): """ Handle error. :param Exception err: error message :raises: Exception if quitonerror = 2 """ if self._quitonerror == ERR_RAISE: raise err from err if self._quitonerror == ERR_LOG: # pass to error handler if there is one if self._errorhandler is None: self._logger.error(err) else: self._errorhandler(err) @property def datastream(self) -> object: """ Getter for stream. :return: data stream :rtype: object """ return self._stream @staticmethod def parse( message: bytes, msgmode: Literal[0, 1, 2] = GET, validate: int = VALCKSUM, userdefined: dict | NoneType = None, ) -> NMEAMessage | NoneType: """ Parse NMEA byte stream to NMEAMessage object. :param bytes message: bytes message to parse :param Literal[0, 1, 2] msgmode: 0=GET, 1=SET, 2=POLL (0) :param int validate: VALNONE (0), VALCKSUM (1), VALMSGID (2), (can be OR'd) (1) :param dict | NoneType userdefined: user-defined payload definition dictionary (None) :return: NMEAMessage object (or None if unknown message and VALMSGID is not set) :rtype: NMEAMessage | NoneType :raises: NMEAParseError (if data stream contains invalid data or unknown message type) """ if msgmode not in (0, 1, 2): raise nme.NMEAParseError( f"Invalid parse mode {msgmode} - must be 0, 1 or 2." ) try: content, talker, msgid, payload, checksum = get_parts(message) if validate & VALCKSUM: ccksum = calc_checksum(content) if checksum.upper() != ccksum: raise nme.NMEAParseError( f"Message {talker}{msgid} invalid checksum {checksum}" f" - should be {ccksum}." ) return NMEAMessage( talker, msgid, msgmode, payload=payload, checksum=checksum, validate=validate, userdefined=userdefined, ) except nme.NMEAMessageError as err: if validate & VALMSGID: raise nme.NMEAParseError(err) return None pynmeagps-1.1.7/src/pynmeagps/nmeatypes_core.py000066400000000000000000001025461523640354200217200ustar00rootroot00000000000000# pylint: disable=line-too-long """ NMEA Protocol core globals and constants Created on 4 Mar 2021 While the NMEA 0183 © protocol is proprietary, the information here has been collated from public domain sources. :author: semuadmin (Steve Smith) """ # pylint: disable=fixme from datetime import datetime, timezone INPUT = 1 """Input message type""" OUTPUT = 0 """Output message type""" GET = 0 """GET (receive, response) message types""" SET = 1 """SET (command) message types""" POLL = 2 """POLL (query) message types""" SETPOLL = 3 """SETPOLL (SET or POLL) message types""" VALNONE = 0 """Do not validate checksum or msgid""" VALCKSUM = 1 """Validate checksum""" VALMSGID = 2 """Validate message id""" NMEA_PROTOCOL = 1 """NMEA Protocol""" UBX_PROTOCOL = 2 """UBX Protocol""" RTCM3_PROTOCOL = 4 """RTCM3 Protocol""" ERR_RAISE = 2 """Raise error and quit""" ERR_LOG = 1 """Log errors""" ERR_IGNORE = 0 """Ignore errors""" DEF_STND = 0 """Standard message definition""" DEF_PROP = 1 """Proprietary message definition""" DEF_USER = 2 """User-defined message definition""" DEF_UNKN = 3 """Unknown (not public domain) message definition""" DEFAULT_BUFSIZE = 4096 """Default socket buffer size""" ENCODE_NONE = 0 """No socket encoding""" ENCODE_CHUNKED = 1 """chunked socket encoding""" ENCODE_GZIP = 2 """gzip socket encoding""" ENCODE_COMPRESS = 4 """compress socket encoding""" ENCODE_DEFLATE = 8 """deflate socket encoding""" EPOCH0_BEIDOU = datetime(2006, 1, 1, 0, 0, 0, tzinfo=timezone.utc) """Base epoch for Beidou time system (BDT)""" EPOCH0_GAL = datetime(1999, 8, 22, 0, 0, 0, tzinfo=timezone.utc) """Base epoch for Galileo time system""" EPOCH0_GPS = datetime(1980, 1, 6, 0, 0, 0, tzinfo=timezone.utc) """Base epoch for GPS time system""" EPOCH0_IRN = datetime(1999, 8, 22, 0, 0, 0, tzinfo=timezone.utc) """Base epoch for IRNSS/NAVIC time system""" GPSEPOCH0 = EPOCH0_GPS # deprecated, for backward compatibility # Geodetic datum spheroid values: # WGS84, ETRS89, EPSG4326 WGS84 = "WGS_84" """WGS84 datum descriptor""" WGS84_SMAJ_AXIS = 6378137.0 # semi-major axis """WGS84 semi-major axis""" WGS84_FLATTENING = 298.257223563 # flattening """WGS84 flattening""" BDS = "C" GAL = "E" GLO = "R" GPS = "G" IRN = "I" QZS = "J" SBA = "S" # *************************************************** # THESE ARE THE NMEA PROTOCOL PAYLOAD ATTRIBUTE TYPES # *************************************************** CH = "CH" # Character DE = "DE" # Decimal DT = "DT" # Date ddmmyy DTL = "DTL" # Date ddmmyyyy DM = "DM" # Date mmddyy HX = "HX" # Hexadecimal Integer IN = "IN" # Integer LA = "LA" # Latitude value ddmm.mmmmm (ddmm.mmmmmmm in HP mode) LAD = "LAD" # Latitude direction (N/S) LN = "LN" # Longitude value dddmm.mmmmm (dddmm.mmmmmmm in HP mode) LND = "LND" # Longitude direction (E/W) QS = "QS" # Quectel status (R/W/OK/ERROR) ST = "ST" # String TM = "TM" # Time hhmmss.ss VALID_TYPES = (CH, DE, DM, DT, DTL, HX, IN, LA, LAD, LN, LND, QS, ST, TM) # ***************************************** # THESE ARE THE NMEA V4 PROTOCOL TALKER IDS # ***************************************** NMEA_TALKERS = { # *************************************************************** # Base Stations: # *************************************************************** "AB": "Independent AIS Base Station", "AD": "Dependent AIS Base Station", # *************************************************************** # Heading Track Controllers: # *************************************************************** "AG": "Heading Track Controller (Autopilot): General", "AI": "Mobile Class A or B AIS Station", "AP": "Heading Track Controller (Autopilot): Magnetic", "AN": "AIS Aids to Navigation Station", "AR": "AIS Receiving Station", "AS": "AIS Station (ITU_R M1371, (Limited Base Station))", "AT": "AIS Transmitting Station", "AX": "AIS Simplex Repeater Station", "BI": "Bilge Systems", "BN": "Bridge Navigational Watch Alarm System", "CA": "Central Alarm Management", # *************************************************************** # Communications: # *************************************************************** "CD": "Digital Selective Calling (DSC)", "CR": "Data Receiver", "CS": "Satellite", "CT": "Radio-Telephone (MF/HF)", "CV": "Radio-Telephone (VHF)", "CX": "Scanning Receiver", "DE": "DECCA Navigator", "DF": "Direction Finder", "DP": "Dynamic Position", "DU": "Duplex Repeater Station", "EC": "Electronic Chart System (ECS)", "EI": "Electronic Chart Display & Information System (ECDIS)", "EP": "Emergency Position Indicating Beacon (EPIRB)", "ER": "Engine Room Monitoring Systems", "FD": "Fire Door Controller/Monitoring Point", "FE": "Fire Extinguisher System", "FR": "Fire Detection Point", "FS": "Fire Sprinkler System", # *************************************************************** # Navigation System Satellite Receivers: # *************************************************************** "GA": "Galileo Positioning System", "GB": "BDS (BeiDou System)", "BD": "BDS (BeiDou System)", # legacy, use GB instead "GI": "NavIC (IRNSS)", "GL": "GLONASS Receiver", "GN": "Global Navigation Satellite System (GNSS)", "GP": "Global Positioning System (GPS)", "GQ": "QZSS", "P": "Proprietary", # *************************************************************** # Heading Sensors: # *************************************************************** "HC": "Compass, Magnetic", "HE": "Gyro, North Seeking", "HF": "Fluxgate", "HN": "Gyro, Non-North Seeking", # *************************************************************** # Other Maritime Instrumentation: # *************************************************************** "HD": "Hull Door Monitoring", "HS": "Hull Stress Monitoring", "IC": "Integrated Communications System", "II": "Integrated Instrumentation", "IN": "Integrated Navigation", "LC": "Loran C", "MP": "Microprocessor Controller", "ND": "Network Device", "NL": "Navigation Light Controller", "RA": "Radar and/or Radar Plotting", "RC": "Propulsion Machinery including Remote Control", "SA": "Physical Shore AIS Station", "SC": "Steering Control System/Device", "SD": "Sounder, Depth", "SG": "Steering Gear/Steering Engine", "SI": "Serial to Network Gateway Function", "SN": "Electronic Positioning System", "SS": "Sounder, Scanning", "TC": "Track Control System", "TI": "Turn Rate Indicator", "UP": "Microprocessor Controller", "U0": "User-configured talker identifier", "U1": "User-configured talker identifier", "U2": "User-configured talker identifier", "U3": "User-configured talker identifier", "U4": "User-configured talker identifier", "U5": "User-configured talker identifier", "U6": "User-configured talker identifier", "U7": "User-configured talker identifier", "U8": "User-configured talker identifier", "U9": "User-configured talker identifier", # *************************************************************** # Velocity Sensors: # *************************************************************** "VD": "Doppler", "VM": "Speed Log, Water, Magnetic", "VR": "Voyage Data Recorder", "VW": "Speed Log, Water, Mechanical", "WD": "Watertight Door Controller/Monitoring Panel", "WI": "Weather Instruments", "WL": "Water Level Detection Systems", "YX": "Transducer", # *************************************************************** # Timekeepers: # *************************************************************** "ZA": "Atomic Clock", "ZC": "Chronometer", "ZQ": "Quartz", "ZV": "Radio Update", } """Recognised NMEA Talkers.""" # format list of permissible NMEA 2-byte header sequences NMEA_HDR = {b"\x24" + i[0:1].encode("utf-8") for i in NMEA_TALKERS} # **************************************************************************** # THESE ARE THE NMEA PROTOCOL CORE MESSAGE IDENTITIES # Payloads for each of these identities are defined in the nmeatypes_* modules # Commented-out entries are those which have not yet been implemented, # principally because no public domain definition has yet been identified. # Full specifications can be found in NMEA 0183—v4.30, IEC 61162-1:2024. # **************************************************************************** NMEA_MSGIDS = { "AAM": "Waypoint arrival alarm", "ABK": "AIS addressed and binary broadcast acknowledgement", "ABM": "AIS addressed binary and safety related message", "ACA": "AIS channel assignment message", "ACK": "Acknowledge alarm", "ACN": "Alert Command", "ACS": "AIS channel management information source", # "AGL": "Alert Group List", "AIR": "AIS interrogation request", "AKD": "Acknowledge detail alarm condition7", "ALA": "Report detailed alarm condition", "ALC": "Cyclic alert list", "ALF": "Alert sentence", "ALR": "Set alarm state", "APA": "Auto Pilot A sentence", # deprecated "APB": "Heading/track controller (autopilot) sentence", "ARC": "Alert command refused", "BBM": "AIS broadcast binary message", "BEC": "Bearing and distance to waypoint - Dead reckoning", "BOD": "Bearing origin to destination", "BWC": "Bearing and distance to waypoint - Great circle", "BWR": "Bearing and distance to waypoint - Rhumb line", "BWW": "Bearing waypoint to waypoint", "CUR": "Water current layer - Multi-layer water current data", "DBT": "Depth below transducer", "DDC": "Display dimming control", "DOR": "Door status detection", "DPT": "Depth", "DSC": "Digital selective calling information", "DSE": "Expanded digital selective calling", "DTM": "Datum reference", "EPV": "Command or report equipment property value", "ETL": "Engine telegraph operation status", "EVE": "General event message", "FIR": "Fire detection", "FMI": "Feyman IMU roll, pitch, yaw", "FSI": "Frequency set information", "GAQ": "Poll Standard Message - Talker ID GA (Galileo)", "GBQ": "Poll Standard Message - Talker ID GB (BeiDou)", "GBS": "GNSS satellite fault detection", "GEN": "Generic binary information", "GFA": "GNSS fix accuracy and integrity", "GGA": "Global positioning system fix data", "GLL": "Geographic position - Latitude/longitude", "GLQ": "Poll Standard Message - Talker ID GL (GLONASS)", "GMP": "GNSS Map Projection Fix Data", "GNQ": "Poll Standard Message - Talker ID GN (Any GNSS)", "GNS": "GNSS Fix Data", "GPQ": "Poll Standard Message - Talker ID GP (GPS, SBAS)", "GQQ": "Poll Standard Message - Talker ID GQ (QZSS)", "GRS": "GNSS range residuals", "GSA": "GNSS DOP and active satellites", "GST": "GNSS pseudorange noise statistics", "GSV": "GNSS satellites in view", "HBT": "Heartbeat supervision sentence", "HCR": "Heading correction report", "HDG": "Heading, deviation and variation", "HDM": "Heading, Magnetic", # deprecated "HDT": "Heading true", "HMR": "Heading monitor receive", "HMS": "Heading monitor set", "HRM": "heel angle, roll period and roll amplitude measurement device", "HSC": "Heading steering command", "HSS": "Hull stress surveillance systems", "HTC": "Heading/track control command", "HTD": "Heading /track control", "LLQ": "Leica local position and quality", "LR1": "AIS long-range reply sentence 1", "LR2": "AIS long-range reply sentence 2", "LR3": "AIS long-range reply sentence 3", "LRF": "AIS long-range function", "LRI": "AIS long-range interrogation", "MOB": "Man over board notification", "MSK": "MSK receiver interface", "MSS": "MSK receiver signal status", "MTW": "Water temperature", "MWD": "Wind direction and speed", "MWV": "Wind speed and angle", "NAK": "Negative acknowledgement", "NRM": "NAVTEX receiver mask", "NRX": "NAVTEX received message", "NSR": "Navigation status report", "OSD": "Own ship data", "POS": "Device position and ship dimensions report or configuration", "PRC": "Propulsion remote control status", "RLM": "Return link message", "RMA": "Recommended minimum specific LORAN-C data", "RMB": "Recommended minimum navigation information", "RMC": "Recommended minimum specific GNSS data", "ROR": "Rudder order status", "ROT": "Rate of turn", "RRT": "Report route transfer", "RPM": "Revolutions", "RSA": "Rudder sensor angle", "RSD": "Radar system data", "RTE": "Routes", "SFI": "Scanning frequency information", "SM1": "SafetyNET Message, All Ships/NavArea", "SM2": "SafetyNET Message, Coastal Warning Area", "SM3": "SafetyNET Message, Circular Area address", "SM4": "SafetyNET Message, Rectangular Area Address", "SMB": "IMO SafetyNET Message Body", "SPW": "Security password sentence", "SSD": "AIS ship static data", "STN": "Multiple data ID", "THS": "True heading and status", "TLB": "Target label", "TLL": "Target latitude and longitude", "TRA": "Heading, Pitch & Roll Information", "TRC": "Thruster control data", "TRL": "AIS transmitter-non-functioning log", "TRD": "Thruster response data", "TRF": "Transit Fix Data", # deprecated "TTD": "Tracked target data", "TTM": "Tracked target message", "TUT": "Transmission of multi-language text", "TXT": "Text transmission", "UID": "User identification code transmission", "VBW": "Dual ground/water speed", "VDM": "AIS VHF data-link message", "VDO": "AIS VHF data-link own-vessel report", "VDR": "Set and drift", "VER": "Version", "VHW": "Water speed and heading", "VLW": "Dual ground/water distance", "VPW": "Speed measured parallel to wind", "VSD": "AIS voyage static data", "VTG": "Course over ground and ground speed", "WAT": "Water level detection", "WCV": "Waypoint closure velocity", "WNC": "Distance waypoint to waypoint", "WPL": "Waypoint location", "XDR": "Transducer measurements", "XTE": "Cross-track error, measured", "XTR": "Cross-track error, dead reckoning", "ZDA": "Time and date", "ZDL": "Time and distance to variable point", "ZFO": "UTC and time from origin waypoint", "ZTG": "UTC and time to destination waypoint", # *************************************************************** # Unicore Extended NMEA (GET) # *************************************************************** "GGAH": "Global Positioning System Fix Data (Secondary Antenna)", "GLLH": "Geographic Position (Secondary Antenna)", "GNSH": "GNSS Fix Data (Secondary Antenna)", "GRSH": "GNSS Range Residuals (Secondary Antenna)", "GSAH": "GNSS DOP and Active Satellites (Secondary Antenna)", "GSTH": "GNSS Pseudorange Error Statistics (Secondary Antenna)", "GSVH": "GNSS Satellites in View (Secondary Antenna)", "RMCH": "Recommended Minimum Specific GNSS Data (Secondary Antenna)", "VTGH": "Course over Ground and Ground Speed (Secondary Antenna)", "THS2": "True Heading and Status", "HPR": "Attitude Parameters", "HPR2": "Attitude Parameters", "TRA2": "Heading, Pitch & Roll Information", "ROT2": "Rate of Turn", "HPD": "Positioning and Heading Information", # *************************************************************** # Dummy message for testing only # *************************************************************** "FOO": "Dummy message", } """ Recognised Standard NMEA Message Identifiers. Payload definitions for standard GET messages are defined in `NMEA_PAYLOADS_GET`. Standard POLL messages are defined in `NMEA_PAYLOADS_POLL`. """ NMEA_MSGIDS_PROP = { # *************************************************************** # NMEA Proprietary message types # *************************************************************** # *************************************************************** # GARMIN Proprietary message types # *************************************************************** "GRMB": "DGPS Beacon Information", "GRMC": "Set Sensor Configuration information", "GRMC1": "Set Additional Sensor Configuration Information", "GRME": "Estimated Error Information", "GRMF": "GPS Fix Data sentence", "GRMH": "Aviation Height and VNAV data", "GRMI": "Set Sensor Initialisation Information", "GRMM": "MapDatum", "GRMO": "Set Output Sentence Enable", "GRMT": "Sensor Status Information", "GRMV": "3D Velocity Information", "GRMW": "Set Additional Waypoint Information", "GRMZ": "Altitude", # *************************************************************** # JVC Kenwood Proprietary message types # *************************************************************** "KLDS": "Position, Speed, Course", "KLSH": "FleetSync GNSS sentence", "KNDS": "Position, Speed, Course", "KNSH": "Position", "KWDWPL": "Waypoint Location", # *************************************************************** # Magellan Proprietary message types # *************************************************************** "MGNWPL": "Waypoint Location", # *************************************************************** # Quectel PAIR Proprietary message types LC29 etc. # *************************************************************** "AIR001": "Ack", "AIR002": "GNSS Subsys Power On", "AIR003": "GNSS Subsys Power Off", "AIR004": "GNSS Subsys Hot Start", "AIR005": "GNSS Subsys Warm Start", "AIR006": "GNSS Subsys Cold Start", "AIR007": "GNSS Subsys Full Cold Start", "AIR010": "Request Aiding", # "AIR011": "Unknown definition", # TODO # "AIR020": "Get firmware release information", # TODO "AIR050": "Common Set Fix Rate", "AIR051": "Common Get Fix Rate", "AIR058": "Common Set Min Snr", "AIR059": "Common Get Min Snr", "AIR062": "Common Set NMEA Output Rate", "AIR063": "Common Get NMEA Output Rate", "AIR066": "Common Set GNSS Search Mode", "AIR067": "Common Get GNSS Search Mode", "AIR070": "Common Set Static Threshold", "AIR071": "Common Get Static Threshold", "AIR072": "Common Set Elev Mask", "AIR073": "Common Get Elev Mask", "AIR074": "Common Set AIC Enable", "AIR075": "Common Get AIC Status", "AIR080": "Common Set Navigation Mode", "AIR081": "Common Get Navigation Mode", "AIR086": "Common Set Debuglog Output", "AIR087": "Common Get Debuglog Output", "AIR100": "Common Set Nmea Output Mode", "AIR101": "Common Get Nmea Output Mode", "AIR104": "Common Set Dual Band", "AIR105": "Common Get Dual Band", # "AIR161": "Force standby mode", # TODO # "AIR351": "Support QZSS NMEA", # TODO "AIR382": "Test Lock System Sleep", "AIR391": "Test Jamming Detect", "AIR400": "DGPS Set Mode", "AIR401": "DGPS Get Mode", "AIR410": "SBAS Enable", "AIR411": "SBAS Get Status", "AIR420": "SLAS Enable", "AIR421": "SLAS Get Status", "AIR432": "RTCM Set Output Mode", "AIR433": "RTCM Get Output Mode", "AIR434": "RTCM Set Output Ant Pnt", "AIR435": "RTCM Get Output Ant Pnt", "AIR436": "RTCM Set Output Ephemeris", "AIR437": "RTCM Get Output Ephemeris", "AIR490": "Set EASYTM status", "AIR491": "Get EASY status", # "AIR496": "EPOC Enable", # TODO # "AIR498": "EPOC Set Config",# TODO # "AIR507": "EPOC Clear Data",# TODO # "AIR508": "EPOC Get Status",# TODO # "AIR509":"EPOC Get Prediction Status", # TODO "AIR511": "NVRAM Save Navigation Data", "AIR512": "NVRAM Clear Navigation Data", "AIR513": "NVRAM Save Setting", "AIR6010": "DR Set Custom Message Output", "AIR6011": "DR Get Custom Message Output", "AIR650": "Low Power Entry Rtc Mode", # "AIR690": "Enter periodic mode for power saving", # TODO # "AIR751": "Fix NMEA output time behind PPS", # TODO "AIR752": "PPS Set Config Cmd", # "AIR820": "Set LOCUS logging status", # TODO # "AIR821": "Get LOCUS logging status", # TODO # "AIR824": "Erase LOCUS logger flash", # TODO # "AIR826": "Dump LOCUS flash data", # TODO "AIR864": "IO Set Baudrate", "AIR865": "IO Get Baudrate", "AIR866": "IO Set Flow Control", "AIR867": "IO Get Flow Control", "AIRSPF": "Jamming status L1", "AIRSPF5": "Jamming status L5", # *************************************************************** # Quectel PQTM Proprietary message types LG290P/LG580P # *************************************************************** "QTMAIDINI": "Inputs aiding position and time data", "QTMAIDPOS": "Inputs aiding position data", "QTMAIDTIME": "Inputs aiding time data", "QTMANTENNASTATUS": "Report Antenna Status", "QTMBKP": "Set Backup Mode", "QTMCFGAIC": "Sets/Gets AIC Function", "QTMCFGANTDELTA": "Sets/Gets Delta Between Ref Point and Antenna", "QTMCFGANTENNA": "Sets/gets GNSS antenna power supply", "QTMCFGANTINF": "Sets/Gets Antenna Information", "QTMCFGBLD": "Sets/Gets Baseline Between Two Antennas", "QTMCFGCNRTHD": "Sets/gets the CNR threshold for position engine", "QTMCFGCNST": "Sets/Gets Constellation Configuration", "QTMCFGDR": "Sets/Gets DR State", "QTMCFGDRHOT": "Sets/Gets DR Hot Start Feature", "QTMCFGDRRTD": "Sets/Gets DR Running TIme and Distance", "QTMCFGEINSMSG": "Sets/Gets INS, IMU, GPS Status", "QTMCFGELETHD": "Sets/Gets Elevation Threshold for Position Engine", "QTMCFGEVENT": "Sets/gets external interrupt event", "QTMCFGFIXRATE": "Sets/Gets Fix Interval", "QTMCFGGEOFENCE": "Sets/Gets Geofence Feature", "QTMCFGGEOSEP": "Sets/Gets Geoidal Separation", "QTMCFGIMUINT": "Sets/Gets Motion Trigger Interruption", "QTMCFGIMUTC": "Sets/Gets IMU Temperature Compensation", "QTMCFGLA": "Sets/Gets Lever Arm", "QTMCFGLAM": "Sets/Gets Lever Arm Mode", "QTMCFGMSGRATE": "Sets/Gets Message Output Rate", "QTMCFGNAVMODE": "Sets/Gets Navigation Mode", "QTMCFGNMEADP": "Sets/Gets NMEA Precision", "QTMCFGNMEATID": "Sets/Gets NMEA Talker ID", "QTMCFGODO": "Sets/Gets Odometer Feature", "QTMCFGPINALT": "Sets/gets the function of module's pin 14", "QTMCFGPPP": "Sets/gets the PPP feature", "QTMCFGPPS": "Sets/Gets PPS (Pulse Per Second) Feature", "QTMCFGPPS2": "Sets/gets the PPS extend feature", "QTMCFGPROT": "Sets/Gets I/O Protocol for Specified Port", "QTMCFGRCVRMODE": "Sets/Gets Receiver Working Mode", # Rover, Base "QTMCFGRSID": "Sets/Gets Reference Station ID", "QTMCFGRTCM": "Sets/Gets RTCM", "QTMCFGRTK": "Sets/Gets RTK Mode", # Absolute, Relative "QTMCFGRTKRL": "Sets/gets the RTK reliability level", "QTMCFGRTKSRCTYPE": "Sets/Gets RTK Source Type", "QTMCFGSAT": "Sets/Gets GNSS Satellite Mask", "QTMCFGSBAS": "Sets/Gets SBAS", "QTMCFGSIGGRP": "Sets/Gets GNSS Signal Group", "QTMCFGSIGNAL": "Sets/Gets Antenna1 Signal Mask", "QTMCFGSIGNAL2": "Sets/Gets Antenna2 Signal Mask", "QTMCFGSTANDALONE": "Sets/gets the Standalone mode", "QTMCFGSTATICHOLD": "Sets/Gets Parking Static Hold", "QTMCFGSVIN": "Sets/Gets Survey-In Feature", # Survey-In or Fixed "QTMCFGUART": "Configure UART Interface", "QTMCFGVEHMOT": "Sets/Gets Vehicle Motion Detection", "QTMCFGWN": "Sets/Gets Reference Start Week No", "QTMCLRMSG": "Disables serial message output.", "QTMCOLD": "Cold Start", "QTMDEBUGOFF": "Disable Debug Log Output", "QTMDEBUGON": "Enable Debug Log Output", "QTMDOP": "Outputs Dilution of Precision", "QTMDRCAL": "Dead Reckoning Calibration Status", "QTMDRCLR": "Clear DR Calibration Data", "QTMDRPVA": "DR Postion, Velosity and Attitude", "QTMDRSAVE": "Save DR Calibration Data", "QTMENV": "Outputs the environment information.", "QTMEOE": "Output End of Epoch Information", "QTMEPE": "Output Estimated Position Error", "QTMGEOFENCESTATUS": "Outputs Geofence Status", "QTMGETUTC": "Get UTC Time Information", "QTMGNSSSTART": "Starts GNSS Engine", "QTMGNSSSTOP": "Stops GNSS Engine", "QTMGPS": "Position Status in GNSS Mode", "QTMHOT": "Hot Start", "QTMIMU": "IMU Raw Data", "QTMIMUTYPE": "IMU Status", "QTMINS": "Navigation Results", "QTMJAMMINGSTATUS": "Jamming Detection Status", "QTMLS": "Outputs Leap Second Forecast Information", "QTMLSTMSG": "Queries message output of serial port", "QTMMCFGRTKSRCTYPE": "Sets/Gets RTK Source Type", # typo in LG580P firmware? "QTMNAV": "Output Navigation Information", "QTMODO": "Outputs Odometer Information", "QTMPL": "Outputs Protection Level Information", "QTMPPPNAV": "Outputs the PPP navigation information", "QTMPVT": "Outputs PVT (GNSS) Result", # Position, Velocity, Track "QTMQVER": "Queries Version Information", "QTMRESETODO": "Reset Odometer Distance", "QTMRESTOREPAR": "Restore to Default Values after Restart", "QTMRTCMIS": "Outputs the RTCM input status", "QTMSAVEPAR": "Save Configuration to Non-Volatile Memory", "QTMSENMSG": "Sensor Information", "QTMSN": "Queries Module Serial Number Code", "QTMSRR": "System Reset and Reboot", "QTMSTD": "Outputs Time, Position, Velocoity and Attitude Deviations", "QTMSVINSTATUS": "Outputs Survey-In Status", "QTMTAR": "Output Time and Attitude", "QTMTXT": "Outputs Short Text Message", # Longer messages use multiple messages "QTMUNIQID": "Query Module Unique ID", "QTMVEHATT": "Output Vehicle Attitude", "QTMVEHMOT": "Output Vehicle Motion after DR Calibration", "QTMVEHMSG": "Output Vehicle Information", "QTMVEL": "Output Velocity Information", "QTMVER": "Firmware Version", "QTMVERNO": "Query Firmware Version", "QTMWARM": "Warm Start", # *************************************************************** # Quectel PSTM Proprietary message types (LG69T) # *************************************************************** "STMCOLD": "Perform cold start", # Command "STMCPU": "Output CPU usage and speed", # Output "STMDR1": "Report gyroscope and accelerometer details", # Output "STMDR2": "Report calibration status and availability", # Output "STMDRBSD": "Report IMU bias error estimates", # Output "STMDRCAL": "Report IMU and odometer calibration status", # Output "STMDRCONFID": "Report navigation and calibration error estimates", # Output "STMDRDEBUG": "Report DR debug information", # Output "STMDREPE": "Report DR horizontal position error estimates", # Output "STMDRGPS": "Report GNSS information", # Output "STMDRMMFB": "Set DR algorithm input data", # Command "STMDRMMFBKF": "Report MMFB data timing, acceptance, and usage", # Output "STMDRPVA": "Report PVT estimated by DR", # Output "STMDRPVASD": "Report DR PVT error estimates", # Output "STMDRSENMSG": "Output the IMU raw data", # Output "STMDRSINT": "Report sensor integration samples", # Type: "STMDRSTYPE": "Report DR type", # Output "STMDRSVF": "Report IMU vehicle dynamics", # Output "STMDRUPD": "Quectel internal debugging", # Output "STMEPHEM": "Load ephemeris into NVM", # Command "STMHOT": "Perform hot start", # Command "STMIMUSELFTESTCMD": "Execute IMU self-test", # Command "STMINITGPS": "Initialise GPS", # Command # "STMINITGPSERROR": "Initialise GPS Nak", # Response "STMINITGPSOK": "Initialise GPS Ack", # Response "STMINITTIME": "Initialise time", # Command "STMINITTIMEERROR": "Initialise time Nak", # Response "STMINITTIMEOK": "Initialise time Ack", # Response "STMPPSDATA": "Output the PPS data", # Output "STMRESTOREPAR": "Restore the factory setting parameters", # Command "STMSAVEPAR": "Save current configuration data block to NVM", # Command "STMSBAS": "Output SBAS satellite data", # Output "STMSETPAR": "Set parameter", # Command "STMSETPARERROR": "Set parameter Nak", # Response "STMSETPAROK": "Set parameter Ack", # Response "STMSRR": "Execute system reset and reboot", # Command "STMTEMP": "Report temperature", # Output "STMTG": "Output time and satellite information", # Output "STMTS": "Output tracked satellite parameters", # Output "STMWARM": "Perform warm start", # Command # *************************************************************** # Sepentrio X5 Proprietary message types # *************************************************************** "SSNHRP": "Heading, Roll, Pitch", "SSNRBD": "Rover Base Direction", "SSNRBP": "Rover Base Position", "SSNRBV": "Rover Base Velocity", "SSNSNC": "NTRIP Client Status", "SSNTFM": "Used RTCM Coordinate Transformation Messages", # *************************************************************** # U-BLOX Proprietary message types # *************************************************************** "UBX00": "Lat/Long Position Data", "UBX03": "Satellite Status", "UBX04": "Time of Day and Clock Information", "UBX05": "Lat/Long Position Data", "UBX06": "Lat/Long Position Data", "UBX40": "Set NMEA Message Output Rate", "UBX41": "Set Protocols and Baudrate", # *************************************************************** # Trimble Proprietary message types # *************************************************************** "ASHR": "RT300 Roll and Pitch", "ASHRALR": "Alarms", "ASHRARA": "True Heading", "ASHRARR": "Vector & Accuracy", "ASHRATT": "True Heading", "ASHRBTS": "Bluetooth Status", "ASHRCAP": "Parameters of Antenna Used at Received Base", "ASHRCPA": "Height of Antenna Used at Received Base", "ASHRCPO": "Position of Received Base", "ASHRDDM": "Differential Decoder Message", "ASHRDDS": "Differential Decoder Status", "ASHRHPR": "True Heading", # "ASHRHR": "Proprietary Roll and Pitch", "ASHRLTN": "Latency", "ASHRMDM": "Modem State and Parameter", "ASHRPOS": "Position", "ASHRPBN": "Position and Velocity Information", "ASHRPTT": "PPS Time Tag", "ASHRPWR": "Power Status", "ASHRRCS": "Recording Status", "ASHRSBD": "BEIDOU Satellites Status", "ASHRSGA": "GALILEO Satellites Status (E1,E5a,E5b)", "ASHRSGL": "GLONASS Satellites Status", "ASHRSGO": "GALILEO Satellites Status (E1,E5a,E5b,E6)", "ASHRSGP": "GPS Satellites Status", "ASHRSIR": "IRNSS Satellites Status", "ASHRSLB": "L-Band Satellites Status", "ASHRSQZ": "QZSS Satellites Status", "ASHRSSB": "SBAS Satellites Status", "ASHRTEM": "Receiver Temperature", "ASHRTHS": "True Heading and Status", "ASHRTTT": "Event Marker", "ASHRVCR": "Vector and Accuracy", "ASHRVCT": "Vector and Accuracy", "ASHRVEL": "Velocity", "FUGDP": "Fugro Dynamic Positioning", "GPPADV110": "Position and satellite information for RTK network operations 110", "GPPADV120": "Position and satellite information for RTK network operations 120", "TNLAVR": "Time, yaw, tilt/roll, range for moving baseline RTK", "TNLBPQ": "Base station position and quality indicator", "TNLDG": "L-band corrections and beacon signal strength and related information", "TNLEVT": "Event marker data", "TNLGGK": "Time, position, position type, DOP", "TNLGGKx": "GNSS Position Message", "TNLPJK": "Local coordinate position output", "TNLPJT": "Projection type", "TNLREX": "Rover Extended Output", "TNLVGK": "Vector information", "TNLVHD": "Heading information", # *************************************************************** # Furuno # *************************************************************** "FECGPATT": "Attitude yaw, pitch, roll", "FECGPHVE": "Heave", # *************************************************************** # Locosys Proprietary Messages (GET and SET) # *************************************************************** "INVCRES": "Clear the NVM data", "INVCSTR": "Start session", "INVMATTIT": "ATTIT information", "INVMIMU": "MEMS RAW-DATA message information", "INVMINR": "Calibration status", "INVMSTR": "Session Status", "INVMSLOPE": "SLOPE information", "LSC": "Set status/poll version", "LSR": "Set status response", "LSVD": "Attitude yaw, pitch, roll", # "MTKnnn": "Proprietary command sets - not implemented", } """ Recognised Proprietary NMEA Message Identifiers. Payload definitions for proprietary GET messages are defined in `NMEA_PAYLOADS_GET_PROP`. Proprietary SET and POLL messages are defined in `NMEA_PAYLOADS_SET` and `NMEA_PAYLOADS_POLL`. """ NMEA_PREFIX_PROP = ("ASHR", "GPPADV", "FEC", "SSN", "TNL", "UBX") """ Proprietary NMEA Message Prefixes where `msgId` is part of payload. For proprietary messages with these prefixes, the `msgId` is defined as the first element of the payload. The unique payload dictionary key is therefore a combination of `prefix` + `msgId`. NB: There are some exceptions which require special handling e.g. `PASHR`. """ pynmeagps-1.1.7/src/pynmeagps/nmeatypes_decodes.py000066400000000000000000000055411523640354200223730ustar00rootroot00000000000000""" nmeatyps_decodes.py NMEA Protocol attribute value decode constants. From various public domain sources. Created on 26 Aug 2023 :author: semuadmin (Steve Smith) :copyright: semuadmin © 2021 :license: BSD 3-Clause """ FIXTYPE_GGA = QUALITY = { 0: "NO FIX", 1: "2D", 2: "3D", 3: "N/A", 4: "RTK FIXED", 5: "RTK FLOAT", 6: "DR ONLY", } """GGA quality/fix type values""" POSMODE = { "A": "2D", "N": "N/A", "E": "DR", "D": "3D", "F": "RTK FLOAT", "R": "RTK FIXED", } """RMC, GNS, GLL posmode values""" GNSSLIST = { 0: "GPS", 1: "SBAS", 2: "Galileo", 3: "BeiDou", 4: "IMES", 5: "QZSS", 6: "GLONASS", 7: "NAVIC", } """GNSS code""" SYSTEMID = { "1": "GPS", "2": "GLONASS", "3": "Galileo", "4": "Beidou", "5": "QZSS", "6": "NavIC", } """NMEA 4.1 System Identifier - key is systemId""" SIGNALID = { ("1", "1"): "GPS L1C/A2", ("1", "6"): "GPS L2 CL", ("1", "5"): "GPS L2 CM", ("1", "7"): "GPS L5 I", ("1", "8"): "GPS L5 Q", ("3", "7"): "Galileo E1 B2/C2", ("3", "1"): "Galileo E5 aI/aQ", ("3", "2"): "Galileo E5 bI/bQ", ("4", "1"): "BeiDou B1I D12/D22", ("4", "B"): "BeiDou B2I D1/D2", ("4", "3"): "BeiDou B1 Cp/Cd", ("4", "5"): "BeiDou B2 ap/ad", ("5", "1"): "QZSS L1C/A2", ("5", "4"): "QZSS L1S", ("5", "5"): "QZSS L2 CM", ("5", "6"): "QZSS L2 CL", ("5", "7"): "QZSS L5 I", ("5", "8"): "QZSS L5 Q", ("2", "1"): "GLONASS L1 OF", ("2", "3"): "GLONASS L2 OF", ("6", "1"): "NavIC L5 A", } """Signal Identifier - key is (systemId, signal Id), values are hex strings""" FMI_STATUS = { 0x00000001: ("Finit", "Filter uninitialized flag"), 0x00000002: ("Ready", "Filter convergence completion flag"), 0x00000004: ("Inaccurate", "Filter convergence is in process"), 0x00000008: ("TiltReject", "Tilt angle over threshold"), 0x00000010: ("GnssReject", "RTK data poor quality"), 0x00000020: ("FReset", "Filter reset flag"), 0x00000040: ("FixRlsStage1", "Installation angle estimation stage 1"), 0x00000080: ("FixRlsStage2", "Installation angle estimation stage 2"), 0x00000100: ("FixRlsStage3", "Installation angle estimation stage 3"), 0x00000200: ("FixRlsStage4", "Installation angle estimation stage 4"), 0x00000400: ("FixRlsOK", "Installation angle estimation complete"), 0x00002000: ("Direction1", "Shake RTK pole to initialize direction 1"), 0x00004000: ("Direction2", "Shake RTK pole to initialize direction 2"), 0x00010000: ("GnssLost", "Invalid RTK data or RTK data missing"), 0x00020000: ("FInitOk", "Filter initialization completed"), 0x00040000: ("PPSReady", "PPS signal detected"), 0x00080000: ("SyncReady", "Module time synchronized with PPS"), 0x00100000: ("GnssConnect", "Serial connection with RTK established"), } """GPFMI status values""" pynmeagps-1.1.7/src/pynmeagps/nmeatypes_get.py000066400000000000000000000776471523640354200215640ustar00rootroot00000000000000""" NMEA Protocol Output (GET) payload definitions THESE ARE THE PAYLOAD DEFINITIONS FOR _GET_ MESSAGES _FROM_ THE RECEIVER (e.g. Periodic Navigation Data; Poll Responses; Info messages). NB: Attribute names must be unique within each message id. NB: Avoid reserved names 'msgID', 'talker', 'payload', 'checksum'. NB: Repeating groups must be defined as a tuple thus 'group': ('numr', {dict}) where - 'numr' is either: a) an integer representing a fixed number of repeats e.g 32 b) a string representing the name of a preceding attribute containing the number of repeats e.g. 'numCh' c) 'None' for an indeterminate repeating group (only one such group is permitted per message type) - {dict} is the nested dictionary containing the repeating attributes Created on 4 Mar Sep 2021 While the NMEA 0183 © protocol is proprietary, the information here has been collated from public domain sources. :author: semuadmin (Steve Smith) """ # pylint: disable=too-many-lines, duplicate-code from pynmeagps.nmeatypes_core import CH, DE, DT, DTL, HX, IN, LA, LAD, LN, LND, ST, TM NMEA_PAYLOADS_GET = { # ********************************************* # STANDARD MESSAGES # ********************************************* "AAM": { "arrce": CH, "perp": CH, "crad": DE, "cUnit": CH, # N nm "wpt": ST, }, "ABK": { "mmsi": ST, "aischan": CH, "itumsg": ST, "msgSeq": IN, "ackType": IN, }, "ABM": { "numSen": IN, "senNum": IN, "mmsi": ST, "aischan": CH, "ituMsg": ST, "data": ST, "fillbits": IN, }, "ACA": { "seqNum": IN, "latne": LA, "latneNS": LAD, "lonne": LN, "lonneEW": LND, "latsw": LA, "latswNS": LAD, "lonswq": LN, "lonswEW": LND, "tranZoneSize": CH, "chanA": CH, "chanAbw": CH, "chanB": CH, "chanBbw": CH, "txrxMode": IN, "pwrLevelCtl": IN, "infoSource": CH, "inuse": IN, "inuseTime": TM, }, "ACK": { "alarmid": ST, }, "ACN": { "time": TM, "mfrcode": ST, "alertid": ST, "alertnum": IN, "alertcmd": ST, "status": CH, }, "ACS": { "seqNum": IN, "mmsi": ST, "utc": TM, "day": IN, "month": IN, "year": IN, }, "AIR": { "mmsi1": ST, "mmsi1msg1": ST, "mmsi1msg1sub": ST, "mmsi1msg2": ST, "mmsi1msg2sub": CH, "mmsi2": ST, "mmsi2msg1": ST, "mmsi2msg1sub": ST, "channel": ST, "mid11": ST, "mid12": ST, "mid21": ST, }, "AKD": { "acktime": TM, "srcsystem": ST, "srcsubsysytem": ST, "srcinstance": ST, "alarmtype": ST, "acksystem": ST, "acksubsystem": ST, "ackinstance": ST, }, "ALA": { "evttime": TM, "srcsystem": ST, "srcsubsysytem": ST, "srcinstance": ST, "alarmtype": ST, "alarmcond": ST, "alarmackstate": ST, "alarmtext": ST, }, "ALC": { "numSen": IN, "senNum": IN, "seqmid": IN, "numAlerts": IN, "alertgroup": ( # repeating group * numAlerts "numAlerts", { "mfrcode": ST, "alertid": ST, "alertinst": ST, "revisionctr": ST, }, ), }, "ALF": { "numSen": IN, "senNum": IN, "seqmid": IN, "timelastchange": TM, "alertcat": CH, # A, B, C "alertpriority": CH, # E, A W, C "alertstate": CH, # A, S, N, O, U, V "mfrcode": ST, "alertid": ST, "alertinst": ST, "revisionctr": ST, "escalationctr": ST, "alerttxt": ST, # max 16 chars }, "ALR": { "timealarmchange": TM, "alarmnum": ST, "alarmcond": ST, "alarmstate": ST, "alarmtext": ST, }, "APA": { "LCgwarn": CH, "LCcwarn": CH, "ctrkerr": DE, "dirs": CH, "ctrkUnit": CH, "aalmcirc": CH, "aalmperp": CH, "bearP2D": DE, "bearP2Du": CH, "wpt": ST, }, "APB": { "LCgwarn": CH, "LCcwarn": CH, "ctrkerr": DE, "dirs": CH, "ctrkUnit": CH, "aalmcirc": CH, "aalmperp": CH, "bearO2D": DE, "bearO2Du": CH, "wpt": ST, "bearD": DE, "bearDu": CH, "bearS": DE, "bearSu": CH, "mode": CH, }, "ARC": { "time": TM, "mfrcode": ST, "alertid": ST, "alertinst": IN, "alertcmd": ST, }, "BBM": { "numSen": IN, "senNum": IN, "seqmid": IN, "aischan": CH, "itumsg": ST, "data": ST, "fillbits": IN, }, "BEC": { "utc": TM, "wptlat": LA, "wptNS": LAD, "wptlon": LN, "wptEW": LND, "bearT": DE, "bearTu": CH, # 'T'rue "bearM": DE, "bearMu": CH, # 'M'agnetic "dist": DE, "distu": CH, # 'N'autical Miles "wpt": ST, }, "BOD": { "bearT": DE, "bearTu": CH, "bearM": DE, "bearMu": CH, "wptD": ST, "wptO": ST, }, "BWC": { "fixutc": TM, "lat": LA, "NS": LAD, "lon": LN, "EW": LND, "bearT": DE, "bearTu": CH, # 'T' "bearM": DE, "bearMu": CH, # 'M' "dist": DE, "distUnit": CH, # 'N' "wpt": ST, "mode": CH, }, "BWR": { "fixutc": TM, "lat": LA, "NS": LAD, "lon": LN, "EW": LND, "bearT": DE, "bearTu": CH, # 'T' "bearM": DE, "bearMu": CH, # 'M' "dist": DE, "distUnit": CH, # 'N' "wpt": ST, "mode": CH, }, "BWW": { "bearT": DE, "bearTu": CH, # 'T' "bearM": DE, "bearMu": CH, # 'M' "wptto": ST, "wptfrom": ST, }, "CUR": { "valid": CH, "datasetnum": IN, "layernum": IN, "depth": DE, "direction": DE, "dirTR": CH, # 'T' or 'R' "speed": DE, "refdepth": DE, "heading": DE, "headingTM": CH, # 'T' or 'M' "speedref": CH, # 'B', 'W', 'P' }, "DBT": { "depthf": DE, "depthfu": CH, # 'f'eet "depthm": DE, "depthmu": CH, # 'M'eter "depthfath": DE, "depthfathu": CH, # 'F'athoms }, "DDC": { "preset": CH, "brightness": IN, "palette": CH, "status": CH, }, "DOR": { "msgtype": CH, "time": TM, "doormontype": ST, "div1ind": ST, "div2ind": ST, "doorcount": IN, "doorstatus": CH, "watertightdoorsetting": CH, "msgtext": ST, }, "DPT": { "depth": DE, # m "offset": DE, "rangescale": DE, }, "DSC": { "format": ST, "address": ST, "category": ST, "distresstype": ST, "commtype": ST, "channel": ST, "timetelno": ST, "mmsi": ST, "distressnature": ST, "acknowledgement": CH, "expansionind": CH, }, "DSE": { "numSen": IN, "senNum": IN, "queryflag": CH, "mmsi": ST, "datagroup": ( "None", # variable repeating group { "code": ST, "data": ST, }, ), }, "DTM": { "datum": ST, "subDatum": ST, "latOfset": DE, "NS": LAD, "lonOfset": DE, "EW": LND, "alt": DE, "refDatum": ST, }, "EPV": { "status": CH, "equipmenttype": ST, "equipmentid": ST, "propertyid": ST, "value": ST, }, "ETL": { "time": TM, "msgtype": CH, "engtelposind": ST, "engsubtelposind": ST, "operatinglocind": CH, "enginenum": CH, }, "EVE": { "time": TM, "tagcode": ST, "eventtext": ST, }, "FIR": { "msgtype": CH, "time": TM, "fdstype": ST, "div1ind": ST, "div2ind": ST, "fdsnum": ST, "condition": CH, "alarmackstate": CH, "text": ST, }, # FMI from Feyman IME19EE Product Integration Guide v1.4.1 "FMI": { "time": TM, "lat": DE, "lon": DE, "alt": DE, "roll": DE, "pitch": DE, "yaw": DE, "hrms": DE, "numSV": IN, "diffAge": IN, "navmod": IN, "status": HX, }, "FSI": { "txfreq": ST, "rxfreq": ST, "opmode": ST, "power": IN, "status": CH, }, "GBS": { "time": TM, "errLat": DE, "errLon": DE, "errAlt": DE, "svid": IN, "prob": DE, "bias": DE, "stddev": DE, "systemId": HX, # NMEA >=4.10 only "signalId": HX, # NMEA >=4.10 only }, "GEN": { "index": HX, "time": TM, "group": ("None", {"data": HX}), # variable repeating group }, "GFA": { "time": TM, "hprotlvl": DE, "vprotlvl": DE, "semimajstddev": DE, "semiminstddev": DE, "semimajorientation": DE, "altstddev": DE, "accuracy": DE, "integrity": ST, # V not in use, 'S'afe, 'C'aution, 'U'nsafe }, "GGA": { "time": TM, "lat": LA, "NS": LAD, "lon": LN, "EW": LND, "quality": IN, "numSV": IN, "HDOP": DE, "alt": DE, # altitude above sea level in m "altUnit": CH, "sep": DE, "sepUnit": CH, "diffAge": DE, "diffStation": IN, }, "GLL": { "lat": LA, "NS": LAD, "lon": LN, "EW": LND, "time": TM, "status": ST, "posMode": ST, }, "GMP": { "utctime": TM, "mapProjection": ST, "mapZone": ST, "gridN": DE, "gridE": DE, "modeInd": CH, "sip": IN, "HDOP": DE, "alt": DE, "geoidSep": DE, "corrAge": DE, "baseId": ST, }, "GNS": { "time": TM, "lat": LA, "NS": LAD, "lon": LN, "EW": LND, "posMode": ST, "numSV": IN, "HDOP": DE, "alt": DE, "sep": DE, "diffAge": DE, "diffStation": IN, "navStatus": CH, # NMEA >=4.10 only }, "GRS": { "time": TM, "mode": IN, "groupSV": ( 12, { # repeating group * 12 "residual": DE, }, ), "systemId": HX, # NMEA >=4.10 only "signalId": HX, # NMEA >=4.10 only }, "GSA": { "opMode": CH, "navMode": IN, "groupSV": ( 12, { # repeating group * 12 "svid": IN, }, ), "PDOP": DE, "HDOP": DE, "VDOP": DE, "systemId": HX, # NMEA >=4.10 only }, "GST": { "time": TM, "rangeRms": DE, "stdMajor": DE, "stdMinor": DE, "orient": DE, "stdLat": DE, "stdLong": DE, "stdAlt": DE, }, "GSV": { "numMsg": IN, "msgNum": IN, "numSV": IN, "group_sv": ( "None", { # repeating group * 1..4 "svid": IN, "elv": IN, # elevation "az": IN, # azimuth "cno": IN, # signal strength }, ), "signalID": HX, # NMEA >=4.10 only }, "HBT": { "repeatint": DE, "status": CH, "seqid": IN, }, "HCR": { "headingt": DE, "mode": CH, "corrstate": CH, "corrvalue": DE, }, "HDG": { "headingM": DE, "deviation": DE, "deviationEW": LND, "variation": DE, "variationEW": LND, }, "HDM": { "headingM": DE, "headingMag": CH, # 'M' }, "HDT": { "headingT": DE, "headingTu": CH, # 'T' }, "HMR": { "sensor1id": ST, "sensor2id": ST, "setdiff": DE, "actualdiff": DE, "warning": CH, # A within set, V exceeds set "sensor1hdg": DE, "sensor1status": CH, "sensor1u": CH, # 'T'rue, 'M'agnetic "sensor1dev": DE, "sensor1EW": LND, "sensor2hdg": DE, "sensor2status": CH, "sensor2u": CH, # 'T'rue, 'M'agnetic "sensor2dev": DE, "sensor2EW": LND, "variation": DE, "variationEW": LND, }, "HMS": { "sensor1id": ST, "sensor2id": ST, "setdiff": DE, }, "HRM": { "heel": DE, "rollperiod": DE, "rollamplitudeport": DE, "rollamplitduestbd": DE, "status": CH, "rollpeakport": DE, "rollpeakstbd": DE, "peakholdresettime": TM, "peakholdresetdat": IN, "peakholdresetmonth": IN, }, "HSC": { "cmdheadingT": DE, "cmdheadingTu": CH, # 'T' "cmdheadingM": DE, "cmdheadingMu": CH, # 'M' "status": CH, }, "HSS": { "measpointid": ST, "value": DE, "status": CH, }, "HTC": { "override": CH, # A in use V not in use "cmdrudderang": DE, "cmdrudderdir": CH, # L port R stbd "steermode": CH, "turnmode": CH, # 'R'adius, 'T'urn rate, 'N'ot controlled "cmdrudderlimit": DE, "cmdoffheadinglimit": DE, "cmdturnradius": DE, "cmdturnrate": DE, "cmdheading": DE, "cmdofftracklimit": DE, "cmdtrack": DE, "headingref": CH, # 'T'rue, 'M'agnetic "status": CH, }, "HTD": { "override": CH, # A in use V not in use "cmdrudderang": DE, "cmdrudderdir": CH, # L port R stbd "steermode": CH, "turnmode": CH, # 'R'adius, 'T'urn rate, 'N'ot controlled "cmdrudderlimit": DE, "cmdoffheadinglimit": DE, "cmdturnradius": DE, "cmdturnrate": DE, "cmdheading": DE, "cmdofftracklimit": DE, "cmdtrack": DE, "headingref": CH, # 'T'rue, 'M'agnetic "rudderstatus": CH, "offheadingstatus": CH, "offtrackstatus": CH, "vesselheading": DE, }, "LR1": { "seqNum": IN, "mmsiresponder": ST, "mmsirequester": ST, "shipname": ST, "callsign": ST, "imonum": ST, }, "LR2": { "seqNum": IN, "mmsiresponder": ST, "date": DTL, "time": TM, "lat": LA, "NS": LAD, "lon": LN, "EW": LND, "cog": DE, # course over ground "cogT": CH, # 'T' "sog": DE, # speed over ground "sogu": CH, # 'N' knots }, "LR3": { "seqNum": IN, "mmsiresponder": ST, "destination": ST, "etadate": DT, "etatime": TM, "draught": DE, "cargo": DE, "length": DE, "breadth": DE, "type": DE, "persons": DE, }, "LRF": { "seqNum": IN, "mmsirequestor": ST, "namerequestor": ST, "function": ST, "replystatus": ST, }, "LRI": { "seqNum": IN, "controlflag": CH, "mmsirequestor": ST, "mmsidestination": ST, "latne": LA, "NSne": LAD, "lonne": LN, "EWne": LND, "latsw": LA, "NSsw": LAD, "lonsw": LN, "EWsw": LND, }, "LLQ": { "utctime": TM, "utcdate": DT, "easting": DE, "eunit": CH, # 'M' "northing": DE, "nunit": CH, # 'M' "gpsQual": IN, "sip": IN, "posQual": DE, "height": DE, "hunit": CH, # 'M' }, "MOB": { "emitterid": ST, "mobstatus": CH, "timeactivation": TM, "mobpossource": CH, "dayactivated": IN, "timeposition": TM, "lat": LA, "NS": LAD, "lon": LN, "EW": LND, "cog": DE, # true "sog": DE, # knots "mmsi": ST, "batterystatus": CH, }, "MSK": { "freq": DE, "fmode": CH, # 'A'uto, 'M'anual "beacbps": IN, "bpsmode": CH, # 'A'uto, 'M'anual "MMSfreq": DE, "channel": CH, "status": CH, }, "MSS": { "strength": IN, "snr": IN, "freq": DE, "beacbps": IN, "channel": CH, }, "MTW": { "temp": DE, "tempu": CH, # degrees C }, "MWD": { "dirT": DE, "dirTu": CH, # 'T'rue "dirM": DE, "dirMu": CH, # 'M'agnetic "speedN": DE, "speedNu": CH, # 'N' knots "speedM": DE, "speedMu": CH, # 'M' m/s }, "MWV": { "angle": DE, "reference": CH, # 'R'elative, 'T'heoretical "speed": DE, "speedu": CH, # K km/h, M m/s N knots "status": CH, }, "NAK": { "talkerid": ST, "formatter": ST, "identifier": ST, "reason": ST, "text": ST, }, "NRM": { "function": CH, "freqtable": CH, "txcovermask": HX, "msgtypemask": HX, "status": CH, }, "NRX": { "numSen": IN, "senNum": IN, "seqid": IN, "msgcode": CH, "freqtable": CH, "time": TM, "day": IN, "month": IN, "year": IN, "totalchar": DE, "totalbad": DE, "status": CH, "body": ST, }, "NSR": { "hdgintegrity": CH, "hdgplausibility": CH, "posintegrity": CH, "posplausibility": CH, "stwintegrity": CH, "stwplausibility": CH, "sogcogintegrity": CH, "sogcogplausibility": CH, "depthintegrity": CH, "depthplausibility": CH, "stwmode": CH, "timeintegrity": CH, "timeplausibility": CH, }, "OSD": { "hdg": DE, "hdgstate": CH, "course": DE, "courseref": CH, "speed": DE, "speedref": CH, "set": DE, "drift": DE, "speedu": CH, # K km/h N knots S mph }, "POS": { "equipmentid": ST, "equipmentnum": IN, "posvalidity": CH, "posx": DE, "posy": DE, "posz": DE, "shipdimvalid": CH, "width": DE, "length": DE, "status": CH, }, "PRC": { "leverdmdpos": DE, "leverdmdstatus": CH, "rpmdemand": DE, "rpmdemandstatus": CH, "pitchdmd": DE, "pitchmode": CH, "operatinglocind": CH, "engineshaftnum": IN, }, "RLM": { "beacon": HX, "time": TM, "code": CH, "body": HX, }, "RMA": { "status": CH, "lat": LA, "NS": LAD, "lon": LN, "EW": LND, "reserved1": ST, "reserved2": ST, "sog": DE, "cog": DE, "var": DE, "dirvar": CH, }, "RMB": { "status": CH, "ctrkerr": DE, "dirs": CH, "wptO": CH, "wptD": CH, "lat": LA, # of wptD "NS": LAD, "lon": LN, # of wptD "EW": LND, "range": DE, "bearing": DE, "velclos": DE, "arrstatus": CH, "valstatus": CH, }, "RMC": { "time": TM, "status": CH, "lat": LA, "NS": LAD, "lon": LN, "EW": LND, "spd": DE, # speed in knots "cog": DE, # course over ground "date": DT, "mv": DE, "mvEW": ST, "posMode": CH, "navStatus": CH, # NMEA >=4.10 only }, "ROR": { "stbdrudderorder": DE, "stbdrudderstatus": CH, "portrudderorder": DE, "portrudderstatus": CH, "operatinglocind": CH, }, "ROT": { "rot": DE, # -ve = turn to port "valid": CH, # A valid, V invalid }, "RRT": { "tfrtype": CH, "tfrroutename": ST, "tfrroutever": ST, "wptid": ST, "filestatus": CH, "appstatus": CH, }, "RPM": { "source": CH, "engineshaftnum": IN, "rpm": DE, "pitch": DE, "status": CH, }, "RSA": { "stbdangle": DE, "stbdstatus": CH, "portangle": DE, "portstatus": CH, }, "RSD": { "origin1range": DE, "origin1bearing": DE, "vrm1": DE, "ebl1": DE, "origin2range": DE, "origin2bearing": DE, "vrm2": DE, "ebl2": DE, "cursorrange": DE, "rangebearing": DE, "rangescale": DE, "rangeu": CH, # K kn N nautical mile S statute mile "displayrot": CH, }, "RTE": { "numMsg": IN, "msgNum": IN, "status": CH, # 'c'/'w' "routeid": ST, "group_wp": ( "None", { # repeating group "wpt": ST, }, ), }, "SFI": { "numSen": IN, "senNum": IN, "freqgroup": ( 6, # repeating group * 6 { "freq": IN, "mode": ST, }, ), }, "SM1": { "MSIstatus": CH, "msgnum": IN, "LESseqnum": IN, "LESid": IN, "oceanregioncode": IN, "prioritycode": IN, "servicecode": IN, "presentationcode": IN, "year": IN, "month": IN, "day": IN, "hour": IN, "minute": IN, "addresscode": IN, }, "SM2": { "MSIstatus": CH, "msgnum": IN, "LESseqnum": IN, "LESid": IN, "oceanregioncode": IN, "prioritycode": IN, "servicecode": IN, "presentationcode": IN, "year": IN, "month": IN, "day": IN, "hour": IN, "minute": IN, "coastalwarnnavarea": IN, "coastalwarnarea": CH, "coastalwarnsubjectind": CH, }, "SM3": { "MSIstatus": CH, "msgnum": IN, "LESseqnum": IN, "LESid": IN, "oceanregioncode": IN, "prioritycode": IN, "servicecode": IN, "presentationcode": IN, "year": IN, "month": IN, "day": IN, "hour": IN, "minute": IN, "circarealat": LA, "circareaNS": LAD, "circarealon": LN, "circareaEW": LND, "circarearadius": IN, }, "SM4": { "MSIstatus": CH, "msgnum": IN, "LESseqnum": IN, "LESid": IN, "oceanregioncode": IN, "prioritycode": IN, "servicecode": IN, "presentationcode": IN, "year": IN, "month": IN, "day": IN, "hour": IN, "minute": IN, "redareaswlat": LA, "recareaswNS": LAD, "redareaswlon": LN, "recareaswEW": LND, "recareaextentN": IN, "recareaextentE": IN, }, "SMB": {"numSen": IN, "senNum": IN, "seqid": IN, "msgnum": IN, "body": ST}, "SPW": { "pwdprotectsentence": ST, "id": ST, "pwdlevel": IN, "password": ST, }, "SSD": { "callsign": ST, "name": ST, "posAref": IN, # bow "posBref": IN, # stern "posCref": IN, # port beam "posDref": IN, # stbd beam "DTEind": ST, "sourceid": ST, }, "STN": { "talkerId": ST, }, "THS": { "headt": DE, "mi": CH, }, "TLB": { "group": ( "None", # indeterminate repeating group { "targetnum": IN, "label": ST, }, ) }, "TLL": { "targetnum": IN, "targetlat": LA, "targetlatNS": LAD, "targetlon": LN, "targetlonEW": LND, "targetname": ST, "time": TM, "status": CH, "reftarget": CH, }, "TRA": { "time": TM, "heading": DE, "pitch": DE, "roll": DE, "quality": IN, "svid": IN, "diffAge": DE, "stationID": ST, }, "TRC": { "thrusternum": IN, "dmdrpm": DE, "rpmmode": CH, "dmdpitch": DE, "pitchmode": CH, "dmdazimuth": DE, "operatinglocind": CH, "status": CH, }, "TRD": { "thrusternum": IN, "rpmresponse": IN, "rpmmode": CH, "pitchresponse": IN, "pitchmode": CH, "azimuthresponse": IN, }, "TRL": { "totlognum": IN, "lognum": IN, "seqid": IN, "switchoffdate": ST, "switchofftime": TM, "switchondate": ST, "switchontime": TM, "reasoncode": IN, }, "TRF": { "time": TM, "date": DT, "lat": LA, "NS": LAD, "lon": LN, "EW": LND, "elangle": DE, "iter": DE, "Dopint": DE, "dist": DE, "svid": IN, }, "TTD": { "numSen": IN, "senNum": IN, "seqid": IN, "data": ST, "fillbits": IN, }, "TTM": { "targetnum": IN, "targetdist": DE, "targetbearing": DE, "targetbearingu": CH, # 'T'rue, 'R'relative "targetspeed": DE, "targetcourse": DE, "targetcourseu": CH, # 'T'rue, 'R'relative "closestapproach": DE, "timetoCPA": DE, "speedu": CH, "targetname": ST, "targetstatus": CH, "reftarget": ST, "time": TM, "acquisitiontype": CH, }, "TUT": { "sourceid": ST, "numSen": HX, "senNum": HX, "seqid": IN, "translationcode": ST, "text": ST, }, "TXT": { "numMsg": IN, "msgNum": IN, "msgType": IN, "text": ST, }, "UID": { "uid1": ST, "uid2": ST, # optional }, "VBW": { "longwaterspd": DE, # -ve = astern "transwaterspd": DE, "waterspdstatus": CH, "longgroundspd": DE, # -ve = astern "transgroundspd": DE, # -ve = port "groundspdstatus": CH, "sterntranswaterspd": DE, # -ve = port "sternwaterspdstatus": CH, "sterntransgroundspd": DE, # -ve = port "sterngroundspdstatus": CH, }, "VDM": { "numSen": IN, "senNum": IN, "seqid": IN, "aischan": ST, "itumsg": ST, "fillbits": IN, }, "VDO": { "numSen": IN, "senNum": IN, "seqid": IN, "aischan": ST, "itumsg": ST, "fillbits": IN, }, "VDR": { "dirT": DE, "dirTu": CH, # 'T'rue "dirM": DE, "dirMu": CH, # 'M'agnetic "spd": DE, "spdu": CH, # N knots }, "VER": { "numSen": IN, "senNum": IN, "devicetype": ST, "vendorid": ST, "uid": ST, "mfrserial": ST, "model": ST, "software": ST, "hardware": ST, "seqid": IN, }, "VHW": { "hdgT": DE, "hgdTu": CH, # 'T'rue "hdgM": DE, "hgdMu": CH, # 'M'agnetic "spdN": DE, "spdNu": CH, # N knots "spdK": DE, "spdKu": CH, # K km/h }, "VLW": { "twd": DE, "twdUnit": CH, "wd": DE, "wdUnit": CH, "tgd": DE, # NMEA >=4.00 only "tgdUnit": CH, # NMEA >=4.00 only "gd": DE, # NMEA >=4.00 only "gdUnit": CH, # NMEA >=4.00 only }, "VPW": { "spdN": DE, "spdNu": CH, # N knots "spdK": DE, "spdKu": CH, # K km/h }, "VSD": { "type": ST, "draught": DE, "pob": IN, "destination": ST, "etatime": TM, "etaday": IN, "etamonth": IN, "regappflags1": IN, "regappflags2": IN, }, "VTG": { "cogt": DE, # course over ground (true) "cogtUnit": CH, "cogm": DE, # course over ground (magnetic) "cogmUnit": CH, "sogn": DE, # speed over ground knots "sognUnit": CH, "sogk": DE, # speed over ground kmph "sogkUnit": CH, "posMode": CH, # NMEA >=2.3 only }, "WAT": { "msgtype": CH, "time": TM, "alarmtype": ST, "loc1ind": IN, "loc2ind": IN, "detectionpoint": IN, "alarmcondition": CH, "overridesetting": CH, "text": ST, }, "WCV": { "vel": DE, "velu": CH, # N knots "wpt": ST, "mode": CH, }, "WNC": { "distN": DE, "distNu": CH, # 'N' nm "distK": DE, "distKu": CH, # 'K' km "wptto": ST, "wptfrom": ST, }, "WPL": { "lat": LA, "NS": LAD, "lon": LN, "EW": LND, "wpt": ST, }, "XDR": { "group": ( "None", # indeterminate repeating group { "tdrtype": CH, "data": DE, # Units of measurement A = Amperes B = Bars B = Binary # C = Celsius D = Degrees H = Hertz I = liters/second # K = Kelvin K = kg/m3 M = Meters M = cubic Meters # N = Newton P = % of full range P = Pascal R = RPM # S = Parts per thousand V = Volts "dataunit": CH, "tdrid": ST, }, ) }, "XTE": { "gwarn": CH, "LCcwarn": CH, "ctrkerr": DE, "dirs": CH, "disUnit": CH, # N nm "mode": CH, }, "XTR": { "ctkerr": DE, "steerdir": CH, "units": CH, # N nm }, "ZDA": { "time": TM, "day": IN, "month": IN, "year": IN, "ltzh": ST, "ltzn": ST, }, "ZDL": { "time": TM, "dist": DE, "type": CH, }, "ZFO": { "observationtime": TM, "elapsedtime": TM, "originwpt": ST, }, "ZTG": { "observationtime": TM, "timetogo": TM, "destwpt": ST, }, # ********************************************* # Dummy message for error testing # ********************************************* "FOO": {"spam": "Z2", "eggs": "Y1"}, } NMEA_PAYLOADS_GET_UNICORE = { # ********************************************* # UNICORE UM98n Extended NMEA Sentences # ********************************************* "GGAH": NMEA_PAYLOADS_GET["GGA"], "GLLH": NMEA_PAYLOADS_GET["GLL"], "GNSH": NMEA_PAYLOADS_GET["GNS"], "GRSH": NMEA_PAYLOADS_GET["GRS"], "GSAH": NMEA_PAYLOADS_GET["GSA"], "GSTH": NMEA_PAYLOADS_GET["GST"], "GSVH": NMEA_PAYLOADS_GET["GSV"], "RMCH": NMEA_PAYLOADS_GET["RMC"], "VTGH": NMEA_PAYLOADS_GET["VTG"], "THS2": NMEA_PAYLOADS_GET["THS"], "HPR": { "time": TM, "heading": DE, "pitch": DE, "roll": DE, "quality": IN, "svid": IN, "diffAge": DE, "stationID": ST, }, "HPR2": { "time": TM, "heading": DE, "pitch": DE, "roll": DE, "quality": IN, "svid": IN, "diffAge": DE, "stationID": ST, }, "TRA2": NMEA_PAYLOADS_GET["TRA"], "ROT2": NMEA_PAYLOADS_GET["ROT"], "HPD": { "wno": IN, "tow": DE, "heading": DE, "pitch": DE, "cog": DE, "lat": DE, "lon": DE, "alt": DE, "velE": DE, "velN": DE, "velU": DE, "veldeltaE": DE, "veldeltaN": DE, "veldeltaU": DE, "baseline": DE, "sivmain": IN, "sivsecond": IN, }, } NMEA_PAYLOADS_GET = {**NMEA_PAYLOADS_GET, **NMEA_PAYLOADS_GET_UNICORE} pynmeagps-1.1.7/src/pynmeagps/nmeatypes_get_prop.py000066400000000000000000001604731523640354200226120ustar00rootroot00000000000000""" NMEA Protocol Proprietary Output (GET) payload definitions THESE ARE THE PAYLOAD DEFINITIONS FOR PROPRIETARY _GET_ MESSAGES _FROM_ THE RECEIVER (e.g. Periodic Navigation Data; Poll Responses; Info messages). NB: Attribute names must be unique within each message id. NB: Avoid reserved names 'msgID', 'talker', 'payload', 'checksum'. NB: Repeating groups must be defined as a tuple thus 'group': ('numr', {dict}) where - 'numr' is either: a) an integer representing a fixed number of repeats e.g 32 b) a string representing the name of a preceding attribute containing the number of repeats e.g. 'numCh' c) 'None' for an indeterminate repeating group (only one such group is permitted per message type) - {dict} is the nested dictionary containing the repeating attributes Created on 4 Mar Sep 2021 While the NMEA 0183 © protocol is proprietary, the information here has been collated from public domain sources. :author: semuadmin (Steve Smith) """ # pylint: disable=too-many-lines, duplicate-code from pynmeagps.nmeatypes_core import ( CH, DE, DM, DT, HX, IN, LA, LAD, LN, LND, QS, ST, TM, ) NMEA_PAYLOADS_GET_PROP = { # ********************************************* # JVC KENWOOD PROPRIETARY MESSAGES # ********************************************* "KLDS": { "time": TM, "status": CH, "lat": LA, "NS": LAD, "lon": LN, "EW": LND, "sog": DE, "cog": DE, "dat": DT, "declination": DE, "dec_dir": CH, "fleet": DE, "senderid": ST, "senderstatus": DE, "reserved": DE, }, "KLSH": { "lat": LA, "NS": LAD, "lon": LN, "EW": LND, "time": TM, "status": CH, "fleetId": ST, "deviceId": ST, }, "KNDS": { "time": TM, "status": CH, "lat": LA, "NS": LAD, "lon": LN, "EW": LND, "sog": DE, "cog": DE, "date": DT, "declination": DE, "dec_dir": CH, "senderid": ST, "senderstatus": DE, "reserved": DE, }, "KNSH": { "lat": LA, "NS": LAD, "lon": LN, "EW": LND, "time": TM, "status": CH, "senderid": ST, }, "KWDWPL": { "time": TM, "status": CH, "lat": LA, "NS": LAD, "lon": LN, "EW": LND, "sog": DE, "cog": DE, "date": DT, "alt": DE, "wpt": ST, "ts": ST, }, # ********************************************* # MAGELLAN PROPRIETARY MESSAGES # ********************************************* "MGNWPL": { "lat": LA, "NS": LAD, "lon": LN, "EW": LND, "alt": DE, "alt_unit": CH, "wpt": ST, "comment": ST, "icon": ST, "type": ST, }, # ********************************************* # GARMIN PROPRIETARY MESSAGES # ********************************************* "GRME": { # estimated error information "HPE": DE, "HPEUnit": CH, "VPE": DE, "VPEUnit": CH, "EPE": DE, "EPEUnit": CH, }, "GRMF": { # GPS fix data sentence "week": IN, "secs": IN, "date": DT, "time": TM, "leapsec": IN, "lat": LA, "NS": LAD, "lon": LN, "EW": LND, "mode": CH, "fix": IN, "spd": DE, "course": IN, "PDOP": DE, "TDOP": DE, }, "GRMH": { # aviation height and VNAV data "status": CH, "vspd": DE, "verr": DE, "spdtgt": DE, "spdwpt": DE, "height": DE, "trk": DE, "course": DE, }, "GRMM": { # map datum "dtm": ST, }, "GRMT": { # sensor status information "ver": ST, "ROMtest": CH, "rcvrtest": CH, "stortest": CH, "rtctest": CH, "osctest": CH, "datatest": CH, "temp": DE, "cfgdata": CH, }, "GRMV": { # 3D velocity information "velE": DE, "velN": DE, "velZ": DE, }, "GRMZ": { # altitude "alt": DE, "altUnit": CH, "fix": IN, }, "GRMB": { # DGPS Beacon information "freq": DE, "bps": IN, "snr": IN, "quality": IN, "dist": DE, "status": IN, "fixsrc": CH, "mode": CH, }, # ********************************************* # U-BLOX PROPRIETARY MESSAGES # ********************************************* "UBX00": { "msgId": ST, # '00' "time": TM, "lat": LA, "NS": LAD, "lon": LN, "EW": LND, "altRef": DE, "navStat": ST, "hAcc": DE, "vAcc": DE, "SOG": DE, "COG": DE, "vVel": DE, "diffAge": DE, "HDOP": DE, "VDOP": DE, "TDOP": DE, "numSVs": IN, "reserved": IN, "DR": IN, }, "UBX03": { "msgId": ST, # '03' "numSv": IN, "groupSV": ( "numSv", { # repeating group * numSv "svid": IN, "status": CH, "azi": DE, "ele": DE, "cno": IN, "lck": IN, }, ), }, "UBX04": { "msgId": ST, # '04' "time": TM, "date": DT, "utcTow": ST, "utcWk": ST, "leapSec": ST, "clkBias": DE, "clkDrift": DE, "tpGran": IN, }, "UBX05": { # deprecated, for backwards compat only "msgId": ST, # '05' "pulses": IN, "period": IN, "gyroMean": IN, "temperature": DE, "direction": CH, "pulseScaleCS": IN, "gyroScaleCS": IN, "gyroBiasCS": IN, "pulseScale": DE, "gyroBias": DE, "gyroScale": DE, "pulseScaleAcc": IN, "gyroBiasAcc": IN, "gyroScaleAcc": IN, "measUsed": HX, }, # *************************************************************** # Trimble Proprietary message types # https://receiverhelp.trimble.com/alloy-gnss/en-us/NMEA-0183messages_MessageOverview.html # *************************************************************** "ASHR": { "utctime": TM, "trueHdg": DE, "trueHdgInd": CH, # "T" "roll": DE, "pitch": DE, "reserved": ST, "rollAcc": DE, "pitchAcc": DE, "hdgAcc": DE, "gnssQual": IN, "imuAlign": IN, }, "ASHRALR": { "msgId": ST, # "ALR" "alarmCode": IN, "alarmSubcode": IN, "streamId": CH, "alarmCat": ST, "alarmLevel": IN, "desc": ST, }, "ASHRARA": { "msgId": ST, # "ARA" "valid": CH, # = "0" when valid "utctime": TM, "hdgSpeed": DE, "pitchSpeed": DE, "rollSpeed": DE, "hdgAcc": DE, "pitchAcc": DE, "rollAcc": DE, "reserved": ST, }, "ASHRARR": { "msgId": ST, # "ARR" "vectNum": IN, "vectMode": IN, "sip": IN, "utctime": TM, "antEcefX": DE, "antEcefY": DE, "antEcefZ": DE, "coord1std": DE, "coord2std": DE, "coord3std": DE, "coord12corr": DE, "coord13corr": DE, "coord23corr": DE, "refId": CH, "vectFrame": IN, # 0 = XYZ "vectOpt": IN, "clkAssum": IN, }, "ASHRATT": { "msgId": ST, # "ATT" "weekTime": DE, "trueHdg": DE, "pitch": DE, "roll": DE, "carrierRmsErr": DE, "baselineRmsErr": DE, "ambiguity": IN, }, "ASHRBTS": { "msgId": ST, # "BTS" "portgroup": ( 3, { "port": CH, # C, H or T "connected": IN, "name": ST, "addr": ST, "linkQual": IN, }, ), }, "ASHRCAP": { "msgId": ST, # "CAP" "name": ST, "L1Noffset": DE, "L1Eoffset": DE, "L1Voffset": DE, "L2Noffset": DE, "L2Eoffset": DE, "L2Voffset": DE, }, "ASHRCPA": { "msgId": ST, # "CPA" "antHeight": DE, "antRadius": DE, "vertOffset": DE, "horAzi": DE, "horDist": DE, "reserved1": ST, "reserved2": ST, "reserved3": ST, "reserved4": ST, }, "ASHRCPO": { "msgId": ST, # "CPO" "lat": LA, "NS": LAD, "lon": LN, "EW": LND, "height": DE, }, "ASHRDDM": { "msgId": ST, # "DDM" "corrPort": CH, "msgTransport": ST, "msgIdent": ST, "count": IN, "baseId": ST, "timeLag": DE, "corrAge": DE, "attr": ST, }, "ASHRDDS": { "msgId": ST, # "DDS" "diffDecodeNum": IN, "timeTag": TM, "numMsgDecoded": IN, "port": CH, "prot": ST, "timeWindow": IN, "percLinkQual": IN, "percDeselectInfo": IN, "percCrc": IN, "latencyStd": IN, "latencyMean": IN, "epochIntMean": IN, "epochIntMin": IN, "numMsgDetected": IN, "msgType": ST, "lastMsgInt": DE, "lastMsgAge": DE, }, "ASHRHPR": { "msgId": ST, # "HPR" "utctime": TM, "trueHdg": DE, "pitch": DE, "roll": DE, "carrierRmsErr": DE, "baselineRmsErr": DE, "ambiguity": IN, "attStatus": IN, "antBaseline": ST, "PDOP": DE, }, "ASHRLTN": { "msgId": ST, # "LTN" "latency": DE, }, "ASHRMDM": { "msgId": ST, # "MDM" "port": ST, "baud": IN, "state": ST, "powerMode": ST, "pinCode": ST, "protocol": IN, "csdMode": IN, # not used "accessPoint": ST, "login": ST, "password": ST, "phoneNum": ST, "autoDial": CH, "maxRedial": IN, "model": ST, "selMode": IN, "gsmAnt": ST, }, "ASHRPBN": { "msgId": ST, # "PBN", "gpsWeek": DE, "siteName": ST, "navX": DE, "navY": DE, "navT": DE, "navXdot": DE, "navYdot": DE, "navTdot": DE, "PDOP": DE, }, "ASHRPOS": { "msgId": ST, # "POS" "solnType": IN, "sip": IN, "utctime": TM, "lat": LA, "NS": LAD, "lon": LN, "EW": LND, "alt": DE, "corrAge": DE, "trueTrk": DE, "sog": DE, "vertVel": DE, "PDOP": DE, "HDOP": DE, "VDOP": DE, "TDOP": DE, "baseId": ST, }, "ASHRPTT": { "msgId": ST, # "PTT" "dow": IN, "timeTag": ST, }, "ASHRPWR": { "msgId": ST, # "PWR" "powerSource": IN, "voltsOut": DE, "reserved1": ST, "percRemaining": IN, "reserved2": ST, "voltsIn": DE, "chargeStatus": IN, "reserved3": ST, "tempInt": DE, "tempBatt": DE, }, "ASHRRCS": { "msgId": ST, # "RCS" "recordStatus": CH, "memory": IN, "fileName": ST, "recordRate": IN, "occupationType": IN, "occupationState": IN, "occupationName": ST, }, "ASHRSBD": { "msgId": ST, # "SBD" "siv": IN, "satgroup": ( # repeating group * siv "siv", { "prn": IN, "azi": DE, "ele": DE, "snrB1": DE, "snrB2": DE, "snrB3": DE, "usageStatus": ST, "corrStatus": ST, }, ), }, "ASHRSGA": { "msgId": ST, # "SGA" "siv": IN, "satgroup": ( # repeating group * siv "siv", { "prn": IN, "azi": DE, "ele": DE, "snrE1": DE, "snrE5b": DE, "snrE5a": DE, "usageStatus": ST, "corrStatus": ST, }, ), }, "ASHRSGL": { "msgId": ST, # "SGL" "siv": IN, "satgroup": ( # repeating group * siv "siv", { "prn": IN, "azi": DE, "ele": DE, "snrL1": DE, "snrL2": DE, "snrL3": DE, "usageStatus": ST, "corrStatus": ST, }, ), }, "ASHRSGO": { "msgId": ST, # "SGO" "siv": IN, "satgroup": ( # repeating group * siv "siv", { "prn": IN, "azi": DE, "ele": DE, "snrE1": DE, "snrE5b": DE, "snrE5a": DE, "snrE6": DE, "reserved": ST, "usageStatus": ST, "corrStatus": ST, }, ), }, "ASHRSGP": { "msgId": ST, # "SGP" "siv": IN, "satgroup": ( # repeating group * siv "siv", { "prn": IN, "azi": DE, "ele": DE, "snrL1": DE, "snrL2": DE, "snrL3": DE, "usageStatus": ST, "corrStatus": ST, }, ), }, "ASHRSIR": { "msgId": ST, # "SIR" "siv": IN, "satgroup": ( # repeating group * siv "siv", { "prn": IN, "azi": DE, "ele": DE, "reserved2": ST, "reserved3": ST, "snrL5": DE, "usageStatus": ST, "corrStatus": ST, }, ), }, "ASHRSLB": { "msgId": ST, # "SLB" "siv": IN, "satgroup": ( # repeating group * siv "siv", { "satNum": IN, "contTrkInt": DE, "azi": DE, "ele": DE, "snr": DE, }, ), }, "ASHRSQZ": { "msgId": ST, # "SQZ" "siv": IN, "satgroup": ( # repeating group * siv "siv", { "prn": IN, "azi": DE, "ele": DE, "snrL1": DE, "snrL2": DE, "snrL3": DE, "usageStatus": ST, "corrStatus": ST, }, ), }, "ASHRSSB": { "msgId": ST, # "SSB" "siv": IN, "satgroup": ( # repeating group * siv "siv", { "prn": IN, "azi": DE, "ele": DE, "snrL1": DE, "reserved": ST, "snrL5": DE, "usageStatus": ST, "corrStatus": ST, }, ), }, "ASHRTEM": { "msgId": ST, # "TEM" "temp": DE, # 1/1000 degrees }, "ASHRTHS": { "msgId": ST, # "THS" "lastComputedHdg": DE, "solnStatus": CH, }, "ASHRTTT": { "msgId": ST, # "TTT" "dow": IN, "timeTag": TM, }, "ASHRVCR": { "msgId": ST, # "VCR" "baselineNum": IN, "baselineMode": IN, "sip": IN, "utctime": TM, "antEcefX": DE, "antEcefY": DE, "antEcefZ": DE, "coord1std": DE, "coord2std": DE, "coord3std": DE, "coord12corr": DE, "coord13corr": DE, "coord23corr": DE, "baseId": ST, "baselineCoordId": ST, # 0 = XYZ }, "ASHRVCT": { "msgId": ST, # "VCT" "baselineMode": IN, "sip": IN, "utctime": TM, "antEcefX": DE, "antEcefY": DE, "antEcefZ": DE, "coord1std": DE, "coord2std": DE, "coord3std": DE, "coord12corr": DE, "coord13corr": DE, "coord23corr": DE, "baseId": ST, "baselineCoordId": ST, # 0 = XYZ "baselineNum": IN, "vrs": IN, "staticModeAssumption": IN, }, "ASHRVEL": { "msgId": ST, # "VEL" "velE": DE, "velN": DE, "velV": DE, "velERmsErr": DE, "velNRmsErr": DE, "velVRmsErr": DE, "velSmoothInt": IN, }, "FUGDP": { "type": ST, # GPS (GP), GLONASS (GL) or GNSS (GN) "utctime": TM, "lat": LA, "NS": LAD, "lon": LN, "EW": LND, "siv": IN, "dpvoaQual": ST, "dgnssMode": ST, "smajErr": DE, "sminErr": DE, "dirErr": IN, }, "GPPADV110": { "msgId": ST, # "110" "lat": DE, "lon": DE, "height": DE, "ele": DE, # optional "azi": DE, # optional }, "GPPADV120": { "msgId": ST, # "120" "satgrp": ( # repeating group * 2 2, { "prn": IN, "ele": DE, "azi": DE, }, ), }, "TNLAVR": { "msgId": ST, # "AVR" "utctime": TM, "yaw": DE, "yawc": ST, # "Yaw" "tilt": DE, "tiltc": ST, # "Tilt" "roll": DE, "rollc": ST, # "Roll" "range": DE, "gpsQual": IN, "PDOP": DE, "sip": IN, }, "TNLBPQ": { "msgId": ST, # "BPQ" "utctime": TM, "utcdate": DT, "lat": LA, "NS": LAD, "lon": LN, "EW": LND, "height": ST, # starts with "EHT-" "hunit": CH, # 'M' "gpsQual": IN, }, "TNLDG": { "msgId": ST, # "DG" TODO check "strength": DE, "snr": DE, "freq": DE, "bitRate": IN, "chan": IN, "trkStatus": CH, "trkPerf": IN, }, "TNLEVT": { "msgId": ST, # "EVT" "utctime": TM, "port": IN, "numEvents": IN, "wno": IN, "dow": IN, "leaps": IN, }, "TNLGGK": { "msgId": ST, # "GGK" "utctime": TM, "utcdate": DT, "lat": LA, "NS": LAD, "lon": LN, "EW": LND, "gpsQual": IN, "sip": IN, "DOP": DE, "height": ST, # starts with "EHT-" "hunit": CH, # 'M' }, "TNLGGKx": { "msgId": ST, # "GGK" "utctime": TM, "utcdate": DT, "lat": LA, "NS": LAD, "lon": LN, "EW": LND, "posType": IN, "sip": IN, "PDOP": DE, "height": DE, "numExt": IN, "sigmaE": DE, "sigmaN": DE, "sigmaV": DE, "propogationAge": DE, }, "TNLPJK": { "msgId": ST, # "PJK" "utctime": TM, "utcdate": DT, "northing": DE, "nunit": CH, # 'N' "easting": DE, "eunit": CH, # 'E" "gpsQual": IN, "sip": IN, "DOP": DE, "height": ST, # starts with "EHT-" or "GHT-" "hunit": CH, # 'M' }, "TNLPJT": { "msgId": ST, # "PJT" "coordName": ST, "projName": ST, }, "TNLREX": { "msgId": ST, # "REX" "utctime": TM, "constUsed": ST, "sip": IN, "lat": LA, "NS": LAD, "lon": LN, "EW": LND, "alt": DE, "latSigmaErr": DE, "lonSigmaErr": DE, "heightSigmaErr": DE, "vectE": DE, "vectN": DE, "vectV": DE, "vectESigmaErr": DE, "vectNSigmaErr": DE, "vectVSigmaErr": DE, "corrSource": ST, }, "TNLVGK": { "msgId": ST, # "VGK" "utctime": TM, "utcdate": DM, # mmddyy "vectE": DE, "vectN": DE, "vectV": DE, "gpsQual": IN, "sip": IN, "DOP": DE, "vunit": CH, # 'M' }, "TNLVHD": { "msgId": ST, # "VHD" "utctime": TM, "utcdate": DM, # mmddyy "azi": DE, "aziRate": DE, "ele": DE, "eleRate": DE, "range": DE, "rangeRate": DE, "gpsQual": IN, "sip": IN, "PDOP": DE, "unit": CH, # 'M' }, # ********************************************* # Furuno # https://www.furuno.com/en/support/sdk/ # ********************************************* "FECGPATT": { "msgId": ST, # 'GPatt' "yaw": DE, "pitch": DE, "roll": DE, }, "FECGPHVE": { "msgId": ST, # 'GPhve' "heave": DE, "status": CH, # 'A' }, # *************************************************************** # Locosys Proprietary Messages GET # https://www.locosystech.com/Templates/att/LS2303x-UDG_datasheet_v0.6.pdf?lng=en # *************************************************************** "LSR": { "msgType": ST, # e.g. "SLOPE", "MEMS", "ATTIT" "value": IN, # e.g. 0 "response": ST, # e.g. "OK" }, "LSVD": { "velE": DE, # cm/s "velN": DE, # cm/s "velD": DE, # cm/s "velEdev": DE, # cm/s deviation "velNdev": DE, # cm/s deviation "velDdev": DE, # cm/s deviation }, "INVMINR": { "status": CH, # 0 uninitialized; 1ã€�2 calibrating/initializing; 3 calibration done }, "INVMSTR": { "value": IN, }, "INVMSLOPE": { "slope": DE, # degrees "altDiff": DE, # meters "moveDist": DE, # meters "slopeAccu": DE, # degrees "altDiffAccu": DE, # meters "moveDistAccu": DE, # meters }, "INVMIMU": { "timeSecond": DE, # sec timestamp "accelX": DE, # m/s^2 "accelY": DE, # m/s^2 "accelZ": DE, # m/s^2 "gyroX": DE, # degree/s "gyroY": DE, # degree/s "gyroZ": DE, # degree/s }, "INVMATTIT": { "roll": DE, # degree "pitch": DE, # degree "yaw": DE, # degree heading }, # *************************************************************** # Quectel LC29H / LC79H Proprietary GET message types # https://www.quectel.com/download/quectel_lc29hlc79h_series_gnss_protocol_specification_v1-5/ # quectel_lc29hlc79h_series_gnss_protocol_specification_v1-5.pdf # *************************************************************** "AIR001": { "commandid": IN, "result": IN, }, "AIR010": { "type": IN, "subsystem": IN, "wno": IN, "tow": IN, }, "AIR051": { "time": IN, }, "AIR059": { "minsnr": IN, }, "AIR063": { "type": IN, "rate": IN, }, "AIR067": { "gpsEnabled": IN, "glonassEnabled": IN, "galileoEnabled": IN, "bdsEnabled": IN, "qzssEnabled": IN, "navicEnabled": IN, }, "AIR071": { "speedThreshold": DE, }, "AIR073": { "degree": IN, }, "AIR075": { "enabled": IN, }, "AIR081": { "navMode": IN, }, "AIR087": { "status": IN, }, "AIR101": { "nmeaMode": IN, "reserved": IN, }, "AIR105": { "dualBandEnabled": IN, }, "AIR391": { "cmdType": IN, }, "AIR401": { "mode": IN, }, "AIR411": { "enabled": IN, }, "AIR421": { "enabled": IN, }, "AIR433": { "mode": IN, }, "AIR435": { "enabled": IN, }, "AIR436": { "enabled": IN, }, "AIR437": {"enabled": IN}, "AIR491": { "enabled": IN, }, "AIR650": { "second": IN, }, "AIR865": { "baudrate": IN, }, "AIR867": { "flowControl": IN, }, "AIR6011": { "type": IN, "rate": IN, }, "AIRSPF": { "status": IN, }, "AIRSPF5": { "status": IN, }, # *************************************************************** # Quectel LG290P Proprietary message types # https://quectel.com/content/uploads/2024/09/Quectel_LG290P03_GNSS_Protocol_Specification_V1.0.pdf # # status attribute must be set to: # 'R' for POLL messages (queries) # 'W' for SET messages (commands) # 'OK' for GET acknowledgements (ACK) # 'ERROR' for GET errors (NAK) # *************************************************************** "QTMACK": { "status": QS, # OK }, "QTMNAK": { "status": QS, # ERROR "errcode": IN, # 1 = invalid parms, 2 = failed exec, 3 = unsupported }, "QTMANTENNASTATUS": { "msgver": IN, # always 1 for this version "antennastatus": IN, "antennapowerind": IN, }, "QTMCFGAIC": { "status": QS, "state": IN, }, "QTMCFGANTDELTA": { "status": QS, "east": DE, "north": DE, "height": DE, }, "QTMCFGANTINF": { "status": QS, "antennadesc": ST, "antennasetupid": IN, "antennasn": ST, }, "QTMCFGBLD": { "status": QS, "distance": DE, }, "QTMCFGCNST": { "status": QS, "gps": IN, "glonass": IN, "galileo": IN, "beidou": IN, "qzss": IN, "navic": IN, }, "QTMCFGDR": { "status": QS, "drstate": IN, }, "QTMCFGDRHOT": {"status": QS, "mode": IN}, "QTMCFGDRRTD": {"status": QS, "time": IN, "distance": IN}, "QTMCFGEINSMSG": { "type": IN, "ins_enabled": IN, "imu_enabled": IN, "gps_enabled": IN, "rate": IN, }, "QTMCFGELETHD": { "status": QS, "elevation": DE, }, "QTMCFGFIXRATE": { "status": QS, "fixinterval": IN, }, "QTMCFGGEOFENCE": { "status": QS, "geofenceindex": IN, "geofencemode": IN, "reserved": IN, "shape": IN, "lat0": DE, "lon0": DE, "radiuslat1": DE, # use for radius or lat1 }, "QTMCFGGEOFENCE_DIS": { "status": QS, "geofenceindex": IN, "geofencemode": IN, # should be 0 }, "QTMCFGGEOFENCE_POLY": { "status": QS, "geofenceindex": IN, "geofencemode": IN, "reserved": IN, "shape": IN, "lat0": DE, "lon0": DE, "radiuslat1": DE, # use for radius or lat1 "lon1": DE, "lat2": DE, "lon2": DE, "lat3": DE, "lon3": DE, }, "QTMCFGGEOSEP": { "status": QS, "mode": IN, "geosep": DE, }, "QTMCFGIMUINT": { "status": QS, "index": IN, "mode": IN, "reserved": IN, "actLevel": IN, }, "QTMCFGIMUTC": { "status": QS, "imustatus": IN, }, "QTMCFGLA": { "status": QS, "type": IN, "laX": DE, "laY": DE, "laZ": DE, }, "QTMCFGLAM": { "status": QS, "mode": IN, }, "QTMCFGMSGRATE": { "status": QS, "msgname": ST, "rate": IN, "msgver": IN, # or offset }, "QTMCFGMSGRATE_NOVER": { "status": QS, "msgname": ST, "rate": IN, }, "QTMCFGMSGRATE_INTF": { "status": QS, "porttype": IN, # 1 = UART "portid": IN, # 1 =UART1, 2 = UART2, 3 = UART3 "msgname": ST, "rate": IN, "msgver": IN, # or offset }, "QTMCFGMSGRATE_INTFNOVER": { "status": QS, "porttype": IN, # 1 = UART "portid": IN, # 1 =UART1, 2 = UART2, 3 = UART3 "msgname": ST, "rate": IN, }, "QTMCFGNAVMODE": { "status": QS, "mode": IN, }, "QTMCFGNMEADP": { "status": QS, "utcdp": IN, "posdp": IN, "altdp": IN, "dopdp": IN, "spddp": IN, "cogdp": IN, }, "QTMCFGNMEATID": { "status": QS, "mainTalker": ST, "gsvTalker": ST, }, "QTMCFGODO": { "status": QS, "state": IN, # 0 disabled, 1 enabled "initdist": DE, }, "QTMCFGPPS": { "status": QS, "ppsindex": IN, "enable": IN, "duration": IN, "ppsmode": IN, "polarity": IN, "reserved": IN, }, "QTMCFGPROT": { "status": QS, "porttype": IN, "portid": IN, "inputprot": ST, # 8 hex digits "outputprot": ST, # 8 hex digits }, "QTMCFGRCVRMODE": { "status": QS, "rcvrmode": IN, # 0 unknown, 1 rover, 2 base }, "QTMCFGRSID": { "status": QS, "rsid": IN, }, "QTMCFGRTCM": { "status": QS, "msmtype": IN, "msmmode": IN, "msmelevthd": DE, "reserved1": ST, # must be 07 with leading 0 "reserved2": ST, # must be 06 with leading 0 "ephmode": IN, "ephinterval": IN, }, "QTMCFGRTK": { "status": QS, "diffmode": IN, # 0 disable, 1 auto, 2 RTD only "relmode": IN, # 1 absolute, 2 relative }, "QTMCFGRTKSRCTYPE": { "status": QS, "srctype": IN, }, "QTMMCFGRTKSRCTYPE": { "status": QS, "srctype": IN, }, "QTMCFGSAT": { "status": QS, "systemid": IN, "signalid": ST, # hex as string "masklow": ST, # hex as string "maskhigh": ST, # hex as string }, "QTMCFGSAT_LOW": { "status": QS, "systemid": IN, "signalid": ST, # hex as string "masklow": ST, # hex as string }, "QTMCFGSBAS": { "status": QS, "value": HX, }, "QTMCFGSIGGRP": { "status": QS, "siggrpnum": IN, }, "QTMCFGSIGNAL": { "status": QS, "gpssig": ST, # hex as string, default 07 "glonasssig": ST, # hex as string, default 03 "galileosig": ST, # hex as string, default 0F "beidousig": ST, # hex as string, default 3F "qzsssig": ST, # hex as string, default 07 "navicsig": ST, # hex as string, default 01 }, "QTMCFGSIGNAL2": { "status": QS, "gpssig": ST, # hex as string, default 07 "glonasssig": ST, # hex as string, default 03 "galileosig": ST, # hex as string, default 0F "beidousig": ST, # hex as string, default 3F "qzsssig": ST, # hex as string, default 07 "navicsig": ST, # hex as string, default 01 }, "QTMCFGSTATICHOLD": { "status": QS, "shstate": IN, }, "QTMCFGSVIN": { "status": QS, "svinmode": IN, "cfgcnt": IN, "acclimit": DE, "ecefx": DE, "ecefy": DE, "ecefz": DE, }, "QTMCFGUART": { "status": QS, "portid": IN, "baudrate": IN, "databit": IN, "parity": IN, "stopbit": IN, "flowctrl": IN, }, "QTMCFGVEHMOT": { "status": QS, "mode": IN, "vehtype": IN, }, "QTMCFGWN": { "status": QS, "weekno": IN, }, "QTMDEBUGON": {"status": QS}, "QTMDEBUGOFF": {"status": QS}, "QTMDOP": { "msgver": IN, # always 1 for this version "tow": IN, "gdop": DE, "pdop": DE, "tdop": DE, "vdop": DE, "hdop": DE, "ndop": DE, "edop": DE, }, "QTMDRCAL": { "msgver": IN, # always 1 for this version "calstate": IN, # 0 = none, 1 = lightly, 2 = fully "navtype": IN, # 0 = none, 1 = GNSS only, 2 = DR only, 3 = GNSS + DR }, "QTMDRCLR": {"status": QS}, "QTMDRPVA": { "msgver": IN, # always 1 for this version "timestamp": IN, "time": DE, "solType": IN, "lat": DE, "lon": DE, "alt": DE, "sep": DE, "velN": DE, "velE": DE, "velD": DE, "speed": DE, "roll": DE, "pitch": DE, "heading": DE, }, "QTMDRSAVE": {"status": QS}, "QTMENV": { "msgver": IN, "tow": IN, "wn": IN, "date": ST, "time": ST, "basescore": IN, "conflevel": DE, "satvis": IN, "satslo": IN, "movedflag": IN, "movingflag": IN, "posdiff": ST, # IN "slotype": IN, "basesatnum": IN, "pubsatnum": IN, "reserved1": IN, "reserved2": IN, "reserved3": IN, "reserved4": IN, "reserved5": IN, }, "QTMEOE": { "msgver": IN, # always 2 for this version "utc": DE, "Date": IN, "WN": IN, "TOW": IN, }, "QTMEPE": { "msgver": IN, # always 2 for this version "epenorth": DE, "epeeast": DE, "epedown": DE, "epe2d": DE, "epe3d": DE, }, "QTMGEOFENCESTATUS": {"msgver": IN, "time": TM, "group": (4, {"staten": IN})}, "QTMGETUTC": { "status": QS, "year": IN, "month": IN, "day": IN, "hour": IN, "minute": IN, "second": IN, "millisecond": IN, "reserved": ST, "leapsecond": IN, }, "QTMGNSSSTART": {"status": QS}, "QTMGNSSSTOP": {"status": QS}, "QTMGPS": { "timestamp": IN, "tow": IN, "lat": DE, "lon": DE, "alt": DE, "speed": DE, "heading": DE, "accuracy": DE, "HDOP": DE, "PDOP": DE, "numSv": IN, "fixMode": IN, }, "QTMJAMMINGSTATUS": { "msgver": IN, # always 1 for this version "jammingstatus": IN, }, "QTMIMU": { "timestamp": IN, "accX": DE, "accY": DE, "accZ": DE, "angrateX": DE, "angrateY": DE, "angrateZ": DE, "wheeltick": IN, "last_tick_timestamp": IN, }, "QTMIMUTYPE": { "msgver": IN, # always 1 for this version "imustatus": IN, # 0 = failed, 1 = OK }, "QTMINS": { "timestamp": IN, "soltype": IN, "lat": DE, "lon": DE, "height": DE, "velN": DE, "velE": DE, "velD": DE, "roll": DE, "pitch": DE, "yaw": DE, }, "QTMLS": { "msgver": IN, # always 1 for this version "tow": IN, "lsRef": HX, "wno": IN, "leapsecond": IN, "flag": IN, "lsfRef": HX, "reserved": ST, "wnlsf": IN, "dn": IN, "lsf": IN, }, "QTMNAV": { "msgver": IN, # always 1 for this version "timestatus": IN, "timeref": IN, "utc": DE, "date": IN, "tow": IN, "wn": IN, "leapsec": IN, "reserved1": ST, "reserved2": ST, "soltype": IN, "reserved3": IN, "lat": DE, "lon": DE, "alt": DE, "sep": DE, "reserved4": ST, "reserved5": ST, "latstd": DE, "lonstd": DE, "altstd": DE, "reserved6": ST, "reserved7": ST, "diffid": DE, "diffAge": DE, "reserved8": ST, "siv": IN, "sip": IN, "reserved9": ST, "reserved10": ST, "reserved11": ST, "reserved12": ST, "reserved13": ST, "reserved14": ST, "hvel": DE, "vvel": DE, "hvelstd": DE, "vvelstd": DE, "reserved15": ST, "reserved16": ST, "cog": DE, "reserved17": ST, "reserved18": ST, }, "QTMODO": { "msgver": IN, # always 1 for this version "time": TM, "state": IN, # 0 disabled, 1 enabled "dist": DE, }, "QTMPL": { "msgver": IN, # always 1 for this version "tow": IN, "pul": DE, "reserved1": IN, "reserved2": IN, "plposn": DE, "plpose": DE, "plposd": DE, "plveln": DE, "plvele": DE, "plveld": DE, "reserved3": IN, "reserved4": IN, "pltime": IN, }, "QTMPPPNAV": { "msgver": IN, "timestatus": IN, "timeref": IN, "utc": ST, "date": ST, "tow": IN, "wn": IN, "leapsec": IN, "datumid": ST, "reserved1": ST, "soltype": IN, "reserved2": ST, "lat": DE, "lon": DE, "alt": DE, "sep": DE, "reserved3": ST, "reserved4": ST, "latstd": DE, "lonstd": DE, "altstd": DE, "reserved5": ST, "reserved6": ST, "diffid": IN, "diffage": DE, "reserved7": ST, "satview": IN, "satused": IN, "reserved8": ST, "reserved9": ST, "reserved10": ST, "reserved11": ST, "reserved12": ST, "reserved13": ST, "hvel": DE, "vvel": DE, "hvelstd": DE, "vvelstd": DE, "reserved14": ST, "reserved15": ST, "cog": DE, "reserved16": ST, "reserved17": ST, }, "QTMPVT": { "msgver": IN, # always 1 for this version "tow": IN, "date": ST, # yyyymmdd "time": TM, "reserved": IN, "fixtype": IN, "numsv": IN, "leaps": IN, "lat": DE, "lon": DE, "alt": DE, "sep": DE, "veln": DE, "vele": DE, "veld": DE, "spd": DE, "hdg": DE, "hdop": DE, "pdop": DE, }, "QTMRTCMIS": { "msgver": IN, "recvutc": ST, "porttype": IN, "portid": IN, "msgtype": IN, "subtype": IN, "refstaid": IN, "flag": HX, "msglen": IN, "msgnum": IN, "satnum": IN, "signum": IN, "siggroup": ( "signum", { "sigid": IN, "sigsatnum": IN, }, ), }, "QTMQVER": { "status": QS, "msgver": IN, "description": ST, "version": ST, "buildDate": ST, "buildTime": ST, }, "QTMRESETODO": {"status": QS}, "QTMRESTOREPAR": {"status": QS}, "QTMSAVEPAR": {"status": QS}, "QTMSENMSG": { "msgver": IN, # 2 or 4 "timestamp": IN, "group": ( "None", { "sensor_info": DE, }, ), }, "QTMSN": { "status": QS, "snid": IN, # fixed to 1 "length": IN, "serialno": ST, }, "QTMSN_ALT": { "snid": IN, # fixed to 1 "length": IN, "status": QS, "serialno": ST, }, "QTMSTD": { "reserved": ST, "utc": TM, "wno": IN, "tow": IN, "stdLat": ST, # defined as ST as fields can be blank "stdLon": ST, "stdAlt": ST, "stdSep": ST, "stdVelN": ST, "stdVelE": ST, "stdVelD": ST, "stdSpd": ST, "stdRoll": ST, "stdPitch": ST, "stdHeading": ST, }, "QTMSVINSTATUS": { "msgver": IN, # always 1 for this version "tow": IN, "valid": IN, "reserved0": IN, "reserved1": IN, "obs": IN, "cfgdur": IN, "meanx": DE, "meany": DE, "meanz": DE, "meanacc": DE, }, "QTMTAR": { "msgver": IN, # always 1 for this version "time": DE, "quality": DE, "res": DE, "length": DE, "pitch": DE, "roll": DE, "heading": DE, "accpitch": DE, "accroll": DE, "accheading": DE, "usedsv": IN, }, "QTMTXT": { "msgver": IN, # always 1 for this version "totalsennum": IN, "sennum": IN, "textid": IN, "text": ST, }, "QTMUNIQID": { "status": QS, "length": IN, "ID": HX, }, "QTMVEHATT": { "msgver": IN, # always 1 for this version "timestamp": IN, "roll": DE, "pitch": DE, "heading": DE, "acc_Roll": DE, "acc_Pitch": DE, "acc_Heading": DE, }, "QTMVEHMOT": { "msgver": IN, # 1 or 2 "group": ( "None", { "vehicle_info": DE, }, ), }, "QTMVEHMSG": { "msgver": IN, # 1 = speed, 2 = tick, 3 = speed 4wd, 4 = ticks 4wd "timestamp": IN, "group": ( "None", { "vehicle_info": DE, }, ), }, "QTMVEL": { "msgver": IN, # always 1 for this version "time": TM, "veln": DE, "vele": DE, "veld": DE, "gndspd": DE, "spd": DE, "hdg": DE, "gndspdacc": DE, "spdacc": DE, "hdgacc": DE, }, "QTMVER": { "msgver": IN, # always 1 for this version "vername": ST, "verstr": ST, "builddate": ST, # yyyy/mm/dd "buildtime": ST, # hh:mm:ss }, "QTMVERNO": { "verstr": ST, "builddate": ST, # yyyy/mm/dd "buildtime": ST, # hh:mm:ss }, # set and get... "QTMCFGSTANDALONE": { "status": QS, "mode": IN, "time": IN, "timeout": IN, "lat": DE, "lon": DE, "alt": DE, }, "QTMCFGRTKRL": { "status": QS, "reliability": IN, "reserved1": IN, "reserved2": IN, "reserved3": IN, }, "QTMCFGPPS2": { "status": QS, "index": IN, "enable": IN, "duration": IN, "mode": IN, "reserved1": IN, "period": IN, "userdelay": IN, "reserved2": IN, "reserved3": IN, "reserved4": IN, "reserved5": IN, }, "QTMCFGPPP": { "status": QS, "mode": HX, "datum": IN, "timeout": IN, "horstd": DE, "verstd": DE, }, "QTMCFGPINALT": { "status": QS, "pinnum": IN, "mode": IN, }, "QTMCFGEVENT": { "status": QS, "index": IN, "mode": IN, "edge": IN, "guard": IN, }, "QTMCFGCNRTHD": { "status": QS, "cnr": DE, }, "QTMCFGANTENNA": { "status": QS, "power": IN, "reserved": IN, }, "QTMLSTMSG": { "status": QS, "porttype": IN, "portid": IN, "msgname": ST, "rate": IN, "msgver/offset": IN, }, # *************************************************************** # Quectel LG69T PSTM Proprietary message types # https://quectel.com/content/uploads/2024/09/quectel_lg69taaadafaiajar_gnss_protocol_specification_v1-5.pdf # *************************************************************** "STMPPSDATA": { "onoff": IN, "ppsvalid": IN, "syncvalid": IN, "outmode": IN, "reftime": IN, "refconst": IN, "pulseduration": DE, # second pulse duration. "pulsedelay": DE, # nanosecond pulse delay. "gpsdelay": DE, # nanosecond gps path rf delay. "reserved1": ST, "bdsdelay": DE, # nanosecond bds path rf delay. "galileodelay": DE, # nanosecond galileo path rf delay. "invertedpolarity": IN, "fixcond": IN, "satth": DE, "elevmask": DE, "constmask": IN, "refsec": DE, # second "fixstatus": DE, "usedsats": DE, # used satellites for time correction. "gpsutcdeltas": DE, # second utc leap seconds. "gpsutcdeltans": DE, # nanosecond utc–gps delta time. "reserved2": ST, "galileoutcdeltans": DE, # nanosecond utc–galileo delta time. "quantizationerror": DE, # second quantization error. "ppsclockfreq": DE, # hz pps clock frequency. "tcxoclockfreq": DE, # hz tcxo clock frequency. "bdsutcdeltatimes": DE, # second utc–bds leap second. }, "STMCPU": { "cpuusage": DE, # cpu usage in percentage (%). "pllonoff": IN, "cpuspeed": HX, # hexadecimal mhz cpu clock frequency. }, "STMTG": { "wno": IN, # week number. "tow": DE, # second time of week. "reserved1": ST, # reserved "cputime": DE, # cpu time. "timevalid": IN, "nco": DE, # hz "configstatus": HX, # hexadecimal "constmask": IN, "timebestsattype": IN, # selected best time satellite type. "timemastersattype": IN, # master time satellite type. "timemasterwn": IN, # master time week number. "timemastertow": DE, # second master time tow. "timemastervalidity": DE, # master week number time validity. "tgauxflags": IN, "clockbias": DE, # meter estimated receiver clock bias. "mfreqconfigmask": DE, # multifrequency configuration mask. "leapsec": IN, # second "ppsedge": DE, # pps edge counter @ 64f0 resolution. "mtbms": DE, # millisecond "mtbtimestamp": DE, # picosecond }, "STMTS": { "dspdat": IN, "satid": IN, # satellite identifier. "psr": DE, # meter pseudo range. "freq": DE, # satellite tracking frequency offset. "cp": DE, # cycle carrier phase measurement. "dspflags": IN, "satcn0": DE, # dbhz satellite carrier to noise ratio. "ttim": DE, # second track time of satellite. "codenoise": DE, "phasenoise": DE, "cycleslipcnt": DE, # total cycle slip counter. "reserved1": ST, "elev": DE, # degree elevation degree. }, "STMSBAS": { "status": IN, "sattrk": IN, "satid": IN, # sbas satellite id. "elev": DE, # degree sbas satellite elevation. "azim": DE, # degree sbas satellite azimuth. "satcn0": DE, # dbhz sbas satellite signal strength c/n0. }, "STMTEMP": { "temperature": DE, "rawtemperature": DE, "calibration": IN, }, "STMDRCONFID": { "latstddev": DE, # meter latitude standard deviation. "lonstddev": DE, # meter longitude standard deviation. "headingstddev": DE, # degree heading standard deviation. "reserved1": ST, "gyrobiasstddev": DE, # millivolt gyroscope bias standard deviation. "odoscalestddev": DE, # millimeter/pulse odometer scale standard deviation. "reserved2": ST, "accoffsetstddev": DE, # g accelerometer offset standard deviation. "heightstddev": DE, # meter height standard deviation. "majorsemiaxis": DE, # meter major semiaxis of 1sigma error ellipse. "minorsemiaxis": DE, # meter minor semiaxis of 1sigma error ellipse. "ellipseangle": DE, # degree angle vs north of 1sigma error ellipse. "speedstddev": DE, # m/s speed standard deviation. }, "STMDRBSD": { "timestamp": DE, "cputime": IN, # time of dr estimation (cpu ticks). "gyroxbiassd": DE, # dps gyroscope x bias standard deviation. "gyroybiassd": DE, # dps gyroscope y bias standard deviation. "gyrozbiassd": DE, # dps gyroscope z bias standard deviation. "accxbiassd": DE, # m/s2 accelerometer x bias standard deviation. "accybiassd": DE, # m/s2 accelerometer y bias standard deviation. "acczbiassd": DE, # m/s2 accelerometer z bias standard deviation. }, "STMDRGPS": { "lat": DE, # degree gnss latitude. "lon": DE, # degree gnss longitude. "vn": DE, # m/s gnss velocity north component. "ve": DE, # m/s gnss velocity east component. "pdop": DE, "hdop": DE, "vdop": DE, "rmsposresidual": DE, # meter rms error on gnss pseudo range measurements. "rmsvelresidual": DE, # m/s rms error on gnss frequency measurements. "vup": DE, # m/s gnss velocity up component. "height": DE, # meter gnss altitude. }, "STMDRPVA": { "timestamp": DE, "cputime": IN, # microsecond time of dr estimation (cpu ticks). "latitude": DE, # degree dr latitude. "longitude": DE, # degree dr longitude. "height": DE, # meter dr height. "vnorth": DE, # m/s dr velocity north component. "veast": DE, # m/s dr velocity east component. "vup": DE, # m/s dr velocity up component. "pitch": DE, # degree dr pitch angle. range: 90.00 to 90.00. "roll": DE, # degree dr roll angle. range: 180.00 to 180.00. "heading": DE, # degree dr heading angle. range: 0.00 to 360.00. }, "STMDRPVASD": { "timestamp": DE, "cputime": IN, # microsecond time of dr estimation (cpu ticks). "latitudesd": DE, # degree dr latitude standard deviation. "longitudesd": DE, # degree dr longitude standard deviation. "heightsd": DE, # meter dr height standard deviation. "vnorthsd": DE, # m/s dr velocity north component standard deviation. "veastsd": DE, # m/s dr velocity east component standard deviation. "vupsd": DE, # m/s dr velocity up component standard deviation. "pitchsd": DE, # degree dr pitch angle standard deviation. "rollsd": DE, # degree dr roll angle standard deviation. "headingsd": DE, # degree dr heading angle standard deviation. "pnesd": DE, # meter dr position north east standard deviation. "pnusd": DE, # meter dr position north up standard deviation. "peusd": DE, # meter dr position east up standard deviation. "vnesd": DE, # m/s dr velocity north east standard deviation. "vnusd": DE, # m/s dr velocity north up standard deviation. "veusd": DE, # m/s dr velocity east up standard deviation. }, "STMDRSINT": { "gyrosamplecount": IN, # number of received gyroscope samples. "accsamplecount": IN, # number of received accelerometer samples. "pressamplecount": IN, # number of received pressure samples. "odometercount": IN, # number of received odometer pulses. "gyrointtime": DE, # second "accinttime": DE, # second "pressureinttime": DE, # second "gyroxavg": DE, # dps "gyroyavg": DE, # dps "gyrozavg": DE, # dps "accxavg": DE, # m/s2 "accyavg": DE, # m/s2 "acczavg": DE, # m/s2 "presavg": DE, # hpa average of received pressure samples. }, "STMDRSVF": { "timestamp": DE, "cputime": IN, # microsecond time of estimation (cpu ticks). "xacceleration": DE, # m/s2 vehicle acceleration in x direction. "yacceleration": DE, # m/s2 vehicle acceleration in y direction. "zacceleration": DE, # m/s2 vehicle acceleration in z direction. "xangularrate": DE, # dps vehicle angular rate in x direction. "yangularrate": DE, # dps vehicle angular rate in y direction. "zangularrate": DE, # dps vehicle angular rate in z direction. }, "STMDRUPD": {"group": ("None", {"data": HX})}, "STMDRSTYPE": { "sensortype": IN, }, "STMDRCAL": { "driscalib": IN, "odoiscalib": IN, "gyrosensitivityiscalib": IN, "gyrobiasiscalib": IN, "imuflag": HX, "gyrointegrityflag": IN, "accintegrity": IN, "calstate": ST, }, "STMDR1": { "mpitch": DE, # degree misalignment sensor vs. vehicle frame – pitch angle. "mroll": DE, # degree misalignment sensor vs. vehicle frame – roll angle. "myaw": DE, # degree misalignment sensor vs. vehicle frame – yaw angle. "gsz": DE, # gyroscope z axis sensitivity. "gbx": DE, # dps gyroscope x axis bias. "gby": DE, # dps gyroscope y axis bias. "gbz": DE, # dps gyroscope z axis bias. "abx": DE, # m/s2 accelerometer x axis bias. "aby": DE, # m/s2 accelerometer y axis bias. "abz": DE, # m/s2 accelerometer z axis bias. "odometerscale": DE, # meter/pulse odometer scale. "res1baro": DE, # reserved for barometer. "res2baro": DE, # reserved for barometer. }, "STMDR2": { "imucal": ST, "ascal": ST, "motionstatus": IN, "errcode": IN, "sysready": IN, "crosstrackerror": DE, # meter crosstrack error vs gnssonly mode. "alongtrackerror": DE, # meter alongtrack error vs gnssonly mode. "sysaligned": IN, "vrows": DE, # meter estimated vehicle tire circumference. "vrost": IN, }, "STMDREPE": { "ehpe": DE, # meter error of dr estimated horizontal position "reserved1": ST, # IN, # reserved fixed as 1.00. }, "STMDRDEBUG": { "laterror": DE, # meter latitude error. "lonerror": DE, # meter longitude error. "headingerror": DE, # degree heading error. "speederror": DE, # m/s speed error. "heighterror": DE, # meter height error. "vverror": DE, # m/s rise speed error. }, "STMDRSENMSG_3": { "msgtype": IN, # 3 = Odometer and Reverse "timestamp": IN, # microsecond cpu tick count since poweron. "odometer": DE, "reverse": IN, # 0 = forward, 1 = reverse }, "STMDRSENMSG_24": { "msgtype": IN, # 24 = IMU temperature "timestamp": IN, # microsecond cpu tick count since poweron. "temperature": DE, "validity": IN, }, "STMDRSENMSG_30": { "msgtype": IN, # 30 = IMU accelerometer "timestamp": IN, # microsecond cpu tick count since poweron. "xacc": DE, "yacc": DE, "zacc": DE, }, "STMDRSENMSG_31": { "msgtype": IN, # 31 = IMU gyroscope "timestamp": IN, # microsecond cpu tick count since poweron. "xgyro": DE, "ygyro": DE, "zgyro": DE, }, "STMDRMMFBKF": { "cputime": DE, # tick "elapsedtime": DE, # tick "utcdeltaseconds": DE, # second "utcdeltams": DE, "posaccepted": DE, "reserved1": ST, "reserved2": ST, "measurementnoiselat": DE, # meter "posmeasurementnoiselon": DE, # meter "reserved3": ST, "reserved4": ST, "headingaccepted": DE, "reserved5": ST, "headingmeasurementnoise": DE, # degree "reserved6": ST, }, # *************************************************************** # Septentrio X5 Proprietary message types # https://www.septentrio.com/en/products/gnss-receivers/gnss-receiver-modules/mosaic-x5#resources # *************************************************************** "SSNHRP": { "msgId": ST, # "HRP" "utc": TM, "date": DT, "hdg": DE, # degrees True "roll": DE, "pitch": DE, "hdgstd": DE, "rollstd": DE, "pitchstd": DE, "sip": IN, "attmode": CH, "magvar": DE, "magvardir": LND, }, "SSNRBD": { "msgId": ST, # "RBD" "utc": TM, "date": DT, "azi": DE, "ele": DE, "sip": IN, "quality": CH, "basemode": CH, "corrage": DE, "roverserial": ST, "baseid": ST, }, "SSNRBP": { "msgId": ST, # "RBP" "utc": TM, "date": DT, "baseN": DE, # meters "baseE": DE, "baseU": DE, "sip": IN, "quality": CH, "basemode": CH, "corrage": DE, "roverserial": ST, "baseid": ST, }, "SSNRBV": { "msgId": ST, # "RBV" "utc": TM, "date": DT, "baserocN": DE, "baserocE": DE, "baserocU": DE, "sip": IN, "quality": CH, "basemode": CH, "corrage": DE, "roverserial": ST, "baseid": ST, }, "SSNSNC": { "msgId": ST, # "SNC" # "[", "rev": CH, "tim": IN, # milliseconds "week": IN, "group": ( "None", { # "[", "cdidx": ST, "status": QS, "errcode": ST, "info": ST, # "]", }, ), # "]", }, "SSNTFM": { "msgId": ST, # "TFM" "utc": TM, "heightind": ST, "usage2122": ST, "usage2324": ST, "usage252627": ST, }, "STMSETPAROK": { "msgId": ST, "status1": ST, "status2": ST, "status3": ST, }, "STMINITGPSERROR": {}, "STMINITGPSOK": {}, "STMINITTIMEERROR": {}, "STMINITTIMEOK": {}, } pynmeagps-1.1.7/src/pynmeagps/nmeatypes_poll.py000066400000000000000000000104071523640354200217300ustar00rootroot00000000000000""" NMEA Protocol Poll payload definitions THESE ARE THE PAYLOAD DEFINITIONS FOR _POLL_ MESSAGES _TO_ THE RECEIVER (e.g. Message Poll requests). NB: Attribute names must be unique within each message id. NB: Avoid reserved names 'msgID', 'talker', 'payload', 'checksum'. Created on 4 Mar Sep 2021 While the NMEA 0183 © protocol is proprietary, the information here has been collated from public domain sources. :author: semuadmin (Steve Smith) """ from pynmeagps.nmeatypes_core import ST NMEA_PAYLOADS_POLL = { # ********************************************* # STANDARD MESSAGES # ********************************************* "GAQ": { # Galileo "msgId": ST, }, "GBQ": { # BeiDou "msgId": ST, }, "GLQ": { # GLONASS "msgId": ST, }, "GNQ": { # Any GNSS "msgId": ST, }, "GPQ": { # GPS, SBAS "msgId": ST, }, "GQQ": { # QZSS "msgId": ST, }, # ********************************************* # PROPRIETARY MESSAGES # ********************************************* # u-blox "UBX00": { "msgId": ST, # '00' }, "UBX03": { "msgId": ST, # '03' }, "UBX04": { "msgId": ST, # '04' }, "UBX05": { "msgId": ST, # '05' }, "UBX06": { "msgId": ST, # '06' }, # Garmin "GRMB": {}, "GRMC": {}, "GRMC1": {}, "GRME": {}, "GRMF": {}, "GRMH": {}, "GRMI": {}, "GRMM": {}, "GRMO": {}, "GRMT": {}, "GRMV": {}, "GRMW": {}, "GRMZ": {}, # Trimble "ASHRALR": { "msgId": ST, # 'ALR' }, "ASHRARA": { "msgId": ST, # 'ARA' }, "ASHRARR": { "msgId": ST, # 'ARR' }, "ASHRATT": { "msgId": ST, # 'ATT' }, "ASHRBTS": { "msgId": ST, # 'BTS' }, "ASHRCAP": { "msgId": ST, # 'CAP' }, "ASHRCPA": { "msgId": ST, # 'CPA' }, "ASHRCPO": { "msgId": ST, # 'CPO' }, "ASHRDDM": { "msgId": ST, # 'DDM' }, "ASHRDDS": { "msgId": ST, # 'DDS' }, "ASHRHPR": { "msgId": ST, # 'HPR' }, "ASHRHR": { "msgId": ST, # 'HR' }, "ASHRLTN": { "msgId": ST, # 'LTN' }, "ASHRMDM": { "msgId": ST, # 'MDM' }, "ASHRPOS": { "msgId": ST, # 'POS' }, "ASHRPBN": { "msgId": ST, # 'PBN' }, "ASHRPTT": { "msgId": ST, # 'PTT' }, "ASHRPWR": { "msgId": ST, # 'PWR' }, "ASHRRCS": { "msgId": ST, # 'RCS' }, "ASHRSBD": { "msgId": ST, # 'SBD' }, "ASHRSGA": { "msgId": ST, # 'SGA' }, "ASHRSGL": { "msgId": ST, # 'SGL' }, "ASHRSGO": { "msgId": ST, # 'SGO' }, "ASHRSGP": { "msgId": ST, # 'SGP' }, "ASHRSIR": { "msgId": ST, # 'SIR' }, "ASHRSLB": { "msgId": ST, # 'SLB' }, "ASHRSQZ": { "msgId": ST, # 'SQZ' }, "ASHRSSB": { "msgId": ST, # 'SSB' }, "ASHRTEM": { "msgId": ST, # 'TEM' }, "ASHRTHS": { "msgId": ST, # 'THS' }, "ASHRTTT": { "msgId": ST, # 'TTT' }, "ASHRVCR": { "msgId": ST, # 'VCR' }, "ASHRVCT": { "msgId": ST, # 'VCT' }, "ASHRVEL": { "msgId": ST, # 'VEL' }, "FUGDP": {}, "GPPADV110": { "msgId": ST, # '110' }, "GPPADV120": { "msgId": ST, # '120' }, "TNLAVR": { "msgId": ST, # 'AVR' }, "TNLBPQ": { "msgId": ST, # 'BPQ' }, "TNLDG": { "msgId": ST, # 'DG' }, "TNLEVT": { "msgId": ST, # 'EVT' }, "TNLGGK": { "msgId": ST, # 'GGK' }, "TNLGGKx": { "msgId": ST, # 'GGKx' }, "TNLPJK": { "msgId": ST, # 'PJK' }, "TNLPJT": { "msgId": ST, # 'PJT' }, "TNLREX": { "msgId": ST, # 'REX' }, "TNLVGK": { "msgId": ST, # 'VGK' }, "TNLVHD": { "msgId": ST, # 'VHD' }, } pynmeagps-1.1.7/src/pynmeagps/nmeatypes_poll_prop.py000066400000000000000000000102071523640354200227660ustar00rootroot00000000000000""" NMEA Protocol Proprietary Poll payload definitions THESE ARE THE PAYLOAD DEFINITIONS FOR PROPRIETARY _POLL_ MESSAGES _TO_ THE RECEIVER (e.g. Message Poll requests). NB: Attribute names must be unique within each message id. NB: Avoid reserved names 'msgID', 'talker', 'payload', 'checksum'. Created on 19 Aug 2024 While the NMEA 0183 © protocol is proprietary, the information here has been collated from public domain sources. :author: semuadmin (Steve Smith) """ from pynmeagps.nmeatypes_core import IN, QS, ST NMEA_PAYLOADS_POLL_PROP = { # *************************************************************** # Quectel LC29H / LC79H Proprietary Poll message types # https://www.quectel.com/download/quectel_lc29hlc79h_series_gnss_protocol_specification_v1-5/ # quectel_lc29hlc79h_series_gnss_protocol_specification_v1-5.pdf "AIR051": {}, "AIR059": {}, "AIR063": {"type": IN}, "AIR067": {}, "AIR071": {}, "AIR073": {}, "AIR075": {}, "AIR081": {}, "AIR087": {}, "AIR101": {}, "AIR105": {}, "AIR401": {}, "AIR411": {}, "AIR421": {}, "AIR433": {}, "AIR435": {}, "AIR491": {}, "AIR865": { "portType": IN, # 0 = UART "portIndex": IN, # 0 = UART1 }, "AIR867": { "portType": IN, # 0 = UART "portIndex": IN, # 0 = UART1 }, "AIR6011": { "type": IN, }, # *************************************************************** # Quectel LG290P Proprietary message types # https://quectel.com/content/uploads/2024/09/Quectel_LG290P03_GNSS_Protocol_Specification_V1.0.pdf # # status attribute must be set to 'R' in all POLL messages # *************************************************************** "QTMCFGAIC": {"status": QS}, "QTMCFGANTDELTA": {"status": QS}, "QTMCFGANTINF": {"status": QS}, "QTMCFGBLD": {"status": QS}, "QTMCFGCNST": {"status": QS}, "QTMCFGDR": {"status": QS}, "QTMCFGELETHD": {"status": QS}, "QTMCFGFIXRATE": {"status": QS}, "QTMCFGGEOFENCE": {"status": QS, "geofenceindex": IN}, "QTMCFGGEOSEP": {"status": QS}, "QTMCFGIMUINT": {"status": QS}, "QTMCFGLA": {"status": QS}, "QTMCFGLAM": {"status": QS}, "QTMCFGMSGRATE": {"status": QS, "msgname": ST, "msgver": IN}, "QTMCFGMSGRATE_NOVER": {"status": QS, "msgname": ST}, "QTMCFGMSGRATE_INTF": { "status": QS, "porttype": IN, "portid": IN, "msgname": ST, "msgver": IN, }, "QTMCFGMSGRATE_INTFNOVER": { "status": QS, "porttype": IN, "portid": IN, "msgname": ST, }, "QTMCFGNAVMODE": {"status": QS}, "QTMCFGNMEADP": {"status": QS}, "QTMCFGNMEATID": {"status": QS}, "QTMCFGODO": {"status": QS}, "QTMCFGPPS": {"status": QS, "ppsindex": IN}, "QTMCFGPROT": {"status": QS, "porttype": IN, "portid": IN}, "QTMCFGRCVRMODE": {"status": QS}, "QTMCFGRSID": {"status": QS}, "QTMCFGRTCM": {"status": QS}, "QTMCFGRTK": {"status": QS}, "QTMCFGRTKSRCTYPE": {"status": QS}, "QTMCFGSAT": {"status": QS, "systemid": IN, "signalid": ST}, "QTMCFGSBAS": {"status": QS}, "QTMCFGSIGGRP": {"status": QS}, "QTMCFGSIGNAL": {"status": QS}, "QTMCFGSIGNAL2": {"status": QS}, "QTMCFGSTATICHOLD": {"status": QS}, "QTMCFGSVIN": {"status": QS}, "QTMCFGUART_CURR": {"status": QS}, "QTMCFGUART": {"status": QS, "portid": IN}, "QTMCFGVEHMOT": {"status": QS}, "QTMCFGWN": {"status": QS}, "QTMGETUTC": {}, "QTMQVER": {"msgver": IN}, "QTMSN": {}, "QTMUNIQID": {}, "QTMVEHATT": {}, "QTMVERNO": {}, "QTMLSTMSG_CURR": {}, # TODO "QTMLSTMSG": { "porttype": IN, "portid": IN, }, # TODO "QTMCFGSTANDALONE": { "status": QS, }, "QTMCFGRTKRL": { "status": QS, }, "QTMCFGPPS2": { "status": QS, "index": IN, }, "QTMCFGPPP": { "status": QS, }, "QTMCFGPINALT": { "status": QS, "pinnum": IN, }, "QTMCFGEVENT": { "status": QS, "index": IN, }, "QTMCFGCNRTHD": { "status": QS, }, "QTMCFGANTENNA": { "status": QS, }, } pynmeagps-1.1.7/src/pynmeagps/nmeatypes_set.py000066400000000000000000000022061523640354200215530ustar00rootroot00000000000000""" NMEA Protocol Set payload definitions THESE ARE THE PAYLOAD DEFINITIONS FOR _SET_ MESSAGES _TO_ THE RECEIVER (e.g. Configuration commands). NB: Attribute names must be unique within each message id. NB: Avoid reserved names 'msgID', 'talker', 'payload', 'checksum'. NB: Repeating groups must be defined as a tuple thus 'group': ('numr', {dict}) where - 'numr' is either: a) an integer representing a fixed number of repeats e.g 32 b) a string representing the name of a preceding attribute containing the number of repeats e.g. 'numCh' c) 'None' for an indeterminate repeating group (only one such group is permitted per message type) - {dict} is the nested dictionary containing the repeating attributes Created on 4 Mar Sep 2021 While the NMEA 0183 © protocol is proprietary, the information here has been collated from public domain sources. :author: semuadmin (Steve Smith) """ NMEA_PAYLOADS_SET = { # ********************************************* # STANDARD MESSAGES # ********************************************* # No standard SET messages that I'm aware of } pynmeagps-1.1.7/src/pynmeagps/nmeatypes_set_prop.py000066400000000000000000000315551523640354200226240ustar00rootroot00000000000000""" NMEA Protocol Proprietary Set payload definitions THESE ARE THE PAYLOAD DEFINITIONS FOR PROPRIETARY _SET_ MESSAGES _TO_ THE RECEIVER (e.g. Configuration commands). NB: Attribute names must be unique within each message id. NB: Avoid reserved names 'msgID', 'talker', 'payload', 'checksum'. NB: Repeating groups must be defined as a tuple thus 'group': ('numr', {dict}) where - 'numr' is either: a) an integer representing a fixed number of repeats e.g 32 b) a string representing the name of a preceding attribute containing the number of repeats e.g. 'numCh' c) 'None' for an indeterminate repeating group (only one such group is permitted per message type) - {dict} is the nested dictionary containing the repeating attributes Created on 19 Aug 2024 While the NMEA 0183 © protocol is proprietary, the information here has been collated from public domain sources. :author: semuadmin (Steve Smith) """ from pynmeagps.nmeatypes_core import CH, DE, DT, HX, IN, LA, LAD, LN, LND, QS, ST, TM from pynmeagps.nmeatypes_get_prop import NMEA_PAYLOADS_GET_PROP NMEA_PAYLOADS_SET_PROP = { # *************************************************************** # Quectel LC29H / LC79H Proprietary SET message types # https://www.quectel.com/download/quectel_lc29hlc79h_series_gnss_protocol_specification_v1-5/ # quectel_lc29hlc79h_series_gnss_protocol_specification_v1-5.pdf # *************************************************************** "AIR002": {}, "AIR003": {}, "AIR004": {}, "AIR005": {}, "AIR006": {}, "AIR007": {}, "AIR050": { "time": IN, }, "AIR058": { "minsnr": IN, }, "AIR062": { "type": IN, "rate": IN, }, "AIR066": { "gpsEnabled": IN, "glonassEnabled": IN, "galileoEnabled": IN, "bdsEnabled": IN, "qzssEnabled": IN, "navicEnabled": IN, }, "AIR070": { "speedThreshold": IN, }, "AIR072": { "degree": IN, }, "AIR074": { "enabled": IN, }, "AIR080": { "navMode": IN, }, "AIR086": { "status": IN, }, "AIR100": { "nmeaMode": IN, "reserved": IN, }, "AIR104": { "dualBandEnabled": IN, }, "AIR382": { "enabled": IN, }, "AIR391": { "cmdType": IN, }, "AIR400": { "mode": IN, }, "AIR410": { "enabled": IN, }, "AIR420": { "enabled": IN, }, "AIR432": { "mode": IN, }, "AIR434": { "enabled": IN, }, "AIR436": { "enabled": IN, }, "AIR437": {}, "AIR490": { "enabled": IN, }, "AIR511": {}, "AIR512": {}, "AIR513": {}, "AIR650": { "second": IN, }, "AIR752": { "ppsType": IN, }, "AIR864": { "portType": IN, # 0 = UART "portIndex": IN, # 0 = UART1 "baudrate": IN, }, "AIR866": { "portType": IN, # 0 = UART "portIndex": IN, # 0 = UART1 "flowControl": IN, }, "AIR6010": { "type": IN, "rate": IN, }, # ********************************************* # GARMIN PROPRIETARY MESSAGES # ********************************************* "GRMI": { # sensor initialisation information "lat": LA, "NS": LAD, "lon": LN, "EW": LND, "date": DT, "time": TM, "rcvr_cmd": CH, }, "GRMC": { # sensor configuration information "fix": CH, "alt": DE, "dtm": ST, "smAxis": DE, "iffac": DE, "xecc": DE, "yecc": DE, "zecc": DE, "diff": CH, "baudRate": IN, "vfilt": IN, "reserved1": ST, "reserved2": ST, "drtime": IN, }, "GRMC1": { # additional sensor configuration information "nmeatim": IN, "bphase": IN, "autopos": IN, "dgpsfr": DE, "dgpsbr": IN, "dgpssc": IN, "nmeaver": IN, "dgpsmod": CH, "pwrsave": CH, "attran": IN, "autopwr": IN, "extpwr": IN, }, "GRMO": { # output sentence enable "msgId": ST, "tgtmode": IN, }, "GRMW": { # additional waypoint information "wptId": ST, "alt": DE, "symnum": HX, "comment": ST, }, # *************************************************************** # Locosys Proprietary Messages SET # https://www.locosystech.com/Templates/att/LS2303x-UDG_datasheet_v0.6.pdf?lng=en # *************************************************************** # Proprietary Locosys command sets not yet implemented, e.g. # Perform a Cold start $PMTK103*30 # Perform a Warm start $PMTK102*31 # Perform a Hot start $PMTK101*32 # Perform a Full Cold start $PMTK104*37 # Disable GLL message $PMTK314,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0*29 # Disable GSV message $PMTK314,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0*29 # Disable GLL & GSV message $PMTK314,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28 # Factory default output message $PMTK314,-1*04 # Navigate with GPS+GALILEO $PMTK353,1,0,1,0,0*2B # Navigate with GPS+GLONASS+GALILEO $PMTK353,1,1,1,0,0*2A # Navigate with GPS+BEIDOU $PMTK353,1,0,0,0,1*2B # Entering Standby Mode1 $PMTK161,0*28 "INVCRES": { "value": IN, # 0 = clear data }, "INVCSTR": { "value": IN, # 14 = start session }, "INVMSLOPE": { "status": IN, # 0 = disable, 1 = enable }, "LSC": { "msgType": ST, # e.g. "SLOPE", "MEMS", "ATTIT", "VER" "value": IN, # e.g. 0 (not present if msgType == "VER") }, # *************************************************************** # Quectel LG290P Proprietary message types # https://quectel.com/content/uploads/2024/09/Quectel_LG290P03_GNSS_Protocol_Specification_V1.0.pdf # *************************************************************** "QTMBKP": { "second": IN, }, "QTMCFGAIC": NMEA_PAYLOADS_GET_PROP["QTMCFGAIC"], "QTMCFGANTDELTA": NMEA_PAYLOADS_GET_PROP["QTMCFGANTDELTA"], "QTMCFGANTENNA": NMEA_PAYLOADS_GET_PROP["QTMCFGANTENNA"], "QTMCFGANTINF": NMEA_PAYLOADS_GET_PROP["QTMCFGANTINF"], "QTMCFGBLD": NMEA_PAYLOADS_GET_PROP["QTMCFGBLD"], "QTMCFGCNRTHD": NMEA_PAYLOADS_GET_PROP["QTMCFGCNRTHD"], "QTMCFGCNST": NMEA_PAYLOADS_GET_PROP["QTMCFGCNST"], "QTMCFGDR": NMEA_PAYLOADS_GET_PROP["QTMCFGDR"], "QTMCFGDRHOT": NMEA_PAYLOADS_GET_PROP["QTMCFGDRHOT"], "QTMCFGDRRTD": NMEA_PAYLOADS_GET_PROP["QTMCFGDRRTD"], "QTMCFGEINSMSG": NMEA_PAYLOADS_GET_PROP["QTMCFGEINSMSG"], "QTMCFGELETHD": NMEA_PAYLOADS_GET_PROP["QTMCFGELETHD"], "QTMCFGEVENT": NMEA_PAYLOADS_GET_PROP["QTMCFGEVENT"], "QTMCFGFIXRATE": NMEA_PAYLOADS_GET_PROP["QTMCFGFIXRATE"], "QTMCFGGEOFENCE_DIS": NMEA_PAYLOADS_GET_PROP["QTMCFGGEOFENCE_DIS"], "QTMCFGGEOFENCE_POLY": NMEA_PAYLOADS_GET_PROP["QTMCFGGEOFENCE_POLY"], "QTMCFGGEOFENCE": NMEA_PAYLOADS_GET_PROP["QTMCFGGEOFENCE"], "QTMCFGGEOSEP": NMEA_PAYLOADS_GET_PROP["QTMCFGGEOSEP"], "QTMCFGIMUINT": NMEA_PAYLOADS_GET_PROP["QTMCFGIMUINT"], "QTMCFGIMUTC": NMEA_PAYLOADS_GET_PROP["QTMCFGIMUTC"], "QTMCFGLA": NMEA_PAYLOADS_GET_PROP["QTMCFGLA"], "QTMCFGLAM": NMEA_PAYLOADS_GET_PROP["QTMCFGLAM"], "QTMCFGMSGRATE": NMEA_PAYLOADS_GET_PROP["QTMCFGMSGRATE"], "QTMCFGMSGRATE_NOVER": NMEA_PAYLOADS_GET_PROP["QTMCFGMSGRATE_NOVER"], "QTMCFGMSGRATE_INTF": NMEA_PAYLOADS_GET_PROP["QTMCFGMSGRATE_INTF"], "QTMCFGMSGRATE_INTFNOVER": NMEA_PAYLOADS_GET_PROP["QTMCFGMSGRATE_INTFNOVER"], "QTMCFGNAVMODE": NMEA_PAYLOADS_GET_PROP["QTMCFGNAVMODE"], "QTMCFGNMEADP": NMEA_PAYLOADS_GET_PROP["QTMCFGNMEADP"], "QTMCFGNMEATID": NMEA_PAYLOADS_GET_PROP["QTMCFGNMEATID"], "QTMCFGODO": NMEA_PAYLOADS_GET_PROP["QTMCFGODO"], "QTMCFGPINALT": NMEA_PAYLOADS_GET_PROP["QTMCFGPINALT"], "QTMCFGPPS": NMEA_PAYLOADS_GET_PROP["QTMCFGPPS"], "QTMCFGRTKRL": NMEA_PAYLOADS_GET_PROP["QTMCFGRTKRL"], "QTMCFGSBAS": NMEA_PAYLOADS_GET_PROP["QTMCFGSBAS"], "QTMCFGSIGGRP": NMEA_PAYLOADS_GET_PROP["QTMCFGSIGGRP"], "QTMCFGSTANDALONE": NMEA_PAYLOADS_GET_PROP["QTMCFGSTANDALONE"], "QTMCFGPPS_DIS": { "status": QS, "ppsindex": IN, "enable": IN, # should be 0 }, "QTMCFGPPP": NMEA_PAYLOADS_GET_PROP["QTMCFGPPP"], "QTMCFGPPS2": NMEA_PAYLOADS_GET_PROP["QTMCFGPPS2"], "QTMCFGPROT": NMEA_PAYLOADS_GET_PROP["QTMCFGPROT"], "QTMCFGRCVRMODE": NMEA_PAYLOADS_GET_PROP["QTMCFGRCVRMODE"], "QTMCFGRSID": NMEA_PAYLOADS_GET_PROP["QTMCFGRSID"], "QTMCFGRTCM": NMEA_PAYLOADS_GET_PROP["QTMCFGRTCM"], "QTMCFGRTK": NMEA_PAYLOADS_GET_PROP["QTMCFGRTK"], "QTMCFGRTKSRCTYPE": NMEA_PAYLOADS_GET_PROP["QTMCFGRTKSRCTYPE"], "QTMCFGSAT_LOW": NMEA_PAYLOADS_GET_PROP["QTMCFGSAT_LOW"], "QTMCFGSAT": NMEA_PAYLOADS_GET_PROP["QTMCFGSAT"], "QTMCFGSIGNAL": NMEA_PAYLOADS_GET_PROP["QTMCFGSIGNAL"], "QTMCFGSIGNAL2": NMEA_PAYLOADS_GET_PROP["QTMCFGSIGNAL2"], "QTMCFGSTATICHOLD": NMEA_PAYLOADS_GET_PROP["QTMCFGSTATICHOLD"], "QTMCFGSVIN": NMEA_PAYLOADS_GET_PROP["QTMCFGSVIN"], "QTMCFGWN": NMEA_PAYLOADS_GET_PROP["QTMCFGWN"], # sets all parms for specified interface: "QTMCFGUART": NMEA_PAYLOADS_GET_PROP["QTMCFGUART"], # sets baud rate for specified interface "QTMCFGUART_BAUD": { "status": QS, "portid": IN, "baudrate": IN, }, # sets all parms for current interface: "QTMCFGUART_CURR": { "status": QS, "baudrate": IN, "databit": IN, "parity": IN, "stopbit": IN, "flowctrl": IN, }, # sets baud rate for current interface: "QTMCFGUART_CURRBAUD": { "status": QS, "baudrate": IN, }, "QTMCFGVEHMOT": NMEA_PAYLOADS_GET_PROP["QTMCFGVEHMOT"], "QTMCOLD": {}, "QTMDEBUGOFF": {}, "QTMDEBUGON": {}, "QTMDRCLR": {}, "QTMDRSAVE": {}, "QTMGNSSSTART": {}, "QTMGNSSSTOP": {}, "QTMHOT": {}, "QTMRESETODO": {}, "QTMRESTOREPAR": {}, "QTMSAVEPAR": {}, "QTMSRR": {}, "QTMWARM": {}, "QTMAIDINI": { "postype": IN, "pos1": DE, "pos2": DE, "pos3": DE, "posacc": DE, "timetype": IN, "leapsec": IN, "year": IN, "month": IN, "day": IN, "hour": IN, "minute": IN, "second": IN, "millisecond": IN, "tacc_s": IN, "reserved": ST, }, "QTMAIDPOS": { "postype": IN, "pos1": DE, "pos2": DE, "pos3": DE, "posacc": DE, }, "QTMAIDTIME": { "timetype": IN, "leapsec": IN, "year": IN, "month": IN, "day": IN, "hour": IN, "minute": IN, "second": IN, "millisecond": IN, "tacc_s": IN, "reserved": ST, }, "QTMCLRMSG": { "porttype": IN, "portid": IN, }, "QTMCLRMSG_CURR": {}, # *************************************************************** # Quectel LG69T PSTM Proprietary message types # https://quectel.com/content/uploads/2024/09/quectel_lg69taaadafaiajar_gnss_protocol_specification_v1-5.pdf # *************************************************************** "STMCOLD": {}, "STMWARM": {}, "STMEPHEM": { "satID": IN, "ephdatalen": IN, "ephdata": HX, }, "STMHOT": {}, "STMSRR": {}, "STMSAVEPAR": {}, "STMRESTOREPAR": {}, "STMSETPAR": { "parm1": IN, "parm2": IN, "parm3": IN, "parm4": IN, "parm5": ST, }, "STMIMUSELFTESTCMD": {"imucat": IN}, "STMINITGPS": { "lat": LA, "NS": LAD, "lon": LN, "EW": LND, "alt": DE, "day": IN, "month": IN, "year": IN, "hour": IN, "minute": IN, "second": IN, }, "STMINITTIME": { "day": IN, "month": IN, "year": IN, "hour": IN, "minute": IN, "second": IN, }, "STMDRMMFB": { "timestamp": DE, "latval": IN, "lonval": IN, "heightval": IN, "headingval": IN, "lat": DE, "lon": DE, "height": DE, "heading": DE, "laterr": DE, "lonerr": DE, "heighterr": DE, "headingerr": DE, }, # ********************************************* # U-BLOX PROPRIETARY MESSAGES # ********************************************* "UBX40": { # set message rates per port "msgId": ST, # '40' "id": ST, # e.g. GLL "rddc": IN, # I2C "rus1": IN, # UART1 "rus2": IN, # UART2 "rusb": IN, # USB "rspi": IN, # SPI "reserved": IN, }, "UBX41": { # configure port protocols "msgId": ST, # '41' "portId": IN, "inProto": HX, "outProto": HX, "baudRate": IN, "autobauding": IN, }, } pynmeagps-1.1.7/src/pynmeagps/socketwrapper.py000066400000000000000000000130651523640354200215710ustar00rootroot00000000000000""" socketwrapper.py Socket stream wrapper providing read(n) and readline() methods. Supports chunked and compressed transfer-encoded datastreams. Created on 12 Feb 2023 :author: semuadmin (Steve Smith) :copyright: semuadmin © 2023 :license: BSD 3-Clause """ import socket from io import BytesIO from logging import getLogger from zlib import MAX_WBITS, decompress from zlib import error as zlibError from pynmeagps.nmeatypes_core import ( DEFAULT_BUFSIZE, ENCODE_CHUNKED, ENCODE_COMPRESS, ENCODE_DEFLATE, ENCODE_GZIP, ENCODE_NONE, ) class SocketWrapper: """ Socket stream wrapper providing read(n) and readline() methods. Supports chunked transfer-encoded datastreams. """ def __init__( self, sock: socket.socket, encoding=ENCODE_NONE, bufsize=DEFAULT_BUFSIZE ): """ Constructor. :param socket.socket sock: socket object :param int encoding: transfer-encoding values \ (0 = none, 1 = chunk, 2 = gzip, 4 = compress, 8 = deflate (can be OR'd) (0) :param int bufsize: internal buffer size """ # configure logger with name "pygnssutils" in calling module self.logger = getLogger(__name__) self._socket = sock self._bufsize = bufsize self._encoding = encoding self._buffer = bytearray() self._partial = b"" # partial chunk self._recv() # populate initial buffer def _recv(self) -> bool: """ Read bytes from socket into internal buffer. :returns: return code (0 = failure, 1 = success) :rtype: bool """ try: data = self._socket.recv(self._bufsize) if len(data) == 0: return False if self._encoding & ENCODE_CHUNKED: data = self._partial + data chunks, self._partial = self.dechunk(data) self._buffer += chunks else: self._buffer += data except (OSError, TimeoutError): return False return True def read(self, num: int) -> bytes: """ Read specified number of bytes from buffer. NB: always check length of return data. :param int num: number of bytes to read :returns: bytes read (which may be less than num) :rtype: bytes """ # if at end of internal buffer, top it up from socket while len(self._buffer) < num: if not self._recv(): return b"" data = self._buffer[:num] self._buffer = self._buffer[num:] return bytes(data) def readline(self) -> bytes: """ Read bytes from buffer until CRLF reached. NB: always check that return data terminator is CRLF. :returns: bytes :rtype: bytes """ line = b"" while True: data = self.read(1) if len(data) == 1: line += data if line[-2:] == b"\r\n": break else: break return line def write(self, data: bytes, **kwargs): """ Write bytes to socket. :param bytes data: data :param dict kwargs: kwargs """ return self._socket.send(data, **kwargs) def in_waiting(self) -> int: """ Return number of bytes in buffer. :returns: length of buffer :rtype: int """ return len(self._buffer) def dechunk(self, segment: bytes) -> tuple: """ Parse segment of chunked transfer-encoded byte stream. Returns complete chunks in this segment and any partial chunk, which should be prepended to next segment read. :param segment: segment of byte stream :returns: tuple of (chunks, partial) :rtype: tuple """ # pylint: disable=logging-fstring-interpolation instream = BytesIO(segment) chunks = b"" partial = b"" while True: length_bytes = instream.readline() if length_bytes[-2:] != b"\r\n": # premature end of length bytes partial = length_bytes break try: chunk_length = int(length_bytes.strip(), 16) except ValueError: # residual bytes at beginning of stream break if chunk_length != 0: chunk = instream.read(chunk_length) if len(chunk) != chunk_length: # premature end of chunk bytes partial = length_bytes + chunk break try: # pragma: no cover if self._encoding & ENCODE_GZIP: chunk = decompress(chunk, wbits=MAX_WBITS | 16) if self._encoding & ENCODE_COMPRESS: chunk = decompress(chunk, wbits=MAX_WBITS) if self._encoding & ENCODE_DEFLATE: chunk = decompress(chunk, wbits=-MAX_WBITS) except zlibError as err: # pragma: no cover self.logger.error(f"Error decompressing data: {err}") # parser will discard data chunks += chunk instream.readline() if chunk_length == 0: # final chunk break return chunks, partial @property def buffer(self) -> bytearray: """ Getter for buffer. :return: buffer :rtype: bytearray """ return self._buffer pynmeagps-1.1.7/tests/000077500000000000000000000000001523640354200146715ustar00rootroot00000000000000pynmeagps-1.1.7/tests/__init__.py000066400000000000000000000000651523640354200170030ustar00rootroot00000000000000''' Created on 27 Sep 2020 @author: semuadmin ''' pynmeagps-1.1.7/tests/feyman_nmea.log000066400000000000000000000040741523640354200176600ustar00rootroot00000000000000$GPFMI,160224.40,0.0,0.0,0.0,0.05268,0.001,0.17453,0.17,12,0,2,001C0001*1A $GPFMI,160224.60,0.0,0.0,0.0,0.05268,0.001,0.17453,0.17,12,0,2,001C0001*18 $GPFMI,213937.46,13.423023,113.193168,45.890494,0.2896,-0.13526,0.06786,0.17,26,5,2,001C0001*30 $GPFMI,213937.46,13.512547,113.637276,45.767895,-0.06149,-0.12598,0.1613,0.17,27,5,2,001C0001*16 $GPFMI,213937.46,13.19525,113.882219,45.719773,0.08723,-0.1516,0.01618,0.17,27,3,2,001C0001*0B $GPFMI,213937.46,13.092634,113.283257,45.018596,0.14348,0.08538,-0.08735,0.17,26,4,2,001C0001*05 $GPFMI,213937.46,13.254565,113.288853,45.661074,0.12346,0.05322,-0.08051,0.17,25,3,2,001C0001*07 $GPFMI,213937.46,13.509901,113.138161,45.996202,-0.06133,-0.09084,0.15226,0.17,27,3,2,001C0001*25 $GPFMI,213937.46,13.76182,113.482807,45.764244,-0.08238,0.11154,-0.14273,0.17,26,2,2,001C0001*15 $GPFMI,213937.46,13.934536,113.034356,45.252132,0.29458,0.04115,-0.05138,0.17,27,2,2,001C0001*07 $GPFMI,213937.46,13.639909,113.402712,45.615865,0.20085,0.16486,0.06866,0.17,27,5,2,001C0001*26 $GPFMI,213937.46,13.269057,113.727968,45.891641,-0.00148,0.1202,-0.28122,0.17,25,2,2,001C0001*16 $GPFMI,213937.46,13.457734,113.13478,45.028494,0.11935,0.03962,0.15073,0.17,24,4,2,001C0001*12 $GPFMI,213937.46,13.114631,113.083719,45.146318,0.11355,0.1324,-0.05383,0.17,26,4,2,001C0001*37 $GPFMI,213937.46,13.561758,113.504337,45.81872,-0.33798,0.02763,-0.13261,0.17,25,3,2,001C0001*12 $GPFMI,213937.46,13.484163,113.286151,45.117093,-0.13497,0.05618,-0.21773,0.17,24,3,2,001C0001*22 $GPFMI,213937.46,13.077227,113.853113,45.633862,0.28419,-0.17017,0.22197,0.17,24,5,2,001C0001*0A $GPFMI,213937.46,13.256628,113.893556,45.736646,-0.22785,-0.11589,0.09098,0.17,26,2,2,001C00E1*57 $GPFMI,213937.46,13.327387,113.710465,45.119112,-0.27196,0.01461,-0.14661,0.17,24,2,2,001C0001*24 $GPFMI,213937.46,13.238952,113.301512,45.781582,0.00436,0.12186,-0.06835,0.17,26,4,2,001C0001*07 $GPFMI,213937.46,13.8632,113.998859,45.163301,0.09648,-0.03866,0.2807,0.17,24,2,2,001A0001*36 $GPFMI,213937.46,13.952284,113.441564,45.696884,-0.21893,-0.06943,-0.04883,0.17,25,2,2,001A0001*02 pynmeagps-1.1.7/tests/furuno_nmea.log000066400000000000000000000000771523640354200177160ustar00rootroot00000000000000$PFEC,GPatt,12.345,23.456,34.567,*50 $PFEC,GPhve,12.345,A*0E pynmeagps-1.1.7/tests/kenwood_nmea.log000066400000000000000000000005361523640354200200460ustar00rootroot00000000000000$PKWDWPL,150803,V,4237.14,N,07120.83,W,,,190316,,test,5,/'*30 $PKLDS,001235,A,3544.6650,N,13940.1900,E,015.0,038.8,110498,10.80,W00,100,2000,15,00,*7E $PKNDS,124640,A,4954.1458,N,11923.5992,W,000.0,000.0,120223,19.20,W00,U00002,207,00,*29 $PKLSH,4000.0000,N,13500.0000,E,021720,A,100,2000,*2C $PKNSH,4000.0000,N,13500.0000,E,021720,A,U00001,*55 pynmeagps-1.1.7/tests/locosys_get_nmea.log000066400000000000000000000004751523640354200207340ustar00rootroot00000000000000$PLSVD,1,0,-16,16,15,13*77 $PLSR,SLOPE,1,OK*41 $PLSR,SLOPE,0,OK*40 $PLSR,MEMS,1,OK*12 $PLSR,MEMS,0,OK*13 $PLSR,ATTIT,1,OK*58 $PLSR,ATTIT,0,OK*59 $PINVMSTR,0*05 $PINVMSLOPE,-3.13,-0.05,0.93,54.42,2.60,1.86*3B $PINVMIMU,1114.106,-0.36990,1.51074,9.81383,0.67139,0.61035,-0.30518*22 $PINVMATTIT,-20.652,32.265,0.000*3C pynmeagps-1.1.7/tests/locosys_set_nmea.log000066400000000000000000000002211523640354200207350ustar00rootroot00000000000000$PINVCRES,0*1A $PINVCSTR,14*3E $PLSC,SLOPE,1*78 $PLSC,SLOPE,0*79 $PLSC,MEMS,1*2B $PLSC,MEMS,0*2A $PLSC,ATTIT,1*61 $PLSC,ATTIT,0*60 $PLSC,VER*61 pynmeagps-1.1.7/tests/pygpsdata-BADHDR.log000066400000000000000000000016111523640354200203110ustar00rootroot00000000000000$GPTXT,01,01,02,HW UBX-G70xx 00070000 *77 $GPTXT,01,01,02,ROM CORE 1.00 (59842) Jun 27 2012 17:43:52*59 $GPTXT,01,01,02,PROTVER 14.00*1E $GPTXT,01,01,02,ANTSUPERV=AC SD PDoS SR*20 $GPTXT,01,01,02,ANTSTATUS=OK*3B $&bTXT,01,01,02,LLC FFFFFFFF-FFFFFFFD-FFFFFFFF-FFFFFFFF-FFFFFFF9*53 $GPRMC,102929.00,A,5327.04024,N,00214.41560,W,0.273,,070321,,,A*62 $GPVTG,,T,,M,0.273,N,0.506,K,A*26 $GPGGA,102929.00,5327.04024,N,00214.41560,W,1,08,1.16,36.3,M,48.5,M,,*7E $GPGSA,A,3,17,15,10,24,20,12,19,23,,,,,2.36,1.16,2.05*09 $GPGSV,4,1,15,01,06,015,,10,30,290,27,12,42,207,26,13,19,141,23*7C $GPGSV,4,2,15,14,07,049,21,15,45,171,27,17,32,065,22,19,33,095,25*77 $GPGSV,4,3,15,20,21,251,31,21,04,355,,23,28,252,33,24,88,273,36*70 $GPGSV,4,4,15,25,05,223,,28,14,049,26,32,10,313,16*4C $GPGLL,5327.04024,N,00214.41560,W,102929.00,A,A*7A $GPRMC,102930.00,A,5327.04033,N,00214.41550,W,0.099,,070321,,,A*69 pynmeagps-1.1.7/tests/pygpsdata-badeof.log000066400000000000000000000003251523640354200206060ustar00rootroot00000000000000$GNDTM,W84,,0.0,N,0.0,E,0.0,W84*71 $GNRMC,103607.00,A,5327.03942,N,00214.42462,W,0.046,,060321,,,A,V*0F $GNVTG,,T,,M,0.046,N,0.085,K,A*32 $GNGNS,103607.00,5327.03942,N,00214.42462,W,AANN,06,5.88,56.0,48.5,,,V*34 $pynmeagps-1.1.7/tests/pygpsdata-maritime.log000066400000000000000000000015431523640354200212000ustar00rootroot00000000000000$IIALF,1,1,0,124304.50,A,W,A,,192,1,1,0,LOST TARGET*14 $IIALF,2,1,1,081950.10,B,A,S,XYZ,0512,1,2,0,HEADING LOST*2D $IIALF,2,2,1,,,,,XYZ,0512,1,2,0,NO SYSTEM HEADING AVAILABLE*0D $AIEPV,R,AI,503123450,101,38400*05 $VRGEN,0000,011200.00,0123,4567,89AB,CDEF,0123,4567,89AB,CDEF*64 $VRGEN,0008,011200.00,0123,4567*6C $INNRM,2,1,00001E1F,00000023,C*38 $CRNRX,007,001,00,IE69,1,135600,27,06,2001,241,3,A,============================*09 $CRNRX,007,002,00,,,,,,,,,,========^0D^0AISSUED ON SATURDAY 06 JANUARY 2001.*29 $CSSM3,123456,005213,798,0,3,14,00,2012,04,05,14,30,3400,N,076,W,300*39 $CSSMB,008,001,0,123456,FROM:Maritime Rescue Coordination Centre xxx^0D)0ATO:*48 $IISPW,EPV,211000001,2,SESAME*1A $IIEPV,C,AI,211000001,111,HEUREKA143*55 $AIEPV,R,AI,211000001,111,HEUREKA143*4C $HCXDR,A,171,D,PITCH,A,-37,D,ROLL,G,367,,MAGX,G,2420,,MAGY,G,-8984,,MAGZ*41 pynmeagps-1.1.7/tests/pygpsdata-mixed.log000066400000000000000000000024651523640354200205030ustar00rootroot00000000000000µb\xSKå ) ÷\ þÿ«t%ªþÉéÛÒ¼nÿö¤ñn +-HhyøçîO/ž`µb5°xSK èÿY$÷Y&÷ðÿY 1{Œ uˆ« ³4Ú.5.YT!ÿ+ Gù‡$GNGGA,104113.00,5327.03557,N,00214.42234,W,1,05,8.68,65.4,M,48.5,M,,*68 $GNGSA,A,3,20,10,23,,,,,,,,,,12.55,8.68,9.07,1*38 $GNGSA,A,3,78,68,,,,,,,,,,,12.55,8.68,9.07,2*38 $GNGSA,A,3,,,,,,,,,,,,,12.55,8.68,9.07,3*38 $GNGSA,A,3,,,,,,,,,,,,,12.55,8.68,9.07,4*3F $GPGSV,1,1,04,10,29,284,30,20,17,247,36,23,24,247,38,28,10,049,07,1*6B $GLGSV,2,1,07,66,10,179,,67,52,218,22,68,46,309,30,69,05,340,17,1*71 $GLGSV,2,2,07,78,30,255,33,85,19,018,,86,25,071,,1*42 $GAGSV,1,1,00,7*73 $GBGSV,1,1,00,1*76 µb\`WKå )÷\@ þÿ«æ%ªþÇéÛ5¼Ñþ%¤aoøÿÿÿ¦~ƒøçîO/͵b5°`WK åÿY$÷Y%÷ðÿY 1{Œ uˆ« ³4Ú.5>YTÿ, GþE$GNGGA,104114.00,5327.03556,N,00214.42166,W,1,05,8.68,65.2,M,48.5,M,,*6C $GNGSA,A,3,20,10,23,,,,,,,,,,12.55,8.68,9.06,1*39 $GNGSA,A,3,78,68,,,,,,,,,,,12.55,8.68,9.06,2*39 $GNGSA,A,3,,,,,,,,,,,,,12.55,8.68,9.06,3*39 $GNGSA,A,3,,,,,,,,,,,,,12.55,8.68,9.06,4*3E pynmeagps-1.1.7/tests/pygpsdata-nmea2.log000066400000000000000000000016701523640354200203740ustar00rootroot00000000000000$GPTXT,01,01,02,u-blox ag - www.u-blox.com*50 $GPTXT,01,01,02,HW UBX-G70xx 00070000 *77 $GPTXT,01,01,02,ROM CORE 1.00 (59842) Jun 27 2012 17:43:52*59 $GPTXT,01,01,02,PROTVER 14.00*1E $GPTXT,01,01,02,ANTSUPERV=AC SD PDoS SR*20 $GPTXT,01,01,02,ANTSTATUS=OK*3B $GPTXT,01,01,02,LLC FFFFFFFF-FFFFFFFD-FFFFFFFF-FFFFFFFF-FFFFFFF9*53 $GPRMC,102929.00,A,5327.04024,N,00214.41560,W,0.273,,070321,,,A*62 $GPVTG,,T,,M,0.273,N,0.506,K,A*26 $GPGGA,102929.00,5327.04024,N,00214.41560,W,1,08,1.16,36.3,M,48.5,M,,*7E $GPGSA,A,3,17,15,10,24,20,12,19,23,,,,,2.36,1.16,2.05*09 $GPGSV,4,1,15,01,06,015,,10,30,290,27,12,42,207,26,13,19,141,23*7C $GPGSV,4,2,15,14,07,049,21,15,45,171,27,17,32,065,22,19,33,095,25*77 $GPGSV,4,3,15,20,21,251,31,21,04,355,,23,28,252,33,24,88,273,36*70 $GPGSV,4,4,15,25,05,223,,28,14,049,26,32,10,313,16*4C $GPGLL,5327.04024,N,00214.41560,W,102929.00,A,A*7A $GPRMC,102930.00,A,5327.04033,N,00214.41550,W,0.099,,070321,,,A*69 pynmeagps-1.1.7/tests/pygpsdata-nmea4.log000066400000000000000000000056021523640354200203750ustar00rootroot00000000000000$GNDTM,W84,,0.0,N,0.0,E,0.0,W84*71 $GNRMC,103607.00,A,5327.03942,N,10214.42462,W,0.046,,060321,,,A,V*0E $GPRTE,2,1,c,0,PBRCPK,PBRTO,PTELGR,PPLAND,PYAMBU,PPFAIR,PWARRN,PMORTL,PLISMR*73 $GNRLM,00000078A9FBAD5,083559.00,3,C45B*06 $GNVTG,,T,,M,0.046,N,0.085,K,A*32 $GNGNS,103607.00,5327.03942,N,00214.42462,W,AANN,06,5.88,56.0,48.5,,,V*34 $GNGGA,103607.00,5327.03942,N,00214.42462,W,1,06,5.88,56.0,M,48.5,M,,*64 $GNGSA,A,3,23,24,20,12,,,,,,,,,9.62,5.88,7.62,1*0C $GNGSA,A,3,66,76,,,,,,,,,,,9.62,5.88,7.62,2*08 $GNGSA,A,3,,,,,,,,,,,,,9.62,5.88,7.62,3*08 $GNGSA,A,3,,,,,,,,,,,,,9.62,5.88,7.62,4*0F $GPGSV,3,1,11,01,06,014,08,12,43,207,28,14,06,049,,15,44,171,23,1*6B $GPGSV,3,2,11,17,32,064,16,19,33,094,,20,20,251,31,21,04,354,,1*63 $GPGSV,3,3,11,23,27,251,31,24,89,268,26,25,05,223,,1*5A $GLGSV,3,1,10,65,07,176,,66,57,223,35,67,42,315,23,68,00,341,29,1*7A $GLGSV,3,2,10,75,37,057,,76,78,303,18,77,27,253,21,84,19,018,,B*07 $GLGSV,3,3,10,85,22,078,,86,01,121,,1*76 $GAGSV,1,1,00,7*73 $GBGSV,1,1,02,21,,,15,25,,,28,1*7E $GNGLL,5327.03942,N,00214.42462,W,103607.00,A,A*68 $GNGRS,103607.00,1,-2.1,0.2,2.7,-0.4,,,,,,,,,1,1*53 $GNGRS,103607.00,1,0.6,5.1,,,,,,,,,,,2,1*52 $GNGRS,103607.00,1,,,,,,,,,,,,,3,7*57 $GNGRS,103607.00,1,,,,,,,,,,,,,4,1*56 $GNGST,103607.00,38,60,38,89,15,24,31*63 $GNZDA,103607.00,06,03,2021,00,00*7F $GNGBS,103607.00,15.1,24.2,31.0,,,,,,*6F $GNVLW,,N,,N,0.000,N,0.000,N*44 $PUBX,00,103607.00,5327.03942,N,00214.42462,W,104.461,G3,29,31,0.085,39.63,-0.007,,5.88,7.62,8.09,6,0,0*69 $PUBX,03,23,1,-,014,06,08,000,12,U,207,43,28,009,14,-,049,06,,000,15,-,171,44,23,000,17,-,064,32,16,000,19,-,094,33,,000,20,U,251,20,31,038,21,-,354,04,,000,23,U,251,27,31,064,24,U,268,89,26,000,25,-,223,05,,000,48,-,,,15,000,52,-,,,28,013,65,-,176,07,,000,66,U,223,57,35,064,67,-,315,42,23,000,68,-,341,00,29,000,75,-,057,37,,000,76,U,303,78,18,000,77,-,253,27,21,000,84,-,018,19,,000,85,-,078,22,,000,86,-,121,01,,000*02 $PUBX,04,103607.00,060321,556567.00,2147,18,-384839,-53.623,16*2C $GPWPL,4917.16,N,12310.64,W,003*65 $GPRMA,A,5327.03942,N,11214.42462,W,,,23.1,23,14.8,W*58 $GPRMB,A,0.66,L,003,004,4917.24,N,12309.57,W,001.3,052.5,000.5,V*20 $PGRME,15.0,M,45.0,M,25.0,M*1C $PGRMM,NAD27 Canada*2F $PGRMZ,246,f,3*1B $GPXTE,A,A,4.07,L,N*6D $GPVBW,12.3,0.07,A,11.78,0.12,A*6F $GPSTN,34*75 $GPBWC,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM*21 $GPBOD,097.0,T,103.2,M,POINTB,POINTA*4A $GPBOD,099.3,T,105.6,M,POINTB,*48 $GPAAM,A,A,0.10,N,WPTNME*32 $GPAPB,A,A,0.10,R,N,V,V,011,M,DEST,011,M,011,M*3C $GPMSK,318.0,A,100,M,2*45 $GPMSS,55,27,318.0,100,*66 $GBGSV,2,2,06,14,55,175,46,40,29,043,18,B*06 $INGGA,103607.00,5327.03942,N,00214.42462,W,1,06,5.88,56.0,M,48.5,M,,*6A $ANNAK,UP,AID,999999990,11,*1E $GPGBS,235503.00,1.6,1.4,3.2,,,,,,*40 $GPGBS,235458.00,1.4,1.3,3.1,03,,-21.4,3.8,1,0*5A $GPHDT,274.07,T*03 $GPHDM,223.12,M*05 $IIHDM,70.6,M*13 $IIHDG,70.6,,,,W*2F $IIROT,-7.3,A*0F pynmeagps-1.1.7/tests/pygpsdata-nmea4lc.log000066400000000000000000000003751523640354200207160ustar00rootroot00000000000000$GNRMC,103607.00,A,5327.03942,N,10214.42462,W,0.046,,060321,,,A,V*0e $GPGSV,3,1,11,01,06,014,08,12,43,207,28,14,06,049,,15,44,171,23,1*6b $GNZDA,103607.00,06,03,2021,00,00*7f $GNGBS,103607.00,15.1,24.2,31.0,,,,,,*6f $GNVLW,,N,,N,0.000,N,0.000,N*44 pynmeagps-1.1.7/tests/pygpsdata-nmea4sm.log000066400000000000000000000006571523640354200207420ustar00rootroot00000000000000$GNDTM,W84,,0.0,N,0.0,E,0.0,W84*71 $GNRMC,103607.00,A,5327.03942,N,00214.42462,W,0.046,,060321,,,A,V*0F $GNVTG,,T,,M,0.046,N,0.085,K,A*32 $GNGNS,103607.00,5327.03942,N,00214.42462,W,AANN,06,5.88,56.0,48.5,,,V*34 $GNGGA,103607.00,5327.03942,N,00214.42462,W,1,06,5.88,56.0,M,48.5,M,,*64 $GNGSA,A,3,23,24,20,12,,,,,,,,,9.62,5.88,7.62,1*0C $GPGSV,3,1,11,01,06,014,08,12,43,207,28,14,06,049,,15,44,171,23,1*6B $GPTHS,23.34,A*31 pynmeagps-1.1.7/tests/pygpsdata-nmeabadck.log000066400000000000000000000003171523640354200212740ustar00rootroot00000000000000$GNRMC,103607.00,A,5327.03942,N,00214.42462,W,XXXXX,,060321,,,A,V*7A $GNRMC,103607.00,A,5327.03942,N,10214.42462,W,0.046,,060321,,,A,V*0E $GNRMC,103607.00,A,5327.03942,N,00214.42462,W,XXXXX,,060321,,,A,V*7A pynmeagps-1.1.7/tests/pygpsdata-nmeabadck2.log000066400000000000000000000007231523640354200213570ustar00rootroot00000000000000$GNRMC,115934.00,A,5327.03889,N,00214.41531,W,0.145,,200122,,,A,V*02 $GNVTG,,T,,M,0.145,N,0.269,K,A*3) $GNGGA,115934.00,5327.03889,N,00214.41531,W,1,12,0.67,37.0,M,48.5,M,,*68 $GNGSA,A,3,14,23,17,15,19,24,13,,,,,,1.25,0.67,1.05,1*08 $GNGSA,A,3,66,68,85,77,76,67,,,,,,,1.25,0.67,1.05,2*02 $GNGSA,A,3,25,02,08,07,03,30,,,,,,,1.25,0.67,1.05,3*0A $GNGSA,A,3,20,23,32,,,,,,,,,,1.25,0.67,1.05,4*05 $GPGSV,3,1,11,01,00,032,,10,27,310,,12,19,205,19,13,38,134,21,1*68 pynmeagps-1.1.7/tests/pygpsdata-nmeafoo1.log000066400000000000000000000006401523640354200210730ustar00rootroot00000000000000$GNRMC,121614.00,A,5327.04173,N,00214.41443,W,0.138,,200122,,,A,V*0D $GNVTG,,T,,M,0.138,N,0.255,K,A*35 $GNGGA,121614.00,5327.04173,N,00214.41443,W,1,12,0.66,37.4,M,48.5,M,,*68 $GNGSA,A,3,14,23,10,17,15,19,24,13,,,,,1.18,0.66,0.98,1*03 $GNGSA,A,3,66,68,85,77,78,76,67,,,,,,1.18,0.66,0.98,2*07 $GNGSA,A,3,02,08,07,30,,,,,,,,,1.18,0.66,0.98,3*04 $GNGSA,A,3,20,23,32,25,10,,,,,,,,1.18,0.66,0.98,4*09 $GNFOO,,*4F pynmeagps-1.1.7/tests/pygpsdata-nmeafoo2.log000066400000000000000000000006401523640354200210740ustar00rootroot00000000000000$GNRMC,121614.00,A,5327.04173,N,00214.41443,W,X.138,,200122,,,A,V*65 $GNVTG,,T,,M,0.138,N,0.255,K,A*35 $GNGGA,121614.00,5327.04173,N,00214.41443,W,1,12,0.66,37.4,M,48.5,M,,*68 $GNGSA,A,3,14,23,10,17,15,19,24,13,,,,,1.18,0.66,0.98,1*03 $GNGSA,A,3,66,68,85,77,78,76,67,,,,,,1.18,0.66,0.98,2*07 $GNGSA,A,3,02,08,07,30,,,,,,,,,1.18,0.66,0.98,3*04 $GNGSA,A,3,20,23,32,25,10,,,,,,,,1.18,0.66,0.98,4*09 $GNFOO,,*4F pynmeagps-1.1.7/tests/pygpsdata-nmeastartup.log000066400000000000000000000005611523640354200217330ustar00rootroot00000000000000$GNRMC,,V,,,,,,,,,,N,V*37 $GNVTG,,,,,,,,,N*2E $GNGGA,,,,,,0,00,99.99,,,,,,*56 $GNGSA,A,1,,,,,,,,,,,,,99.99,99.99,99.99,1*33 $GNGSA,A,1,,,,,,,,,,,,,99.99,99.99,99.99,2*30 $GNGSA,A,1,,,,,,,,,,,,,99.99,99.99,99.99,3*31 $GNGSA,A,1,,,,,,,,,,,,,99.99,99.99,99.99,4*36 $GPGSV,1,1,00,1*64 $GLGSV,1,1,00,1*78 $GAGSV,1,1,00,7*73 $GBGSV,1,1,00,1*76 $GNGLL,,,,,,V,N*7A pynmeagps-1.1.7/tests/pygpsdata-um981.log000066400000000000000000000005651523640354200202570ustar00rootroot00000000000000$GNGGA,130058.00,5327.03598945,N,00214.41467156,W,1,08,7.5,36.3017,M,51.6775,M,,*5B $GNGLL,5327.03598945,N,-0214.41467156,W,130058.00,A,A*7E $GNRMC,130058.00,A,5327.03598945,N,00214.41467156,W,0.097,125.7,240226,0.2,W,A,C*4F $GNGGA,130059.00,5327.03598242,N,00214.41468053,W,1,08,7.5,36.3232,M,51.6775,M,,*58 $GNGLL,5327.03598242,N,-0214.41468053,W,130059.00,A,A*78 pynmeagps-1.1.7/tests/pygpsdata-userdefined.log000066400000000000000000000001311523640354200216560ustar00rootroot00000000000000$U1XYZ,102929.00,4427.04024,N,00114.41560,W*10 $U2ZYX,103829.00,somedata,somemoredata*2A pynmeagps-1.1.7/tests/pygpsdata_quectel_pair_get.log000066400000000000000000000025711523640354200227710ustar00rootroot00000000000000$PAIR001,004,0*3F $PAIR001,003,1*39 $PAIR001,003,0*38 $PAIR001,004,0*3F $PAIR001,005,0*3E $PAIR001,006,0*3D $PAIR001,007,0*3C $PAIR010,0,0,2044,369413*33 $PAIR001,050,0*3E $PAIR001,051,0*3F $PAIR051,1000*13 $PAIR001,058,0*36 $PAIR001,059,0*37 $PAIR059,15*1E $PAIR001,062,0*3F $PAIR001,063,0*3E $PAIR063,0,3*3C $PAIR001,066,0*3B $PAIR001,067,0*3A $PAIR067,1,1,1,1,1,0*3A $PAIR001,070,0*3C $PAIR001,071,0*3D $PAIR071,0.4*3A $PAIR001,072,0*3E $PAIR001,073,0*3F $PAIR073,5*27 $PAIR001,074,0*38 $PAIR001,075,0*39 $PAIR075,1*25 $PAIR001,080,0*33 $PAIR001,081,0*32 $PAIR081,0*2F $PAIR001,086,0*35 $PAIR001,087,0*34 $PAIR087,0*29 $PAIR001,100,0*3A $PAIR001,101,0*3B $PAIR101,1,0*3B $PAIR001,382,0*32 $PAIR001,003,0*38 $PAIR001,104,0*3E $PAIR001,002,0*39 $PAIR001,105,0*3F $PAIR105,1*23 $PAIR001,382,0*32 $PAIR001,391,0*30 $PAIRSPF,0*53 $PAIRSPF5,0*66 $PAIRSPF,1*52 $PAIRSPF5,1*67 $PAIRSPF,2*51 $PAIRSPF5,2*64 $PAIRSPF,3*50 $PAIRSPF5,3*65 $PAIR001,400,0*3F $PAIR001,401,0*3E $PAIR401,2*21 $PAIR001,410,0*3E $PAIR001,411,0*3F $PAIR411,1*23 $PAIR001,420,0*3D $PAIR001,421,0*3C $PAIR421,1*20 $PAIR001,433,0*3F $PAIR433,-1*0E $PAIR001,434,0*38 $PAIR001,435,0*39 $PAIR435,1*25 $PAIR001,436,0*3A $PAIR001,437,0*3B $PAIR437,1*27 $PAIR001,650,0*38 $PAIR650,0*25 $PAIR001,752,0*3B $PAIR001,864,0*31 $PAIR001,865,0*30 $PAIR865,115200*1A $PAIR001,866,0*33 $PAIR001,867,0*32 $PAIR867,0*2F $PAIR6011,1,0*0D $PAIR491,1,0*37 pynmeagps-1.1.7/tests/pygpsdata_quectel_pair_poll.log000066400000000000000000000004111523640354200231470ustar00rootroot00000000000000$PAIR051*3E $PAIR059*36 $PAIR063,0*23 $PAIR067*3B $PAIR071*3C $PAIR073*3E $PAIR075*38 $PAIR081*33 $PAIR087*35 $PAIR101*3A $PAIR105*3E $PAIR401*3F $PAIR411*3E $PAIR421*3D $PAIR433*3E $PAIR435*38 $PAIR437*3A $PAIR865,0,0*31 $PAIR867,0,0*33 $PAIR6011,1*11 $PAIR491*36 pynmeagps-1.1.7/tests/pygpsdata_quectel_pair_set.log000066400000000000000000000011521523640354200227770ustar00rootroot00000000000000$PAIR002*38 $PAIR003*39 $PAIR004*3E $PAIR005*3F $PAIR007*3D $PAIR050,1000*12 $PAIR058,15*1F $PAIR062,0,3*3D $PAIR066,1,0,1,1,0,0*3B $PAIR070,4*25 $PAIR072,5*26 $PAIR074,1*24 $PAIR080,1*2F $PAIR081*33 $PAIR086,1*29 $PAIR382,1*2E $PAIR003*39 $PAIR104,0*23 $PAIR002*38 $PAIR382,1*2E $PAIR391,1*2C $PAIR400,2*20 $PAIR410,1*22 $PAIR420,1*21 $PAIR432,1*22 $PAIR434,1*24 $PAIR436,1*26 $PAIR511*3F $PAIR382,1*2E $PAIR003*39 $PAIR511*3F $PAIR002*38 $PAIR513*3D $PAIR382,1*2E $PAIR003*39 $PAIR513*3D $PAIR002*38 $PAIR650,0*25 $PAIR752,2,100*39 $PAIR864,0,0,115200*1B $PAIR866,0,0,1*2F $PAIR6010,0,1*0C $PAIR490,1*2A $PAIR512*3C pynmeagps-1.1.7/tests/quectel_nmea_command.log000066400000000000000000000054211523640354200215360ustar00rootroot00000000000000$PQTMCOLD*1C $PQTMWARM*11 $PQTMHOT*4B $PQTMSRR*4B $PQTMSAVEPAR*5A $PQTMRESTOREPAR*13 $PQTMCFGUART,W,115200*18 $PQTMCFGUART,W,1,115200*05 $PQTMCFGUART,W,115200,8,0,1,0*11 $PQTMCFGUART,W,1,115200,8,0,1,0*0C $PQTMCFGPPS,W,1,1,100,1,1,0*73 $PQTMCFGPPS,W,1,0*73 $PQTMCFGPROT,W,1,1,00000005,00000005*38 $PQTMCFGNMEADP,W,3,8,3,2,3,2*39 $PQTMCFGMSGRATE,W,GGA,1*0A $PQTMCFGMSGRATE,W,PQTMEPE,1,2*1D $PQTMCFGMSGRATE,W,RTCM3-1005,1*59 $PQTMCFGMSGRATE,W,RTCM3-107X,1,0*2F $PQTMCFGMSGRATE,W,RTCM3-1019,1*54 $PQTMCFGGEOFENCE,W,0,0*27 $PQTMGNSSSTART*51 $PQTMGNSSSTOP*09 $PQTMCFGSVIN,W,1,3600,1.2,-2519265.0514,4849534.9045,3277834.6432*01 $PQTMCFGRCVRMODE,W,2*29 $PQTMDEBUGON*48 $PQTMDEBUGOFF*06 $PQTMCFGFIXRATE,W,1000*59 $PQTMCFGRTK,W,1,1*6C $PQTMCFGCNST,W,1,1,1,1,0,0*2B $PQTMCFGODO,W,1,10.5*4E $PQTMRESETODO*09 $PQTMCFGSIGNAL,W,7,3,F,3F,7,1*0E $PQTMCFGSAT,W,1,1,FFFFFFFF*4B $PQTMCFGSAT,W,4,1,BFFCBFFF,1C003FFF*60 $PQTMCFGRSID,W,1024*06 $PQTMCFGRTCM,W,4,0,-90,07,06,1,0*25 $PQTMBKP,66*6D $PQTMCFGAIC,W,1*77 $PQTMCFGNAVMODE,W,10*56 $PQTMCFGNMEATID,W,GP,0*58 $PQTMCFGDR,W,1*2A $PQTMCFGIMUINT,W,1,1,0,1,64*0C $PQTMCFGLA,W,1,0.212,0.514,0.113*31 $PQTMCFGLAM,W,2*7F $PQTMCFGRTKSRCTYPE,W,1*2B $PQTMCFGSTATICHOLD,W,1*2B $PQTMCFGVEHMOT,W,1,1*2C $PQTMDRCLR*53 $PQTMDRSAVE*0F $PQTMCFGANTDELTA,W,0.0000,0.0000,0.0000*10 $PQTMCFGANTINF,W,,0,*27 $PQTMCFGBLD,W,1.000*68 $PQTMCFGELETHD,W,5.0*32 $PQTMCFGGEOSEP,W,0,0.0000*04 $PQTMCFGSBAS,W,003F*7B $PQTMCFGSIGGRP,W,1*24 $PQTMCFGSIGNAL2,W,7,3,F,3F,7,1*3C $PQTMCFGWN,W,2346*17 $PQTMCFGMSGRATE,W,GGA,1*0A $PQTMCFGMSGRATE,W,PQTMEPE,1,2*1D $PQTMCFGMSGRATE,W,RTCM3-1005,1*59 $PQTMCFGMSGRATE,W,RTCM3-107X,1,0*2F $PQTMCFGMSGRATE,W,RTCM3-1019,1*54 $PQTMCFGMSGRATE,W,1,1,GGA,1*0A $PQTMCFGMSGRATE,W,1,1,0AB2,1,1*57 $PSTMCOLD*1E $PSTMWARM*13 $PSTMHOT*49 $PSTMSRR*49 $PSTMSAVEPAR*58 $PSTMRESTOREPAR*11 $PSTMIMUSELFTESTCMD,1*16 $PSTMDRMMFB,160836.000,1,1,1,1,45.4567890,9.4567890,180.5,90.0,10.1,10.2,4.7,0.3*3E $PSTMSETPAR,35,0,0,0,F*47 $PSTMSETPAR,35,0,2,0,1*32 $PSTMSETPAR,32,3,0,1,0x40*48 $PSTMSETPAR,32,3,0,1,0x4*78 $PSTMSETPAR,32,3,0,1,0x100000*4D $PSTMSETPAR,32,6,0,1,0x400000*4D $PSTMINITGPS,3149.121,N,11707.883,E,0530,05,08,2022,09,44,12*5E $PSTMINITTIME,05,08,2022,09,44,12*10 $PSTMEPHEM,12,64,0f06bc34bc3437373790f40045a7ff00fcf5d522480b4bf71b00fbff8931000096126f271f869101c3870ca107afce79a763e13e360a1ce8e7003100380ff903*36 $PQTMAIDINI,1,31.821692,117.115337,131.88,0,1,18,2023,11,10,06,05,11,636,0,0*3D $PQTMAIDTIME,1,18,2023,11,10,06,05,11,636,0,0*67 $PQTMAIDPOS,1,31.821692,117.115337,131.88,0*19 $PQTMCLRMSG*1C $PQTMCLRMSG,1,1*1C $PQTMCFGSTANDALONE,W,1,100,86400,31.82200600,117.11544530,44.51*0C $PQTMCFGRTKRL,W,3,0,0,0*71 $PQTMCFGPPS2,W,1,1,100,1,1,0,1000,0,1,0,0,0*71 $PQTMCFGPPP,W,1,2,120,0.10,0.15*68 $PQTMCFGPINALT,W,14,1*1B $PQTMCFGEVENT,W,1,1,1,150*68 $PQTMCFGCNRTHD,W,10.0*15 $PQTMCFGANTENNA,W,0,0*7E pynmeagps-1.1.7/tests/quectel_nmea_get.log000066400000000000000000000131171523640354200207000ustar00rootroot00000000000000$PQTMVER,1,MODULE,LG290P03AANR01A03S,2024/04/30,10:53:07*32 $PQTMVERNO,LG290P03AANR01A03S,2024/04/30,10:53:07*18 $PQTMEPE,2,1.000,1.000,1.000,1.414,1.732*52 $PQTMVEL,1,154512.100,1.251,2.452,1.245,2.752,3.021,180.512,0.124,0.254,0.250*67 $PQTMGEOFENCESTATUS,1,124521.000,1,2,2,2*27 $PQTMTXT,1,01,01,01,0x105f0cf810417c00*1B $PQTMSVINSTATUS,1,1000,1,,01,20,100,-2484434.3645,4875976.9741,3266161.3412,1.2415*3C $PQTMPVT,1,1000,20221225,163355.000,,0,00,,,,,,,,,,,99.99,99.99*79 $PQTMPVT,1,31075000,20221225,083737.000,,3,09,18,31.12738291,117.26372910,34.212,5.267,3.212,2.928,0.238,4.346,34.12,2.16,4.38*51 $PQTMDOP,1,570643000,1.01,0.88,0.49,0.73,0.50,0.36,0.35*7C $PQTMDOP,1,,99.99,99.99,99.99,99.99,99.99,99.99,99.99*70 $PQTMPL,1,55045200,5.00,1,1,2879,2718,4766,5344,4323,10902,,,*1C $PQTMODO,1,120635.000,1,112.3*6E $PQTMLS,1,195494,1,2299,18,0,1,,137,7,18*2C $PQTMQVER,OK,1,MODULE,LC29HCANR11A07S_DSA4,2025/06/03,15:28:01*52 $PQTMJAMMINGSTATUS,1,1*47 $PQTMSTD,1,131137.000,2368,393115,5.36,4.62,12.12,,0.13,0.13,0.14,0.23,,,*77 $PQTMSTD,1,131523.314,0,393341,,,,,,,,,,,*66 $PQTMGETUTC,OK,2024,10,22,2,52,30,295,,18*0C $PQTMSN,OK,1,16,1234567890ABCDEF*31 $PQTMDRCAL,1,0,1*5C $PQTMIMUTYPE,1,2*52 $PQTMVEHMSG,2,153954,100,1*27 $PQTMINS,240951,1,31.82222216,117.11578436,62.555605,-0.004233,0.005535,-0.004011,0.00,0.00,127.41*40 $PQTMIMU,45454,-1.356730,-0.210568,9.757930,0.564879,0.549612,-0.412209,0,0*77 $PQTMGPS,86139,94183,31.82218794,117.11579022,65.755080,0.027,94.68,2.533952,0.555471,0.886183,29,3*6B $PQTMCFGEINSMSG,1,1,1,1,10*3F $PQTMVEHMOT,1,0.288124,0.159930*0A $PQTMSENMSG,2,1000,22.21,0.124521,1.241541,0.912451,0.145785,1.241541,8.954214*2D $PQTMSENMSG,4,1000,22.21,0.124521,1.241541,0.912451,0.145785,1.241541,8.954214*2B $PQTMCFGDRRTD,OK,600,10000*21 $PQTMCFGIMUTC,OK,1*29 $PQTMDRPVA,1,1000,163355.000,0,,,,,,,,,,,*7C $PQTMDRPVA,1,75000,083737.000,2,31.12738291,117.26372910,34.212,5.267,3.212,2.928,0.238,4.346,0.392663,1.300793,0.030088*5E $PQTMCFGDRHOT,OK,1*2A $PQTMCFGDR,OK,1*79 $PQTMCFGIMUINT,OK,1,1,0,1,64*5F $PQTMCFGLA,OK,1,0.212,0.514,0.113*62 $PQTMCFGLAM,OK,2*2C $PQTMCFGRTKSRCTYPE,OK,1*78 $PQTMCFGSTATICHOLD,OK,1*78 $PQTMCFGVEHMOT,OK,1,1*7F $PQTMDRCLR,OK*7B $PQTMDRSAVE,OK*27 $PQTMVEHATT,1,1000,10.002154,20.235412,160.145185,1.254123,5.451214,5.102154*3D $PQTMANTENNASTATUS,1,1,1*4E $PQTMCFGANTDELTA,OK,0.0000,0.0000,0.0000*43 $PQTMCFGANTINF,OK,,0,*74 $PQTMCFGBLD,OK,1.000*3B $PQTMCFGELETHD,OK,5.0*61 $PQTMCFGGEOSEP,OK,0,0.0000*57 $PQTMCFGSBAS,OK,003F*28 $PQTMCFGSIGGRP,OK,1*77 $PQTMCFGSIGNAL2,OK,07,03,0F,3F,07,01*5F $PQTMCFGWN,OK,2346*44 $PQTMNAV,1,1,1,190423.000,20241224,212681000,2346,18,,,12,,31.45874521,117.41532415,45.1254,-6.1245,,,1.2451,2.1254,5.1242,,,290,1.0,,78,56,,,,,,,1.2101,1.2148,0.4578,1.1547,,,45.124,,*43 $PQTMEOE,1,190423.000,20241224,2346,212681000*65 $PQTMTAR,1,165034.000,4,,0.860,1.124780,1.254125,50.968541,0.254125,0.125485,0.012547,21*52 $PQTMANTENNASTATUS,1,1,1*4E $PQTMCFGANTDELTA,OK,0.0000,0.0000,0.0000*43 $PQTMCFGANTINF,OK,,0,*74 $PQTMCFGBLD,OK,1.000*3B $PQTMCFGELETHD,OK,5.0*61 $PQTMCFGGEOSEP,OK,0,0.0000*57 $PQTMCFGSBAS,OK,003F*28 $PQTMCFGSIGGRP,OK,1*77 $PQTMCFGSIGNAL2,OK,07,03,0F,3F,07,01*5F $PQTMCFGWN,OK,2346*44 $PQTMNAV,1,1,1,190423.000,20241224,212681000,2346,18,,,12,,31.45874521,117.41532415,45.1254,-6.1245,,,1.2451,2.1254,5.1242,,,290,1.0,,78,56,,,,,,,1.2101,1.2148,0.4578,1.1547,,,45.124,,*43 $PQTMEOE,1,190423.000,20241224,2346,212681000*65 $PQTMTAR,1,165034.000,4,,0.860,1.124780,1.254125,50.968541,0.254125,0.125485,0.012547,21*52 $PQTMCFGMSGRATE,OK,GGA,1*59 $PQTMCFGMSGRATE,OK,PQTMEPE,1,2*4E $PQTMCFGMSGRATE,OK,RTCM3-1005,1*0A $PQTMCFGMSGRATE,OK,RTCM3-107X,1,0*7C $PQTMCFGMSGRATE,OK,RTCM3-1019,1*07 $PQTMCFGMSGRATE,OK,1,1,GGA,1*59 $PQTMCFGMSGRATE,OK,1,1,0AB2,1,1*04 $PSTMPPSDATA,1,1,1,0,1,0,0.500000,0,350,305,818,841,0,0,0,0,22665,40,3,17,18,0,0,0,1.432e-09,65473968.17,26000003.91,4*11 $PSTMCPU,51.47,0,261*5C $PSTMTG,2206,283972.00000004,0,4168902594,9,-47355.8601,a02a,88205,0,0,2206,283972.00000004,10,126,9.026,-2147483507,18,708337484,4075040,864102909*04 $PSTMTS,2,146,39046353.062,-45299.46,-203324611.195,21511,33,245073,730,1622,0,0,26*1F $PSTMSBAS,1,2,129,45,141,19*1D $PSTMTEMP,31,134,1*3F $PSTMDRCONFID,100.00,100.00,180.0419,0.1004,0.00,0.0857,0.000993,0.050002,100.3594,100.0,100.0,90.0,0.0*0A $PSTMDRBSD,073922.000,1837870321,0.058,0.058,0.019,0.078,0.077,0.058*42 $PSTMDRGPS,31.853778470,117.253363350,0.69824,-3.58118,1.254,0.793,0.969,30.811,0.520,-0.07642,28.7*76 $PSTMDRPVA,064420.000,24295094,31.8222713,117.1157794,33.1,0.00,0.00,0.21,1.64,-2.10,26.61*48 $PSTMDRPVASD,064420.000,24295094,17.9,22.0,13.5,2.83,1.77,1.80,0.00,0.00,180.01,11.5,7.2,8.9,1.32,0.61,0.79*78 $PSTMDRSINT,100,100,0,0,1.000,1.000,0.000,-0.42,0.79,-0.07,0.26,0.35,10.05,0.00*05 $PSTMDRSVF,064420.000,24295094,0.00,0.00,0.00,0.00,0.00,0.00*50 $PSTMDRSTYPE,3*58 $PSTMDRCAL,1,1,1,1,ff,1,1,F*04 $PSTMDR1,-1.63,-1.67,-91.75,1.009,-0.754,-0.197,0.118,-0.029,0.254,0.176,0.081,0.000,0.000*39 $PSTMDR2,CC,AA,2,0,1,0.48,2.23,1,0.000,0*2D $PSTMDREPE,141.42,-1.00*72 $PSTMDRDEBUG,0.6,-2.0,-8.7,0.0,10.5,0.0*62 $PSTMDRMMFBKF,85064305,846411,-9,0,0,0.00,0.00,0.00,0.00,0.00,0.00,0,0.00,0.00,0.00*14 $PSTMDRSENMSG,3,1924310999,0,0*3B $PSTMDRSENMSG,24,3248649605,-486,1*28 $PSTMDRSENMSG,30,3248512115,299,-35,16514*02 $PSTMDRSENMSG,31,5855224,62,-119,-49*2A $PQTMSN,1,16,OK,1234567890ABCDEF*31 $PQTMPPPNAV,1,1,1,190423.000,20241224,212681000,2346,18,1,,7,,31.45874521,117.41532415,45.1254,-6.1245,,,1.2451,2.1254,5.1242,,,9001,1.0,,78,56,,,,,,,1.2101,1.2148,0.4578,1.1547,,,45.124,,*25 $PQTMENV,1,212681000,2236,20241224,190423.000,90,34.52,64,34,0,0,0.000,50,51,27,,,,,*55 $PQTMRTCMIS,1,190423.412,1,1,1074,,290,04,82,1451,9,3,2,9,15,5,24,3*1C pynmeagps-1.1.7/tests/quectel_nmea_poll.log000066400000000000000000000021661523640354200210710ustar00rootroot00000000000000$PQTMCFGUART,R*36 $PQTMCFGUART,R,1*2B $PQTMCFGPPS,R,1*6A $PQTMCFGPROT,R,1,1*3D $PQTMCFGNMEADP,R*37 $PQTMCFGMSGRATE,R,GGA*12 $PQTMCFGMSGRATE,R,PQTMEPE,2*05 $PQTMCFGMSGRATE,R,RTCM3-1005*41 $PQTMCFGMSGRATE,R,RTCM3-107X*2B $PQTMCFGMSGRATE,R,RTCM3-1019*4C $PQTMCFGGEOFENCE,R,0*3E $PQTMCFGSVIN,R*26 $PQTMCFGRCVRMODE,R*32 $PQTMCFGFIXRATE,R*71 $PQTMCFGRTK,R*69 $PQTMCFGCNST,R*2E $PQTMCFGODO,R*60 $PQTMCFGSIGNAL,R*3A $PQTMCFGSAT,R,1,1*62 $PQTMCFGSAT,R,4,1*67 $PQTMCFGRSID,R*28 $PQTMGETUTC*0C $PQTMSN*05 $PQTMCFGAIC,R*6F $PQTMCFGNAVMODE,R*7E $PQTMCFGNMEATID,R*7A $PQTMQVER,1*15 $PQTMCFGDR,R*32 $PQTMCFGIMUINT,R,1*3B $PQTMCFGLA,R,1*34 $PQTMCFGLAM,R*64 $PQTMCFGRTKSRCTYPE,R*33 $PQTMCFGSTATICHOLD,R*33 $PQTMCFGVEHMOT,R*29 $PQTMCFGMSGRATE,R,GGA*12 $PQTMCFGMSGRATE,R,PQTMEPE,2*05 $PQTMCFGMSGRATE,R,RTCM3-1005*41 $PQTMCFGMSGRATE,R,RTCM3-107X*2B $PQTMCFGMSGRATE,R,RTCM3-1019*4C $PQTMCFGMSGRATE,R,1,1,GGA*12 $PQTMCFGMSGRATE,R,1,1,0AB2,1*4F $PQTMCFGSTANDALONE,R*21 $PQTMCFGRTKRL,R*77 $PQTMCFGPPS2,R,1*58 $PQTMCFGPPP,R*74 $PQTMCFGPINALT,R,14*03 $PQTMCFGANTENNA,R*7B $PQTMCFGEVENT,R,1*75 $PQTMCFGCNRTHD,R*23 $PQTMCFGANTENNA,R*7B $PQTMLSTMSG*0A $PQTMLSTMSG,1,1*0A pynmeagps-1.1.7/tests/quectel_nmea_response.log000066400000000000000000000066761523640354200217730ustar00rootroot00000000000000$PQTMVER,1,MODULE,LG290P03AANR01A03S,2024/04/30,10:53:07*32 $PQTMUNIQID,OK,16,81D62010EE0AF375BDF5952CDC3757A1*3E $PQTMSAVEPAR,OK*72 $PQTMRESTOREPAR,OK*3B $PQTMVERNO,LG290P03AANR01A03S,2024/04/30,10:53:07*18 $PQTMCFGUART,OK*60 $PQTMCFGUART,OK*60 $PQTMCFGUART,OK*60 $PQTMCFGUART,OK*60 $PQTMCFGUART,OK,1,115200,8,0,1,0*5F $PQTMCFGUART,OK,1,115200,8,0,1,0*5F $PQTMCFGPPS,OK*21 $PQTMCFGPPS,OK,1,1,100,1,1,0*20 $PQTMCFGPPS,OK*21 $PQTMCFGPROT,OK*6B $PQTMCFGPROT,OK,1,1,00000005,00000005*6B $PQTMCFGNMEADP,OK*61 $PQTMCFGNMEADP,OK,3,8,3,2,3,2*6A $PQTMEPE,2,1.000,1.000,1.000,1.414,1.732*52 $PQTMCFGMSGRATE,OK*29 $PQTMCFGMSGRATE,OK,GGA,1,*75 $PQTMCFGMSGRATE,OK*29 $PQTMCFGMSGRATE,OK,PQTMEPE,1,2*4E $PQTMCFGMSGRATE,OK*29 $PQTMCFGMSGRATE,OK,RTCM3-1005,1*0A $PQTMCFGMSGRATE,OK*29 $PQTMCFGMSGRATE,OK,RTCM3-107X,1,0*7C $PQTMCFGMSGRATE,OK*29 $PQTMCFGMSGRATE,OK,RTCM3-1019,1*07 $PQTMVEL,1,154512.100,1.251,2.452,1.245,2.752,3.021,180.512,0.124,0.254,0.250*67 $PQTMCFGGEOFENCE,OK*74 $PQTMCFGGEOFENCE,OK*74 $PQTMCFGGEOFENCE,OK,0,1,0,0,31.451248,117.451245,100.5*4B $PQTMCFGGEOFENCE,OK,0,0*74 $PQTMGEOFENCESTATUS,1,124521.000,1,2,2,2*27 $PQTMGNSSSTART,OK*79 $PQTMGNSSSTOP,OK*21 $PQTMTXT,1,01,01,01,0x105f0cf810417c00*1B $PQTMCFGSVIN,OK*70 $PQTMCFGSVIN,OK,1,3600,1.2,-2519265.0514,4849534.9045,3277834.6432*52 $PQTMSVINSTATUS,1,1000,1,,01,20,100,-2484434.3645,4875976.9741,3266161.3412,1.2415*3C $PQTMPVT,1,1000,20221225,163355.000,,0,00,,,,,,,,,,,99.99,99.99*79 $PQTMPVT,1,31075000,20221225,083737.000,,3,09,18,31.12738291,117.26372910,34.212,5.267,3.212,2.928,0.238,4.346,34.12,2.16,4.38*51 $PQTMCFGRCVRMODE,OK*64 $PQTMCFGRCVRMODE,OK,2*7A $PQTMDEBUGON,OK*60 $PQTMDEBUGOFF,OK*2E $PQTMCFGFIXRATE,OK*27 $PQTMCFGFIXRATE,OK,1000*0A $PQTMCFGRTK,OK*3F $PQTMCFGRTK,OK,1,1*3F $PQTMCFGCNST,OK*78 $PQTMCFGCNST,OK,1,1,1,1,0,0*78 $PQTMDOP,1,570643000,1.01,0.88,0.49,0.73,0.50,0.36,0.35*7C $PQTMDOP,1,,99.99,99.99,99.99,99.99,99.99,99.99,99.99*70 $PQTMPL,1,55045200,5.00,1,1,2879,2718,4766,5344,4323,10902,,,*1C $PQTMCFGODO,OK*36 $PQTMCFGODO,ERROR,1*77 $PQTMCFGODO,OK,1,10.5*1D $PQTMRESETODO,OK*21 $PQTMODO,1,120635.000,1,112.3*6E $PQTMCFGSIGNAL,OK*6C $PQTMCFGSIGNAL,OK,07,03,0F,3F,07,01*6D $PQTMCFGSAT,OK*34 $PQTMCFGSAT,OK,1,01,FFFFFFFF*28 $PQTMCFGSAT,OK*34 $PQTMCFGSAT,ERROR,2*76 $PQTMCFGRSID,OK*7E $PQTMCFGRSID,OK,1024*55 $PQTMCFGRTCM,OK*7A $PQTMCFGGEOFENCE,R,0,1,0,3,31.451248,117.451245,100.5,12.36,85.24,118.72,56.45,140.13*19 $PQTMCFGDR,OK*64 $PQTMCFGDR,OK,1*79 $PQTMCFGIMUINT,OK*70 $PQTMCFGIMUINT,OK,1,1,0,1,64*5F $PQTMCFGLA,OK*7F $PQTMCFGLA,OK,1,0.212,0.514,0.113*62 $PQTMCFGLAM,OK*32 $PQTMCFGLAM,OK,2*2C $PQTMCFGRTKSRCTYPE,OK*65 $PQTMCFGRTKSRCTYPE,OK,1*78 $PQTMCFGSTATICHOLD,OK*65 $PQTMCFGSTATICHOLD,OK,1*78 $PQTMCFGVEHMOT,OK*7F $PQTMCFGVEHMOT,OK,1,1*7F $PQTMDRCLR,OK*7B $PQTMDRSAVE,OK*27 $PQTMCFGMSGRATE,OK,GGA,1*59 $PQTMCFGMSGRATE,OK,PQTMEPE,1,2*4E $PQTMCFGMSGRATE,OK,RTCM3-1005,1*0A $PQTMCFGMSGRATE,OK,RTCM3-107X,1,0*7C $PQTMCFGMSGRATE,OK,RTCM3-1019,1*07 $PQTMCFGMSGRATE,OK,1,1,GGA,1*59 $PQTMCFGMSGRATE,OK,1,1,0AB2,1,1*04 $PSTMSETPAROK,P01,L15,F00,0000000a*11 $PSTMSETPAROK,P35,L00,F00,0000000f*15 $PSTMSETPAROK,P35,L00,F02,00000001*40 $PSTMSETPAROK,P35,L00,F00,00000007*44 $PSTMINITGPSOK*40 $PSTMINITTIMEOK*11 $PQTMCFGSTANDALONE,OK,1,100,86400,31.82200600,117.11544530,44.51*5F $PQTMCFGRTKRL,OK,3,0,0,0*22 $PQTMCFGPPS2,OK,1,1,100,1,1,0,1000,0,1,0,0,0*22 $PQTMCFGPPP,OK,01,2,120,0.10,0.15*0B $PQTMCFGPINALT,OK,14,1*48 $PQTMCFGEVENT,OK,1,1,1,150*3B $PQTMCFGCNRTHD,OK,10.0*46 $PQTMCFGANTENNA,OK,0,0*2D $PQTMLSTMSG,OK,1,1,RTCM3-107X,1,0*77 pynmeagps-1.1.7/tests/septentriox5_nmea.log000066400000000000000000000003341523640354200210450ustar00rootroot00000000000000$PSSN,HRP,104751.00,230324,23.455,1.954,0.0125,0.123,0.0234,0.03765,11,0,4.56453,W*20 $PSSN,TFM,104751.00,2,1021,1023,1025*5F $PSSN,SNC,[0,379359000,1840,[1,2,0,0]]*4C $PSSN,SNC,[0,379359123,1841,[1,2,0,0],[2,3,0,0]]*4A pynmeagps-1.1.7/tests/test_constructor.py000066400000000000000000000441221523640354200206720ustar00rootroot00000000000000""" Constructor method tests for pynmeagps Created on 4 Mar 2021 *** NB: must be saved in UTF-8 format *** :author: semuadmin (Steve Smith) """ import unittest from datetime import datetime from pynmeagps import ( NMEAMessage, NMEAReader, GET, SET, POLL, NMEAMessageError, VALCKSUM, VALMSGID, ) class FillTest(unittest.TestCase): def setUp(self): self.maxDiff = None def tearDown(self): pass def testFill_GNGLL(self): # test GET constructor with full payload keyword EXPECTED_RESULT = "" res = NMEAMessage( "GN", "GLL", GET, payload=["5327.04319", "S", "00214.41396", "E", "223232.00", "A", "A"], ) self.assertEqual(str(res), EXPECTED_RESULT) res2 = NMEAReader.parse(res.serialize()) self.assertEqual(str(res2), EXPECTED_RESULT) def testFill_GNGLL_HP(self): # test GET constructor in high precision mode EXPECTED_RESULT = "" EXPECTED_PAYLOAD = [ "4307.4074073", "N", "00259.2592593", "W", "162924.12", "A", "A", ] res = NMEAMessage( "GN", "GLL", GET, lat=43.123456789, lon=-2.987654321, time=datetime(2023, 11, 22, 16, 29, 24, 123456).time(), status="A", posMode="A", hpnmeamode=1, ) self.assertEqual(str(res), EXPECTED_RESULT) self.assertEqual(res.payload, EXPECTED_PAYLOAD) res2 = NMEAReader.parse(res.serialize()) print(res.serialize()) print(res2.serialize()) def testFill_GNGLL_SP(self): # test GET constructor in standard precision mode EXPECTED_RESULT = "" EXPECTED_PAYLOAD = ["4307.40741", "N", "00259.25926", "W", "223232", "A", "A"] res = NMEAMessage( "GN", "GLL", GET, lat=43.123456789, lon=-2.987654321, time="22:32:32", status="A", posMode="A", hpnmeamode=0, ) self.assertEqual(str(res), EXPECTED_RESULT) self.assertEqual(res.payload, EXPECTED_PAYLOAD) def testFill_GNGLL_NSEW1( self, ): # derive lat/lon sign from NS/EW values EXPECTED_RESULT = "" EXPECTED_PAYLOAD = ["4307.40741", "S", "00259.25926", "W", "223232", "A", "A"] res = NMEAMessage( "GN", "GLL", GET, lat=-43.123456789, lon=-2.987654321, time="22:32:32", status="A", posMode="A", hpnmeamode=0, ) self.assertEqual(str(res), EXPECTED_RESULT) self.assertEqual(res.payload, EXPECTED_PAYLOAD) def testFill_GNGLL_NSEW2( self, ): # derive lat/lon sign from NS/EW values EXPECTED_RESULT = "" EXPECTED_PAYLOAD = ["4307.40741", "N", "00259.25926", "E", "223232", "A", "A"] res = NMEAMessage( "GN", "GLL", GET, lat=43.123456789, lon=2.987654321, time="22:32:32", status="A", posMode="A", hpnmeamode=0, ) self.assertEqual(str(res), EXPECTED_RESULT) self.assertEqual(res.payload, EXPECTED_PAYLOAD) def testFill_GNGLL_NSEW3( self, ): # derive lat/lon sign from NS/EW values EXPECTED_RESULT = "" EXPECTED_PAYLOAD = ["4307.40741", "N", "00259.25926", "W", "223232", "A", "A"] res = NMEAMessage( "GN", "GLL", GET, lat=43.123456789, lon=-2.987654321, time="22:32:32", status="A", posMode="A", hpnmeamode=0, ) self.assertEqual(str(res), EXPECTED_RESULT) self.assertEqual(res.payload, EXPECTED_PAYLOAD) def testFill_GRMI( self, ): # test population ot TM and DT attributes by strings EXPECTED_RESULT = "" EXPECTED_PAYLOAD = [ "4307.40741", "N", "00259.25926", "W", "180925", "223232", "D", ] res = NMEAMessage( "P", "GRMI", SET, lat=43.123456789, lon=-2.987654321, date="2025-09-18", time="22:32:32", rcvr_cmd="D", ) self.assertEqual(str(res), EXPECTED_RESULT) self.assertEqual(res.payload, EXPECTED_PAYLOAD) def testFill_GNGLLUPD(self): # test that NMEAMessage is immutable after init EXPECTED_ERROR = ( "Object is immutable. Updates to lon not permitted after initialisation." ) with self.assertRaises(NMEAMessageError) as context: res = NMEAMessage( "GN", "GLL", GET, payload=["5327.04319", "S", "00214.41396", "E", "223232.00", "A", "A"], ) res.lon = 54.6666 self.assertTrue(EXPECTED_ERROR in str(context.exception)) def testFill_BADMODE(self): # test invalid mode EXPECTED_ERROR = "Invalid msgmode 4 - must be 0, 1 or 2." with self.assertRaises(NMEAMessageError) as context: NMEAMessage( "GN", "GLL", 4, payload=["5327.04319", "S", "00214.41396", "E", "223232.00", "A", "A"], ) self.assertTrue(EXPECTED_ERROR in str(context.exception)) def testFill_GNGNQ(self): # test POLL constructor with msgId kwarg EXPECTED_RESULT = "" res = NMEAMessage("GN", "GNQ", POLL, msgId="GGA") self.assertEqual(str(res), EXPECTED_RESULT) def testFill_PUBX401( self, ): # test SET constructor with PUBX message and payload kwarg EXPECTED_RESULT = "" EXPECTED_PAYLOAD = b"$PUBX,40,GLL,0,1,0,1,0,0*5C\r\n" res = NMEAMessage( "P", "UBX", SET, payload=["40", "GLL", "0", "1", "0", "1", "0", "0"] ) self.assertEqual(str(res), EXPECTED_RESULT) self.assertEqual(res.serialize(), EXPECTED_PAYLOAD) def testFill_PUBX402( self, ): # test SET constructor with PUBX message and individual kwargs EXPECTED_RESULT = "" EXPECTED_PAYLOAD = b"$PUBX,40,3,0,1,0,1,0,0*28\r\n" res = NMEAMessage("P", "UBX", SET, msgId="40", id=3, rus1=1, rusb=1) self.assertEqual(str(res), EXPECTED_RESULT) self.assertEqual(res.serialize(), EXPECTED_PAYLOAD) def testFill_PUBX412( self, ): # test SET constructor with PUBX message and individual kwargs EXPECTED_RESULT = "" EXPECTED_PAYLOAD = b"$PUBX,41,1,1,1,115200,0*1C\r\n" res = NMEAMessage( "P", "UBX", SET, msgId="41", portId=1, inProto=1, outProto=1, baudRate=115200, autobauding=0, ) self.assertEqual(str(res), EXPECTED_RESULT) self.assertEqual(res.serialize(), EXPECTED_PAYLOAD) def testFill_PUBX4ERR(self): # test SET constructor with missing msgId EXPECTED_ERROR = ( "PUBX message definitions must include payload or msgId keyword arguments." ) with self.assertRaises(NMEAMessageError) as context: NMEAMessage("P", "UBX", SET, id=3, rus1=1, rusb=1) self.assertTrue(EXPECTED_ERROR in str(context.exception)) def testFill_UNKNOWN(self): # test GET constructor with unknown msgId EXPECTED_ERROR = "Unknown msgID GNXXX, msgmode GET." with self.assertRaises(NMEAMessageError) as context: NMEAMessage("GN", "XXX", GET, payload=[0, 0, 0], validate=VALMSGID) self.assertTrue(EXPECTED_ERROR in str(context.exception)) def testFill_UNKNOWN2(self): # test GET constructor with unknown talker EXPECTED_ERROR = "Unknown talker XX." with self.assertRaises(NMEAMessageError) as context: NMEAMessage( "XX", "XXX", GET, payload=[0, 0, 0], validate=VALCKSUM | VALMSGID ) self.assertTrue(EXPECTED_ERROR in str(context.exception)) def testFill_UNKNOWN3(self): # test GET constructor with unknown talker EXPECTED_RESULT = "" res = NMEAMessage("XX", "XXX", GET, payload=[0, 0, 0], validate=VALCKSUM) self.assertEqual(str(res), EXPECTED_RESULT) def testFill_UNKNOWN4(self): # test GET constructor with unknown UBX msgid EXPECTED_ERROR = "Unknown msgID UBX08 msgmode GET." with self.assertRaises(NMEAMessageError) as context: NMEAMessage("GN", "UBX", GET, payload=["08", 0, 0], validate=VALMSGID) self.assertTrue(EXPECTED_ERROR in str(context.exception)) def testFill_UNKNOWN5(self): # test GET constructor with unknown msgId EXPECTED_ERROR = "Unknown msgID GNXXX, msgmode GET." msg = NMEAMessage("GN", "XXX", GET, payload=[0, 0, 0], validate=VALCKSUM) def testGSV(self): EXPECTED_RESULT = "" EXPECTED_BIN = ( b"$GPGSV,1,1,16,4,48,25,45,6,78,120,39,18,62,26,52,23,17,99,47,0*56\r\n" ) msg = NMEAMessage( "GP", "GSV", GET, numMsg=1, msgNum=1, numSV=16, svid_01=4, elv_01=48, az_01=25, cno_01=45, svid_02=6, elv_02=78, az_02=120, cno_02=39, svid_03=18, elv_03=62, az_03=26, cno_03=52, svid_04=23, elv_04=17, az_04=99, cno_04=47, ) self.assertEqual(str(msg), EXPECTED_RESULT) self.assertEqual(msg.serialize(), EXPECTED_BIN) self.assertEqual(str(NMEAReader.parse(msg.serialize())), EXPECTED_RESULT) def testGSV2(self): EXPECTED_RESULT = "" EXPECTED_BIN = b"$GLGSV,1,1,16,4,48,25,45,6,78,120,39,0*40\r\n" msg = NMEAMessage( "GL", "GSV", GET, numMsg=1, msgNum=1, numSV=16, svid_01=4, elv_01=48, az_01=25, cno_01=45, svid_02=6, elv_02=78, az_02=120, cno_02=39, ) self.assertEqual(str(msg), EXPECTED_RESULT) self.assertEqual(msg.serialize(), EXPECTED_BIN) self.assertEqual(str(NMEAReader.parse(msg.serialize())), EXPECTED_RESULT) def testALC_empty(self): """ALC sentence with zero alert entries.""" EXPECTED_STR = "" EXPECTED_BIN = b"$IIALC,01,01,00,0*7E\r\n" msg = NMEAMessage( "II", "ALC", GET, numSen="01", senNum="01", seqmid="00", numAlerts=0, ) self.assertEqual(str(msg), EXPECTED_STR) self.assertEqual(msg.serialize(), EXPECTED_BIN) # Parse round-trip: IN fields normalise to int, so zero-padding is lost parsed = NMEAReader.parse(msg.serialize()) self.assertEqual(parsed.numAlerts, 0) self.assertEqual(parsed.seqmid, 0) def testALC_single_entry_null_alertinst(self): """ALC sentence with one alert entry, null alert instance (single-instance alert).""" EXPECTED_STR = "" EXPECTED_BIN = b"$IIALC,01,01,00,1,,192,,3*76\r\n" msg = NMEAMessage( "II", "ALC", GET, numSen="01", senNum="01", seqmid="00", numAlerts=1, mfrcode_01="", alertid_01="192", alertinst_01="", revisionctr_01="3", ) self.assertEqual(str(msg), EXPECTED_STR) self.assertEqual(msg.serialize(), EXPECTED_BIN) parsed = NMEAReader.parse(msg.serialize()) self.assertEqual(parsed.numAlerts, 1) self.assertEqual(parsed.alertid_01, "192") self.assertEqual(parsed.alertinst_01, "") self.assertEqual(parsed.revisionctr_01, "3") def testALC_two_entries_mixed_alertinst(self): """ALC sentence with two alert entries; first has a numeric alert instance, second has null alert instance. Verifies all four fields per entry decode correctly — regression test for the missing alertinst field bug.""" EXPECTED_STR = ( "" ) EXPECTED_BIN = b"$IIALC,01,01,00,2,,192,1,3,XYZ,512,,7*1E\r\n" msg = NMEAMessage( "II", "ALC", GET, numSen="01", senNum="01", seqmid="00", numAlerts=2, mfrcode_01="", alertid_01="192", alertinst_01="1", revisionctr_01="3", mfrcode_02="XYZ", alertid_02="512", alertinst_02="", revisionctr_02="7", ) self.assertEqual(str(msg), EXPECTED_STR) self.assertEqual(msg.serialize(), EXPECTED_BIN) parsed = NMEAReader.parse(msg.serialize()) # Verify all four fields of entry 1 self.assertEqual(parsed.mfrcode_01, "") self.assertEqual(parsed.alertid_01, "192") self.assertEqual(parsed.alertinst_01, "1") self.assertEqual(parsed.revisionctr_01, "3") # Verify all four fields of entry 2 self.assertEqual(parsed.mfrcode_02, "XYZ") self.assertEqual(parsed.alertid_02, "512") self.assertEqual(parsed.alertinst_02, "") self.assertEqual(parsed.revisionctr_02, "7") def testALC_parse_wire_sentence(self): """Parse a raw ALC wire sentence and verify field mapping. Confirms that alertinst is not conflated with revisionctr — the defect this fix addresses (IEC 61162-1 §8.3.13, comment 4). """ raw = b"$IIALC,01,01,05,1,,10001,2,3*4B\r\n" parsed = NMEAReader.parse(raw) self.assertEqual(parsed.numAlerts, 1) self.assertEqual(parsed.alertid_01, "10001") self.assertEqual(parsed.alertinst_01, "2") self.assertEqual(parsed.revisionctr_01, "3") def testALC_two_entries_mixed_alertinst(self): """ALC sentence with two alert entries; first has a numeric alert instance, second has null alert instance. Verifies all four fields per entry decode correctly — regression test for the missing alertinst field bug.""" EXPECTED_RESULT = ( "" ) EXPECTED_BIN = b"$IIALC,01,01,00,2,,192,1,3,XYZ,512,,7*1E\r\n" msg = NMEAMessage( "II", "ALC", GET, numSen="01", senNum="01", seqmid="00", numAlerts=2, mfrcode_01="", alertid_01="192", alertinst_01="1", revisionctr_01="3", mfrcode_02="XYZ", alertid_02="512", alertinst_02="", revisionctr_02="7", ) self.assertEqual(str(msg), EXPECTED_RESULT) self.assertEqual(msg.serialize(), EXPECTED_BIN) parsed = NMEAReader.parse(msg.serialize()) # Verify all four fields of entry 1 self.assertEqual(parsed.mfrcode_01, "") self.assertEqual(parsed.alertid_01, "192") self.assertEqual(parsed.alertinst_01, "1") self.assertEqual(parsed.revisionctr_01, "3") # Verify all four fields of entry 2 self.assertEqual(parsed.mfrcode_02, "XYZ") self.assertEqual(parsed.alertid_02, "512") self.assertEqual(parsed.alertinst_02, "") self.assertEqual(parsed.revisionctr_02, "7") def testALC_parse_wire_sentence(self): """Parse a raw ALC wire sentence and verify field mapping. Confirms that alertinst is not conflated with revisionctr — the defect this fix addresses (IEC 61162-1 §8.3.13, comment 4). """ raw = b"$IIALC,01,01,05,1,,10001,2,3*4B\r\n" parsed = NMEAReader.parse(raw) self.assertEqual(parsed.numAlerts, 1) self.assertEqual(parsed.alertid_01, "10001") self.assertEqual(parsed.alertinst_01, "2") self.assertEqual(parsed.revisionctr_01, "3") if __name__ == "__main__": # import sys;sys.argv = ['', 'Test.testName'] unittest.main() pynmeagps-1.1.7/tests/test_parse.py000066400000000000000000000105421523640354200174160ustar00rootroot00000000000000""" Parse method tests for pynmeagps (most parse() functionality tested in test_stream.py) Created on 4 Mar 2021 *** NB: must be saved in UTF-8 format *** :author: semuadmin (Steve Smith) """ import unittest from pynmeagps import ( NMEAReader, NMEAMessageError, NMEAParseError, ERR_RAISE, SET, VALCKSUM, VALMSGID, ) class ParseTest(unittest.TestCase): def setUp(self): self.maxDiff = None self.messageGLL = "$GNGLL,5327.04319,S,00214.41396,E,223232.00,A,A*68\r\n" self.messageGLL_HP = ( "$GNGLL,5327.0431923,S,00214.4139641,E,223232.00,A,A*6C\r\n" ) self.messagePKLSH = "$PKLSH,3851.3330,N,09447.9417,W,012212,V,100,1202,*24\r\n" self.messagePUBX = "$PUBX,00,103607.00,5327.03942,N,00214.42462,W,104.461,G3,29,31,0.085,39.63,-0.007,,5.88,7.62,8.09,6,0,0*69\r\n" self.messagePGRMM = "$PGRMM,WGS84*26\r\n" self.messagePGRMO = "$PGRMO,PGRMM,2*30\r\n" self.messageNK = "$GNXXX,5327.04319,S,00214.41396,E,223232.00,A,A*77\r\n" self.messageBADCK = "$GNGLL,5327.04319,S,00214.41396,E,223232.00,A,A*22\r\n" def tearDown(self): pass def testParseGLL(self): # standard message res = NMEAReader.parse(self.messageGLL) self.assertEqual( str(res), ( "" ), ) def testParseGLL_HPMODE(self): # standard message, HP mode res = NMEAReader.parse(self.messageGLL_HP) self.assertEqual( str(res), ( "" ), ) print(res.serialize()) def testParsePKLSH(self): # Proprietary JVCKenwood message res = NMEAReader.parse(self.messagePKLSH) self.assertEqual( str(res), "", ) def testParsePUBX(self): # proprietary UBX message res = NMEAReader.parse(self.messagePUBX) self.assertEqual( str(res), ( "" ), ) def testParsePGRMM(self): # proprietary GARMIN message res = NMEAReader.parse(self.messagePGRMM) self.assertEqual(str(res), ("")) def testParsePGRMO(self): # parse SET message res = NMEAReader.parse(self.messagePGRMO, msgmode=SET) self.assertEqual(str(res), ("")) def testParseNK( self, ): # unknown message identifier with validate (VALCKSUM + VALMSGID) - should be rejected EXPECTED_ERROR = "Unknown msgID GNXXX, msgmode GET." with self.assertRaises(NMEAParseError) as context: NMEAReader.parse(self.messageNK, validate=VALCKSUM | VALMSGID) self.assertTrue(EXPECTED_ERROR in str(context.exception)) def testParseNK2( self, ): # unknown message identifier with validate VALCKSUM only - should just be ignored. EXPECTED_RESULT = "" res = NMEAReader.parse(self.messageNK, validate=VALCKSUM) self.assertEqual(str(res), EXPECTED_RESULT) def testParseBADMODE(self): # invalid mode setting EXPECTED_ERROR = "Invalid parse mode 4 - must be 0, 1 or 2." with self.assertRaises(NMEAParseError) as context: NMEAReader.parse(self.messageGLL, msgmode=4) self.assertTrue(EXPECTED_ERROR in str(context.exception)) def testParseBADCK(self): # invalid checksum EXPECTED_ERROR = "Message GNGLL invalid checksum 22 - should be 68." with self.assertRaises(NMEAParseError) as context: NMEAReader.parse(self.messageBADCK) self.assertTrue(EXPECTED_ERROR in str(context.exception)) if __name__ == "__main__": # import sys;sys.argv = ['', 'Test.testName'] unittest.main() pynmeagps-1.1.7/tests/test_quectel.py000066400000000000000000001667221523640354200177620ustar00rootroot00000000000000""" Quectel LG290P Stream method tests for pynmeagps Created on 19 Aug 2024 *** NB: must be saved in UTF-8 format *** :author: semuadmin (Steve Smith) """ import os import sys import unittest from pynmeagps import ( NMEAReader, NMEAMessage, VALCKSUM, VALMSGID, SET, GET, POLL, hex2str, ) DIRNAME = os.path.dirname(__file__) class QuectelStreamTest(unittest.TestCase): def setUp(self): self.maxDiff = None def tearDown(self): pass def catchio(self): """ Capture stdout as string. """ self._saved_stdout = sys.stdout self._strout = os.StringIO() sys.stdout = self._strout def restoreio(self) -> str: """ Return captured output and restore stdout. """ sys.stdout = self._saved_stdout return self._strout.getvalue().strip() def testNMEAQUECTELGET(self): # test proprietary Quectel LG290P GET messages EXPECTED_RESULTS = ( "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ) i = 0 raw = 0 with open(os.path.join(DIRNAME, "quectel_nmea_get.log"), "rb") as stream: nmr = NMEAReader( stream, validate=VALCKSUM | VALMSGID, quitonerror=2, msgmode=GET ) for raw, parsed in nmr: if raw is not None: #print(f'"{parsed}",') self.assertEqual(str(parsed), EXPECTED_RESULTS[i]) i += 1 self.assertEqual(i, len(EXPECTED_RESULTS)) def testNMEAQUECTELPOLL(self): # test proprietary Quectel LG290P POLL messages EXPECTED_RESULTS = ( "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ) i = 0 raw = 0 with open(os.path.join(DIRNAME, "quectel_nmea_poll.log"), "rb") as stream: nmr = NMEAReader( stream, validate=VALCKSUM | VALMSGID, quitonerror=2, msgmode=POLL ) for raw, parsed in nmr: if raw is not None: #print(f'"{parsed}",') self.assertEqual(str(parsed), EXPECTED_RESULTS[i]) i += 1 self.assertEqual(i, len(EXPECTED_RESULTS)) def testNMEAQUECTELCOMMAND( self, ): # test proprietary Quectel LG290P Command (SET) messages EXPECTED_RESULTS = ( "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ) i = 0 raw = 0 with open(os.path.join(DIRNAME, "quectel_nmea_command.log"), "rb") as stream: nmr = NMEAReader( stream, validate=VALCKSUM | VALMSGID, quitonerror=2, msgmode=SET ) for raw, parsed in nmr: if raw is not None: # print(f'"{parsed}",') self.assertEqual(str(parsed), EXPECTED_RESULTS[i]) i += 1 self.assertEqual(i, len(EXPECTED_RESULTS)) def testNMEAQUECTELRESPONSE( self, ): # test proprietary Quectel LG290P Response (GET) messages EXPECTED_RESULTS = ( "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ) i = 0 raw = 0 with open(os.path.join(DIRNAME, "quectel_nmea_response.log"), "rb") as stream: nmr = NMEAReader( stream, validate=VALCKSUM | VALMSGID, quitonerror=1, msgmode=GET ) for raw, parsed in nmr: if raw is not None: #print(f'"{parsed}",') self.assertEqual(str(parsed), EXPECTED_RESULTS[i]) i += 1 self.assertEqual(i, len(EXPECTED_RESULTS)) def testConstructors_QUECTEL(self): # test Quectel constructors EXPECTED_PARSED = [ "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ] EXPECTED_BINARY = [ "$PQTMCFGPPS,R,0*6B\r\n", "$PQTMCFGPPS,OK,0,0,0,0,0,0*21\r\n", "$PQTMCFGUART,W,115200*18\r\n", "$PQTMCFGUART,W,1,115200*05\r\n", "$PQTMCFGUART,W,115200,8,0,1,0*11\r\n", "$PQTMCFGUART,W,1,115200,8,0,0,0*0D\r\n", "$PQTMCFGUART,R*36\r\n", "$PQTMCFGUART,R,1*2B\r\n", "$PQTMCFGMSGRATE,W,GLL,1*0C\r\n", "$PQTMCFGMSGRATE,W,GLL,1,2*12\r\n", "$PQTMCFGMSGRATE,R,GLL*14\r\n", "$PQTMCFGMSGRATE,R,PQTMEPE,2*05\r\n", "$PQTMCFGMSGRATE,W,1,1,0AB2,1,1*57\r\n", "$PQTMVERNO*58\r\n", "$PQTMVER,1,MODULE,LG290P03AANR01A03S,2024/04/30,10:53:07*32\r\n", "$PQTMHOT*4B\r\n", "$PQTMWARM*11\r\n", "$PQTMCOLD*1C\r\n", "$PQTMGNSSSTART*51\r\n", "$PQTMGNSSSTOP*09\r\n", "$PQTMDEBUGON*48\r\n", "$PQTMDEBUGOFF*06\r\n", "$PQTMUNIQID*16\r\n", "$PQTMRESETODO*09\r\n", "$PQTMRESTOREPAR*13\r\n", "$PQTMSAVEPAR*5A\r\n", "$PQTMCFGCNST,W,1,1,1,1,0,0*2B\r\n", "$PQTMCFGFIXRATE,W,1000*59\r\n", "$PQTMSRR*4B\r\n", "$PQTMCFGRSID,W,1024*06\r\n", "$PQTMCFGPROT,W,1,1,00000005,00000005*38\r\n", "$PQTMCFGNMEADP,W,3,8,3,2,3,2*39\r\n", "$PQTMCFGODO,W,1,10.5*4E\r\n", "$PQTMCFGPPS,W,1,1,100,1,1,0*73\r\n", "$PQTMCFGRCVRMODE,W,2*29\r\n", "$PQTMCFGRTCM,W,4,0,-90.0,07,06,1,0*3B\r\n", "$PQTMCFGRTK,W,1,1*6C\r\n", "$PQTMCFGSAT,W,1,1,FFFFFFFF*4B\r\n", "$PQTMCFGSAT,W,4,1,BFFCBFFF,1C003FFF*60\r\n", "$PQTMCFGSIGNAL,W,7,3,F,3F,7,1*0E\r\n", "$PQTMCFGSVIN,W,1,3600,1.2,-2519265.0514,4849534.9045,3277834.6432*01\r\n", "$PQTMCFGGEOFENCE,W,0,1,0,0,31.451248,117.451245,100.5*18\r\n", "$PQTMCFGGEOFENCE,W,0,1,0,3,31.451248,117.451245,100.5,12.36,85.24,118.72,56.45,140.13*1C\r\n", "$PQTMCFGPPS,R,1*6A\r\n", "$PQTMCFGPPS,W,1,0*73\r\n", "$PQTMCFGPROT,R,1,1*3D\r\n", "$PQTMCFGNMEADP,R*37\r\n", "$PQTMCFGMSGRATE,R,GGA*12\r\n", "$PQTMCFGGEOFENCE,W,0,0*27\r\n", "$PQTMCFGGEOFENCE,R,0*3E\r\n", "$PQTMCFGSVIN,R*26\r\n", "$PQTMCFGRCVRMODE,R*32\r\n", "$PQTMCFGFIXRATE,R*71\r\n", "$PQTMCFGRTK,R*69\r\n", "$PQTMCFGCNST,R*2E\r\n", "$PQTMCFGODO,R*60\r\n", "$PQTMCFGSIGNAL,R*3A\r\n", "$PQTMCFGSAT,R,1,1*62\r\n", "$PQTMCFGRSID,R*28\r\n", ] msgs = [ NMEAMessage("P", "QTMCFGPPS", POLL), NMEAMessage("P", "QTMCFGPPS", GET), NMEAMessage("P", "QTMCFGUART", SET, baudrate=115200), NMEAMessage("P", "QTMCFGUART", SET, portid=1, baudrate=115200), NMEAMessage("P", "QTMCFGUART", SET, baudrate=115200, databit=8, stopbit=1), NMEAMessage("P", "QTMCFGUART", SET, portid=1, baudrate=115200, databit=8), NMEAMessage("P", "QTMCFGUART", POLL), NMEAMessage("P", "QTMCFGUART", POLL, portid=1), NMEAMessage("P", "QTMCFGMSGRATE", SET, msgname="GLL", rate=1), NMEAMessage("P", "QTMCFGMSGRATE", SET, msgname="GLL", rate=1, msgver=2), NMEAMessage("P", "QTMCFGMSGRATE", POLL, msgname="GLL"), NMEAMessage("P", "QTMCFGMSGRATE", POLL, msgname="PQTMEPE", msgver=2), NMEAMessage( "P", "QTMCFGMSGRATE", SET, porttype=1, portid=1, msgname="0AB2", rate=1, msgver=1, ), NMEAMessage("P", "QTMVERNO", POLL), NMEAMessage( "P", "QTMVER", GET, msgver=1, vername="MODULE", verstr="LG290P03AANR01A03S", builddate="2024/04/30", buildtime="10:53:07", ), NMEAMessage("P", "QTMHOT", SET), NMEAMessage("P", "QTMWARM", SET), NMEAMessage("P", "QTMCOLD", SET), NMEAMessage("P", "QTMGNSSSTART", SET), NMEAMessage("P", "QTMGNSSSTOP", SET), NMEAMessage("P", "QTMDEBUGON", SET), NMEAMessage("P", "QTMDEBUGOFF", SET), NMEAMessage("P", "QTMUNIQID", POLL), NMEAMessage("P", "QTMRESETODO", SET), NMEAMessage("P", "QTMRESTOREPAR", SET), NMEAMessage("P", "QTMSAVEPAR", SET), NMEAMessage( "P", "QTMCFGCNST", SET, gps=1, glonass=1, galileo=1, beidou=1, qzss=0, navic=0, ), NMEAMessage("P", "QTMCFGFIXRATE", SET, fixinterval=1000), NMEAMessage("P", "QTMSRR", SET), NMEAMessage("P", "QTMCFGRSID", SET, rsid=1024), NMEAMessage( "P", "QTMCFGPROT", SET, porttype=1, portid=1, inputprot=hex2str(0x05, 8), # hex as padded string outputprot=hex2str(0x05, 8), # hex as padded string ), NMEAMessage( "P", "QTMCFGNMEADP", SET, utcdp=3, posdp=8, altdp=3, dopdp=2, spddp=3, cogdp=2, ), NMEAMessage( "P", "QTMCFGODO", SET, state=1, initdist=10.5, ), NMEAMessage( "P", "QTMCFGPPS", SET, ppsindex=1, enable=1, duration=100, ppsmode=1, polarity=1, ), NMEAMessage("P", "QTMCFGRCVRMODE", SET, rcvrmode=2), NMEAMessage( "P", "QTMCFGRTCM", SET, msmtype=4, msmmode=0, msmelevthd=-90.0, reserved1="07", reserved2="06", ephmode=1, ephinterval=0, ), NMEAMessage("P", "QTMCFGRTK", SET, diffmode=1, relmode=1), NMEAMessage( "P", "QTMCFGSAT", SET, systemid=1, signalid=1, masklow=hex2str(0xFFFFFFFF, 8), # hex as padded string ), NMEAMessage( "P", "QTMCFGSAT", SET, systemid=4, signalid=1, masklow=hex2str(0xBFFCBFFF, 8), # hex as padded string maskhigh=hex2str(0x1C003FFF, 8), # hex as padded string ), NMEAMessage( "P", "QTMCFGSIGNAL", SET, gpssig=hex2str(0x07), # hex as unpadded string glonasssig=hex2str(0x03), galileosig=hex2str(0x0F), beidousig=hex2str(0x3F), qzsssig=hex2str(0x07), navicsig=hex2str(0x01), ), NMEAMessage( "P", "QTMCFGSVIN", SET, svinmode=1, cfgcnt=3600, acclimit=1.2, ecefx=-2519265.0514, ecefy=4849534.9045, ecefz=3277834.6432, ), NMEAMessage( "P", "QTMCFGGEOFENCE", SET, geofenceindex=0, geofencemode=1, shape=0, lat0=31.451248, lon0=117.451245, radiuslat1=100.5, ), NMEAMessage( "P", "QTMCFGGEOFENCE", SET, geofenceindex=0, geofencemode=1, shape=3, lat0=31.451248, lon0=117.451245, radiuslat1=100.5, lon1=12.36, lat2=85.24, lon2=118.72, lat3=56.45, lon3=140.13, ), NMEAMessage( "P", "QTMCFGPPS", POLL, ppsindex=1, ), NMEAMessage( "P", "QTMCFGPPS", SET, ppsindex=1, enable=0, ), NMEAMessage( "P", "QTMCFGPROT", POLL, porttype=1, portid=1, ), NMEAMessage( "P", "QTMCFGNMEADP", POLL, ), NMEAMessage("P", "QTMCFGMSGRATE", POLL, msgname="GGA"), NMEAMessage( "P", "QTMCFGGEOFENCE", SET, geofenceindex=0, geofencemode=0, ), NMEAMessage( "P", "QTMCFGGEOFENCE", POLL, geofenceindex=0, ), NMEAMessage( "P", "QTMCFGSVIN", POLL, ), NMEAMessage("P", "QTMCFGRCVRMODE", POLL), NMEAMessage("P", "QTMCFGFIXRATE", POLL), NMEAMessage("P", "QTMCFGRTK", POLL), NMEAMessage("P", "QTMCFGCNST", POLL), NMEAMessage("P", "QTMCFGODO", POLL), NMEAMessage("P", "QTMCFGSIGNAL", POLL), NMEAMessage("P", "QTMCFGSAT", POLL, systemid=1, signalid=1), NMEAMessage("P", "QTMCFGRSID", POLL), ] for i, msg in enumerate(msgs): self.assertEqual(str(msg), EXPECTED_PARSED[i]) self.assertEqual(msg.serialize().decode(), EXPECTED_BINARY[i]) def testNMEAQUECTELAIRGET(self): # Test Quectel PAIR GET responses EXPECTED_RESULTS = ( "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ) i = 0 raw = 0 with open( os.path.join(DIRNAME, "pygpsdata_quectel_pair_get.log"), "rb" ) as stream: nmr = NMEAReader(stream) for raw, parsed in nmr: if raw is not None: # print(f'"{parsed}",') self.assertEqual(str(parsed), EXPECTED_RESULTS[i]) i += 1 self.assertEqual(i, len(EXPECTED_RESULTS)) def testNMEAQUECTELAIRSET(self): # Test Quectel PAIR SET commands EXPECTED_RESULTS = ( "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ) i = 0 raw = 0 with open( os.path.join(DIRNAME, "pygpsdata_quectel_pair_set.log"), "rb" ) as stream: nmr = NMEAReader(stream, msgmode=SET) for raw, parsed in nmr: if raw is not None: # print(f'"{parsed}",') self.assertEqual(str(parsed), EXPECTED_RESULTS[i]) i += 1 self.assertEqual(i, len(EXPECTED_RESULTS)) def testNMEAQUECTELAIRPOLL(self): # Test Quectel PAIR POLL commands EXPECTED_RESULTS = ( "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ) i = 0 raw = 0 with open( os.path.join(DIRNAME, "pygpsdata_quectel_pair_poll.log"), "rb" ) as stream: nmr = NMEAReader(stream, msgmode=POLL) for raw, parsed in nmr: if raw is not None: # print(f'"{parsed}",') self.assertEqual(str(parsed), EXPECTED_RESULTS[i]) i += 1 self.assertEqual(i, len(EXPECTED_RESULTS)) pynmeagps-1.1.7/tests/test_socket.py000066400000000000000000000204251523640354200175750ustar00rootroot00000000000000""" Socket reader tests for pynmeagps - uses dummy socket class to achieve 99% test coverage of SocketStream. Created on 11 May 2022 *** NB: must be saved in UTF-8 format *** :author: semuadmin (Steve Smith) """ import unittest from socket import socket from pynmeagps import ( NMEAReader, NMEAMessage, POLL, ENCODE_CHUNKED, ENCODE_NONE, DEFAULT_BUFSIZE, SocketWrapper, ) def chunk(data: bytes, chunksize: int = 10) -> bytes: """ Test method to chunk encode a byte stream """ ckb = f"{chunksize:x}\r\n".encode("ascii") output = b"" eod = False pos = 0 while not eod: if len(data[pos : pos + chunksize]) < chunksize: eod = True output += ckb + data[pos : pos + chunksize] + b"\r\n" pos += chunksize output += b"0\r\n\r\n" return output SOCKETPOOL = ( b"$GNDTM,W84,,0.0,N,0.0,E,0.0,W84*71\r\n" b"$GNRMC,103607.00,A,5327.03942,N,10214.42462,W,0.046,,060321,,,A,V*0E\r\n" b"$GPRTE,2,1,c,0,PBRCPK,PBRTO,PTELGR,PPLAND,PYAMBU,PPFAIR,PWARRN,PMORTL,PLISMR*73\r\n" ) * 10 SOCKETPOOL_CHUNKED = chunk(SOCKETPOOL) class DummySocket(socket): """ Dummy socket class which simulates recv() method and TimeoutError. """ def __init__(self, pool: bytes, *args, **kwargs): self._timeout = kwargs.pop("timeout", False) super().__init__(*args, **kwargs) self._stream = pool * round(DEFAULT_BUFSIZE / len(pool)) self._buffer = self._stream def recv(self, num: int) -> bytes: if self._timeout: raise TimeoutError if len(self._buffer) < num: self._buffer = self._buffer + self._stream buff = self._buffer[:num] self._buffer = self._buffer[num:] return buff def send(self, data: bytes): if self._timeout: raise TimeoutError return None class SocketTest(unittest.TestCase): def setUp(self): self.maxDiff = None def tearDown(self): pass # ******************************************* # Helper methods # ******************************************* def testSocketWrapper(self): # test SocketWrapper getters stream = DummySocket(SOCKETPOOL) sw = SocketWrapper(stream) self.assertIsInstance(sw.buffer, bytearray) self.assertEqual(sw.in_waiting(), 4096) def testSocketStub(self): EXPECTED_RESULTS = ( "", "", "", "", "", "", "", "", "", "", "", "", ) raw = None stream = DummySocket(SOCKETPOOL) nmr = NMEAReader(stream, encoding=ENCODE_NONE, bufsize=1024) buff = nmr._stream.buffer # test buffer getter method i = 0 for raw, parsed in nmr: if raw is not None: # print(f'"{parsed}",') self.assertEqual(str(parsed), EXPECTED_RESULTS[i]) i += 1 if i >= 12: break self.assertEqual(i, 12) def testSocketChunked(self): EXPECTED_RESULTS = ( "", "", "", "", "", "", "", "", "", "", "", "", ) raw = None stream = DummySocket(SOCKETPOOL_CHUNKED) nmr = NMEAReader(stream, encoding=ENCODE_CHUNKED, bufsize=1024) buff = nmr._stream.buffer # test buffer getter method i = 0 for raw, parsed in nmr: if raw is not None: # print(f'"{parsed}",') self.assertEqual(str(parsed), EXPECTED_RESULTS[i]) i += 1 if i >= 12: break self.assertEqual(i, 12) def testSocketSend(self): stream = DummySocket(SOCKETPOOL) nmr = NMEAReader(stream, bufsize=1024) msg = NMEAMessage("EI", "GNQ", POLL, msgId="RMC") res = nmr.datastream.write(msg.serialize()) self.assertEqual(res, None) def testSocketIter(self): # test for extended stream raw = None stream = DummySocket(SOCKETPOOL) nmr = NMEAReader(stream) i = 0 for raw, parsed in nmr: if raw is None: raise EOFError i += 1 if i >= 123: break self.assertEqual(i, 123) def testSocketError(self): # test for simulated socket timeout raw = None stream = DummySocket(SOCKETPOOL, timeout=True) nmr = NMEAReader(stream) i = 0 for raw, parsed in nmr: i += 1 if i >= 12: break self.assertEqual(i, 0) if __name__ == "__main__": # import sys;sys.argv = ['', 'Test.testName'] unittest.main() pynmeagps-1.1.7/tests/test_static.py000066400000000000000000000636541523640354200176070ustar00rootroot00000000000000""" Helper, property, static and magic method tests for pynmeagps Created on 3 Oct 2020 *** NB: must be saved in UTF-8 format *** :author: semuadmin (Steve Smith) """ from datetime import datetime, timezone, date, time import os import unittest from pynmeagps import ( NMEAMessage, NMEAMessageError, NMEAReader, NMEATypeError, NMEA_MSGIDS, NMEA_MSGIDS_PROP, NMEA_PAYLOADS_GET, NMEA_PAYLOADS_POLL, NMEA_PAYLOADS_SET, EPOCH0_GPS, EPOCH0_BEIDOU, BDS, GPS, GAL, IRN, ) from pynmeagps.nmeahelpers import ( area, bearing, calc_checksum, date2str, date2utc, ddd2dmm, deg2dmm, deg2dms, dmm2ddd, dms2deg, ecef2llh, hex2str, generate_checksum, get_parts, groupsize, haversine, knots2spd, latlon2dmm, latlon2dms, llh2ecef, llh2iso6709, msgdesc, planar, time2str, time2utc, leapsecond, utc2wnotow, wnotow2utc, ) from pynmeagps.nmeatypes_core import GET, POLL from pynmeagps.nmeatypes_decodes import ( GNSSLIST, FIXTYPE_GGA, FMI_STATUS, SIGNALID, SYSTEMID, ) class StaticTest(unittest.TestCase): def setUp(self): self.maxDiff = None dirname = os.path.dirname(__file__) self.messageCRAP = "$GNRMC,,%$£" self.messageBLANK = "$GNRMC,,A,,N,,W,0.046,,,,,A,V*0F" self.messageGLL = "$GNGLL,5327.04319,S,00214.41396,E,223232.00,A,A*68\r\n" self.messagePUBX = "$PUBX,00,103607.00,5327.03942,N,00214.42462,W,104.461,G3,29,31,0.085,39.63,-0.007,,5.88,7.62,8.09,6,0,0*69\r\n" self.messageBADCK = "$GNGLL,5327.04319,S,00214.41396,E,223232.00,A,A*22\r\n" self.msgGLL = NMEAReader.parse(self.messageGLL) self.msgPUBX00 = NMEAReader.parse(self.messagePUBX) self.msgGNQ = NMEAMessage("EI", "GNQ", POLL, msgId="RMC") self.streamNMEA2 = open(os.path.join(dirname, "pygpsdata-nmea2.log"), "rb") def tearDown(self): self.streamNMEA2.close() # ******************************************* # Helper methods # ******************************************* def testMissingDefs(self): # sanity check for missing payload definitions for msg in NMEA_MSGIDS: self.assertTrue( msg in NMEA_PAYLOADS_GET.keys() or msg in NMEA_PAYLOADS_POLL or msg in NMEA_PAYLOADS_SET ) def testMissingCodes(self): # sanity check for missing sentence IDs for msg in NMEA_PAYLOADS_GET: self.assertTrue(msg in NMEA_MSGIDS.keys() or msg in NMEA_MSGIDS_PROP.keys()) def testGetParts(self): res = get_parts(self.messageGLL) self.assertEqual( res, ( "GNGLL,5327.04319,S,00214.41396,E,223232.00,A,A", "GN", "GLL", ["5327.04319", "S", "00214.41396", "E", "223232.00", "A", "A"], "68", ), ) def testGenChecksum(self): # test generation of new checksum res = generate_checksum( "GN", "GLL", ["5327.04319", "S", "00214.41396", "E", "223232.00", "A", "A"] ) self.assertEqual(res, "68") res = generate_checksum( "GN", "GLL", ["1234.04319", "N", "00056.41396", "E", "123232.00", "A", "A"] ) self.assertEqual(res, "75") def testGetPartsCRAP(self): # test badly formed NMEA message EXPECTED_ERROR = "Badly formed message $GNRMC,,%$£" with self.assertRaises(NMEAMessageError) as context: get_parts(self.messageCRAP) self.assertTrue(EXPECTED_ERROR in str(context.exception)) def testCalcChecksum(self): content, _, _, _, _ = get_parts(self.messageGLL) res = calc_checksum(content) self.assertEqual(res, "68") content, _, _, _, _ = get_parts(self.messagePUBX) res = calc_checksum(content) self.assertEqual(res, "69") def testDMM2DDD(self): res = dmm2ddd("5314.12345") self.assertEqual(res, 53.2353908333) res = dmm2ddd("02348.3822990") self.assertEqual(res, 23.80637165) res = dmm2ddd("00234.3822990") self.assertEqual(res, 2.5730383167) res = dmm2ddd("00214.12345") self.assertEqual(res, 2.2353908333) res = dmm2ddd("12825.12344") self.assertEqual(res, 128.418724) res = dmm2ddd("1282512344") self.assertEqual(res, "") res = dmm2ddd("2345.x5678") self.assertEqual(res, "") def testDDD2DMM(self): res = ddd2dmm(3.75000, "LA") self.assertEqual(res, "0345.00000") res = ddd2dmm(53.75000, "LA") self.assertEqual(res, "5345.00000") res = ddd2dmm(-2.75000, "LN") self.assertEqual(res, "00245.00000") res = ddd2dmm(128.418724, "LN") self.assertEqual(res, "12825.12344") res = ddd2dmm("", "LN") self.assertEqual(res, "") def testDDD2DMM_HPMode(self): # high precision mode res = ddd2dmm(3.123456789, "LA", True) self.assertEqual(res, "0307.4074073") res = ddd2dmm(53.123456789, "LA", True) self.assertEqual(res, "5307.4074073") res = ddd2dmm(-2.123456789, "LN", True) self.assertEqual(res, "00207.4074073") res = ddd2dmm(128.123456789, "LN", True) self.assertEqual(res, "12807.4074073") res = ddd2dmm("", "LN", True) self.assertEqual(res, "") def testDMS2DEG(self): res = dms2deg("51°20′45.6″N") self.assertEqual(res, 51.346) self.assertEqual(deg2dms(res, "LA"), "51°20′45.6″N") res = dms2deg("51°30′0.0″N") self.assertEqual(res, 51.5) self.assertEqual(deg2dms(res, "LA"), "51°30′0.0″N") res = dms2deg("51°30.00′") self.assertEqual(res, 51.5) self.assertEqual(deg2dms(res, "LA"), "51°30′0.0″N") res = dms2deg("2°20′45.6″W") self.assertEqual(res, -2.346) self.assertEqual(deg2dms(res, "LN"), "2°20′45.6″W") res = dms2deg("33° 45′ 0.0″S") self.assertEqual(res, -33.75) self.assertEqual(deg2dms(res, "LA"), "33°45′0.0″S") res = dms2deg("33°45′N") self.assertEqual(res, 33.75) self.assertEqual(deg2dms(res, "LN"), "33°45′0.0″E") res = dms2deg("33°45.3564′N") self.assertEqual(res, 33.75594) self.assertEqual(deg2dms(res, "LA"), "33°45′21.384″N") res = dms2deg("28°S") self.assertEqual(res, -28.0) self.assertEqual(deg2dms(res, "LA"), "28°0′0.0″S") res = dms2deg("45.1234°") self.assertEqual(res, 45.1234) with self.assertRaises(ValueError) as context: res = dms2deg("51°°20′45.6″") with self.assertRaises(ValueError) as context: res = dms2deg("51°20′45.6″Z") with self.assertRaises(ValueError) as context: res = dms2deg("51°20′45.6″34.25`N") with self.assertRaises(ValueError) as context: res = dms2deg("xx°yy′zz.6″N") def testDate2UTC(self): res = date2utc("") self.assertEqual(res, "") res = date2utc("120320") self.assertEqual(res, date(2020, 3, 12)) res = date2utc("031220", "DM") self.assertEqual(res, date(2020, 3, 12)) res = date2utc("12032020", "DTL") self.assertEqual(res, date(2020, 3, 12)) def testTime2UTC(self): res = time2utc("") self.assertEqual(res, "") res = time2utc("081123.000") self.assertEqual(res, time(8, 11, 23)) def testTime2str(self): res = time2str(time(8, 11, 23)) self.assertEqual(res, "081123.00") res = time2str("wsdfasdf") self.assertEqual(res, "") def testDate2str(self): res = date2str(date(2021, 3, 7)) self.assertEqual(res, "070321") res = date2str(date(2021, 3, 7), "DTL") self.assertEqual(res, "07032021") res = date2str(date(2021, 3, 7), "DM") self.assertEqual(res, "030721") res = date2str("wsdfasdf") self.assertEqual(res, "") def testKnots2spd(self): res = knots2spd(1.0, "MS") self.assertAlmostEqual(res, 0.5144447324, 5) res = knots2spd(1.0, "FS") self.assertAlmostEqual(res, 1.68781084, 5) res = knots2spd(1.0, "mph") self.assertAlmostEqual(res, 1.15078, 5) res = knots2spd(1.0, "kmph") self.assertAlmostEqual(res, 1.852001, 5) def testKnots2spdBAD(self): EXPECTED_ERROR = ( "Invalid conversion unit CRAP - must be in ['MS', 'FS', 'MPH', 'KMPH']." ) with self.assertRaises(KeyError) as context: knots2spd(1.0, "CRAP") self.assertTrue(EXPECTED_ERROR in str(context.exception)) EXPECTED_ERROR = "Invalid knots value CRAP - must be float or integer." with self.assertRaises(TypeError) as context: knots2spd("CRAP", "MS") self.assertTrue(EXPECTED_ERROR in str(context.exception)) def testMsgDesc(self): res = msgdesc("GGA") self.assertEqual(res, "Global positioning system fix data") res = msgdesc("UBX03") self.assertEqual(res, "Satellite Status") res = msgdesc("XXX") self.assertEqual(res, "Unknown msgID XXX") # ******************************************* # NMEAMessage property methods # ******************************************* def testIdentity(self): res = self.msgGLL.identity self.assertEqual(res, "GNGLL") def testTalkerS(self): res = self.msgGLL.talker self.assertEqual(res, "GN") def testTalkerP(self): res = self.msgPUBX00.talker self.assertEqual(res, "P") def testMsgIDS(self): res = self.msgGLL.msgID self.assertEqual(res, "GLL") def testMsgIDP(self): res = self.msgPUBX00.msgID self.assertEqual(res, "UBX") def testMsgmode0(self): res = self.msgGLL.msgmode self.assertEqual(res, GET) def testMsgmode2(self): res = self.msgGNQ.msgmode self.assertEqual(res, POLL) def testdatastream(self): # test datastream getter EXPECTED_RESULT = "" res = str(type(NMEAReader(self.streamNMEA2).datastream)) self.assertEqual(res, EXPECTED_RESULT) def testPayloadS(self): res = self.msgGLL.payload self.assertEqual( res, ["5327.04319", "S", "00214.41396", "E", "223232.00", "A", "A"] ) def testPayloadP(self): res = self.msgPUBX00.payload self.assertEqual( res, [ "00", "103607.00", "5327.03942", "N", "00214.42462", "W", "104.461", "G3", "29", "31", "0.085", "39.63", "-0.007", "", "5.88", "7.62", "8.09", "6", "0", "0", ], ) def testChecksumS(self): res = self.msgGLL.checksum self.assertEqual(res, "68") def testChecksumP(self): res = self.msgPUBX00.checksum self.assertEqual(res, "69") # ******************************************* # NMEAMessage static methods # ******************************************* def testSerializeS(self): res = self.msgGLL.serialize() self.assertEqual(res, b"$GNGLL,5327.04319,S,00214.41396,E,223232.00,A,A*68\r\n") def testSerializeP(self): res = self.msgPUBX00.serialize() self.assertEqual( res, b"$PUBX,00,103607.00,5327.03942,N,00214.42462,W,104.461,G3,29,31,0.085,39.63,-0.007,,5.88,7.62,8.09,6,0,0*69\r\n", ) def testStrS( self, ): # double check that parsing of serialized message reproduces original message res1 = self.msgGLL res2 = NMEAReader.parse(self.msgGLL.serialize()) self.assertEqual(str(res1), str(res2)) def testStrP(self): res1 = self.msgPUBX00 res2 = NMEAReader.parse(self.msgPUBX00.serialize()) self.assertEqual(str(res1), str(res2)) def testNomVal(self): for att in ("CH", "ST", "LA", "LN"): res = NMEAMessage.nomval(att) self.assertEqual(res, "") res = NMEAMessage.nomval("HX") self.assertEqual(res, "0") res = NMEAMessage.nomval("IN") self.assertEqual(res, 0) res = NMEAMessage.nomval("DE") self.assertEqual(res, 0.0) res = NMEAMessage.nomval("TM") self.assertIsInstance(res, time) res = NMEAMessage.nomval("DT") self.assertIsInstance(res, date) def testNomValBAD(self): EXPECTED_ERROR = "Unknown attribute type XX." with self.assertRaises(NMEATypeError) as context: NMEAMessage.nomval("XX") self.assertTrue(EXPECTED_ERROR in str(context.exception)) def testVal2Str(self): for att in ("CH", "ST"): res = NMEAMessage.val2str("AB", att) self.assertEqual(res, "AB") res = NMEAMessage.val2str("B", "HX") self.assertEqual(res, "B") res = NMEAMessage.val2str(23, "IN") self.assertEqual(res, "23") res = NMEAMessage.val2str(15.286, "DE") self.assertEqual(res, "15.286") res = NMEAMessage.val2str(55.5, "LA") self.assertEqual(res, "5530.00000") res = NMEAMessage.val2str(2.75, "LN") self.assertEqual(res, "00245.00000") res = NMEAMessage.val2str(datetime(2021, 5, 7, 2, 45, 23), "TM") self.assertEqual(res, "024523.00") res = NMEAMessage.val2str(datetime(2020, 6, 7, 3, 27, 24), "DT") self.assertEqual(res, "070620") res = NMEAMessage.val2str(datetime(2020, 6, 7, 3, 27, 24), "DTL") self.assertEqual(res, "07062020") res = NMEAMessage.val2str("2020-06-07", "DTL") self.assertEqual(res, "07062020") res = NMEAMessage.val2str("20210708", "DTL") self.assertEqual(res, "08072021") res = NMEAMessage.val2str(datetime(2020, 6, 7, 3, 27, 24), "DM") self.assertEqual(res, "060720") res = NMEAMessage.val2str("2020-06-07", "DM") self.assertEqual(res, "060720") def testVal2StrBAD(self): EXPECTED_ERROR = "Unknown attribute type XX." with self.assertRaises(NMEATypeError) as context: NMEAMessage.val2str(23.45, "XX") self.assertTrue(EXPECTED_ERROR in str(context.exception)) # ******************************************* # NMEAMessage magic methods # ******************************************* def testReprS(self): res = repr(self.msgGLL) self.assertEqual( res, "NMEAMessage('GN','GLL', 0, payload=['5327.04319', 'S', '00214.41396', 'E', '223232.00', 'A', 'A'])", ) def testReprP(self): res = repr(self.msgPUBX00) self.assertEqual( res, "NMEAMessage('P','UBX', 0, payload=['00', '103607.00', '5327.03942', 'N', '00214.42462', 'W', '104.461', 'G3', '29', '31', '0.085', '39.63', '-0.007', '', '5.88', '7.62', '8.09', '6', '0', '0'])", ) def testEvalReprS( self, ): # double check that evaluation of repr(message) reproduces original message res1 = self.msgGLL res2 = eval(repr(self.msgGLL)) self.assertEqual(str(res1), str(res2)) def testEvalReprP(self): res1 = self.msgPUBX00 res2 = eval(repr(self.msgPUBX00)) self.assertEqual(str(res1), str(res2)) # ******************************************* # NMEAMessage helpers # ******************************************* def testdeg2dms(self): res = deg2dms(53.346, "LA") self.assertEqual(res, ("53°20′45.6″N")) res = deg2dms("xxx", "LA") self.assertEqual(res, "") def testdeg2dmm(self): res = deg2dmm(-2.5463, "LN") self.assertEqual(res, ("2°32.778′W")) res = deg2dmm("xxx", "LN") self.assertEqual(res, "") def testlatlon2dms(self): res = latlon2dms(53.346, -2.5463) self.assertEqual(res, ("53°20′45.6″N", "2°32′46.68″W")) def testlatlon2dmm(self): res = latlon2dmm(53.346, -2.5463) self.assertEqual(res, ("53°20.76′N", "2°32.778′W")) def testlatlon2dmm(self): res = latlon2dmm(53.346, -2.5463) self.assertEqual(res, ("53°20.76′N", "2°32.778′W")) def testllh2iso6709(self): res = llh2iso6709(53.12, -2.165, 35) self.assertEqual(res, "+53.12-2.165+35CRSWGS_84/") res = llh2iso6709(-53.12, +2.165, 68.45) self.assertEqual(res, "-53.12+2.165+68.45CRSWGS_84/") def testecef2llh(self): vals = [ (3822566.3113, -144427.5123, 5086857.1208), (3980570.0700029507, 0.0, 4966833.391498124), (10000, 10000, 10000), ] res = [ (53.24168283407136, -2.1637695489854565, 214.9785466667861), (51.4779280000001, 0, 5.8584775974986524e-09), (0, 0, -1.0e7), ] for i, val in enumerate(vals): lat, lon, alt = ecef2llh(val[0], val[1], val[2]) self.assertAlmostEqual(lat, res[i][0], 5) self.assertAlmostEqual(lon, res[i][1], 5) self.assertAlmostEqual(alt, res[i][2], 5) def testllh2ecef(self): vals = [ (53.24168283407126, -2.1637695489854565, 214.97854665775156), (51.477928, 0, 0), ] res = [ (3822566.311300003, -144427.51230000015, 5086857.120799987), (3980570.0700029545, 0.0, 4966833.3914981127), ] for i, val in enumerate(vals): x, y, z = llh2ecef(val[0], val[1], val[2]) self.assertAlmostEqual(x, res[i][0], 5) self.assertAlmostEqual(y, res[i][1], 5) self.assertAlmostEqual(z, res[i][2], 5) def testllh2eceftab(self): # test conversion there and back vals = [ (53.24, -2.16, 214.98), (-7.48, 67.87, 43.12), (-34.51, -56.09, 1745.98), (90, 90, -435184.65), (0, 0, 0), ] for i, val in enumerate(vals): x, y, z = llh2ecef(val[0], val[1], val[2]) lat, lon, alt = ecef2llh(x, y, z) self.assertAlmostEqual(lat, val[0], 2) self.assertAlmostEqual(lon, val[1], 2) self.assertAlmostEqual(alt, val[2], 2) def testhaversine(self): res = haversine(51.23, -2.41, 34.205, 56.34) self.assertAlmostEqual(res, 5010.722, 3) res = haversine(-12.645, 34.867, 145.1745, -56.27846) self.assertAlmostEqual(res, 10715.371, 3) res = haversine(53.45, -2.14, 53.451, -2.141) self.assertAlmostEqual(res, 0.1296, 3) def testplanar(self): # test planar res = planar(53, 2, 53.000001, 2.000001) # print(res) self.assertAlmostEqual(res, 0.12992378701316823, 7) res = planar(53, 2, 53.00001, 2.00001) # print(res) self.assertAlmostEqual(res, 1.299237873027932, 7) res = planar(53, 2, 53.0001, 2.0001) # print(res) self.assertAlmostEqual(res, 12.992378723687828, 7) def testbearing(self): res = bearing(51.23, -2.41, 53.205, -2.34) self.assertAlmostEqual(res, 1.216362703824359, 4) res = bearing(51.23145, -2.41, 51.23145, -2.34) self.assertAlmostEqual(res, 89.9727111358776, 4) res = bearing(51.23, -2.41, 34.205, 56.34) self.assertAlmostEqual(res, 88.58134073451902, 4) res = bearing(-12.645, 34.867, -34.1745, 48.27846) self.assertAlmostEqual(res, 152.70835788275326, 4) def testarea(self): res = area(51.23, -2.41, 53.205, -2.34) self.assertAlmostEqual(res, 1049.5657, 4) res = area(53.48280729, -2.24225376, 53.46814647, -2.20192543) self.assertAlmostEqual(res, 4.3606, 4) res = area(53.69865772, -2.68269539, 53.22939103, -1.39218885) self.assertAlmostEqual(res, 4467.6282, 4) res = area(-12.645, 34.867, -34.1745, 48.27846) self.assertAlmostEqual(res, 3264291.8230, 4) def testhex2str(self): hex = 0x1234ABCD self.assertEqual(hex2str(hex, 8), "1234ABCD") hex = 0x123B self.assertEqual(hex2str(hex, 8), "0000123B") self.assertEqual(hex2str(hex, 6), "00123B") self.assertEqual(hex2str(hex), "123B") def testdecodes(self): self.assertEqual(GNSSLIST[0], "GPS") self.assertEqual(FIXTYPE_GGA[1], "2D") self.assertEqual(FMI_STATUS[2], ("Ready", "Filter convergence completion flag")) self.assertEqual(SIGNALID[("1", "5")], "GPS L2 CM") self.assertEqual(SYSTEMID["4"], "Beidou") def testmaxidx(self): PYLD1 = {"svid_01": 7, "svid_02": 8, "elv_03": 15, "svid_04": 23} self.assertEqual(groupsize(**PYLD1), 4) PYLD2 = {"svid_01": 7, "svid_02": 8, "cno_03": 15} self.assertEqual(groupsize(**PYLD2), 3) PYLD3 = {"svid": 7, "elv": 8, "az": 15} self.assertEqual(groupsize(**PYLD3), 0) PYLD4 = {} self.assertEqual(groupsize(**PYLD4), 0) # def testutc2wnotow(self): # dat = datetime(2026, 2, 20, 23, 21, 36, 123000, tzinfo=timezone.utc) # wno, tow, ls = utc2wnotow(dat) # # print(wno, tow, ls) # self.assertEqual((wno, tow), (358, 516114123)) # dat = datetime(2026, 2, 20, 23, 21, 36, 123000) # wno, tow, ls = utc2wnotow(dat) # # print(wno, tow, ls) # self.assertEqual((wno, tow), (2406, 516114123)) # wno, tow, ls = utc2wnotow() # # print(wno, tow, ls) # self.assertIsInstance(wno, int) # self.assertIsInstance(tow, int) # self.assertIsInstance(ls, int) def testwnotow2utcGPS(self): res = wnotow2utc(2406, 516114123, None, GPS, False, True) # print(res) self.assertEqual( res, datetime(1986, 11, 21, 23, 21, 50, 123000, tzinfo=timezone.utc) ) res = wnotow2utc(2406, 516114123, None, GPS, False, False) # print(res) self.assertEqual( res, datetime(2026, 2, 20, 23, 21, 36, 123000, tzinfo=timezone.utc) ) utc = wnotow2utc(2406, 516114000) self.assertEqual( (utc.year, utc.month, utc.day, utc.hour, utc.minute, utc.second), (1986, 11, 21, 23, 21, 50), ) res = wnotow2utc(366, 381600000, None, GPS, False) # print(res) self.assertEqual(str(res), "1987-01-15 09:59:56+00:00") res = wnotow2utc(1390, 381600000, None, GPS, False) # print(res) self.assertEqual(str(res), "1987-01-15 09:59:56+00:00") def testwnotow2utcBDS(self): res = wnotow2utc(1390, 381600000, None, BDS, False) # print(res) self.assertEqual(str(res), "2032-08-26 09:59:56+00:00") def testwnotow2utcGAL(self): res = wnotow2utc(1390, 381600000, None, GAL, False) # print(res) self.assertEqual(str(res), "2026-04-16 09:59:42+00:00") res = wnotow2utc(1390, 381600000, None, GAL, False, False) # print(res) self.assertEqual(str(res), "2026-04-16 09:59:42+00:00") def testwnotow2utcIRN(self): res = wnotow2utc(1390, 381600000, None, IRN, False) # print(res) self.assertEqual(str(res), "2006-08-31 09:59:46+00:00") res = wnotow2utc(1390, 381600000, None, IRN, False, False) # print(res) self.assertEqual(str(res), "2026-04-16 09:59:42+00:00") def testleapsecondGPS(self): self.assertEqual(leapsecond(EPOCH0_GPS, "G"), 0) self.assertEqual(leapsecond(datetime(1985, 1, 1, 0, 0, 0)), 3) self.assertEqual(leapsecond(datetime(1997, 8, 1, 0, 0, 0)), 12) self.assertEqual(leapsecond(datetime(2025, 9, 18, 16, 51, 34)), 18) self.assertEqual( leapsecond(datetime(2025, 9, 18, 16, 51, 34, tzinfo=timezone.utc)), 18 ) self.assertEqual(leapsecond(datetime(1974, 9, 18, 16, 51, 34)), -6) self.assertEqual(leapsecond(datetime(1971, 9, 18, 16, 51, 34)), 0) def testleapsecondBDS(self): self.assertEqual(leapsecond(EPOCH0_BEIDOU, "C"), 0) self.assertEqual(leapsecond(datetime(2025, 1, 1, 16, 51, 34), BDS), 4) def testleapsecondGAL(self): self.assertEqual(leapsecond(datetime(2025, 1, 1, 16, 51, 34), GAL), 18) def testleapsecondIRN(self): self.assertEqual(leapsecond(datetime(2025, 1, 1, 16, 51, 34), IRN), 18) def testtimeconv(self): for gnss in ("G", "E", "C", "I"): for wno, tow in ( (203, 162864000), (1023, 472651000), (366, 111222000), (986, 1945000), (666, 162864000), (888, 472651000), (420, 111222000), (54, 1945000), ): utc = wnotow2utc(wno, tow, None, gnss) wno2, tow2, ls = utc2wnotow(utc, gnss) self.assertEqual((wno, tow), (wno2, tow2)) def testutc2wnotow(self): wno, tow, ls = utc2wnotow() # print(wno, tow, ls) wno, tow, ls = utc2wnotow(datetime(2026, 4, 1, 2, 3, 4)) # print(wno, tow, ls) self.assertEqual((wno, tow, ls), (364, 266602000, 18)) if __name__ == "__main__": # import sys;sys.argv = ['', 'Test.testName'] unittest.main() pynmeagps-1.1.7/tests/test_stream.py000066400000000000000000002050151523640354200176000ustar00rootroot00000000000000""" Stream method tests for pynmeagps Created on 4 Mar 2021 *** NB: must be saved in UTF-8 format *** :author: semuadmin (Steve Smith) """ import os import sys import unittest from logging import ERROR from pynmeagps import ( NMEAReader, NMEAParseError, NMEATypeError, VALCKSUM, ERR_RAISE, ERR_IGNORE, ERR_LOG, GET, SET, TM, LA, LAD, LN, LND, ST, FMI_STATUS, ) DIRNAME = os.path.dirname(__file__) class StreamTest(unittest.TestCase): def setUp(self): self.maxDiff = None def tearDown(self): pass def catchio(self): """ Capture stdout as string. """ self._saved_stdout = sys.stdout self._strout = os.StringIO() sys.stdout = self._strout def restoreio(self) -> str: """ Return captured output and restore stdout. """ sys.stdout = self._saved_stdout return self._strout.getvalue().strip() def testNMEASTARTUP(self): # stream of NMEA device during start up (blank data) EXPECTED_RESULTS = ( "", "", "", "", "", "", "", "", "", "", "", "", ) i = 0 raw = 0 with open(os.path.join(DIRNAME, "pygpsdata-nmeastartup.log"), "rb") as stream: nmr = NMEAReader(stream, nmeaonly=False, validate=1) while raw is not None: (raw, parsed) = nmr.read() if raw is not None: self.assertEqual(str(parsed), EXPECTED_RESULTS[i]) i += 1 self.assertEqual( i, 12 ) # if this fails, may be because log file terminators = LF rather than CRLF def testNMEA4( self, ): # stream of NMEA v4.10 device (u-blox M9N) (NB everything after PUBX is synthetic) EXPECTED_RESULTS = ( "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ) i = 0 raw = 0 with open(os.path.join(DIRNAME, "pygpsdata-nmea4.log"), "rb") as stream: nmr = NMEAReader(stream, nmeaonly=False, validate=3) while raw is not None: (raw, parsed) = nmr.read() if raw is not None: # print(f'"{parsed}",') self.assertEqual(str(parsed), EXPECTED_RESULTS[i]) i += 1 self.assertEqual(i, len(EXPECTED_RESULTS)) def testNMEA2(self): # stream of NMEA v2.30 device (u-blox M6N) EXPECTED_RESULTS = ( "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ) i = 0 raw = 0 with open(os.path.join(DIRNAME, "pygpsdata-nmea2.log"), "rb") as stream: nmr = NMEAReader(stream, nmeaonly=False) while raw is not None: (raw, parsed) = nmr.read() if raw is not None: self.assertEqual(str(parsed), EXPECTED_RESULTS[i]) i += 1 self.assertEqual(i, 17) def testMIXED( self, ): # stream of mixed NMEA & UBX data with nmea_only set to FALSE - should be ignored EXPECTED_RESULTS = ( "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ) i = 0 raw = 0 with open(os.path.join(DIRNAME, "pygpsdata-mixed.log"), "rb") as stream: nmr = NMEAReader(stream, nmeaonly=False) while raw is not None: (raw, parsed) = nmr.read() if raw is not None: self.assertEqual(str(parsed), EXPECTED_RESULTS[i]) i += 1 self.assertEqual(i, 15) def testMIXED2( self, ): # stream of mixed NMEA & UBX data with nmea_only set to TRUE - should be rejected EXPECTED_ERROR = "Unknown protocol header b'$\\x11'" with open(os.path.join(DIRNAME, "pygpsdata-mixed.log"), "rb") as stream: with self.assertRaises(NMEAParseError) as context: i = 0 raw = 0 nmr = NMEAReader(stream, nmeaonly=True, quitonerror=ERR_RAISE) while raw is not None: (raw, _) = nmr.read() if raw is not None: i += 1 self.assertTrue(EXPECTED_ERROR in str(context.exception)) def testNMEAMARITIME(self): # test NMEA maritime messages EXPECTED_RESULTS = ( "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ) i = 0 raw = 0 with open(os.path.join(DIRNAME, "pygpsdata-maritime.log"), "rb") as stream: nmr = NMEAReader(stream, nmeaonly=True, quitonerror=2) for raw, parsed in nmr: if raw is not None: # print(f'"{parsed}",') self.assertEqual(str(parsed), EXPECTED_RESULTS[i]) i += 1 self.assertEqual(i, len(EXPECTED_RESULTS)) def testNMEAITER(self): # NMEAReader iterator EXPECTED_RESULTS = ( "", "", "", "", "", "", "", "", ) i = 0 raw = 0 with open(os.path.join(DIRNAME, "pygpsdata-nmea4sm.log"), "rb") as stream: nmr = NMEAReader(stream, nmeaonly=False) for raw, parsed in nmr: if raw is not None: self.assertEqual(str(parsed), EXPECTED_RESULTS[i]) i += 1 self.assertEqual(i, 8) def testNMEAITERATE(self): # NMEAReader helper method EXPECTED_RESULTS = ( "", "", "", "", "", "", "", "", ) i = 0 raw = 0 with open(os.path.join(DIRNAME, "pygpsdata-nmea4sm.log"), "rb") as stream: nmr = NMEAReader(stream, nmeaonly=False) for raw, parsed in nmr: if raw is not None: self.assertEqual(str(parsed), EXPECTED_RESULTS[i]) i += 1 self.assertEqual(i, 8) def testNMEAITERATE_ERR1( self, ): # NMEAReader iterator with bad checksum EXPECTED_ERROR = "Message GNVTG invalid checksum 3) - should be 30" with open(os.path.join(DIRNAME, "pygpsdata-nmeabadck2.log"), "rb") as stream: with self.assertRaises(NMEAParseError) as context: nmr = NMEAReader( stream, nmeaonly=False, validate=VALCKSUM, msgmode=0, quitonerror=ERR_RAISE, ) for raw, parsed in nmr: pass self.assertTrue(EXPECTED_ERROR in str(context.exception)) def testNMEAITERATE_ERR2( self, ): # NMEAReader iterator ignoring bad checksum and passing error handler EXPECTED_RESULT = "" with open(os.path.join(DIRNAME, "pygpsdata-nmeabadck2.log"), "rb") as stream: nmr = NMEAReader( stream, nmeaonly=False, validate=VALCKSUM, msgmode=0, quitonerror=ERR_LOG, errorhandler=lambda e: print(f"I ignored the following error: {e}"), ) res = "" for raw, parsed in nmr: res = str(parsed) self.assertEqual(EXPECTED_RESULT, res) def testNMEAITERATE_ERR3( self, ): # NMEAReader iterator ignoring bad checksum and continuing EXPECTED_RESULT = "" with open(os.path.join(DIRNAME, "pygpsdata-nmeabadck2.log"), "rb") as stream: nmr = NMEAReader( stream, nmeaonly=False, validate=VALCKSUM, msgmode=0, quitonerror=ERR_IGNORE, ) res = "" for raw, parsed in nmr: res = str(parsed) self.assertEqual(EXPECTED_RESULT, res) def testNMEAFOO1(self): # stream containing invalid attribute type EXPECTED_ERROR = "Unknown attribute type Z2" with open(os.path.join(DIRNAME, "pygpsdata-nmeafoo1.log"), "rb") as stream: with self.assertRaises(NMEATypeError) as context: i = 0 raw = 0 nmr = NMEAReader( stream, nmeaonly=False, quitonerror=ERR_RAISE, ) for raw, parsed in nmr: i += 1 self.assertTrue(EXPECTED_ERROR in str(context.exception)) def testNMEAFOO2(self): # stream containing invalid value for attribute type EXPECTED_ERROR = "Incorrect type for attribute spd in msgID RMC" with open(os.path.join(DIRNAME, "pygpsdata-nmeafoo2.log"), "rb") as stream: with self.assertRaises(NMEATypeError) as context: i = 0 raw = 0 nmr = NMEAReader(stream, nmeaonly=False, quitonerror=ERR_RAISE) for raw, parsed in nmr: i += 1 self.assertTrue(EXPECTED_ERROR in str(context.exception)) def testNMEABADMODE(self): # invalid stream mode EXPECTED_ERROR = "Invalid stream mode 4 - must be 0, 1 or 2." with open(os.path.join(DIRNAME, "pygpsdata-nmeafoo1.log"), "rb") as stream: with self.assertRaises(NMEAParseError) as context: NMEAReader(stream, nmeaonly=False, validate=1, msgmode=4) self.assertTrue(EXPECTED_ERROR in str(context.exception)) def testBADEOF(self): # stream with premature EOF - should just be tolerated EXPECTED_RESULTS = ( "", "", "", "", ) i = 0 raw = 0 with open(os.path.join(DIRNAME, "pygpsdata-badeof.log"), "rb") as stream: nmr = NMEAReader(stream, nmeaonly=False) for raw, parsed in nmr: if raw is not None: # print(parsed) self.assertEqual(str(parsed), EXPECTED_RESULTS[i]) i += 1 def testNMEATRIMBLE(self): # test proprietary Trimble messages EXPECTED_RESULTS = ( "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ) i = 0 raw = 0 with open(os.path.join(DIRNAME, "trimble_nmea.log"), "rb") as stream: nmr = NMEAReader(stream, nmeaonly=False) for raw, parsed in nmr: if raw is not None: # print(parsed) self.assertEqual(str(parsed), EXPECTED_RESULTS[i]) i += 1 self.assertEqual(i, 21) def testNMEAFURUNO(self): # test proprietary Furuno messages EXPECTED_RESULTS = ( "", "", ) i = 0 raw = 0 with open(os.path.join(DIRNAME, "furuno_nmea.log"), "rb") as stream: nmr = NMEAReader(stream, nmeaonly=False, quitonerror=2) for raw, parsed in nmr: if raw is not None: # print(f'"{parsed}",') self.assertEqual(str(parsed), EXPECTED_RESULTS[i]) i += 1 self.assertEqual(i, 2) def testNMEAKENWOOD(self): # test proprietary Kenwood messages EXPECTED_RESULTS = ( "", "", "", "", "", ) i = 0 raw = 0 with open(os.path.join(DIRNAME, "kenwood_nmea.log"), "rb") as stream: nmr = NMEAReader(stream, nmeaonly=False, quitonerror=2) for raw, parsed in nmr: if raw is not None: # print(f'"{parsed}",') self.assertEqual(str(parsed), EXPECTED_RESULTS[i]) i += 1 self.assertEqual(i, 5) def testNMEALOCOSYSGET(self): # test proprietary Locosys GET messages EXPECTED_RESULTS = ( "", "", "", "", "", "", "", "", "", "", "", ) i = 0 raw = 0 with open(os.path.join(DIRNAME, "locosys_get_nmea.log"), "rb") as stream: nmr = NMEAReader(stream, nmeaonly=False, quitonerror=2) for raw, parsed in nmr: if raw is not None: # print(f'"{parsed}",') self.assertEqual(str(parsed), EXPECTED_RESULTS[i]) i += 1 self.assertEqual(i, 11) def testNMEALOCOSYSSET(self): # test proprietary Locosys SET messages EXPECTED_RESULTS = ( "", "", "", "", "", "", "", "", "", ) i = 0 raw = 0 with open(os.path.join(DIRNAME, "locosys_set_nmea.log"), "rb") as stream: nmr = NMEAReader(stream, nmeaonly=False, quitonerror=2, msgmode=SET) for raw, parsed in nmr: if raw is not None: # print(f'"{parsed}",') self.assertEqual(str(parsed), EXPECTED_RESULTS[i]) i += 1 self.assertEqual(i, 9) def testNMEASEPX5GET(self): # test proprietary Sepentrio X5 GET messages EXPECTED_RESULTS = ( "", "", "", "", ) i = 0 raw = 0 with open(os.path.join(DIRNAME, "septentriox5_nmea.log"), "rb") as stream: nmr = NMEAReader(stream, nmeaonly=False, quitonerror=2) for raw, parsed in nmr: if raw is not None: # print(f'"{parsed}",') self.assertEqual(str(parsed), EXPECTED_RESULTS[i]) i += 1 self.assertEqual(i, 4) def testNMEAGPFMI(self): # test proprietary Feyman GPFMI IMU GET messages def getstatus(status) -> str: st = "" for key, (desc, _) in FMI_STATUS.items(): if int(status, 16) & key: st += f"{desc} " return st EXPECTED_RESULTS = ( "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ) EXPECTED_RESULTS2 = ( "Finit PPSReady SyncReady GnssConnect ", "Finit PPSReady SyncReady GnssConnect ", "Finit PPSReady SyncReady GnssConnect ", "Finit PPSReady SyncReady GnssConnect ", "Finit PPSReady SyncReady GnssConnect ", "Finit PPSReady SyncReady GnssConnect ", "Finit PPSReady SyncReady GnssConnect ", "Finit PPSReady SyncReady GnssConnect ", "Finit PPSReady SyncReady GnssConnect ", "Finit PPSReady SyncReady GnssConnect ", "Finit PPSReady SyncReady GnssConnect ", "Finit PPSReady SyncReady GnssConnect ", "Finit PPSReady SyncReady GnssConnect ", "Finit PPSReady SyncReady GnssConnect ", "Finit PPSReady SyncReady GnssConnect ", "Finit PPSReady SyncReady GnssConnect ", "Finit PPSReady SyncReady GnssConnect ", "Finit FReset FixRlsStage1 FixRlsStage2 PPSReady SyncReady GnssConnect ", "Finit PPSReady SyncReady GnssConnect ", "Finit PPSReady SyncReady GnssConnect ", "Finit FInitOk SyncReady GnssConnect ", "Finit FInitOk SyncReady GnssConnect ", ) i = 0 raw = 0 with open(os.path.join(DIRNAME, "feyman_nmea.log"), "rb") as stream: nmr = NMEAReader(stream, nmeaonly=False, quitonerror=2) for raw, parsed in nmr: if raw is not None: # print(f'"{parsed}",') self.assertEqual(str(parsed), EXPECTED_RESULTS[i]) self.assertEqual(getstatus(parsed.status), EXPECTED_RESULTS2[i]) i += 1 self.assertEqual(i, 22) def testNMEAUnicore(self): # test Unicore extended NMEA GET messages EXPECTED_RESULTS = ( "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ) i = 0 with open(os.path.join(DIRNAME, "unicore_nmea.log"), "rb") as stream: nmr = NMEAReader(stream, nmeaonly=False, quitonerror=2) for raw, parsed in nmr: if raw is not None: # print(f'"{parsed}",') self.assertEqual(str(parsed), EXPECTED_RESULTS[i]) i += 1 self.assertEqual(i, len(EXPECTED_RESULTS)) def testNMEAUM981(self): # test Unicore GLL firmware error workaround EXPECTED_RESULTS = ( "", "", "", "", "", ) i = 0 with open(os.path.join(DIRNAME, "pygpsdata-um981.log"), "rb") as stream: nmr = NMEAReader(stream, nmeaonly=False, quitonerror=2) for raw, parsed in nmr: if raw is not None: # print(f'"{parsed}",') self.assertEqual(str(parsed), EXPECTED_RESULTS[i]) i += 1 self.assertEqual(i, len(EXPECTED_RESULTS)) def testBADHDR_FAIL(self): # invalid header in data with quitonerror = 2 EXPECTED_ERROR = "Unknown protocol header b'$&'." with self.assertRaises(NMEAParseError) as context: i = 0 with open(os.path.join(DIRNAME, "pygpsdata-BADHDR.log"), "rb") as stream: ubr = NMEAReader(stream, quitonerror=ERR_RAISE, nmeaonly=True) for _, _ in ubr: i += 1 self.assertTrue(EXPECTED_ERROR in str(context.exception)) def testBADHDR_LOG(self): # invalid header in data with quitonerror = 1 i = 0 with self.assertLogs(level=ERROR) as log: with open(os.path.join(DIRNAME, "pygpsdata-BADHDR.log"), "rb") as stream: ubr = NMEAReader(stream, quitonerror=ERR_LOG, nmeaonly=True) for raw, parsed in ubr: i += 1 self.assertEqual( ["ERROR:pynmeagps.nmeareader:Unknown protocol header b'$&'."], log.output, ) def testBADHDR_IGNORE(self): # invalid header in data with quitonerror = 0 i = 0 with open(os.path.join(DIRNAME, "pygpsdata-BADHDR.log"), "rb") as stream: ubr = NMEAReader(stream, quitonerror=ERR_IGNORE, nmeaonly=True) for raw, parsed in ubr: i += 1 self.assertEqual(i, 15) def testLCCHECKSUM(self): # lower case checksum bytes EXPECTED_RESULTS = ( "", "", "", "", "", ) i = 0 with open(os.path.join(DIRNAME, "pygpsdata-nmea4lc.log"), "rb") as stream: ubr = NMEAReader(stream, quitonerror=ERR_RAISE) for raw, parsed in ubr: # print(f'"{parsed}",') self.assertEqual(str(parsed), EXPECTED_RESULTS[i]) i += 1 self.assertEqual(i, 5) def testuserdefined(self): # test user-defined payload definition dictionary EXPECTED_RESULTS = ( "", "", ) USERDEFINED = { "XYZ": {"fixutc": TM, "lat": LA, "NS": LAD, "lon": LN, "EW": LND}, "ZYX": {"fixutc": TM, "data1": ST, "data2": ST}, } i = 0 with open(os.path.join(DIRNAME, "pygpsdata-userdefined.log"), "rb") as stream: ubr = NMEAReader( stream, quitonerror=ERR_RAISE, nmeaonly=True, userdefined=USERDEFINED ) for raw, parsed in ubr: # print(f'"{parsed}",') self.assertEqual(str(parsed), EXPECTED_RESULTS[i]) i += 1 self.assertEqual(i, 2) if __name__ == "__main__": # import sys;sys.argv = ['', 'Test.testName'] unittest.main() pynmeagps-1.1.7/tests/trimble_nmea.log000066400000000000000000000025431523640354200200360ustar00rootroot00000000000000$GPLLQ,034137.00,210712,23.45,M,13.07,M,3,15,0.011,3.14,M*08 $GPROT,35.6,A*01 $PFUGDP,GN,033615.00,3953.88002,N,10506.75324,W,13,9,FF,0.1,0.1,149,0.1*13 $PTNL,AVR,212405.20,+52.1531,Yaw,-0.0806,Tilt,,,12.575,3,1.4,16*39 $PTNL,AVR,212604.30,+52.1800,Yaw,,,-0.0807,Roll,12.579,3,1.4,16*21 $PTNL,BPQ,224445.06,021207,3723.09383914,N,12200.32620132,W,EHT-5.923,M,5*60 $PTNL,DG,44.0,33.0,287.0,100,0,4,1,0,,,*12 $PTNL,DG,124.0,10.5,1557855.0,1200,2,4,0,3,,,*10 $PTNL,EVT,221212.000008,1,5026,1893,1,17*4F $PTNL,EVT,221213.000008,1,5027,1893,1,17*4F $PTNL,GGK,102939.00,051910,5000.97323841,N,00827.62010742,E,5,09,1.9,EHT150.790,M*73 $PTNL,PJK,202831.50,011112,+805083.350,N,+388997.346,E,10,09,1.5,GHT+25.478,M*77 $PTNL,PJK,010717.00,081796,+732646.511,N,+1731051.091,E,1,05,2.7,EHT+28.345,M*7A $PTNL,PJT,NAD83(Conus),California Zone 4 0404,*5D $PTNL,VHD,030556.00,093098,187.718,-22.138,-76.929,-5.015,0.033,0.006,3,07,2.4,M*22 $PTNL,VGK,160159.00,010997,-0000.161,00009.985,-0000.002,3,07,1,4,M*09 $PASHR,ARR,1,3,12,160159.00,123.45,123.45,-123.45,12.34,12.34,12.34,2.34,2.34,-2.34,S,0,2,1*72 $PASHR,BTS,C,1,btsdev1,hs-344-fg,87,H,1,btsdev2,pc-377xs,68,T,0,,,*5F $PGPPADV,110,39.88113582,-105.07838455,1614.125*71 $PGPPADV,120,21,76.82,68.51,29,20.66,317.47,28,52.38,276.81,22,42.26,198.96*67 $PASHR,142509.000,179.885,T,-0.624,0.245,,0.029,0.029,0.502,2,3*17 pynmeagps-1.1.7/tests/unicore_nmea.log000066400000000000000000000073211523640354200200430ustar00rootroot00000000000000$GNGGAH,073346.00,4004.73874301,N,11614.19077585,E,1,28,0.6,64.2831,M,8.4925,M,,*35 $GNGLLH,4004.73814597,N,11614.19908275,E,054501.00,A,D*37 $GNGNSH,074444.00,4004.73864213,N,11614.19082153,E,ANAAA,28,0.7,64.6536,8.4925,,,S*25 $GNGRSH,055209.00,0,0.0,0.8,0.1,,0.1,2.2,0.2,,,,,,1,1*18 $GNGRSH,055209.00,0,0.1,0.4,0.1,,0.1,1.5,0.2,,,,,,1,4*14 $GNGRSH,055209.00,0,0.0,0.2,,,0.0,0.1,,,,,,,1,8*18 $GNGRSH,055209.00,0,0.1,0.4,0.1,0.1,,,,,,,,,2,1*14 $GNGRSH,055209.00,0,0.1,0.1,0.1,0.3,,,,,,,,,2,3*11 $GNGRSH,055209.00,0,0.6,0.7,0.3,0.8,0.1,0.1,,,,,,,3,7*1C $GNGRSH,055209.00,0,0.2,0.2,0.1,0.2,0.0,0.0,,,,,,,3,1*13 $GNGRSH,055209.00,0,0.1,0.1,0.1,0.1,0.0,0.0,,,,,,,3,2*13 $GNGRSH,055209.00,0,0.3,0.2,0.6,0.1,0.4,0.8,1.2,0.9,0.4,0.4,1.0,2.0,4,1*14 $GNGRSH,055209.00,0,0.9,0.6,0.6,0.4,0.3,0.2,0.8,1.3,0.2,,,,4,1*3D $GNGRSH,055209.00,0,0.2,0.1,0.1,0.1,0.1,0.2,0.2,0.2,0.1,0.1,0.2,0.2,4,8*1E $GNGRSH,055209.00,0,0.2,0.2,0.2,0.1,0.1,0.1,0.2,0.2,0.1,,,,4,8*32 $GNGRSH,055209.00,0,,,0.1,0.0,,0.2,,,,,,,4,11*0B $GNGRSH,055209.00,0,0.2,0.1,0.1,,,0.1,0.2,,0.1,,,,4,11*26 $GNGRSH,055209.00,0,0.3,0.2,0.1,,,,,,,,,,5,1*38 $GNGRSH,055209.00,0,1.2,0.4,0.1,,,,,,,,,,5,6*39 $GNGRSH,055209.00,0,0.2,0.0,0.1,,,,,,,,,,5,8*32 $GNGSAH,M,3,26,29,31,32,,,,,,,,,1.1,0.6,0.9,1*76 $GNGSAH,M,3,01,04,09,19,21,31,,,,,,,1.1,0.6,0.9,3*7D $GNGSAH,M,3,01,03,04,06,07,09,16,19,20,22,28,36,1.1,0.6,0.9,4*73 $GNGSAH,M,3,37,39,40,46,59,60,,,,,,,1.1,0.6,0.9,4*7D $GNGSTH,055543.00,0.45,0.01,0.01,127.6430,0.010,0.010,0.019*0F $GPGSVH,2,1,08,16,28,217,38,32,39,140,45,03,29,290,32,31,66,033,50,1*2F $GPGSVH,2,2,08,04,12,313,34,26,69,220,46,25,16,046,34,29,28,071,37,1*2A $GPGSVH,2,1,07,32,39,140,41,03,29,290,37,31,66,033,46,04,12,313,35,4*21 $GPGSVH,2,2,07,26,69,220,46,25,16,046,35,29,28,071,41,4*11 $GLGSVH,2,1,05,74,15,049,37,66,38,321,45,76,41,264,42,72,21,168,35,1*3F $GLGSVH,2,2,05,65,63,206,44,1*07 $GLGSVH,1,1,04,66,38,321,42,76,41,264,43,72,21,168,36,65,63,206,43,3*31 $GBGSVH,6,1,21,27,15,113,36,46,73,006,50,06,81,019,49,07,43,199,36,1*36 $GBGSVH,6,2,21,16,79,068,51,19,55,235,42,10,33,205,34,28,13,062,34,1*3A $GBGSVH,6,3,21,36,40,265,35,59,38,145,43,40,52,184,43,20,24,178,35,1*3B $GBGSVH,6,4,21,22,31,308,40,04,25,124,36,03,42,188,35,01,34,140,41,1*37 $GBGSVH,6,5,21,60,28,227,38,39,74,097,51,09,72,329,46,02,32,224,35,1*3A $GBGSVH,6,6,21,37,24,062,35,1*0D $GBGSVH,6,1,21,27,15,113,39,46,73,006,52,06,81,019,49,07,43,199,41,8*32 $GBGSVH,6,2,21,16,79,068,48,19,55,235,47,10,33,205,36,28,13,062,39,8*31 $GBGSVH,6,3,21,36,40,265,45,59,38,145,43,40,52,184,46,20,24,178,37,8*32 $GBGSVH,6,4,21,22,31,308,41,04,25,124,37,03,42,188,38,01,34,140,39,8*3C $GBGSVH,6,5,21,60,28,227,40,39,74,097,53,09,72,329,47,02,32,224,35,8*3F $GBGSVH,6,6,21,37,24,062,44,8*02 $GBGSVH,3,1,09,06,81,019,50,07,43,199,43,16,79,068,50,10,33,205,40,B*42 $GBGSVH,3,2,09,04,25,124,40,03,42,188,42,01,34,140,40,09,72,329,49,B*49 $GBGSVH,3,3,09,02,32,224,39,B*79 $GAGSVH,2,1,06,19,27,146,38,04,79,220,51,09,34,312,39,31,44,232,43,2*32 $GAGSVH,2,2,06,21,25,048,44,01,76,038,52,2*3C $GAGSVH,2,1,06,19,27,146,34,04,79,220,48,09,34,312,40,31,44,232,38,7*31 $GAGSVH,2,2,06,21,25,048,36,01,76,038,50,7*3E $GQGSVH,1,1,03,03,13,146,35,02,71,090,49,07,42,163,35,1*19 $GQGSVH,1,1,03,03,13,146,32,02,71,090,49,07,42,163,42,6*19 $GNRMCH,055808.00,A,4004.73817916,N,11614.19891207,E,0.004,99.7,311221,6.9,W,D,V*3A $GNVTGH,113.125,T,120.041,M,0.01474,N,0.02730,K,D*73 $GNTHS2,88.3640,T*0F $GNHPR,074615.00,320.9610,-66.1712,000.0000,4,47,0.00,0999*45 $GNHPR2,-00001.00,088.3640,-00.0873,000.0000,4,32,0.00,0401*63 $GNTRA2,090415.00,88.36,-0.09,0.00,4,30,0.00,0000*7D $GNROT2,-0.0,A*30 $GPHPD,2319,462170.00,251.77,-48.16,178.48,40.0789783,116.2365145,63.03,0.001,0.000,-0.003,-0.001,-0.002,-0.001,0.000,48,48*76