├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── checkpr.yml │ └── main.yml ├── .gitignore ├── .vscode ├── settings.json └── tasks.json ├── CITATION.bib ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── RELEASE_NOTES.md ├── SECURITY.md ├── docs ├── Makefile ├── conf.py ├── index.rst ├── make.bat ├── modules.rst └── pyubx2.rst ├── examples ├── 2023-4-17_82912_serial-COM3.ubx ├── __init__.py ├── benchmark.py ├── datums.py ├── f9p_basestation.py ├── gnssapp.py ├── gpxtracker.py ├── mon_span.ubx ├── mon_span_spectrum.py ├── socket_server.py ├── tcpserver_threaded.py ├── ubxconfigdb.py ├── ubxfactoryreset.py ├── ubxfile.py ├── ubxfile_ucenter.py ├── ubxoptions.py ├── ubxpoller.py ├── ubxsetrates.py ├── ubxsocket.py ├── utilities.py └── webserver │ ├── favicon.ico │ ├── gpshttpserver.py │ ├── index.html │ ├── scripts.js │ ├── styles.css │ └── ubxserver.py ├── images └── sponsor.png ├── pyproject.toml ├── src └── pyubx2 │ ├── __init__.py │ ├── _version.py │ ├── exceptions.py │ ├── ubxhelpers.py │ ├── ubxmessage.py │ ├── ubxreader.py │ ├── ubxtypes_configdb.py │ ├── ubxtypes_core.py │ ├── ubxtypes_decodes.py │ ├── ubxtypes_get.py │ ├── ubxtypes_poll.py │ ├── ubxtypes_set.py │ └── ubxvariants.py └── tests ├── __init__.py ├── assistnow.log ├── configdb_baseline.py ├── pygpsdata-ALL.log ├── pygpsdata-BADCK2.log ├── pygpsdata-BADEOF1.log ├── pygpsdata-BADEOF2.log ├── pygpsdata-BADEOF3.log ├── pygpsdata-BADHDR.log ├── pygpsdata-BADNMEAEOF.log ├── pygpsdata-CFG.log ├── pygpsdata-ESF.log ├── pygpsdata-HNR.log ├── pygpsdata-INF.log ├── pygpsdata-ITER.log ├── pygpsdata-MIXED-RTCM3.log ├── pygpsdata-MIXED-RTCM3BADCRC.log ├── pygpsdata-MIXED.log ├── pygpsdata-MIXED2.log ├── pygpsdata-MIXED3.log ├── pygpsdata-MIXED3BADCK.log ├── pygpsdata-MON.log ├── pygpsdata-NAV.log ├── pygpsdata-NAVHPPOS.log ├── pygpsdata-NMEA.log ├── pygpsdata-NMEABADEND.log ├── pygpsdata-RXM.log ├── pygpsdata-RXMRAWX.log ├── pygpsdata-SEC.log ├── test_assistnow.py ├── test_bitfields.py ├── test_configdb.py ├── test_constructor.py ├── test_decodes.py ├── test_exceptions.py ├── test_parse.py ├── test_socket.py ├── test_specialcases.py ├── test_static.py ├── test_stream.py ├── test_stream_no_parsing.py └── ucenter-ZEDF9P-configdebug.log /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | buy_me_a_coffee: semuconsulting 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: pyubx2 Bug Report 3 | 4 | about: Create a report to help us improve 5 | 6 | title: '' 7 | 8 | labels: '' 9 | 10 | assignees: semuadmin 11 | 12 | --- 13 | 14 | **Describe the bug** 15 | 16 | A clear and concise description of what the bug is. Please include: 17 | 18 | 1. The pyubx2 version (`>>> pyubx2.version`) 19 | 2. The **complete** Python script. Embed your code here (please do *NOT* attach *.py, *.zip, *.tgz or other executable / zipped files) ... 20 | ```python 21 | your code here 22 | ``` 23 | 3. The error message and full traceback. 24 | 4. A binary / hexadecimal dump of the UBX data stream. 25 | 26 | **To Reproduce** 27 | 28 | Steps to reproduce the behaviour: 29 | 1. Any relevant device configuration (if other than factory defaults). 30 | 2. Any causal UBX command input(s). 31 | 32 | **Expected Behaviour** 33 | 34 | A clear and concise description of what you expected to happen. 35 | 36 | **Desktop (please complete the following information):** 37 | 38 | - The operating system you're using [e.g. Windows 11, MacOS Sequoia, Ubuntu Noble]. 39 | - The type of serial connection [e.g. USB, UART1, I2C]. 40 | 41 | **GNSS/GPS Device (please complete the following information as best you can):** 42 | 43 | - Device Model/Generation: [e.g. u-blox NEO-9M]. 44 | - Firmware Version: [e.g. SPG 4.03]. 45 | - Protocol: [e.g. 32.00]. 46 | 47 | 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. 48 | 49 | **Additional context** 50 | 51 | Add any other context about the problem here. 52 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: pyubx2 Community Support 4 | url: https://github.com/semuconsulting/pyubx2/discussions 5 | about: Please ask and answer questions here. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: pyubx2 Feature request 3 | 4 | about: Suggest an idea for this project 5 | 6 | title: '' 7 | 8 | labels: '' 9 | 10 | assignees: semuadmin 11 | 12 | --- 13 | 14 | **Is your feature request related to a problem? Please describe.** 15 | 16 | A clear and concise description of what the problem is e.g. I'd like to be able to do this [...] 17 | 18 | **Describe the solution you'd like** 19 | 20 | A clear and concise description of what you want to happen. 21 | 22 | **Describe alternatives you've considered** 23 | 24 | A clear and concise description of any alternative solutions or features you've considered. 25 | 26 | **Would you be willing to assist with testing?** 27 | 28 | If the request relates to a specific u-blox device that is not currently supported, would you be willing to assist with testing e.g. by contributing a test device to the project, providing real data dumps, or writing unittest scripts? 29 | 30 | **Additional context** 31 | 32 | Add any other context about the feature request here. 33 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # pyubx2 Pull Request Template 2 | 3 | ## Description 4 | 5 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context and, where applicable, any u-blox documentation sources you have used. List any dependencies that are required for this change. 6 | 7 | Please do **NOT** submit PRs containing information (e.g. UBX message definitions) which is subject to a u-blox Non-Disclosure Agreement (NDA). 8 | 9 | Fixes # (issue) 10 | 11 | ## Testing 12 | 13 | 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. 14 | 15 | If you're adding new UBX message definitions for Generation 9+ devices, please check for any corresponding configuration database updates (`ubxtypes_configdb.py`). 16 | 17 | - [ ] Test A 18 | - [ ] Test B 19 | 20 | ## Checklist: 21 | 22 | - [ ] I agree to abide by the code of conduct (see [CODE_OF_CONDUCT.md](https://github.com/semuconsulting/pyubx2/blob/master/CODE_OF_CONDUCT.md)). 23 | - [ ] My code follows the style guidelines of this project (see [CONTRIBUTING.MD](https://github.com/semuconsulting/pyubx2/blob/master/CONTRIBUTING.md)). 24 | - [ ] I have performed a self-review of my own code. 25 | - [ ] (*if appropriate*) I have cited my u-blox documentation source(s). 26 | - [ ] I have commented my code, particularly in hard-to-understand areas. 27 | - [ ] I have made corresponding changes to the documentation. 28 | - [ ] (*if appropriate*) I have added test cases to the `tests/test_*.py` unittest suite to maintain >= 99% code coverage. 29 | - [ ] I have tested my code against the full `tests/test_*.py` unittest suite. 30 | - [ ] My changes generate no new warnings. 31 | - [ ] Any dependent changes have been merged and published in downstream modules. 32 | - [ ] I have [signed](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits) my commits. 33 | - [ ] I understand and acknowledge that the code will be published under a BSD 3-Clause license. 34 | -------------------------------------------------------------------------------- /.github/workflows/checkpr.yml: -------------------------------------------------------------------------------- 1 | name: checkpr 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: [3.9, "3.10", "3.11", "3.12", "3.13"] 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install deploy dependencies 20 | run: pip install .[deploy] 21 | - name: Install test dependencies 22 | run: pip install .[test] 23 | - name: Install code dependencies 24 | run: pip install . 25 | - name: Lint with pylint 26 | run: pylint -E src 27 | - name: Scan security vulnerabilities with bandit 28 | run: bandit -c pyproject.toml -r . 29 | - name: Generate coverage report 30 | run: pytest 31 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: pyubx2 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | python-version: [3.9, "3.10", "3.11", "3.12", "3.13"] 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Set up Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v5 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | - name: Install deploy dependencies 22 | run: | 23 | pip install .[deploy] 24 | - name: Install test dependencies 25 | run: | 26 | pip install .[test] 27 | - name: Install code dependencies 28 | run: | 29 | pip install . 30 | - name: Lint with pylint 31 | run: | 32 | pylint -E src 33 | - name: Security vulnerability analysis with bandit 34 | run: | 35 | bandit -c pyproject.toml -r . 36 | - name: Generate test coverage report 37 | run: | 38 | pytest 39 | - name: "Upload coverage to Codecov" 40 | uses: codecov/codecov-action@v4 41 | with: 42 | token: ${{ secrets.CODECOV_TOKEN }} # supposedly not required for public repos 43 | fail_ci_if_error: true 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dropbox 3 | .project 4 | .coverage 5 | .pydevproject 6 | *.code-workspace 7 | Pipfile*.* 8 | /.settings 9 | /htmlcov 10 | /build 11 | /dist 12 | /docs/_build 13 | /references 14 | /pyubx2_* 15 | /pyubx2-* 16 | src/pyubx2.egg* 17 | /__pycache__ 18 | /.dbeaver 19 | /pyubx2/__pycache__ 20 | /tests/__pycache__ 21 | *.pyc 22 | /examples/ubxsandpit.py 23 | /examples/temp*.* 24 | /.pytest_cache/ 25 | pylint_report.txt 26 | venv 27 | output.* 28 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.testing.pytestEnabled": false, 3 | "python.testing.unittestEnabled": true, 4 | "editor.formatOnSave": true, 5 | "modulename": "${workspaceFolderBasename}", 6 | "distname": "${workspaceFolderBasename}", 7 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | // Use the Install Build/Test Dependencies tasks to install the necessary toolchain. 5 | "version": "2.0.0", 6 | "tasks": [ 7 | { 8 | "label": "Install Dependencies", 9 | "type": "process", 10 | "command": "${config:python.defaultInterpreterPath}", 11 | "args": [ 12 | "-m", 13 | "pip", 14 | "install", 15 | "--upgrade", 16 | "." 17 | ], 18 | "problemMatcher": [] 19 | }, 20 | { 21 | "label": "Clean", 22 | "type": "shell", 23 | "command": "rm", 24 | "args": [ 25 | "-rfvd", 26 | "build", 27 | "dist", 28 | "htmlcov", 29 | "docs/_build", 30 | "${config:modulename}.egg-info", 31 | ], 32 | "windows": { 33 | "command": "Get-ChildItem", 34 | "args": [ 35 | "-Path", 36 | "build\\,", 37 | "dist\\,", 38 | "htmlcov\\,", 39 | "docs\\_build,", 40 | "${config:modulename}.egg-info", 41 | "-Recurse", 42 | "|", 43 | "Remove-Item", 44 | "-Recurse", 45 | "-Confirm:$false", 46 | "-Force", 47 | ], 48 | }, 49 | "options": { 50 | "cwd": "${workspaceFolder}" 51 | }, 52 | "problemMatcher": [] 53 | }, 54 | { 55 | "label": "Sort Imports", 56 | "type": "process", 57 | "command": "${config:python.defaultInterpreterPath}", 58 | "args": [ 59 | "-m", 60 | "isort", 61 | "src", 62 | "--jobs", 63 | "-1" 64 | ], 65 | "problemMatcher": [] 66 | }, 67 | { 68 | "label": "Format", 69 | "type": "process", 70 | "command": "${config:python.defaultInterpreterPath}", 71 | "args": [ 72 | "-m", 73 | "black", 74 | "src" 75 | ], 76 | "problemMatcher": [] 77 | }, 78 | { 79 | "label": "Pylint", 80 | "type": "process", 81 | "command": "${config:python.defaultInterpreterPath}", 82 | "args": [ 83 | "-m", 84 | "pylint", 85 | "src" 86 | ], 87 | "problemMatcher": [] 88 | }, 89 | { 90 | "label": "Security", 91 | "type": "process", 92 | "command": "${config:python.defaultInterpreterPath}", 93 | "args": [ 94 | "-m", 95 | "bandit", 96 | "-c", 97 | "pyproject.toml", 98 | "-r", 99 | "." 100 | ], 101 | "problemMatcher": [] 102 | }, 103 | { 104 | "label": "Build", 105 | "type": "process", 106 | "command": "${config:python.defaultInterpreterPath}", 107 | "args": [ 108 | "-m", 109 | "build", 110 | ".", 111 | "--wheel", 112 | "--sdist", 113 | ], 114 | "problemMatcher": [] 115 | }, 116 | { 117 | "label": "Test", 118 | "type": "process", 119 | "command": "${config:python.defaultInterpreterPath}", 120 | "args": [ 121 | "-m", 122 | "pytest" 123 | ], 124 | "problemMatcher": [] 125 | }, 126 | { 127 | "label": "Sphinx", 128 | "type": "process", 129 | "command": "sphinx-apidoc", 130 | "args": [ 131 | "--ext-autodoc", 132 | "--ext-viewcode", 133 | "--templatedir=docs", 134 | "-f", 135 | "-o", 136 | "docs", 137 | "src/${config:modulename}" 138 | ], 139 | "problemMatcher": [] 140 | }, 141 | { 142 | "label": "Sphinx HTML", 143 | "type": "process", 144 | "command": "/usr/bin/make", 145 | "windows": { 146 | "command": "${workspaceFolder}/docs/make.bat" 147 | }, 148 | "args": [ 149 | "html" 150 | ], 151 | "options": { 152 | "cwd": "${workspaceFolder}/docs" 153 | }, 154 | "dependsOrder": "sequence", 155 | "dependsOn": [ 156 | "Sphinx" 157 | ], 158 | "problemMatcher": [] 159 | }, 160 | { 161 | "label": "Sphinx Deploy to S3", // needs AWS credentials 162 | "type": "process", 163 | "command": "aws", 164 | "args": [ 165 | "s3", 166 | "cp", 167 | "${workspaceFolder}/docs/_build/html", 168 | "s3://www.semuconsulting.com/${config:modulename}/", 169 | "--recursive" 170 | ], 171 | "dependsOrder": "sequence", 172 | "dependsOn": [ 173 | "Sphinx HTML" 174 | ], 175 | "problemMatcher": [] 176 | }, 177 | { 178 | "label": "Install Locally", 179 | "type": "shell", 180 | "command": "${config:python.defaultInterpreterPath}", 181 | "args": [ 182 | "-m", 183 | "pip", 184 | "install", 185 | "--user", 186 | "--force-reinstall", 187 | "*.whl" 188 | ], 189 | "dependsOrder": "sequence", 190 | "dependsOn": [ 191 | "Clean", 192 | "Security", 193 | "Sort Imports", 194 | "Format", 195 | "Pylint", 196 | "Test", 197 | "Build", 198 | "Sphinx HTML" 199 | ], 200 | "options": { 201 | "cwd": "dist" 202 | }, 203 | "problemMatcher": [] 204 | }, 205 | { 206 | "label": "Benchmark", 207 | "type": "process", 208 | "command": "${config:python.defaultInterpreterPath}", 209 | "args": [ 210 | "${workspaceFolder}/examples/benchmark.py", 211 | ], 212 | "problemMatcher": [] 213 | }, 214 | ] 215 | } -------------------------------------------------------------------------------- /CITATION.bib: -------------------------------------------------------------------------------- 1 | @Misc{pyubx2, 2 | author = {{SEMU Consulting}}, 3 | howpublished = {GitHub repository}, 4 | note = {Viewed last: xxxx:xx:xx}, 5 | title = {Python library for reading, parsing and generating UBX (proprietary u-blox GNSS/GPS protocol) messages.}, 6 | year = {2020}, 7 | url = {https://github.com/semuconsulting/pyubx2}, 8 | } 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at semuadmin@semuconsulting.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # pyubx2 How to contribute 2 | 3 | **pyubx2** 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. 4 | 5 | If you're intending to make significant changes, please raise them in the [Discussions Channel](https://github.com/semuconsulting/pyubx2/discussions/categories/ideas) beforehand. 6 | 7 | Being one of our contributors, you agree and confirm that: 8 | 9 | * The work is all your own. 10 | * Your work will be distributed under a BSD 3-Clause License once your pull request is merged. 11 | * You submitted work fulfils or mostly fulfils our coding conventions, styles and standards. 12 | 13 | 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. 14 | 15 | If you're adding or amending UBX payload definitions or configuration database keys, it would be helpful to quote/hyperlink the documentation source (e.g. specific u-blox Interface Specification). 16 | 17 | ## Coding conventions 18 | 19 | * This is open source software. Code should be as simple and transparent as possible. Favour clarity over brevity. 20 | * The code should be compatible with Python >= 3.9. 21 | * 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. 22 | * Avoid external library dependencies unless there's a compelling reason not to. 23 | * 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. 24 | * Code should be documented in accordance with [Sphinx](https://www.sphinx-doc.org/en/master/) docstring conventions. 25 | * Code should formatted using [black](https://pypi.org/project/black/) (>= 24.4). 26 | * We use and recommend [pylint](https://pypi.org/project/pylint/) (>=3.0.1) for code analysis. 27 | * We use and recommend [bandit](https://pypi.org/project/bandit/) (>=1.7.5) for security vulnerability analysis. 28 | * Commits must be [signed](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits). 29 | 30 | ## Testing 31 | 32 | While we endeavour to test on as wide a variety of u-blox devices as possible, as a volunteer project we only have a limited number of devices available. We particularly welcome testing contributions relating to specialised devices (e.g. high precision HP, real-time kinematics RTK, automotive dead-reckoning ADR, etc.). 33 | 34 | 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 >99% code coverage. 35 | 36 | Please write unitttest examples for new code you create and add them to the `/tests` folder following the naming convention `test_*.py`. 37 | 38 | We test on the following platforms using a variety of u-blox devices from Generation 7 throught Generation 10: 39 | * Windows 11 40 | * MacOS (Intel & Apple Silicon) 41 | * Linux (Ubuntu 22.04 LTS Jammy Jellyfish, 24.04 LTS Noble Numbat) 42 | * Raspberry Pi OS (32-bit & 64-bit) 43 | 44 | ## Submitting changes 45 | 46 | Please send a [GitHub Pull Request to pyubx2](https://github.com/semuconsulting/pyubx2/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). 47 | 48 | Please use the supplied [Pull Request Template](https://github.com/semuconsulting/pyubx2/blob/master/.github/pull_request_template.md). 49 | 50 | Please sign all commits - see [Signing GitHub Commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits) for instructions. 51 | 52 | Always write a clear log message for your commits. One-line messages are fine for small changes, but bigger changes should look like this: 53 | 54 | $ git commit -m "A brief summary of the commit 55 | > 56 | > A paragraph describing what changed and its impact." 57 | 58 | 59 | 60 | Thanks, 61 | 62 | semuadmin -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License ("BSD License 2.0", "Revised BSD License", "New BSD License", or "Modified BSD License") 2 | 3 | Copyright (c) 2020, SEMU Consulting 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | * Neither the name of the nor the 14 | names of its contributors may be used to endorse or promote products 15 | derived from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 21 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 24 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | global-exclude __pycache__ 3 | global-exclude *.pyc 4 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # pyubx2 Security Policy 2 | 3 | ## Supported Versions 4 | 5 | The following versions are currently being supported with security updates. 6 | 7 | ![Release](https://img.shields.io/github/v/release/semuconsulting/pyubx2) 8 | 9 | ## Reporting a Vulnerability 10 | 11 | Please report any suspected security vulnerabilities via the supplied 12 | [Issue Template](https://github.com/semuconsulting/pyubx2/blob/master/.github/ISSUE_TEMPLATE/bug_report.md). 13 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | 16 | sys.path.insert(0, os.path.abspath("../src")) 17 | 18 | from pyubx2 import version as VERSION 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = "pyubx2" 23 | copyright = "2021, SEMU Consulting" 24 | author = "SEMU Consulting" 25 | 26 | # The full version, including alpha/beta/rc tags 27 | release = VERSION 28 | version = VERSION 29 | 30 | # -- General configuration --------------------------------------------------- 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 34 | # ones. 35 | extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode"] 36 | 37 | # Add any paths that contain templates here, relative to this directory. 38 | templates_path = ["_templates"] 39 | 40 | # List of patterns, relative to source directory, that match files and 41 | # directories to ignore when looking for source files. 42 | # This pattern also affects html_static_path and html_extra_path. 43 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 44 | 45 | # -- Options for HTML output ------------------------------------------------- 46 | 47 | # The theme to use for HTML and HTML Help pages. See the documentation for 48 | # a list of builtin themes. 49 | # 50 | html_theme = "sphinx_rtd_theme" 51 | html_title = " v documentation." 52 | 53 | # Add any paths that contain custom static files (such as style sheets) here, 54 | # relative to this directory. They are copied after the builtin static files, 55 | # so a file named "default.css" will overwrite the builtin "default.css". 56 | html_static_path = ["_static"] 57 | html_last_updated_fmt = "%b %d %Y" 58 | html_theme_options = { 59 | "logo_only": False, 60 | "prev_next_buttons_location": "bottom", 61 | "style_external_links": False, 62 | "vcs_pageview_mode": "", 63 | "style_nav_header_background": "white", 64 | "flyout_display": "hidden", 65 | "version_selector": True, 66 | "language_selector": True, 67 | # Toc options 68 | "collapse_navigation": True, 69 | "sticky_navigation": True, 70 | "navigation_depth": 4, 71 | "includehidden": True, 72 | "titles_only": False, 73 | } 74 | 75 | autodoc_default_options = { 76 | "members": True, 77 | "member-order": "bysource", 78 | "special-members": "__init__", 79 | "undoc-members": True, 80 | "exclude-members": "__weakref__", 81 | } 82 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. pyubx2 documentation master file, created by 2 | sphinx-quickstart on Wed Feb 24 11:32:55 2021. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to pyubx2's documentation! 7 | ================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | 14 | 15 | Indices and tables 16 | ================== 17 | 18 | * :ref:`genindex` 19 | * :ref:`modindex` 20 | * :ref:`search` 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/modules.rst: -------------------------------------------------------------------------------- 1 | pyubx2 2 | ====== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | pyubx2 8 | -------------------------------------------------------------------------------- /docs/pyubx2.rst: -------------------------------------------------------------------------------- 1 | pyubx2 package 2 | ============== 3 | 4 | Submodules 5 | ---------- 6 | 7 | pyubx2.exceptions module 8 | ------------------------ 9 | 10 | .. automodule:: pyubx2.exceptions 11 | :members: 12 | :show-inheritance: 13 | :undoc-members: 14 | 15 | pyubx2.ubxhelpers module 16 | ------------------------ 17 | 18 | .. automodule:: pyubx2.ubxhelpers 19 | :members: 20 | :show-inheritance: 21 | :undoc-members: 22 | 23 | pyubx2.ubxmessage module 24 | ------------------------ 25 | 26 | .. automodule:: pyubx2.ubxmessage 27 | :members: 28 | :show-inheritance: 29 | :undoc-members: 30 | 31 | pyubx2.ubxreader module 32 | ----------------------- 33 | 34 | .. automodule:: pyubx2.ubxreader 35 | :members: 36 | :show-inheritance: 37 | :undoc-members: 38 | 39 | pyubx2.ubxtypes\_configdb module 40 | -------------------------------- 41 | 42 | .. automodule:: pyubx2.ubxtypes_configdb 43 | :members: 44 | :show-inheritance: 45 | :undoc-members: 46 | 47 | pyubx2.ubxtypes\_core module 48 | ---------------------------- 49 | 50 | .. automodule:: pyubx2.ubxtypes_core 51 | :members: 52 | :show-inheritance: 53 | :undoc-members: 54 | 55 | pyubx2.ubxtypes\_decodes module 56 | ------------------------------- 57 | 58 | .. automodule:: pyubx2.ubxtypes_decodes 59 | :members: 60 | :show-inheritance: 61 | :undoc-members: 62 | 63 | pyubx2.ubxtypes\_get module 64 | --------------------------- 65 | 66 | .. automodule:: pyubx2.ubxtypes_get 67 | :members: 68 | :show-inheritance: 69 | :undoc-members: 70 | 71 | pyubx2.ubxtypes\_poll module 72 | ---------------------------- 73 | 74 | .. automodule:: pyubx2.ubxtypes_poll 75 | :members: 76 | :show-inheritance: 77 | :undoc-members: 78 | 79 | pyubx2.ubxtypes\_set module 80 | --------------------------- 81 | 82 | .. automodule:: pyubx2.ubxtypes_set 83 | :members: 84 | :show-inheritance: 85 | :undoc-members: 86 | 87 | pyubx2.ubxvariants module 88 | ------------------------- 89 | 90 | .. automodule:: pyubx2.ubxvariants 91 | :members: 92 | :show-inheritance: 93 | :undoc-members: 94 | 95 | Module contents 96 | --------------- 97 | 98 | .. automodule:: pyubx2 99 | :members: 100 | :show-inheritance: 101 | :undoc-members: 102 | -------------------------------------------------------------------------------- /examples/2023-4-17_82912_serial-COM3.ubx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/pyubx2/43e9a9d4a2f4cad0191ec42c1468efcec744cbab/examples/2023-4-17_82912_serial-COM3.ubx -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on 27 Sep 2020 3 | 4 | :author: semuadmin 5 | :copyright: SEMU Consulting © 2020 6 | :license: BSD 3-Clause 7 | """ 8 | -------------------------------------------------------------------------------- /examples/f9p_basestation.py: -------------------------------------------------------------------------------- 1 | """ 2 | f9p_basestation.py 3 | 4 | Example showing how to configure a u-blox ZED-F9P 5 | receiver to operate in RTK Base Station mode (either 6 | Survey-In or Fixed Timing Mode). This can be used to 7 | complement PyGPSClient's NTRIP Caster functionality. 8 | 9 | It also optionally formats a user-defined preset 10 | configuration message string suitable for copying 11 | and pasting into the PyGPSClient ubxpresets file. 12 | 13 | Created on 26 Apr 2022 14 | 15 | :author: semuadmin 16 | :copyright: SEMU Consulting © 2022 17 | :license: BSD 3-Clause 18 | """ 19 | 20 | from serial import Serial 21 | 22 | from pyubx2 import UBXMessage, val2sphp 23 | 24 | TMODE_SVIN = 1 25 | TMODE_FIXED = 2 26 | SHOW_PRESET = True # hide or show PyGPSClient preset string 27 | 28 | def send_msg(serial_out: Serial, ubx: UBXMessage): 29 | """ 30 | Send config message to receiver. 31 | """ 32 | 33 | print("Sending configuration message to receiver...") 34 | print(ubx) 35 | serial_out.write(ubx.serialize()) 36 | 37 | 38 | def config_rtcm(port_type: str) -> UBXMessage: 39 | """ 40 | Configure which RTCM3 messages to output. 41 | """ 42 | 43 | print("\nFormatting RTCM MSGOUT CFG-VALSET message...") 44 | layers = 1 # 1 = RAM, 2 = BBR, 4 = Flash (can be OR'd) 45 | transaction = 0 46 | cfg_data = [] 47 | for rtcm_type in ( 48 | "1005", 49 | "1077", 50 | "1087", 51 | "1097", 52 | "1127", 53 | "1230", 54 | ): 55 | cfg = f"CFG_MSGOUT_RTCM_3X_TYPE{rtcm_type}_{port_type}" 56 | cfg_data.append([cfg, 1]) 57 | 58 | ubx = UBXMessage.config_set(layers, transaction, cfg_data) 59 | 60 | if SHOW_PRESET: 61 | print( 62 | "Set ZED-F9P RTCM3 MSGOUT Basestation, " 63 | f"CFG, CFG_VALSET, {ubx.payload.hex()}, 1\n" 64 | ) 65 | 66 | return ubx 67 | 68 | 69 | def config_svin(port_type: str, acc_limit: int, svin_min_dur: int) -> UBXMessage: 70 | """ 71 | Configure Survey-In mode with specied accuracy limit. 72 | """ 73 | 74 | print("\nFormatting SVIN TMODE CFG-VALSET message...") 75 | tmode = TMODE_SVIN 76 | layers = 1 77 | transaction = 0 78 | acc_limit = int(round(acc_limit / 0.1, 0)) 79 | cfg_data = [ 80 | ("CFG_TMODE_MODE", tmode), 81 | ("CFG_TMODE_SVIN_ACC_LIMIT", acc_limit), 82 | ("CFG_TMODE_SVIN_MIN_DUR", svin_min_dur), 83 | (f"CFG_MSGOUT_UBX_NAV_SVIN_{port_type}", 1), 84 | ] 85 | 86 | ubx = UBXMessage.config_set(layers, transaction, cfg_data) 87 | 88 | if SHOW_PRESET: 89 | print( 90 | "Set ZED-F9P to Survey-In Timing Mode Basestation, " 91 | f"CFG, CFG_VALSET, {ubx.payload.hex()}, 1\n" 92 | ) 93 | 94 | return ubx 95 | 96 | 97 | def config_fixed(acc_limit: int, lat: float, lon: float, height: float) -> UBXMessage: 98 | """ 99 | Configure Fixed mode with specified coordinates. 100 | """ 101 | 102 | print("\nFormatting FIXED TMODE CFG-VALSET message...") 103 | tmode = TMODE_FIXED 104 | pos_type = 1 # LLH (as opposed to ECEF) 105 | layers = 1 106 | transaction = 0 107 | acc_limit = int(round(acc_limit / 0.1, 0)) 108 | lats, lath = val2sphp(lat) 109 | lons, lonh = val2sphp(lon) 110 | 111 | height = int(height) 112 | cfg_data = [ 113 | ("CFG_TMODE_MODE", tmode), 114 | ("CFG_TMODE_POS_TYPE", pos_type), 115 | ("CFG_TMODE_FIXED_POS_ACC", acc_limit), 116 | ("CFG_TMODE_HEIGHT_HP", 0), 117 | ("CFG_TMODE_HEIGHT", height), 118 | ("CFG_TMODE_LAT", lats), 119 | ("CFG_TMODE_LAT_HP", lath), 120 | ("CFG_TMODE_LON", lons), 121 | ("CFG_TMODE_LON_HP", lonh), 122 | ] 123 | 124 | ubx = UBXMessage.config_set(layers, transaction, cfg_data) 125 | 126 | if SHOW_PRESET: 127 | print( 128 | "Set ZED-F9P to Fixed Timing Mode Basestation, " 129 | f"CFG, CFG_VALSET, {ubx.payload.hex()}, 1\n" 130 | ) 131 | 132 | return ubx 133 | 134 | 135 | if __name__ == "__main__": 136 | # Amend as required... 137 | PORT = "/dev/tty.usbmodem2101" 138 | PORT_TYPE = "USB" # choose from "USB", "UART1", "UART2" 139 | BAUD = 38400 140 | TIMEOUT = 5 141 | 142 | 143 | TMODE = TMODE_FIXED # "TMODE_SVIN" or 1 = Survey-In, "TMODE_FIXED" or 2 = Fixed 144 | ACC_LIMIT = 200 # accuracy in mm 145 | 146 | # only used if TMODE = SVIN ... 147 | SVIN_MIN_DUR = 90 # seconds 148 | 149 | # only used if TMODE = FIXED ... 150 | ARP_LAT = 12.123456789 151 | ARP_LON = -115.987654321 152 | ARP_HEIGHT = 137000 # cm 153 | 154 | print(f"Configuring receiver on {PORT} @ {BAUD:,} baud.\n") 155 | with Serial(PORT, BAUD, timeout=TIMEOUT) as stream: 156 | 157 | # configure RTCM3 outputs 158 | msg = config_rtcm(PORT_TYPE) 159 | send_msg(stream, msg) 160 | 161 | # configure either Survey-In or Fixed Timing Mode 162 | if TMODE == TMODE_SVIN: 163 | msg = config_svin(PORT_TYPE, ACC_LIMIT, SVIN_MIN_DUR) 164 | else: 165 | msg = config_fixed(ACC_LIMIT, ARP_LAT, ARP_LON, ARP_HEIGHT) 166 | send_msg(stream, msg) 167 | -------------------------------------------------------------------------------- /examples/gnssapp.py: -------------------------------------------------------------------------------- 1 | """ 2 | pygnssutils - gnssapp.py 3 | 4 | *** FOR ILLUSTRATION ONLY - NOT FOR PRODUCTION USE *** 5 | 6 | Skeleton GNSS application which continuously receives, parses and prints 7 | NMEA, UBX or RTCM data from a receiver until the stop Event is set or 8 | stop() method invoked. Assumes receiver is connected via serial USB or UART1 port. 9 | 10 | The app also implements basic methods needed by certain pygnssutils classes. 11 | 12 | Optional keyword arguments: 13 | 14 | - sendqueue - any data placed on this Queue will be sent to the receiver 15 | (e.g. UBX commands/polls or NTRIP RTCM data). Data must be a tuple of 16 | (raw_data, parsed_data). 17 | - idonly - determines whether the app prints out the entire parsed message, 18 | or just the message identity. 19 | - enableubx - suppresses NMEA receiver output and substitutes a minimum set 20 | of UBX messages instead (NAV-PVT, NAV-SAT, NAV-DOP, RXM-RTCM). 21 | - showhacc - show estimate of horizonal accuracy in metres (if available). 22 | 23 | Created on 27 Jul 2023 24 | 25 | :author: semuadmin 26 | :copyright: SEMU Consulting © 2023 27 | :license: BSD 3-Clause 28 | """ 29 | # pylint: disable=invalid-name, too-many-instance-attributes 30 | 31 | from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser 32 | from queue import Empty, Queue 33 | from threading import Event, Thread 34 | from time import sleep 35 | 36 | from pynmeagps import NMEAMessageError, NMEAParseError 37 | from pyrtcm import RTCMMessage, RTCMMessageError, RTCMParseError 38 | from serial import Serial 39 | 40 | from pyubx2 import ( 41 | NMEA_PROTOCOL, 42 | RTCM3_PROTOCOL, 43 | UBX_PROTOCOL, 44 | UBXMessage, 45 | UBXMessageError, 46 | UBXParseError, 47 | UBXReader, 48 | ) 49 | 50 | CONNECTED = 1 51 | 52 | 53 | class GNSSSkeletonApp: 54 | """ 55 | Skeleton GNSS application which communicates with a GNSS receiver. 56 | """ 57 | 58 | def __init__( 59 | self, port: str, baudrate: int, timeout: float, stopevent: Event, **kwargs 60 | ): 61 | """ 62 | Constructor. 63 | 64 | :param str port: serial port e.g. "/dev/ttyACM1" 65 | :param int baudrate: baudrate 66 | :param float timeout: serial timeout in seconds 67 | :param Event stopevent: stop event 68 | """ 69 | 70 | self.port = port 71 | self.baudrate = baudrate 72 | self.timeout = timeout 73 | self.stopevent = stopevent 74 | self.sendqueue = kwargs.get("sendqueue", None) 75 | self.idonly = kwargs.get("idonly", True) 76 | self.enableubx = kwargs.get("enableubx", False) 77 | self.showhacc = kwargs.get("showhacc", False) 78 | self.stream = None 79 | self.lat = 0 80 | self.lon = 0 81 | self.alt = 0 82 | self.sep = 0 83 | 84 | def __enter__(self): 85 | """ 86 | Context manager enter routine. 87 | """ 88 | 89 | return self 90 | 91 | def __exit__(self, exc_type, exc_value, exc_traceback): 92 | """ 93 | Context manager exit routine. 94 | 95 | Terminates app in an orderly fashion. 96 | """ 97 | 98 | self.stop() 99 | 100 | def run(self): 101 | """ 102 | Run GNSS reader/writer. 103 | """ 104 | 105 | self.enable_ubx(self.enableubx) 106 | 107 | self.stream = Serial(self.port, self.baudrate, timeout=self.timeout) 108 | self.stopevent.clear() 109 | 110 | read_thread = Thread( 111 | target=self._read_loop, 112 | args=( 113 | self.stream, 114 | self.stopevent, 115 | self.sendqueue, 116 | ), 117 | daemon=True, 118 | ) 119 | read_thread.start() 120 | 121 | def stop(self): 122 | """ 123 | Stop GNSS reader/writer. 124 | """ 125 | 126 | self.stopevent.set() 127 | if self.stream is not None: 128 | self.stream.close() 129 | 130 | def _read_loop(self, stream: Serial, stopevent: Event, sendqueue: Queue): 131 | """ 132 | THREADED 133 | Reads and parses incoming GNSS data from the receiver, 134 | and sends any queued output data to the receiver. 135 | 136 | :param Serial stream: serial stream 137 | :param Event stopevent: stop event 138 | :param Queue sendqueue: queue for messages to send to receiver 139 | """ 140 | 141 | ubr = UBXReader( 142 | stream, protfilter=(NMEA_PROTOCOL | UBX_PROTOCOL | RTCM3_PROTOCOL) 143 | ) 144 | while not stopevent.is_set(): 145 | try: 146 | if stream.in_waiting: 147 | _, parsed_data = ubr.read() 148 | if parsed_data: 149 | # extract current navigation solution 150 | self._extract_coordinates(parsed_data) 151 | 152 | # if it's an RXM-RTCM message, show which RTCM3 message 153 | # it's acknowledging and whether it's been used or not."" 154 | if parsed_data.identity == "RXM-RTCM": 155 | nty = ( 156 | f" - {parsed_data.msgType} " 157 | f"{'Used' if parsed_data.msgUsed > 0 else 'Not used'}" 158 | ) 159 | else: 160 | nty = "" 161 | 162 | if self.idonly: 163 | print(f"GNSS>> {parsed_data.identity}{nty}") 164 | else: 165 | print(parsed_data) 166 | 167 | # send any queued output data to receiver 168 | self._send_data(ubr.datastream, sendqueue) 169 | 170 | except ( 171 | UBXMessageError, 172 | UBXParseError, 173 | NMEAMessageError, 174 | NMEAParseError, 175 | RTCMMessageError, 176 | RTCMParseError, 177 | ) as err: 178 | print(f"Error parsing data stream {err}") 179 | continue 180 | 181 | def _extract_coordinates(self, parsed_data: object): 182 | """ 183 | Extract current navigation solution from NMEA or UBX message. 184 | 185 | :param object parsed_data: parsed NMEA or UBX navigation message 186 | """ 187 | 188 | if hasattr(parsed_data, "lat"): 189 | self.lat = parsed_data.lat 190 | if hasattr(parsed_data, "lon"): 191 | self.lon = parsed_data.lon 192 | if hasattr(parsed_data, "alt"): 193 | self.alt = parsed_data.alt 194 | if hasattr(parsed_data, "hMSL"): # UBX hMSL is in mm 195 | self.alt = parsed_data.hMSL / 1000 196 | if hasattr(parsed_data, "sep"): 197 | self.sep = parsed_data.sep 198 | if hasattr(parsed_data, "hMSL") and hasattr(parsed_data, "height"): 199 | self.sep = (parsed_data.height - parsed_data.hMSL) / 1000 200 | if self.showhacc and hasattr(parsed_data, "hAcc"): # UBX hAcc is in mm 201 | unit = 1 if parsed_data.identity == "PUBX00" else 1000 202 | print(f"Estimated horizontal accuracy: {(parsed_data.hAcc / unit):.3f} m") 203 | 204 | def _send_data(self, stream: Serial, sendqueue: Queue): 205 | """ 206 | Send any queued output data to receiver. 207 | Queue data is tuple of (raw_data, parsed_data). 208 | 209 | :param Serial stream: serial stream 210 | :param Queue sendqueue: queue for messages to send to receiver 211 | """ 212 | 213 | if sendqueue is not None: 214 | try: 215 | while not sendqueue.empty(): 216 | data = sendqueue.get(False) 217 | raw, parsed = data 218 | source = "NTRIP>>" if isinstance(parsed, RTCMMessage) else "GNSS<<" 219 | if self.idonly: 220 | print(f"{source} {parsed.identity}") 221 | else: 222 | print(parsed) 223 | stream.write(raw) 224 | sendqueue.task_done() 225 | except Empty: 226 | pass 227 | 228 | def enable_ubx(self, enable: bool): 229 | """ 230 | Enable UBX output and suppress NMEA. 231 | 232 | :param bool enable: enable UBX and suppress NMEA output 233 | """ 234 | 235 | layers = 1 236 | transaction = 0 237 | cfg_data = [] 238 | for port_type in ("USB", "UART1"): 239 | cfg_data.append((f"CFG_{port_type}OUTPROT_NMEA", not enable)) 240 | cfg_data.append((f"CFG_{port_type}OUTPROT_UBX", enable)) 241 | cfg_data.append((f"CFG_MSGOUT_UBX_NAV_PVT_{port_type}", enable)) 242 | cfg_data.append((f"CFG_MSGOUT_UBX_NAV_SAT_{port_type}", enable * 4)) 243 | cfg_data.append((f"CFG_MSGOUT_UBX_NAV_DOP_{port_type}", enable * 4)) 244 | cfg_data.append((f"CFG_MSGOUT_UBX_RXM_RTCM_{port_type}", enable)) 245 | 246 | msg = UBXMessage.config_set(layers, transaction, cfg_data) 247 | self.sendqueue.put((msg.serialize(), msg)) 248 | 249 | def get_coordinates(self) -> tuple: 250 | """ 251 | Return current receiver navigation solution. 252 | (method needed by certain pygnssutils classes) 253 | 254 | :return: tuple of (connection status, lat, lon, alt and sep) 255 | :rtype: tuple 256 | """ 257 | 258 | return (CONNECTED, self.lat, self.lon, self.alt, self.sep) 259 | 260 | def set_event(self, eventtype: str): 261 | """ 262 | Create event. 263 | (stub method needed by certain pygnssutils classes) 264 | 265 | :param str eventtype: name of event to create 266 | """ 267 | 268 | # create event of specified eventtype 269 | 270 | 271 | if __name__ == "__main__": 272 | arp = ArgumentParser( 273 | formatter_class=ArgumentDefaultsHelpFormatter, 274 | ) 275 | arp.add_argument( 276 | "-P", "--port", required=False, help="Serial port", default="/dev/ttyACM1" 277 | ) 278 | arp.add_argument( 279 | "-B", "--baudrate", required=False, help="Baud rate", default=38400, type=int 280 | ) 281 | arp.add_argument( 282 | "-T", "--timeout", required=False, help="Timeout in secs", default=3, type=float 283 | ) 284 | 285 | args = arp.parse_args() 286 | send_queue = Queue() 287 | stop_event = Event() 288 | 289 | try: 290 | print("Starting GNSS reader/writer...\n") 291 | with GNSSSkeletonApp( 292 | args.port, 293 | int(args.baudrate), 294 | float(args.timeout), 295 | stop_event, 296 | sendqueue=send_queue, 297 | idonly=False, 298 | enableubx=True, 299 | showhacc=True, 300 | ) as gna: 301 | gna.run() 302 | while True: 303 | sleep(1) 304 | 305 | except KeyboardInterrupt: 306 | stop_event.set() 307 | print("Terminated by user") 308 | -------------------------------------------------------------------------------- /examples/gpxtracker.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple CLI utility which creates a GPX track file 3 | from a binary UBX dump (such as that created by 4 | PyGPSClient's datalogging facility) using pyubx2.UBXReader(). 5 | 6 | Dump must contain UBX NAV-PVT, NAV-POSLLH or NAV-HPPOSLLH messages. 7 | If the message doesn't include an explicit date, the utility will use 8 | today's date in conjunction with the message iTOW. 9 | 10 | There are a number of free online GPX viewers 11 | e.g. https://maplorer.com/view_gpx.html 12 | 13 | Could have used minidom for XML but didn't seem worth it. 14 | 15 | Created on 27 Oct 2020 16 | 17 | @author: semuadmin 18 | """ 19 | 20 | import os 21 | from datetime import datetime, date 22 | from time import strftime 23 | from pyubx2 import UBXReader, VALCKSUM, itow2utc 24 | import pyubx2.exceptions as ube 25 | 26 | XML_HDR = '' 27 | 28 | GPX_NS = " ".join( 29 | ( 30 | 'xmlns="http://www.topografix.com/GPX/1/1"', 31 | 'creator="pyubx2" version="1.1"', 32 | 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"', 33 | 'xsi:schemaLocation="http://www.topografix.com/GPX/1/1', 34 | 'http://www.topografix.com/GPX/1/1/gpx.xsd"', 35 | ) 36 | ) 37 | GITHUB_LINK = "https://github.com/semuconsulting/pyubx2" 38 | 39 | 40 | class UBXTracker: 41 | """ 42 | UBXTracker class. 43 | """ 44 | 45 | def __init__(self, infile, outdir): 46 | """ 47 | Constructor. 48 | """ 49 | 50 | self._filename = infile 51 | self._outdir = outdir 52 | self._infile = None 53 | self._trkfname = None 54 | self._trkfile = None 55 | self._ubxreader = None 56 | self._connected = False 57 | 58 | def open(self): 59 | """ 60 | Open datalog file. 61 | """ 62 | 63 | self._infile = open(self._filename, "rb") 64 | self._connected = True 65 | 66 | def close(self): 67 | """ 68 | Close datalog file. 69 | """ 70 | 71 | if self._connected and self._infile: 72 | self._infile.close() 73 | 74 | def reader(self): 75 | """ 76 | Reads and parses UBX message data from stream 77 | using UBXReader iterator method 78 | """ 79 | 80 | i = 0 81 | self._ubxreader = UBXReader(self._infile, validate=VALCKSUM) 82 | 83 | self.write_gpx_hdr() 84 | 85 | for _, msg in self._ubxreader: 86 | try: 87 | if msg.identity == "NAV-PVT": 88 | time = ( 89 | datetime( 90 | msg.year, msg.month, msg.day, msg.hour, msg.min, msg.second 91 | ).isoformat() 92 | + "Z" 93 | ) 94 | if msg.fixType == 3: 95 | fix = "3d" 96 | elif msg.fixType == 2: 97 | fix = "2d" 98 | else: 99 | fix = "none" 100 | self.write_gpx_trkpnt( 101 | msg.lat, 102 | msg.lon, 103 | ele=msg.hMSL / 1000, # height in meters 104 | time=time, 105 | fix=fix, 106 | ) 107 | if msg.identity in ("NAV-POSLLH", "NAV-HPPOSLLH"): 108 | time = ( 109 | date.today().isoformat() 110 | + "T" 111 | + itow2utc(msg.iTOW).isoformat() 112 | + "Z" 113 | ) 114 | self.write_gpx_trkpnt( 115 | msg.lat, 116 | msg.lon, 117 | ele=msg.hMSL / 1000, # height in meters 118 | time=time, 119 | ) 120 | 121 | i += 1 122 | except (ube.UBXMessageError, ube.UBXTypeError, ube.UBXParseError) as err: 123 | print(f"Something went wrong {err}") 124 | continue 125 | 126 | self.write_gpx_tlr() 127 | 128 | print(f"\n{i} NAV message{'' if i == 1 else 's'} read from {self._filename}") 129 | print(f"{i} trackpoint{'' if i == 1 else 's'} written to {self._trkfname}") 130 | 131 | def write_gpx_hdr(self): 132 | """ 133 | Open gpx file and write GPX track header tags 134 | """ 135 | 136 | timestamp = strftime("%Y%m%d%H%M%S") 137 | self._trkfname = os.path.join(self._outdir, f"gpxtrack-{timestamp}.gpx") 138 | self._trkfile = open(self._trkfname, "a") 139 | 140 | date = datetime.now().isoformat() + "Z" 141 | gpxtrack = ( 142 | XML_HDR + "" 143 | f"" 144 | f'pyubx2' 145 | "" 146 | "GPX track from UBX NAV-PVT datalog" 147 | ) 148 | 149 | self._trkfile.write(gpxtrack) 150 | 151 | def write_gpx_trkpnt(self, lat: float, lon: float, **kwargs): 152 | """ 153 | Write GPX track point from NAV-PVT message content 154 | """ 155 | 156 | trkpnt = f'' 157 | 158 | # these are the permissible elements in the GPX schema for wptType 159 | # http://www.topografix.com/GPX/1/1/#type_wptType 160 | for tag in ( 161 | "ele", 162 | "time", 163 | "magvar", 164 | "geoidheight", 165 | "name", 166 | "cmt", 167 | "desc", 168 | "src", 169 | "link", 170 | "sym", 171 | "type", 172 | "fix", 173 | "sat", 174 | "hdop", 175 | "vdop", 176 | "pdop", 177 | "ageofdgpsdata", 178 | "dgpsid", 179 | "extensions", 180 | ): 181 | if tag in kwargs: 182 | val = kwargs[tag] 183 | trkpnt += f"<{tag}>{val}" 184 | 185 | trkpnt += "" 186 | 187 | self._trkfile.write(trkpnt) 188 | 189 | def write_gpx_tlr(self): 190 | """ 191 | Write GPX track trailer tags and close file 192 | """ 193 | 194 | gpxtrack = "" 195 | self._trkfile.write(gpxtrack) 196 | self._trkfile.close() 197 | 198 | 199 | if __name__ == "__main__": 200 | print("UBX datalog to GPX file converter\n") 201 | infilep = input("Enter input UBX datalog file: ").strip('"') 202 | outdirp = input("Enter output directory: ").strip('"') 203 | tkr = UBXTracker(infilep, outdirp) 204 | print(f"\nProcessing file {infilep}...") 205 | tkr.open() 206 | tkr.reader() 207 | tkr.close() 208 | print("\nOperation Complete") 209 | -------------------------------------------------------------------------------- /examples/mon_span.ubx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/pyubx2/43e9a9d4a2f4cad0191ec42c1468efcec744cbab/examples/mon_span.ubx -------------------------------------------------------------------------------- /examples/mon_span_spectrum.py: -------------------------------------------------------------------------------- 1 | """ 2 | mon_span_spectrum.py 3 | 4 | Simple illustration of how to plot MON-SPAN spectrum data 5 | as a spectrum analysis chart using pyubx2 and matplotlib. 6 | 7 | Each MON-SPAN message can contain multiple RF Blocks. 8 | 9 | The sample mon_span.ubx file contains multiple MON-SPAN 10 | messages from M9N and F9P receivers, containing one (L1) 11 | and two (L1, L2) frequency blocks respectively. 12 | 13 | Created on 19 Nov 2020 14 | 15 | :author: semuadmin 16 | :copyright: SEMU Consulting © 2020 17 | :license: BSD 3-Clause 18 | """ 19 | 20 | import matplotlib.pyplot as plt 21 | import numpy as np 22 | from pyubx2 import UBXMessage, UBXReader 23 | 24 | RF_SIGS = { 25 | "L1": 1.57542, 26 | "L2": 1.22760, 27 | "L5": 1.17645, 28 | } 29 | 30 | 31 | def plot_spectrum(msg: UBXMessage): 32 | """ 33 | Plot frequency spectrum from MON-SPAN message 34 | 35 | :param UBXMessage msg: MON-SPAN message 36 | """ 37 | 38 | # MON-SPAN message can contain multiple RF blocks 39 | numrf = msg.numRfBlocks 40 | 41 | # plot each RF block 42 | maxdb = 0 43 | minhz = 999 * 1e9 44 | maxhz = 0 45 | for i in range(1, numrf + 1): 46 | # get MON-SPAN message attributes for this RF block 47 | idx = f"_{i:02}" 48 | spec = getattr(msg, "spectrum" + idx) 49 | spn = getattr(msg, "span" + idx) 50 | res = getattr(msg, "res" + idx) 51 | ctr = getattr(msg, "center" + idx) 52 | pga = getattr(msg, "pga" + idx) 53 | 54 | # set data coordinates 55 | x_axis = np.arange(ctr - spn / 2, ctr + spn / 2, res) / 1e9 # plot as GHz 56 | y_axis = np.array(spec) # - pga # adjust by receiver gain 57 | minhz = min(minhz, np.min(x_axis)) 58 | maxhz = max(maxhz, np.max(x_axis)) 59 | maxdb = max(maxdb, np.max(y_axis)) 60 | 61 | # create plot 62 | plt.plot(x_axis, y_axis, label=f"RF {i}") 63 | 64 | # plot L1, L2, L5 markers if within frequency span 65 | for nam, frq in RF_SIGS.items(): 66 | if minhz < frq < maxhz: 67 | x_axis = np.array([frq, frq]) 68 | y_axis = np.array([0, maxdb]) 69 | plt.plot(x_axis, y_axis, label=f"{nam}", linestyle="dotted") 70 | 71 | # display plot 72 | plt.title("MON-SPAN Spectrum Analysis") 73 | plt.legend(fontsize="small") 74 | plt.xlabel("GHz") 75 | plt.ylabel("dB") 76 | plt.ylim(bottom=0) 77 | plt.grid() 78 | plt.show() 79 | 80 | 81 | if __name__ == "__main__": 82 | # read binary UBX data stream containing one or more MON-SPAN messages 83 | with open("mon_span.ubx", "rb") as stream: 84 | ubr = UBXReader(stream) 85 | for raw_data, parsed_data in ubr: 86 | if parsed_data.identity == "MON-SPAN": 87 | plot_spectrum(parsed_data) 88 | -------------------------------------------------------------------------------- /examples/socket_server.py: -------------------------------------------------------------------------------- 1 | """ 2 | TCP socket server for PyGPSClient application. 3 | 4 | (could also be used independently of a tkinter app framework) 5 | 6 | Reads raw data from GNSS receiver message queue and 7 | outputs this to multiple TCP socket clients. 8 | 9 | Operates in two modes according to ntripmode setting: 10 | 11 | 0 - open socket mode - will stream GNSS data to any connected client 12 | without authentication. 13 | 1 - NTRIP server mode - implements NTRIP server protocol and will 14 | respond to NTRIP client authentication, sourcetable and RTCM3 data 15 | stream requests. 16 | NB: THIS ASSUMES THE CONNECTED GNSS RECEIVER IS OPERATING IN BASE 17 | STATION (SURVEY-IN OR FIXED) MODE AND OUTPUTTING THE RELEVANT RTCM3 MESSAGES. 18 | 19 | For NTRIP mode, set authorization credentials via env variables: 20 | export PYGPSCLIENT_USER="user" 21 | export PYGPSCLIENT_PASSWORD="password" 22 | 23 | Created on 16 May 2022 24 | 25 | :author: semuadmin 26 | :copyright: SEMU Consulting © 2022 27 | :license: BSD 3-Clause 28 | """ 29 | 30 | from os import getenv 31 | from socketserver import ThreadingTCPServer, StreamRequestHandler 32 | from threading import Thread, Event 33 | from queue import Queue 34 | from base64 import b64encode 35 | from datetime import datetime, timezone 36 | 37 | # from pygpsclient import version as PYGPSVERSION 38 | 39 | RTCM = b"rtcm" 40 | BUFSIZE = 1024 41 | PYGPSMP = "pygpsclient" 42 | PYGPSVERSION = "1.3.5" 43 | 44 | 45 | class SocketServer(ThreadingTCPServer): 46 | """ 47 | Socket server class. 48 | 49 | This instantiates a daemon ClientHandler thread for each 50 | connected client. 51 | """ 52 | 53 | def __init__( 54 | self, app, ntripmode: int, maxclients: int, msgqueue: Queue, *args, **kwargs 55 | ): 56 | """ 57 | Overridden constructor. 58 | 59 | :param Frame app: reference to main application class (if any) 60 | :param int ntripmode: 0 = open socket server, 1 = NTRIP server 61 | :param int maxclients: max no of clients allowed 62 | :param Queue msgqueue: queue containing raw GNSS messages 63 | """ 64 | 65 | self.__app = app # Reference to main application class 66 | 67 | self._ntripmode = ntripmode 68 | self._maxclients = maxclients 69 | self._msgqueue = msgqueue 70 | self._connections = 0 71 | self._stream_thread = None 72 | self._stopmqread = Event() 73 | # set up pool of client queues 74 | self.clientqueues = [] 75 | for _ in range(self._maxclients): 76 | self.clientqueues.append({"client": None, "queue": Queue()}) 77 | self._start_read_thread() 78 | self.daemon_threads = True # stops deadlock on abrupt termination 79 | 80 | super().__init__(*args, **kwargs) 81 | 82 | def server_close(self): 83 | """ 84 | Overridden server close routine. 85 | """ 86 | 87 | self.stop_read_thread() 88 | super().server_close() 89 | 90 | def _start_read_thread(self): 91 | """ 92 | Start GNSS message reader thread. 93 | """ 94 | 95 | while not self._msgqueue.empty(): # flush queue 96 | self._msgqueue.get() 97 | 98 | self._stopmqread.clear() 99 | self._stream_thread = Thread( 100 | target=self._read_thread, 101 | args=(self._stopmqread, self._msgqueue, self.clientqueues), 102 | daemon=True, 103 | ) 104 | self._stream_thread.start() 105 | 106 | def stop_read_thread(self): 107 | """ 108 | Stop GNSS message reader thread. 109 | """ 110 | 111 | self._stopmqread.set() 112 | 113 | def _read_thread(self, stopmqread: Event, msgqueue: Queue, clientqueues: dict): 114 | """ 115 | THREADED 116 | Read from main GNSS message queue and place 117 | raw data on an output queue for each connected client. 118 | 119 | :param Event stopmqread: stop event for mq read thread 120 | :param Queue msgqueue: input message queue 121 | :param Dict clientqueues: pool of output queues for use by clients 122 | """ 123 | 124 | while not stopmqread.is_set(): 125 | raw = msgqueue.get() 126 | for i in range(self._maxclients): 127 | # if client connected to this queue 128 | if clientqueues[i]["client"] is not None: 129 | clientqueues[i]["queue"].put(raw) 130 | 131 | @property 132 | def credentials(self) -> bytes: 133 | """ 134 | Getter for basic authorization credentials. 135 | 136 | Assumes credentials have been defined in 137 | environment variables PYGPSCLIENT_USER and 138 | PYGPSCLIENT_PASSWORD 139 | """ 140 | 141 | user = getenv("PYGPSCLIENT_USER") 142 | password = getenv("PYGPSCLIENT_PASSWORD") 143 | if user is None or password is None: 144 | return None 145 | user = user + ":" + password 146 | return b64encode(user.encode(encoding="utf-8")) 147 | 148 | @property 149 | def connections(self): 150 | """ 151 | Getter for client connections. 152 | """ 153 | 154 | return self._connections 155 | 156 | @connections.setter 157 | def connections(self, clients: int): 158 | """ 159 | Setter for client connections. 160 | Also updates no. of clients on settings panel. 161 | 162 | :param int clients: no of client connections 163 | """ 164 | 165 | self._connections = clients 166 | if hasattr(self.__app, "update_clients"): 167 | self.__app.update_clients(self._connections) 168 | 169 | @property 170 | def ntripmode(self) -> int: 171 | """ 172 | Getter for ntrip mode. 173 | 174 | :return: 0 = open socket server, 1 = ntrip mode 175 | :rtype: int 176 | """ 177 | 178 | return self._ntripmode 179 | 180 | @property 181 | def latlon(self) -> tuple: 182 | """ 183 | Get current lat / lon from receiver. 184 | 185 | :return=: tuple of (lat, lon) 186 | :rtype: tuple 187 | """ 188 | 189 | if hasattr(self.__app, "gnss_status"): 190 | return (self.__app.gnss_status.lat, self.__app.gnss_status.lon) 191 | else: 192 | return ("", "") 193 | 194 | 195 | class ClientHandler(StreamRequestHandler): 196 | """ 197 | Threaded TCP client connection handler class. 198 | """ 199 | 200 | def __init__(self, *args, **kwargs): 201 | """ 202 | Overridden constructor. 203 | """ 204 | 205 | self._qidx = None 206 | self._msgqueue = None 207 | self._allowed = False 208 | 209 | super().__init__(*args, **kwargs) 210 | 211 | def setup(self, *args, **kwargs): 212 | """ 213 | Overridden client handler setup routine. 214 | Allocates available message queue to client. 215 | """ 216 | 217 | # find next unused client queue in pool... 218 | for i, clq in enumerate(self.server.clientqueues): 219 | if clq["client"] is None: 220 | self.server.clientqueues[i]["client"] = self.client_address[1] 221 | self._msgqueue = clq["queue"] 222 | while not self._msgqueue.empty(): # flush queue 223 | self._msgqueue.get() 224 | self._qidx = i 225 | self._allowed = True 226 | break 227 | if self._qidx is None: # no available client queues in pool 228 | return 229 | 230 | if self._allowed: 231 | self.server.connections = self.server.connections + 1 232 | super().setup(*args, **kwargs) 233 | 234 | def finish(self, *args, **kwargs): 235 | """ 236 | Overridden client handler finish routine. 237 | De-allocates message queue from client. 238 | """ 239 | 240 | if self._qidx is not None: 241 | self.server.clientqueues[self._qidx]["client"] = None 242 | 243 | if self._allowed: 244 | self.server.connections = self.server.connections - 1 245 | super().finish(*args, **kwargs) 246 | 247 | def handle(self): 248 | """ 249 | Overridden main client handler. 250 | 251 | If in NTRIP server mode, will respond to NTRIP client authentication 252 | and sourcetable requests and, if valid, stream relevant RTCM3 data 253 | from the input message queue to the socket. 254 | 255 | If in open socket server mode, will simply stream content of 256 | input message queue to the socket. 257 | """ 258 | 259 | while self._allowed: # if connection allowed, loop until terminated 260 | 261 | try: 262 | 263 | if self.server.ntripmode: # NTRIP server mode 264 | 265 | self.data = self.request.recv(BUFSIZE) 266 | resp = self._process_ntrip_request(self.data) 267 | if resp is None: 268 | break 269 | if resp == RTCM: # start RTCM3 stream 270 | while True: 271 | self._write_from_mq() 272 | else: # sourcetable or error response 273 | self.wfile.write(resp) 274 | self.wfile.flush() 275 | 276 | else: # open socket server mode 277 | 278 | self._write_from_mq() 279 | 280 | except ( 281 | ConnectionRefusedError, 282 | ConnectionAbortedError, 283 | ConnectionResetError, 284 | BrokenPipeError, 285 | TimeoutError, 286 | ): 287 | break 288 | 289 | def _process_ntrip_request(self, data: bytes) -> bytes: 290 | """ 291 | Process NTRIP client request. 292 | 293 | :param bytes data: client request 294 | :return: client response 295 | :rtype: bytes or None if request rejected 296 | """ 297 | 298 | strreq = False 299 | authorized = False 300 | validmp = False 301 | mountpoint = "" 302 | 303 | request = data.strip().split(b"\r\n") 304 | for part in request: 305 | if part[0:21] == b"Authorization: Basic ": 306 | authorized = part[21:] == self.server.credentials 307 | if part[0:3] == b"GET": 308 | get = part.split(b" ") 309 | mountpoint = get[1].decode("utf-8") 310 | if mountpoint == "": # no mountpoint, hence sourcetable request 311 | strreq = True 312 | elif mountpoint == f"/{PYGPSMP}": # valid mountpoint 313 | validmp = True 314 | 315 | if not authorized: # respond with 401 316 | http = ( 317 | self._format_http_header(401) 318 | + f'WWW-Authenticate: Basic realm="{mountpoint}"\r\n' 319 | + "Connection: close\r\n" 320 | ) 321 | return bytes(http, "UTF-8") 322 | if strreq or (not strreq and not validmp): # respond with nominal sourcetable 323 | http = self._format_sourcetable() 324 | return bytes(http, "UTF-8") 325 | if validmp: # respond by opening RTCM3 stream 326 | return RTCM 327 | return None 328 | 329 | def _format_sourcetable(self) -> str: 330 | """ 331 | Format nominal HTTP sourcetable response. 332 | 333 | :return: HTTP response string 334 | :rtype: str 335 | """ 336 | 337 | lat, lon = self.server.latlon 338 | ipaddr, port = self.server.server_address 339 | # sourcetable based on ZED-F9P capabilities 340 | sourcetable = ( 341 | f"STR;{PYGPSMP};PyGPSClient;RTCM 3.3;" 342 | + "1005(5),1077(1),1087(1),1097(1),1127(1),1230(1);" 343 | + f"0;GPS+GLO+GAL+BEI;SNIP;SRB;{lat};{lon};1;0;sNTRIP;none;N;N;0;\r\n" 344 | ) 345 | sourcefooter = ( 346 | f"NET;SNIP;PyGPSClient;N;N;PyGPSClient;{ipaddr}:{port};info@semuconsulting.com;;\r\n" 347 | + "ENDSOURCETABLE\r\n" 348 | ) 349 | http = ( 350 | self._format_http_header(200) 351 | + "Connection: close\r\n" 352 | + "Content-Type: gnss/sourcetable\r\n" 353 | + f"Content-Length: {len(sourcetable) + len(sourcefooter)}\r\n" 354 | + sourcetable 355 | + sourcefooter 356 | ) 357 | return http 358 | 359 | def _format_http_header(self, code: int = 200) -> str: 360 | """ 361 | Format HTTP NTRIP header. 362 | 363 | :param int code: HTTP response code (200) 364 | :return: HTTP NTRIP header 365 | :rtype: str 366 | """ 367 | 368 | codes = {200: "OK", 401: "Unauthorized", 403: "Forbidden", 404: "Not Found"} 369 | 370 | dat = datetime.now(timezone.utc) 371 | server_date = dat.strftime("%d %b %Y") 372 | http_date = dat.strftime("%a, %d %b %Y %H:%M:%S %Z") 373 | header = ( 374 | f"HTTP/1.1 {code} {codes[code]}\r\n" 375 | + "Ntrip-Version: Ntrip/2.0\r\n" 376 | + "Ntrip-Flags: \r\n" 377 | + f"Server: PyGPSClient_NTRIP_Caster_{PYGPSVERSION}/of:{server_date}\r\n" 378 | + f"Date: {http_date}\r\n" 379 | ) 380 | return header 381 | 382 | def _write_from_mq(self): 383 | """ 384 | Get data from message queue and write to socket. 385 | """ 386 | 387 | raw = self._msgqueue.get() 388 | if raw is not None: 389 | self.wfile.write(raw) 390 | self.wfile.flush() 391 | -------------------------------------------------------------------------------- /examples/tcpserver_threaded.py: -------------------------------------------------------------------------------- 1 | """ 2 | FOR TESTING ONLY 3 | 4 | Threaded TCP socket server test harness. 5 | 6 | Sends arbitrary NMEA, UBX & RTCM3 messages to connected clients. 7 | 8 | Created on 26 Apr 2022 9 | 10 | :author: semuadmin 11 | :copyright: SEMU Consulting © 2022 12 | :license: BSD 3-Clause 13 | """ 14 | 15 | from socketserver import ThreadingTCPServer, StreamRequestHandler 16 | import random 17 | 18 | # from serial import Serial 19 | from datetime import datetime 20 | 21 | from time import sleep 22 | from pyubx2 import UBXMessage, GET # , UBXReader 23 | from pynmeagps import NMEAMessage 24 | from pyrtcm import RTCMMessage 25 | 26 | SERPORT = "/dev/tty.usbmodem141101" 27 | BAUD = 9600 28 | TIMEOUT = 3 29 | HOST = "localhost" 30 | PORT = 50012 # Arbitrary non-privileged port 31 | DELAY = 0.25 32 | 33 | 34 | class GNSSServer(StreamRequestHandler): 35 | """ 36 | Threaded TCP client connection handler class. 37 | """ 38 | 39 | @staticmethod 40 | def create_unknownUBX_msg() -> UBXMessage: 41 | """ 42 | Create unknown UBX message to test error handling. 43 | """ 44 | 45 | return b"\xb5b\x06\x99\x08\x00\xf0\x01\x00\x01\x00\x01\x00\x00\x9a\xba" 46 | 47 | @staticmethod 48 | def create_UBX_msg() -> UBXMessage: 49 | """ 50 | Create arbitrary UBX message. 51 | """ 52 | # pylint: disable=invalid-name 53 | 54 | dat = datetime.now() 55 | msg = UBXMessage( 56 | "NAV", 57 | "NAV-PVT", 58 | GET, 59 | year=dat.year, 60 | month=dat.month, 61 | day=dat.day, 62 | hour=dat.hour, 63 | min=dat.minute, 64 | second=dat.second, 65 | validDate=1, 66 | validTime=1, 67 | fixType=3, 68 | lat=random.uniform(-90.0, 90.0), 69 | lon=random.uniform(-180.0, 180.0), 70 | hMSL=random.randint(0, 100000), 71 | numSV=random.randint(1, 26), 72 | ) 73 | return msg.serialize() 74 | # or use live stream from receiver (clunky) ... 75 | # with Serial(SERPORT, BAUD, timeout=TIMEOUT) as stream: 76 | # ubr = UBXReader(stream, protfilter=7) 77 | # raw, _ = ubr.read() 78 | # return raw 79 | 80 | @staticmethod 81 | def create_NMEA_msg() -> NMEAMessage: 82 | """ 83 | Create arbitrary NMEA message. 84 | """ 85 | # pylint: disable=invalid-name 86 | 87 | lat = random.uniform(-90.0, 90.0) 88 | lon = random.uniform(-180.0, 180.0) 89 | msg = NMEAMessage( 90 | "GN", 91 | "GLL", 92 | GET, 93 | lat=lat, 94 | lon=lon, 95 | NS="N" if lat > 0 else "S", 96 | EW="E" if lon > 0 else "W", 97 | status="A", 98 | posMode="A", 99 | ) 100 | return msg.serialize() 101 | 102 | @staticmethod 103 | def create_RTCM3_msg() -> RTCMMessage: 104 | """ 105 | Create arbitrary RTCM3 message. 106 | """ 107 | # pylint: disable=invalid-name 108 | 109 | msg = RTCMMessage( 110 | payload=b">\xd0\x00\x03\x8aX\xd9I<\x87/4\x10\x9d\x07\xd6\xafH " 111 | ) 112 | return msg.serialize() 113 | 114 | def handle(self): 115 | """ 116 | Handle client connection. 117 | """ 118 | 119 | print(f"Client connected: {self.client_address[0]}:{self.client_address[1]}") 120 | while True: 121 | try: 122 | # put multiple random msgs on buffer, mixed in with junk 123 | # to exercise the clients' parsing routine 124 | data = bytearray() 125 | n = random.randint(1, 5) 126 | for _ in range(n): 127 | r = random.randint(1, 7) 128 | if r in (1, 2, 3): 129 | data += self.create_NMEA_msg() + b"\x04\x05\x06" 130 | elif r == 4: 131 | data += self.create_RTCM3_msg() + b"\x07\x08\x09" 132 | elif r == 5: 133 | data += self.create_unknownUBX_msg() + b"\x03\x04\x05" 134 | else: 135 | data += self.create_UBX_msg() + b"\x01\x02\x03" 136 | # data = self.create_UBX_msg() 137 | if data is not None: 138 | self.wfile.write(data) 139 | self.wfile.flush() 140 | sleep(DELAY) 141 | except (ConnectionAbortedError, BrokenPipeError): 142 | print( 143 | f"Client disconnected: {self.client_address[0]}:{self.client_address[1]}" 144 | ) 145 | break 146 | 147 | 148 | if __name__ == "__main__": 149 | 150 | print(f"Creating TCP server on {HOST}:{PORT}") 151 | server = ThreadingTCPServer((HOST, PORT), GNSSServer) 152 | 153 | print("Starting TCP server, waiting for client connections...") 154 | try: 155 | server.serve_forever() 156 | except KeyboardInterrupt: 157 | print("TCP server terminated by user") 158 | -------------------------------------------------------------------------------- /examples/ubxconfigdb.py: -------------------------------------------------------------------------------- 1 | """ 2 | ubxconfigdb.py 3 | 4 | This example illustrates how to send UBX configuration database 5 | commands (CFG-VALSET & CFG-VALDEL) to a receiver while simultaneously 6 | reading CFG-VALGET responses and acknowledgements from the receiver. 7 | 8 | python3 ubxconfigdb.py port="/dev/ttyACM0" baudrate=38400 timeout=0.1 9 | 10 | You can use any of the configuration database keys defined in 11 | UBX_CONFIG_DATABASE. 12 | 13 | NB: IF YOU'RE MODIFYING PARAMETERS RELATING TO THE DEVICE'S SERIAL 14 | PORT CONFIGURATION (E.G. "CFG_UART1_BAUDRATE"), YOU WILL NEED 15 | TO DISCONNECT THE DEVICE AND RECONNECT USING THE UPDATED SERIAL 16 | CONFIGURATION BEFORE MAKING ANY FURTHER CONFIGURATION CHANGES. 17 | 18 | NB: These will only work on Generation 9+ devices running UBX protocol 19 | 23.01 or later (e.g. NEO-M9N or ZED-F9P). 20 | 21 | It connects to the receiver's serial port and sets up a 22 | UBXReader read thread. With the read thread running 23 | in the background, it sends a series of CFG-VAL* commands to 24 | the device to apply the designated configuration commands. 25 | 26 | The read thread reads and parses any responses and outputs 27 | them to the terminal. 28 | 29 | Created on 2 Oct 2020 30 | 31 | @author: semuadmin 32 | """ 33 | 34 | from queue import Queue 35 | from sys import argv 36 | from threading import Event, Thread 37 | from time import sleep 38 | 39 | from serial import Serial 40 | 41 | from pyubx2 import ( 42 | POLL_LAYER_BBR, 43 | POLL_LAYER_RAM, 44 | SET_LAYER_BBR, 45 | UBX_PROTOCOL, 46 | UBXMessage, 47 | UBXReader, 48 | ) 49 | 50 | # example configuration database keys and values 51 | # you could use any of the available keys in UBX_CONFIG_DATABASE 52 | # provided they are appropriate for your particular device 53 | # BUT note proviso above re. changing serial port configuration 54 | 55 | CONFIG_KEY1 = "CFG_MSGOUT_UBX_MON_COMMS_UART1" 56 | CONFIG_VAL1 = 1 57 | CONFIG_KEY2 = "CFG_MSGOUT_UBX_MON_TXBUF_UART1" 58 | CONFIG_VAL2 = 1 59 | 60 | 61 | def io_data( 62 | ubr: UBXReader, 63 | readqueue: Queue, 64 | sendqueue: Queue, 65 | stop: Event, 66 | ): 67 | """ 68 | THREADED 69 | Read and parse inbound UBX data and place 70 | raw and parsed data on queue. 71 | 72 | Send any queued outbound messages to receiver. 73 | """ 74 | # pylint: disable=broad-exception-caught 75 | 76 | while not stop.is_set(): 77 | try: 78 | (raw_data, parsed_data) = ubr.read() 79 | if parsed_data: 80 | readqueue.put((raw_data, parsed_data)) 81 | 82 | # refine this if outbound message rates exceed inbound 83 | while not sendqueue.empty(): 84 | data = sendqueue.get(False) 85 | if data is not None: 86 | ubr.datastream.write(data.serialize()) 87 | sendqueue.task_done() 88 | 89 | except Exception as err: 90 | print(f"\n\nSomething went wrong - {err}\n\n") 91 | continue 92 | 93 | 94 | def process_data(queue: Queue, stop: Event): 95 | """ 96 | THREADED 97 | Get UBX data from queue and display. 98 | """ 99 | 100 | while not stop.is_set(): 101 | if queue.empty() is False: 102 | (_, parsed) = queue.get() 103 | print(parsed) 104 | queue.task_done() 105 | 106 | 107 | def main(**kwargs): 108 | """ 109 | Main Routine. 110 | """ 111 | 112 | port = kwargs.get("port", "/dev/ttyACM0") 113 | baudrate = int(kwargs.get("baudrate", 38400)) 114 | timeout = float(kwargs.get("timeout", 0.1)) 115 | read_queue = Queue() 116 | send_queue = Queue() 117 | stop_event = Event() 118 | 119 | with Serial(port, baudrate, timeout=timeout) as stream: 120 | # create UBXReader instance, reading only UBX messages 121 | ubxreader = UBXReader(stream, protfilter=UBX_PROTOCOL) 122 | 123 | stop_event.clear() 124 | io_thread = Thread( 125 | target=io_data, 126 | args=( 127 | ubxreader, 128 | read_queue, 129 | send_queue, 130 | stop_event, 131 | ), 132 | daemon=True, 133 | ) 134 | process_thread = Thread( 135 | target=process_data, 136 | args=( 137 | read_queue, 138 | stop_event, 139 | ), 140 | daemon=True, 141 | ) 142 | 143 | print("\nStarting handler threads. Press Ctrl-C to terminate...") 144 | io_thread.start() 145 | process_thread.start() 146 | 147 | # proceed until user presses Ctrl-C 148 | while not stop_event.is_set(): 149 | try: 150 | 151 | # STEP 1: poll the existing configuration in volate memory (for comparison) 152 | print( 153 | "\nPolling UART configuration in the volatile RAM memory layer via CFG-VALGET..." 154 | ) 155 | print("(This should result in ACK-ACK and CFG-VALGET responses)") 156 | position = 0 157 | layer = POLL_LAYER_RAM # volatile memory 158 | keys = [CONFIG_KEY1, CONFIG_KEY2] 159 | msg = UBXMessage.config_poll(layer, position, keys) 160 | send_queue.put(msg) 161 | sleep(1) 162 | 163 | # STEP 2: poll the existing configuration in non-volatile memory (battery-backed RAM or BBR) 164 | print( 165 | "\nPolling UART configuration in the BBR memory layer via CFG-VALGET...", 166 | "\n(This should result in an ACK-NAK response in the ", 167 | "absence of an existing BBR configuration setting)", 168 | ) 169 | layer = POLL_LAYER_BBR 170 | keys = [CONFIG_KEY1, CONFIG_KEY2] 171 | msg = UBXMessage.config_poll(layer, position, keys) 172 | send_queue.put(msg) 173 | sleep(1) 174 | 175 | # STEP 3: set the configuration in the non-volatile memory layer 176 | # *** NB: SET and DEL messages use different memory layer values to POLL *** 177 | print( 178 | "\nSetting UART configuration in the BBR memory layer via CFG-VALSET...", 179 | "\n(This should result in an ACK-ACK response)", 180 | ) 181 | transaction = 0 182 | layers = SET_LAYER_BBR 183 | cfgdata = [(CONFIG_KEY1, CONFIG_VAL1), (CONFIG_KEY2, CONFIG_VAL2)] 184 | msg = UBXMessage.config_set(layers, transaction, cfgdata) 185 | send_queue.put(msg) 186 | sleep(2) 187 | 188 | # STEP 4: poll the newly-set configuration in the non-volatile memory layer 189 | print( 190 | "\nPolling UART configuration in the BBR memory layer via CFG-VALGET...", 191 | "\n(This should result in ACK-ACK and CFG-VALGET responses, provided the", 192 | "configuration is valid for your particular device)", 193 | ) 194 | position = 0 195 | layer = POLL_LAYER_BBR 196 | keys = [CONFIG_KEY1, CONFIG_KEY2] 197 | msg = UBXMessage.config_poll(layer, position, keys) 198 | send_queue.put(msg) 199 | sleep(2) 200 | 201 | # STEP 5: unset (delete) the previously-set configuration in the non-volatile memory layer 202 | print( 203 | "\nUnsetting UART configuration in the BBR memory layer via CFG-VALDEL...", 204 | "\n(This should result in an ACK-ACK response)", 205 | ) 206 | layers = SET_LAYER_BBR 207 | keys = [CONFIG_KEY1, CONFIG_KEY2] 208 | msg = UBXMessage.config_del(layers, transaction, keys) 209 | send_queue.put(msg) 210 | sleep(2) 211 | 212 | # STEP 6: poll the configuration in the non-volatile memory layer 213 | # to check that the configuration has been removed 214 | print( 215 | "\nPolling UART configuration in the BBR memory", 216 | "layer via CFG-VALGET...", 217 | "\n(This should result in an ACK-NAK response as the", 218 | "BBR configuration setting has now been removed)", 219 | ) 220 | layer = POLL_LAYER_BBR 221 | keys = ["CFG_UART1_BAUDRATE", "CFG_UART2_BAUDRATE"] 222 | msg = UBXMessage.config_poll(layer, position, keys) 223 | send_queue.put(msg) 224 | sleep(2) 225 | print("\nCommands sent. Waiting for any final acknowledgements...\n") 226 | sleep(2) 227 | stop_event.set() 228 | print("\nStop signal set. Waiting for threads to complete...") 229 | 230 | except KeyboardInterrupt: # capture Ctrl-C 231 | print("\n\nTerminated by user.") 232 | stop_event.set() 233 | 234 | io_thread.join() 235 | process_thread.join() 236 | print("\nProcessing complete") 237 | 238 | 239 | if __name__ == "__main__": 240 | 241 | main(**dict(arg.split("=") for arg in argv[1:])) 242 | -------------------------------------------------------------------------------- /examples/ubxfactoryreset.py: -------------------------------------------------------------------------------- 1 | """ 2 | ubxfactoryreset.py 3 | 4 | This example illustrates how to send a UBX command to a receiver 5 | (in this case a CFG-CFG factory reset command) while 6 | simultaneously reading acknowledgements from the receiver. 7 | 8 | Usage: 9 | 10 | python3 ubxfactoryreset.py port="/dev/ttyACM0" baudrate=38400 timeout=0.1 11 | 12 | It connects to the receiver's serial port and sets up a 13 | UBXReader read thread. With the read thread running 14 | in the background, it sends a factory reset command CFG-CFG. 15 | 16 | The read thread picks up any acknowledgement and outputs 17 | it to the terminal. 18 | 19 | NB: THIS RESETS THE CURRENT CONFIGURATIONS IN ALL MEMORY 20 | LAYERS (BBR, Flash and EEPROM) - USE WITH CAUTION!! 21 | 22 | Created on 2 Oct 2020 23 | 24 | @author: semuadmin 25 | """ 26 | 27 | # pylint: disable=invalid-name 28 | 29 | from sys import argv 30 | from threading import Event, Lock, Thread 31 | from time import sleep 32 | 33 | from serial import Serial 34 | 35 | from pyubx2 import SET, UBX_PROTOCOL, UBXMessage, UBXReader 36 | 37 | 38 | def read_messages(stream, lock, stopevent, ubxreader): 39 | """ 40 | Reads, parses and prints out incoming UBX messages 41 | """ 42 | # pylint: disable=unused-variable, broad-except 43 | 44 | while not stopevent.is_set: 45 | if stream.in_waiting: 46 | try: 47 | lock.acquire() 48 | _, parsed_data = ubxreader.read() 49 | lock.release() 50 | if parsed_data: 51 | print(parsed_data) 52 | except Exception as err: 53 | print(f"\n\nSomething went wrong {err}\n\n") 54 | continue 55 | 56 | 57 | def start_thread(stream, lock, stopevent, ubxreader): 58 | """ 59 | Start read thread 60 | """ 61 | 62 | thr = Thread( 63 | target=read_messages, args=(stream, lock, stopevent, ubxreader), daemon=True 64 | ) 65 | thr.start() 66 | return thr 67 | 68 | 69 | def send_message(stream, lock, message): 70 | """ 71 | Send message to device 72 | """ 73 | 74 | lock.acquire() 75 | stream.write(message.serialize()) 76 | lock.release() 77 | 78 | 79 | def main(**kwargs): 80 | """ 81 | Main Routine. 82 | """ 83 | 84 | port = kwargs.get("port", "/dev/ttyACM0") 85 | baudrate = int(kwargs.get("baudrate", 38400)) 86 | timeout = float(kwargs.get("timeout", 0.1)) 87 | 88 | with Serial(port, baudrate, timeout=timeout) as stream: 89 | 90 | # create UBXReader instance, reading only UBX messages 91 | ubr = UBXReader(stream, protfilter=UBX_PROTOCOL) 92 | 93 | print("\nStarting read thread...\n") 94 | stopevent = Event() 95 | stopevent.clear() 96 | serial_lock = Lock() 97 | read_thread = start_thread(stream, serial_lock, stopevent, ubr) 98 | 99 | # send the factory reset command CFG-CFG 100 | print("\nSending factory reset command CFG-CFG...\n") 101 | msg = UBXMessage( 102 | "CFG", 103 | "CFG-CFG", 104 | SET, 105 | clearMask=b"\x1f\x1f\x00\x00", # clear everything 106 | loadMask=b"\x1f\x1f\x00\x00", # reload everything 107 | devBBR=1, # clear from battery-backed RAM 108 | devFlash=1, # clear from flash memory 109 | devEEPROM=1, # clear from EEPROM memory 110 | ) 111 | send_message(stream, serial_lock, msg) 112 | 113 | print("\nFactory reset command sent. Waiting for acknowledgement...\n") 114 | sleep(1) 115 | print("\nStopping reader thread...\n") 116 | stopevent.set() 117 | read_thread.join() 118 | print("\nProcessing Complete") 119 | 120 | 121 | if __name__ == "__main__": 122 | 123 | main(**dict(arg.split("=") for arg in argv[1:])) 124 | -------------------------------------------------------------------------------- /examples/ubxfile.py: -------------------------------------------------------------------------------- 1 | """ 2 | ubxfile.py 3 | 4 | Usage: 5 | 6 | python3 ubxfile.pyh filename=pygpsdata.log 7 | 8 | This example illustrates a simple example implementation of a 9 | UBXMessage and/or NMEAMessage binary logfile reader using the 10 | UBXReader iterator functions and an external error handler. 11 | 12 | Created on 25 Oct 2020 13 | 14 | @author: semuadmin 15 | """ 16 | 17 | from sys import argv 18 | 19 | from pyubx2.ubxreader import ( 20 | ERR_LOG, 21 | GET, 22 | UBX_PROTOCOL, 23 | VALCKSUM, 24 | UBXReader, 25 | NMEA_PROTOCOL, 26 | RTCM3_PROTOCOL, 27 | ) 28 | 29 | 30 | def errhandler(err): 31 | """ 32 | Handles errors output by iterator. 33 | """ 34 | 35 | print(f"\nERROR: {err}\n") 36 | 37 | 38 | def main(**kwargs): 39 | """ 40 | Main Routine. 41 | """ 42 | 43 | filename = kwargs.get("filename", "pygpsdata.log") 44 | 45 | print(f"Opening file {filename}...") 46 | with open(filename, "rb") as stream: 47 | 48 | count = 0 49 | 50 | ubr = UBXReader( 51 | stream, 52 | protfilter=UBX_PROTOCOL | NMEA_PROTOCOL | RTCM3_PROTOCOL, 53 | quitonerror=ERR_LOG, 54 | validate=VALCKSUM, 55 | msgmode=GET, 56 | parsebitfield=True, 57 | errorhandler=errhandler, 58 | ) 59 | for _, parsed_data in ubr: 60 | print(parsed_data) 61 | count += 1 62 | 63 | print(f"\n{count} messages read.\n") 64 | print("Test Complete") 65 | 66 | 67 | if __name__ == "__main__": 68 | 69 | main(**dict(arg.split("=") for arg in argv[1:])) 70 | -------------------------------------------------------------------------------- /examples/ubxfile_ucenter.py: -------------------------------------------------------------------------------- 1 | """ 2 | ubxfile_ucenter.py 3 | 4 | Usage: 5 | 6 | python3 ubxfile_ucenter.py filename="2023-4-17_82912_serial-COM3.ubx" 7 | 8 | This example illustrates how to suppress 'msgmode' warnings while 9 | parsing a u-center *.ubx recording containing mixed message modes 10 | (i.e. debug and configuration data): 11 | 12 | - GET - NAV messages and CFG-VALGET poll responses from receiver 13 | - POLL - CFG-VALGET polls to receiver 14 | - SET - CFG-VALSET commands to receiver 15 | 16 | The supplied "2023-4-17_82912_serial-COM3.ubx" file contains 17 | 951 GET messages, 27 SET messages and 70 POLL messages. 18 | 19 | Created on 22 Apr 2023 20 | 21 | @author: semuadmin 22 | """ 23 | 24 | from sys import argv 25 | 26 | from pyubx2 import ERR_IGNORE, GET, POLL, SET, UBXReader 27 | 28 | 29 | def main(**kwargs): 30 | """ 31 | Main Routine. 32 | """ 33 | 34 | filename = kwargs.get("filename", "2023-4-17_82912_serial-COM3.ubx") 35 | 36 | for mode in (GET, SET, POLL): 37 | i = 0 38 | with open(filename, "rb") as stream: 39 | ubr = UBXReader(stream, quitonerror=ERR_IGNORE, msgmode=mode) 40 | for _, parsed in ubr: 41 | if parsed is not None: 42 | i += 1 43 | print(parsed) 44 | 45 | print(f'\n{i} {("GET","SET","POLL")[mode]} messages parsed\n\n') 46 | 47 | 48 | if __name__ == "__main__": 49 | 50 | main(**dict(arg.split("=") for arg in argv[1:])) 51 | -------------------------------------------------------------------------------- /examples/ubxoptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | ubxoptions.py 3 | 4 | Series of worked examples illustrating the various options available for 5 | parsing and constructing UBX messages. The example used here is a CFG-GNSS 6 | (GNSS Configuration) message. 7 | 8 | Usage: 9 | 10 | python3 ubxoptions.py 11 | 12 | The examples may be run without connecting a receiver. 13 | 14 | The CFG-GNSS message contains configuration settings for one or more GNSS 15 | constellations and augmentation systems handled by the receiver (in this 16 | particular example the IMES setting is omitted). For each constellation, 17 | a bitfield property 'flags' (X4) contains two bit flags 'enable' (U1) and 18 | 'sigCfMask' (U8). 19 | 20 | The CFG-GNSS message serves as both a command input (SET) message, and an 21 | output (GET) message in response to a CFG-GNSS POLL. 22 | 23 | Created on 29 Sep 2021 24 | @author: semuadmin 25 | """ 26 | 27 | # from serial import Serial 28 | from pyubx2 import GET, SET, VALCKSUM, UBXMessage, UBXReader 29 | 30 | GPS = 0 31 | SBAS = 1 32 | GALILEO = 2 33 | BEIDOU = 3 34 | IMES = 4 35 | QZSS = 5 36 | GLONASS = 6 37 | 38 | # This is the raw CFG-GNSS UBX message, as output by a u-blox M9N receiver. 39 | CFG_GNSS = b"\xb5b\x06>4\x00\x00**\x06\x00\x08\x10\x00\x01\x00\x01\x00\x01\x03\x03\x00\x01\x00\x01\x00\x02\x08\x0c\x00\x01\x00\x01\x00\x03\x02\x05\x00\x01\x00\x01\x00\x05\x03\x04\x00\x01\x00\x05\x00\x06\x08\x0c\x00\x01\x00\x01\x00G\xde" 40 | print("\nHere is the raw CFG-GNSS message, as output by a u-blox M9N receiver:\n") 41 | print(CFG_GNSS) 42 | 43 | # To poll this message, we could create and send a CFG-GNSS POLL message to a connected receiver: 44 | # serialOut = Serial("/dev/ttyACM1", 9600, timeout=5) 45 | # msg0 = UBXMessage("CFG", "CFG-GNSS", POLL) 46 | # serialOut.write(msg0.serialize()) 47 | 48 | # This is the CFG-GNSS message parsed with all the optional keyword arguments set to their defaults 49 | # (the arguments could in this case be omitted). 50 | # Each bitfield is rendered as its individual bit flags 'enable_01=1, sigCfMask_01=1' etc. 51 | print("\nHere is the CFG-GNSS message parsed with default options:\n") 52 | msg1 = UBXReader.parse(CFG_GNSS, validate=VALCKSUM, msgmode=GET, parsebitfield=True) 53 | print(msg1) 54 | 55 | # This is the CFG-GNSS message parsed with 'parsebitfield' set to False and other options left at the defaults 56 | # Each bitfield is rendered as a sequence of bytes 'flags_01=b'\x01\x00\x01\x00' etc. 57 | print( 58 | "\nHere is the CFG-GNSS message parsed with the parsebitfield option set to False:\n" 59 | ) 60 | msg2 = UBXReader.parse(CFG_GNSS, parsebitfield=False) 61 | print(msg2) 62 | 63 | # Note that the raw payloads are identical; only the parsed format differs: 64 | print( 65 | f"\nThe raw payloads of these messages are identical, only the parsed format differs: {msg1.payload == msg2.payload}\n" 66 | ) 67 | print(msg1.payload) 68 | print(msg2.payload) 69 | 70 | # Now construct a new CFG-GNSS input (SET) message from the payload of the previously parsed GET message: 71 | print( 72 | "\nHere is a new CFG-GNSS input (SET) message constructed from the raw payload of the parsed (GET) message:\n" 73 | ) 74 | msg3 = UBXMessage("CFG", "CFG-GNSS", SET, payload=msg1.payload) 75 | print(msg3) 76 | 77 | # Now construct an identical message by setting all the individual property keywords. 78 | # Note that properties which are part of a repeating group must be suffixed with the appropriate index '_01', '_02', etc. 79 | # Note that we don't bother setting any reserved fields - they default to zeros, as will any other properties we omit to define 80 | # explicitly. 81 | print( 82 | "\nHere is a new CFG-GNSS input (SET) message constructed by setting all the individual property keywords:\n" 83 | ) 84 | msg4 = UBXMessage( 85 | "CFG", 86 | "CFG-GNSS", 87 | SET, 88 | msgVer=0, 89 | numTrkChHw=42, 90 | numTrkChUse=42, 91 | numConfigBlocks=6, 92 | gnssId_01=GPS, 93 | resTrkCh_01=8, 94 | maxTrkCh_01=16, 95 | enable_01=1, 96 | sigCfMask_01=1, 97 | gnssId_02=SBAS, 98 | resTrkCh_02=3, 99 | maxTrkCh_02=3, 100 | enable_02=1, 101 | sigCfMask_02=1, 102 | gnssId_03=GALILEO, 103 | resTrkCh_03=8, 104 | maxTrkCh_03=12, 105 | enable_03=1, 106 | sigCfMask_03=1, 107 | gnssId_04=BEIDOU, 108 | resTrkCh_04=2, 109 | maxTrkCh_04=5, 110 | enable_04=1, 111 | sigCfMask_04=1, 112 | gnssId_05=QZSS, 113 | resTrkCh_05=3, 114 | maxTrkCh_05=4, 115 | enable_05=1, 116 | sigCfMask_05=5, 117 | gnssId_06=GLONASS, 118 | resTrkCh_06=8, 119 | maxTrkCh_06=12, 120 | enable_06=1, 121 | sigCfMask_06=1, 122 | ) 123 | print(msg4) 124 | 125 | # Verify that the two UBXMessage objects are the same: 126 | print( 127 | f"\nVerify that the string representations of the messages are the same: {str(msg3) == str(msg4)}" 128 | ) 129 | print( 130 | f"\nVerify that the message payloads are the same: {msg3.payload == msg4.payload}" 131 | ) 132 | 133 | # Now construct a new CFG-GNSS input (SET) message with only one GNSS block (numConfigBlocks=1), and 134 | # with the 'parsebitfield' option set to False. In this case, rather than setting the two bit flags 135 | # 'enable' and 'sigCfMask' individually, we set the entire 'flags' bitfield as a 4-byte sequence, 136 | # remembering to include the unused reserved bits. 137 | print( 138 | "\nHere is a new CFG-GNSS input (SET) message with only one GNSS block (numConfigBlocks=1) and parsebitfield=False:\n" 139 | ) 140 | msg5 = UBXMessage( 141 | "CFG", 142 | "CFG-GNSS", 143 | SET, 144 | msgVer=0, 145 | numTrkChHw=42, 146 | numTrkChUse=42, 147 | numConfigBlocks=1, 148 | gnssId_01=GPS, 149 | resTrkCh_01=8, 150 | maxTrkCh_01=16, 151 | flags_01=b"\x01\x00\x10\x00", 152 | parsebitfield=False, 153 | ) 154 | print(msg5) 155 | print("\nHere are some individual properties from this message:\n") 156 | print( 157 | f"numConfigBlocks={msg5.numConfigBlocks}, gnssId={msg5.gnssId_01}, maxTrkCh={msg5.maxTrkCh_01}, flags={msg5.flags_01}" 158 | ) 159 | 160 | # If we now serialize and parse this message with parsebitfield=True (the default), the individual bit flags 161 | # are once again rendered. 162 | print("\nHere is the previous CFG-GNSS message parsed with parsebitfield=True:\n") 163 | msg6 = UBXReader.parse(msg5.serialize(), msgmode=SET, parsebitfield=True) 164 | print(msg6) 165 | print("\nHere are the individual bit flags from this message:\n") 166 | print(f"enable={msg6.enable_01}, sigCfMask={msg6.sigCfMask_01}") 167 | -------------------------------------------------------------------------------- /examples/ubxpoller.py: -------------------------------------------------------------------------------- 1 | """ 2 | ubxpoller.py 3 | 4 | This example illustrates how to read, write and display UBX messages 5 | "concurrently" using threads and queues. This represents a useful 6 | generic pattern for many end user applications. 7 | 8 | Usage: 9 | 10 | python3 ubxpoller.py port="/dev/ttyACM0" baudrate=38400 timeout=0.1 11 | 12 | It implements two threads which run concurrently: 13 | 1) an I/O thread which continuously reads UBX data from the 14 | receiver and sends any queued outbound command or poll messages. 15 | 2) a process thread which processes parsed UBX data - in this example 16 | it simply prints the parsed data to the terminal. 17 | UBX data is passed between threads using queues. 18 | 19 | Press CTRL-C to terminate. 20 | 21 | FYI: Since Python implements a Global Interpreter Lock (GIL), 22 | threads are not strictly concurrent, though this is of minor 23 | practical consequence here. 24 | 25 | Created on 07 Aug 2021 26 | 27 | :author: semuadmin 28 | :copyright: SEMU Consulting © 2021 29 | :license: BSD 3-Clause 30 | """ 31 | 32 | from queue import Queue 33 | from sys import argv 34 | from threading import Event, Thread 35 | from time import sleep 36 | 37 | from serial import Serial 38 | 39 | from pyubx2 import POLL, UBX_PAYLOADS_POLL, UBX_PROTOCOL, UBXMessage, UBXReader 40 | 41 | 42 | def io_data( 43 | ubr: UBXReader, 44 | readqueue: Queue, 45 | sendqueue: Queue, 46 | stop: Event, 47 | ): 48 | """ 49 | THREADED 50 | Read and parse inbound UBX data and place 51 | raw and parsed data on queue. 52 | 53 | Send any queued outbound messages to receiver. 54 | """ 55 | # pylint: disable=broad-exception-caught 56 | 57 | while not stop.is_set(): 58 | try: 59 | (raw_data, parsed_data) = ubr.read() 60 | if parsed_data: 61 | readqueue.put((raw_data, parsed_data)) 62 | 63 | # refine this if outbound message rates exceed inbound 64 | while not sendqueue.empty(): 65 | data = sendqueue.get(False) 66 | if data is not None: 67 | ubr.datastream.write(data.serialize()) 68 | sendqueue.task_done() 69 | 70 | except Exception as err: 71 | print(f"\n\nSomething went wrong - {err}\n\n") 72 | continue 73 | 74 | 75 | def process_data(queue: Queue, stop: Event): 76 | """ 77 | THREADED 78 | Get UBX data from queue and display. 79 | """ 80 | 81 | while not stop.is_set(): 82 | if queue.empty() is False: 83 | (_, parsed) = queue.get() 84 | print(parsed) 85 | queue.task_done() 86 | 87 | 88 | def main(**kwargs): 89 | """ 90 | Main routine. 91 | """ 92 | 93 | port = kwargs.get("port", "/dev/ttyACM0") 94 | baudrate = int(kwargs.get("baudrate", 38400)) 95 | timeout = float(kwargs.get("timeout", 0.1)) 96 | read_queue = Queue() 97 | send_queue = Queue() 98 | stop_event = Event() 99 | 100 | with Serial(port, baudrate, timeout=timeout) as stream: 101 | ubxreader = UBXReader(stream, protfilter=UBX_PROTOCOL) 102 | stop_event.clear() 103 | io_thread = Thread( 104 | target=io_data, 105 | args=( 106 | ubxreader, 107 | read_queue, 108 | send_queue, 109 | stop_event, 110 | ), 111 | daemon=True, 112 | ) 113 | process_thread = Thread( 114 | target=process_data, 115 | args=( 116 | read_queue, 117 | stop_event, 118 | ), 119 | daemon=True, 120 | ) 121 | 122 | print("\nStarting handler threads. Press Ctrl-C to terminate...") 123 | io_thread.start() 124 | process_thread.start() 125 | 126 | # loop until user presses Ctrl-C 127 | while not stop_event.is_set(): 128 | try: 129 | # DO STUFF IN THE BACKGROUND... 130 | # poll all available NAV messages (receiver will only respond 131 | # to those NAV message types it supports; responses won't 132 | # necessarily arrive in sequence) 133 | count = 0 134 | for nam in UBX_PAYLOADS_POLL: 135 | if nam[0:4] == "NAV-": 136 | print(f"Polling {nam} message type...") 137 | msg = UBXMessage("NAV", nam, POLL) 138 | send_queue.put(msg) 139 | count += 1 140 | sleep(1) 141 | stop_event.set() 142 | print(f"{count} NAV message types polled.") 143 | 144 | except KeyboardInterrupt: # capture Ctrl-C 145 | print("\n\nTerminated by user.") 146 | stop_event.set() 147 | 148 | print("\nStop signal set. Waiting for threads to complete...") 149 | io_thread.join() 150 | process_thread.join() 151 | print("\nProcessing complete") 152 | 153 | 154 | if __name__ == "__main__": 155 | 156 | main(**dict(arg.split("=") for arg in argv[1:])) 157 | -------------------------------------------------------------------------------- /examples/ubxsetrates.py: -------------------------------------------------------------------------------- 1 | """ 2 | ubxsetrates.py 3 | 4 | This example illustrates how to send UBX commands to a receiver 5 | (in this case a series of CFG-MSG commands) while simultaneously 6 | reading acknowledgements from the receiver. 7 | 8 | Usage: 9 | 10 | python3 ubxsetrates.py port="/dev/ttyACM0" baudrate=38400 timout=0.1 rate=4 11 | 12 | It implements two threads which run concurrently: 13 | 1) an I/O thread which continuously reads UBX data from the 14 | receiver and sends any queued outbound command or poll messages. 15 | 2) a process thread which processes parsed UBX data - in this example 16 | it simply prints the parsed data to the terminal. 17 | UBX data is passed between threads using queues. 18 | 19 | NB: the rate value means 'per navigation solution' e.g. a rate of 20 | 4 means 'every 4th navigation solution', which at a standard solution 21 | interval of 1000ms corresponds to every 4 seconds. 22 | 23 | The process thread reads and parses any responses and outputs 24 | them to the terminal. You should also start seeing any incoming 25 | UBX-NAV messages arriving at the designated rate. 26 | 27 | The response may be an ACK-ACK acknowledgement message, or an 28 | ACK-NAK message signifying that this particular navigation message 29 | type is not supported by the receiver. 30 | 31 | Created on 07 Aug 2021 32 | 33 | :author: semuadmin 34 | :copyright: SEMU Consulting © 2021 35 | :license: BSD 3-Clause 36 | """ 37 | 38 | from queue import Queue 39 | from sys import argv 40 | from threading import Event, Thread 41 | from time import sleep 42 | 43 | from serial import Serial 44 | 45 | from pyubx2 import SET, UBX_MSGIDS, UBX_PROTOCOL, UBXMessage, UBXReader 46 | 47 | 48 | def io_data( 49 | ubr: UBXReader, 50 | readqueue: Queue, 51 | sendqueue: Queue, 52 | stop: Event, 53 | ): 54 | """ 55 | THREADED 56 | Read and parse inbound UBX data and place 57 | raw and parsed data on queue. 58 | 59 | Send any queued outbound messages to receiver. 60 | """ 61 | # pylint: disable=broad-exception-caught 62 | 63 | while not stop.is_set(): 64 | try: 65 | (raw_data, parsed_data) = ubr.read() 66 | if parsed_data: 67 | readqueue.put((raw_data, parsed_data)) 68 | 69 | # refine this if outbound message rates exceed inbound 70 | while not sendqueue.empty(): 71 | data = sendqueue.get(False) 72 | if data is not None: 73 | ubr.datastream.write(data.serialize()) 74 | sendqueue.task_done() 75 | 76 | except Exception as err: 77 | print(f"\n\nSomething went wrong - {err}\n\n") 78 | continue 79 | 80 | 81 | def process_data(queue: Queue, stop: Event): 82 | """ 83 | THREADED 84 | Get UBX data from queue and display. 85 | """ 86 | 87 | while not stop.is_set(): 88 | if queue.empty() is False: 89 | (_, parsed) = queue.get() 90 | print(parsed) 91 | queue.task_done() 92 | 93 | 94 | def main(**kwargs): 95 | """ 96 | Main routine. 97 | """ 98 | 99 | port = kwargs.get("port", "/dev/ttyACM0") 100 | baudrate = int(kwargs.get("baudrate", 38400)) 101 | timeout = float(kwargs.get("timeout", 0.1)) 102 | rate = int(kwargs.get("rate", 4)) 103 | read_queue = Queue() 104 | send_queue = Queue() 105 | stop_event = Event() 106 | 107 | with Serial(port, baudrate, timeout=timeout) as stream: 108 | ubxreader = UBXReader(stream, protfilter=UBX_PROTOCOL) 109 | stop_event.clear() 110 | io_thread = Thread( 111 | target=io_data, 112 | args=( 113 | ubxreader, 114 | read_queue, 115 | send_queue, 116 | stop_event, 117 | ), 118 | daemon=True, 119 | ) 120 | process_thread = Thread( 121 | target=process_data, 122 | args=( 123 | read_queue, 124 | stop_event, 125 | ), 126 | daemon=True, 127 | ) 128 | 129 | print("\nStarting handler threads. Press Ctrl-C to terminate...") 130 | io_thread.start() 131 | process_thread.start() 132 | 133 | # loop until user presses Ctrl-C 134 | while not stop_event.is_set(): 135 | try: 136 | # DO STUFF IN THE BACKGROUND... 137 | 138 | print("\nSending CFG-MSG message rate configuration messages...\n") 139 | for msgid, msgname in UBX_MSGIDS.items(): 140 | if msgid[0] == 0x01: # NAV 141 | msg = UBXMessage( 142 | "CFG", 143 | "CFG-MSG", 144 | SET, 145 | msgClass=msgid[0], 146 | msgID=msgid[1], 147 | rateUART1=rate, 148 | rateUSB=rate, 149 | ) 150 | print( 151 | f"Setting message rate for {msgname} message type to {rate}...\n" 152 | ) 153 | send_queue.put(msg) 154 | sleep(1) 155 | 156 | print("\nCommands sent. Waiting for any final acknowledgements...\n") 157 | sleep(1) 158 | 159 | except KeyboardInterrupt: # capture Ctrl-C 160 | print("\n\nTerminated by user.") 161 | stop_event.set() 162 | 163 | print("\nStop signal set. Waiting for threads to complete...") 164 | io_thread.join() 165 | process_thread.join() 166 | print("\nProcessing complete") 167 | 168 | 169 | if __name__ == "__main__": 170 | 171 | main(**dict(arg.split("=") for arg in argv[1:])) 172 | -------------------------------------------------------------------------------- /examples/ubxsocket.py: -------------------------------------------------------------------------------- 1 | """ 2 | ubxsocket.py 3 | 4 | A simple example implementation of a GNSS socket reader 5 | using the pyubx2.UBXReader iterator functions. 6 | Parses UBX, NMEA and RTCM3 messages. 7 | 8 | Usage: 9 | 10 | python3 ubxsocket.py ipaddress=127.0.0.1 ipport=50012 11 | 12 | Designed to be used in conjunction with the 13 | tcpserver_thread.py test harness, but can be 14 | used with any accessible open TCP socket. 15 | 16 | Created on 05 May 2022 17 | 18 | @author: semuadmin 19 | """ 20 | 21 | import socket 22 | from datetime import datetime 23 | from sys import argv 24 | 25 | from pyubx2.ubxreader import ( 26 | NMEA_PROTOCOL, 27 | RTCM3_PROTOCOL, 28 | UBX_PROTOCOL, 29 | VALCKSUM, 30 | UBXReader, 31 | ) 32 | 33 | 34 | def main(**kwargs): 35 | """ 36 | Reads and parses UBX, NMEA and RTCM3 message data from stream. 37 | """ 38 | 39 | ipaddress = kwargs.get("ipaddress", "localhost") 40 | ipport = kwargs.get("ipport", 50012) 41 | 42 | print(f"Opening socket {ipaddress}:{ipport}...") 43 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as stream: 44 | stream.connect((ipaddress, ipport)) 45 | 46 | count = 0 47 | start = datetime.now() 48 | 49 | ubr = UBXReader( 50 | stream, 51 | protfilter=UBX_PROTOCOL | NMEA_PROTOCOL | RTCM3_PROTOCOL, 52 | validate=VALCKSUM, 53 | ) 54 | try: 55 | for _, parsed_data in ubr: 56 | print(parsed_data) 57 | count += 1 58 | except KeyboardInterrupt: 59 | dur = datetime.now() - start 60 | secs = dur.seconds + dur.microseconds / 1e6 61 | print("Session terminated by user") 62 | print( 63 | f"{count:,d} messages read in {secs:.2f} seconds:", 64 | f"{count/secs:.2f} msgs per second", 65 | ) 66 | 67 | 68 | if __name__ == "__main__": 69 | 70 | main(**dict(arg.split("=") for arg in argv[1:])) 71 | -------------------------------------------------------------------------------- /examples/utilities.py: -------------------------------------------------------------------------------- 1 | """ 2 | utilities.py 3 | 4 | Illustration of the various utility methods available in pyubx2 5 | 6 | Created on 14 Jan 2023 7 | 8 | @author: semuadmin 9 | :copyright: SEMU Consulting © 2022 10 | :license: BSD 3-Clause 11 | """ 12 | 13 | from datums import DATUMS # assumes this is in same folder 14 | 15 | from pyubx2 import ( 16 | bearing, 17 | ecef2llh, 18 | haversine, 19 | latlon2dmm, 20 | latlon2dms, 21 | llh2ecef, 22 | llh2iso6709, 23 | ) 24 | 25 | LAT1, LON1, ALT1 = 53.24, -2.16, 42.45 26 | LAT2, LON2 = 53.32, -2.08 27 | 28 | print(f"\nConvert {LAT1}, {LON1} to degrees, minutes, seconds ...") 29 | lat, lon = latlon2dms(LAT1, LON1) 30 | print(f"D.M.S = {lat}, {lon}") 31 | 32 | print(f"\nConvert {LAT2}, {LON2} to degrees, minutes ...") 33 | lat, lon = latlon2dmm(LAT2, LON2) 34 | print(f"D.MM = {lat}, {lon}") 35 | 36 | print(f"\nConvert {LAT1}, {LON1}, {ALT1}m to ISO6709 format ...") 37 | iso6709 = llh2iso6709(LAT1, LON1, ALT1) 38 | print(f"ISO6709 = {iso6709}") 39 | 40 | print( 41 | f"\nFind spherical distance between {LAT1}, {LON1} and", 42 | f" {LAT2}, {LON2} using default WGS84 datum...", 43 | ) 44 | dist = haversine(LAT1, LON1, LAT2, LON2) 45 | print(f"Distance: {dist/1000} km") 46 | 47 | print( 48 | f"\nFind bearing between {LAT1}, {LON1} and", 49 | f" {LAT2}, {LON2} using default WGS84 datum...", 50 | ) 51 | brng = bearing(LAT1, LON1, LAT2, LON2) 52 | print(f"Distance: {brng} degrees") 53 | 54 | X, Y, Z = 3822566.3113, -144427.5123, 5086857.1208 55 | print(f"\nConvert ECEF X: {X}, Y: {Y}, Z: {Z} to geodetic using default WGS84 datum...") 56 | lat, lon, height = ecef2llh(X, Y, Z) 57 | print(f"Geodetic lat: {lat}, lon: {lon}, height: {height}") 58 | 59 | print(f"\nConvert geodetic {lat}, {lon}, {height} back to ECEF ...") 60 | x, y, z = llh2ecef(lat, lon, height) 61 | print(f"ECEF X: {x}, Y: {y}, Z: {z}") 62 | 63 | # Refer to DATUMS.py in the /examples folder for list of common 64 | # international datums with semi-major axis, flattening and 65 | # delta_x,y,z reference values 66 | 67 | DATUM = "North_America_83" 68 | datum_dict = DATUMS[DATUM] 69 | ellipsoid, a, f, delta_x, delta_y, delta_z = datum_dict.values() 70 | 71 | print( 72 | f"\nFind spherical distance between {LAT1}, {LON1} and", 73 | f"{LAT2}, {LON2} using alternate {DATUM} ({ellipsoid}) datum...", 74 | ) 75 | dist = haversine(LAT1, LON1, LAT2, LON2, a / 1000) 76 | print(f"Distance: {dist/1000} km") 77 | 78 | print( 79 | f"\nConvert ECEF X: {X}, Y: {Y}, Z: {Z} to", 80 | f"geodetic using alternate {DATUM} ({ellipsoid}) datum ...", 81 | ) 82 | lat, lon, height = ecef2llh(X - delta_x, Y - delta_z, Z - delta_z, a, f) 83 | print(f"Geodetic lat: {lat}, lon: {lon}, height: {height}") 84 | 85 | print(f"\nConvert geodetic {lat}, {lon}, {height} back to ECEF ...") 86 | x, y, z = llh2ecef(lat, lon, height, a, f) 87 | print(f"ECEF X: {x + delta_x}, Y: {y + delta_y}, Z: {z + delta_z}") 88 | -------------------------------------------------------------------------------- /examples/webserver/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/pyubx2/43e9a9d4a2f4cad0191ec42c1468efcec744cbab/examples/webserver/favicon.ico -------------------------------------------------------------------------------- /examples/webserver/gpshttpserver.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is a simple HTTP Server utilising the native 3 | Python 3 http.server library. 4 | 5 | NB: http.server is NOT recommended for production use - it 6 | only implements basic security checks. 7 | 8 | This example implements a REST API /gps to retrieve GPS data 9 | from the designated GPSClass object. 10 | 11 | A dummy GPSDataStub object is provided to simulate 12 | the output from a GPS device. 13 | 14 | NB: Must be executed from the root folder i.e. /examples/webserver/. 15 | Press CTRL-C to terminate. 16 | 17 | The web page can be accessed at http://localhost:8080. The dummy parsed 18 | data can also be accessed directly via the REST API http://localhost:8080/gps. 19 | 20 | Created on 17 May 2021 21 | 22 | :author: semuadmin 23 | :license: (c) SEMU Consulting 2021 - BSD 3-Clause License 24 | """ 25 | # pylint: disable=invalid-name 26 | 27 | from http.server import SimpleHTTPRequestHandler, HTTPServer 28 | import json 29 | import random 30 | from datetime import datetime 31 | 32 | ADDRESS = "localhost" 33 | TCPPORT = 8080 34 | HTML = "/index.html" 35 | CSS = "/styles.css" 36 | JS = "/scripts.js" 37 | ICON = "/favicon.ico" 38 | 39 | 40 | class GPSDataStub: 41 | """ 42 | Stub data class to simulate GPS data. 43 | """ 44 | 45 | def get_data(self): 46 | """ 47 | Simulated REST API /gps response. 48 | """ 49 | 50 | now = datetime.now() 51 | dic = { 52 | "date": now.strftime("%Y-%m-%d"), 53 | "time": now.strftime("%H:%M:%S"), 54 | "latitude": round(random.uniform(-90.0, 90.0), 5), 55 | "longitude": round(random.uniform(-180.0, 180.0), 5), 56 | "elevation": round(random.uniform(-50.0, 100.0), 2), 57 | "speed": round(random.uniform(0, 100.0), 2), 58 | "track": round(random.uniform(0, 360.0), 2), 59 | "siv": random.randrange(0, 33), 60 | "pdop": round(random.uniform(0, 99.0), 2), 61 | "hdop": round(random.uniform(0, 99.0), 2), 62 | "vdop": round(random.uniform(0, 99.0), 2), 63 | "fix": random.randrange(1, 4), 64 | } 65 | return json.dumps(dic) 66 | 67 | 68 | class GPSHTTPServer(HTTPServer): 69 | """ 70 | HTTPServer subclass incorporating reference to GPSClass object which 71 | must implement a get_data() method in support of the /gps REST API. 72 | """ 73 | 74 | def __init__(self, server_address, RequestHandlerClass, GPSClass): 75 | """ 76 | Constructor. 77 | """ 78 | 79 | self.gps = GPSClass 80 | super().__init__(server_address, RequestHandlerClass) 81 | 82 | 83 | class GPSHTTPHandler(SimpleHTTPRequestHandler): 84 | """ 85 | HTTP Request Handler subclass. 86 | """ 87 | 88 | def do_GET(self): 89 | """ 90 | Handle GET request. 91 | """ 92 | 93 | if self.path == "/": 94 | self.path = HTML 95 | 96 | mimetype = self.guess_type(self.path) 97 | rc = 200 98 | 99 | try: 100 | if self.path in (HTML, JS, CSS, ICON): 101 | res = open(self.path[1:]).read() 102 | elif self.path == "/gps": # invoke GPS REST API 103 | res = self.server.gps.get_data() 104 | mimetype = "application/json" 105 | else: 106 | res = "Unknown Request" 107 | rc = 501 108 | 109 | self.send_response(rc) 110 | self.send_header("Content-type", mimetype) 111 | self.end_headers() 112 | self.wfile.write(res.encode()) 113 | except UnicodeDecodeError as err: 114 | pass 115 | 116 | 117 | if __name__ == "__main__": 118 | 119 | gps = GPSDataStub() 120 | print("\nStarting HTTP Server on http://" + ADDRESS + ":" + str(TCPPORT) + " ...") 121 | httpd = GPSHTTPServer((ADDRESS, TCPPORT), GPSHTTPHandler, gps) 122 | 123 | try: 124 | httpd.serve_forever() 125 | except KeyboardInterrupt: 126 | httpd.shutdown() 127 | 128 | print("\nHTTP Server stopped.") 129 | -------------------------------------------------------------------------------- /examples/webserver/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | GPS Server Demo 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |

GPS Server Demo

19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 |
UTC Date0000-00-00
UTC Time00:00:00
Latitude00.00000
Longitude00.00000
Elevation000.00
Speed000.00
Track000.00
FixNo Fix
SIV0
PDOP00.00
HDOP00.00
VDOP00.00
70 | 74 |
75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /examples/webserver/scripts.js: -------------------------------------------------------------------------------- 1 | /*! JavaScript for GPS Server Demo */ 2 | 3 | var gInterval = 5000; // 5 seconds 4 | var gTimer = 0; 5 | var FIXDESC = ["No Fix", "2D", "3D"]; 6 | 7 | // Refresh web page 8 | function refreshPage(obj) { 9 | "use strict"; 10 | 11 | document.getElementById('date').innerHTML = obj.date; 12 | document.getElementById('time').innerHTML = obj.time; 13 | document.getElementById('latitude').innerHTML = obj.latitude; 14 | document.getElementById('longitude').innerHTML = obj.longitude; 15 | document.getElementById('elevation').innerHTML = obj.elevation; 16 | document.getElementById('speed').innerHTML = obj.speed; 17 | document.getElementById('track').innerHTML = obj.track; 18 | document.getElementById('siv').innerHTML = obj.siv; 19 | document.getElementById('pdop').innerHTML = obj.pdop; 20 | document.getElementById('hdop').innerHTML = obj.hdop; 21 | document.getElementById('vdop').innerHTML = obj.vdop; 22 | document.getElementById('fix').innerHTML = FIXDESC[obj.fix - 1]; 23 | 24 | } 25 | 26 | // Execute REST GET request to retrieve latest gps data 27 | function getGPS() { 28 | "use strict"; 29 | 30 | var obj; 31 | var xhr = new XMLHttpRequest(); 32 | xhr.open('GET', '/gps'); 33 | xhr.onload = function () { 34 | if (xhr.status === 200) { 35 | obj = JSON.parse(xhr.responseText); 36 | refreshPage(obj); 37 | } 38 | else { 39 | alert('Request failed. Returned status of ' + xhr.status); 40 | } 41 | }; 42 | xhr.send(); 43 | 44 | } 45 | 46 | // Set interval timer for GET request 47 | function setTimer() { 48 | "use strict"; 49 | 50 | clearInterval(gTimer); 51 | gTimer = setInterval("getGPS()", gInterval); 52 | } 53 | 54 | // Functions to call when body first loaded 55 | function start() { 56 | "use strict"; 57 | 58 | getGPS(); 59 | setTimer(); 60 | 61 | } 62 | -------------------------------------------------------------------------------- /examples/webserver/styles.css: -------------------------------------------------------------------------------- 1 | /* Stylesheet for GPS Server Demo */ 2 | 3 | @charset "UTF-8"; 4 | html,body { 5 | height:100%; 6 | } 7 | html { 8 | -ms-text-size-adjust:100%; 9 | -webkit-text-size-adjust:100%; 10 | } 11 | body { 12 | font-family:Arial,Helvetica,sans-serif; 13 | } 14 | div.main-container { 15 | display:block; 16 | position:relative; 17 | min-height:95vh; 18 | background-color:white; 19 | overflow:auto; 20 | margin:0 auto; 21 | padding:0; 22 | padding-bottom:40px; 23 | width:50%; 24 | } 25 | header { 26 | margin:0 auto; 27 | text-align:center; 28 | } 29 | footer { 30 | position:absolute; 31 | margin:0 auto; 32 | width:100%; 33 | bottom:0; 34 | height:40px; 35 | padding-top: 5px; 36 | padding-bottom: 5px; 37 | text-align:center; 38 | } 39 | h1 { 40 | color: #444444; 41 | font-size:3.0em; 42 | } 43 | table { 44 | margin:0 auto; 45 | font-size:1.5em; 46 | padding:10px; 47 | border-collapse: collapse; 48 | } 49 | td { 50 | color: #444444; 51 | padding-left:20px; 52 | padding-right:20px; 53 | border-style: solid; 54 | border-width: 2px; 55 | border-color: lightgrey; 56 | } 57 | td.right-align { 58 | text-align:right; 59 | } 60 | -------------------------------------------------------------------------------- /examples/webserver/ubxserver.py: -------------------------------------------------------------------------------- 1 | """ 2 | ubxserver.py 3 | 4 | This example illustrates a simple HTTP wrapper around pyubx2.UBXReader. 5 | 6 | Usage: 7 | 8 | python3 ubxserver.py ipaddress=127.0.0.1 ipport=8080 serport="/dev/ttyACM0" baudrate=38400 timeout=0.1 9 | 10 | It displays selected GPS data from NAV-PVT, NAV-POSLLH, NAV-DOP and NAV-SAT 11 | messages on a dynamically updated web page using the native Python 3 http.server 12 | library and a RESTful API implemented by the pyubx2 streaming and parsing service. 13 | 14 | NB: Must be executed from the root folder i.e. /examples/webserver/: 15 | 16 | > python3 ubxserver.py 17 | 18 | Press CTRL-C to terminate. 19 | 20 | The web page can be accessed at http://localhost:8080. The parsed 21 | data can also be accessed directly via the REST API http://localhost:8080/gps. 22 | 23 | Created on 17 May 2021 24 | 25 | :author: semuadmin 26 | :license: (c) SEMU Consulting 2021 - BSD 3-Clause License 27 | """ 28 | 29 | # pylint: disable=invalid-name 30 | 31 | import json 32 | from io import BufferedReader 33 | from sys import argv 34 | from threading import Event, Thread 35 | from time import sleep 36 | 37 | from serial import Serial, SerialException, SerialTimeoutException 38 | import pyubx2.exceptions as ube 39 | from pyubx2 import GET, UBX_PROTOCOL, UBXMessage, UBXReader 40 | from gpshttpserver import GPSHTTPHandler, GPSHTTPServer 41 | 42 | 43 | class UBXServer: 44 | """ 45 | UBXServer class. 46 | """ 47 | 48 | def __init__(self, port, baudrate, timeout, validate=1): 49 | """ 50 | Constructor. 51 | """ 52 | 53 | self._serial_object = None 54 | self._serial_thread = None 55 | self._ubxreader = None 56 | self._connected = False 57 | self._reading = False 58 | self._port = port 59 | self._baudrate = baudrate 60 | self._timeout = timeout 61 | self._validate = validate 62 | self._stopevent = Event() 63 | self.gpsdata = { 64 | "date": "1900-01-01", 65 | "time": "00.00.00", 66 | "latitude": 0.0, 67 | "longitude": 0.0, 68 | "elevation": 0.0, 69 | "speed": 0.0, 70 | "track": 0.0, 71 | "siv": 0, 72 | "pdop": 99, 73 | "hdop": 99, 74 | "vdop": 99, 75 | "fix": 0, 76 | } 77 | 78 | def __del__(self): 79 | """ 80 | Destructor. 81 | """ 82 | 83 | self.stop_read_thread() 84 | self.disconnect() 85 | 86 | def connect(self): 87 | """ 88 | Open serial connection. 89 | """ 90 | 91 | self._connected = False 92 | try: 93 | print( 94 | f"Connecting to serial port {self._port} at {self._baudrate} baud ..." 95 | ) 96 | self._serial_object = Serial( 97 | self._port, self._baudrate, timeout=self._timeout 98 | ) 99 | self._ubxreader = UBXReader( 100 | BufferedReader(self._serial_object), 101 | protfilter=UBX_PROTOCOL, 102 | validate=self._validate, 103 | msgmode=GET, 104 | ) 105 | self._connected = True 106 | except (SerialException, SerialTimeoutException) as err: 107 | print(f"Error connecting to serial port {err}") 108 | 109 | return self._connected 110 | 111 | def disconnect(self): 112 | """ 113 | Close serial connection. 114 | """ 115 | 116 | if self._connected and self._serial_object: 117 | print("Disconnecting from serial port...") 118 | try: 119 | self._serial_object.close() 120 | except (SerialException, SerialTimeoutException) as err: 121 | print(f"Error disconnecting from serial port {err}") 122 | self._connected = False 123 | 124 | return self._connected 125 | 126 | def start_read_thread(self): 127 | """ 128 | Start the serial reader thread. 129 | """ 130 | 131 | if self._connected: 132 | print("\nStarting serial read thread ...") 133 | self._reading = True 134 | self._serial_thread = Thread( 135 | target=self._read_thread, args=(self._stopevent,) 136 | ) 137 | self._serial_thread.start() 138 | 139 | def stop_read_thread(self): 140 | """ 141 | Stop the serial reader thread. 142 | """ 143 | 144 | if self._serial_thread is not None: 145 | self._stopevent.set() 146 | self._serial_thread.join() 147 | print("\nSerial read thread stopped") 148 | 149 | def _read_thread(self, stopevent): 150 | """ 151 | THREADED PROCESS 152 | Reads and parses UBX message data from stream 153 | """ 154 | # pylint: disable=unused-variable 155 | 156 | while not stopevent.is_set(): 157 | if self._serial_object.in_waiting: 158 | try: 159 | (raw_data, parsed_data) = self._ubxreader.read() 160 | if isinstance(parsed_data, UBXMessage): 161 | self.set_data(parsed_data) 162 | except ( 163 | ube.UBXStreamError, 164 | ube.UBXMessageError, 165 | ube.UBXTypeError, 166 | ube.UBXParseError, 167 | ) as err: 168 | print(f"Something went wrong {err}") 169 | continue 170 | 171 | def set_data(self, parsed_data): 172 | """ 173 | Set GPS data dictionary from UBX sentences. 174 | """ 175 | 176 | try: 177 | if parsed_data.identity == "NAV-PVT": 178 | self.gpsdata["date"] = ( 179 | f"{parsed_data.day:02}/{parsed_data.month:02}/{parsed_data.year}" 180 | ) 181 | self.gpsdata["time"] = ( 182 | f"{parsed_data.hour:02}:{parsed_data.min:02}:{parsed_data.second:02}" 183 | ) 184 | self.gpsdata["latitude"] = parsed_data.lat 185 | self.gpsdata["longitude"] = parsed_data.lon 186 | self.gpsdata["elevation"] = parsed_data.height / 1000 187 | self.gpsdata["speed"] = parsed_data.gSpeed 188 | self.gpsdata["track"] = parsed_data.headVeh 189 | self.gpsdata["fix"] = parsed_data.fixType 190 | self.gpsdata["pDOP"] = parsed_data.pDOP 191 | if parsed_data.identity in ("NAV-POSLLH", "NAV-HPPOSLLH"): 192 | self.gpsdata["latitude"] = parsed_data.lat 193 | self.gpsdata["longitude"] = parsed_data.lon 194 | self.gpsdata["elevation"] = parsed_data.height / 1000 195 | if parsed_data.identity == "NAV-DOP": 196 | self.gpsdata["pdop"] = parsed_data.pDOP 197 | self.gpsdata["hdop"] = parsed_data.hDOP 198 | self.gpsdata["vdop"] = parsed_data.vDOP 199 | if parsed_data.identity == "NAV-SAT": 200 | self.gpsdata["siv"] = parsed_data.numSvs 201 | except ube.UBXMessageError as err: 202 | print(err) 203 | self._stopevent.set() 204 | 205 | def get_data(self): 206 | """ 207 | Return GPS data in JSON format. 208 | 209 | This is used by the REST API /gps implemented in the 210 | GPSHTTPServer class. 211 | """ 212 | 213 | return json.dumps(self.gpsdata) 214 | 215 | 216 | def main(**kwargs): 217 | """ 218 | Main Routine. 219 | """ 220 | 221 | ipaddress = kwargs.get("ipaddress", "localhost") 222 | ipport = kwargs.get("ipport", 8080) 223 | serport = kwargs.get("serport", "/dev/ttyACM0") 224 | baudrate = int(kwargs.get("baudrate", 38400)) 225 | timeout = float(kwargs.get("timeout", 0.1)) 226 | 227 | ubs = UBXServer(serport, baudrate, timeout) 228 | httpd = GPSHTTPServer((ipaddress, ipport), GPSHTTPHandler, ubs) 229 | 230 | if ubs.connect(): 231 | ubs.start_read_thread() 232 | print( 233 | f"\nStarting HTTP Server on http://{ipaddress}:{ipport} ...", 234 | "\nPress Ctrl-C to terminate.\n", 235 | ) 236 | httpd_thread = Thread(target=httpd.serve_forever, daemon=True) 237 | httpd_thread.start() 238 | 239 | try: 240 | while True: 241 | pass 242 | except KeyboardInterrupt: 243 | print("\n\nTerminated by user\n\n") 244 | 245 | ubs.stop_read_thread() 246 | httpd.shutdown() 247 | print("\nHTTP Server stopped.") 248 | sleep(2) # wait for shutdown 249 | ubs.disconnect() 250 | print("\nProcessing Complete") 251 | 252 | 253 | if __name__ == "__main__": 254 | 255 | main(**dict(arg.split("=") for arg in argv[1:])) 256 | -------------------------------------------------------------------------------- /images/sponsor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/pyubx2/43e9a9d4a2f4cad0191ec42c1468efcec744cbab/images/sponsor.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=75.0.0", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "pyubx2" 7 | dynamic = ["version"] 8 | authors = [{ name = "semuadmin", email = "semuadmin@semuconsulting.com" }] 9 | maintainers = [{ name = "semuadmin", email = "semuadmin@semuconsulting.com" }] 10 | description = "UBX protocol parser and generator" 11 | license = { file = "LICENSE" } 12 | readme = "README.md" 13 | requires-python = ">=3.9" 14 | classifiers = [ 15 | "Operating System :: OS Independent", 16 | "Development Status :: 5 - Production/Stable", 17 | "Environment :: MacOS X", 18 | "Environment :: Win32 (MS Windows)", 19 | "Environment :: X11 Applications", 20 | "Environment :: Console", 21 | "Intended Audience :: Developers", 22 | "Intended Audience :: Science/Research", 23 | "Intended Audience :: End Users/Desktop", 24 | "Programming Language :: Python :: 3", 25 | "Programming Language :: Python :: 3.9", 26 | "Programming Language :: Python :: 3.10", 27 | "Programming Language :: Python :: 3.11", 28 | "Programming Language :: Python :: 3.12", 29 | "Programming Language :: Python :: 3.13", 30 | "Topic :: Utilities", 31 | "Topic :: Software Development :: Libraries :: Python Modules", 32 | "Topic :: Scientific/Engineering :: GIS", 33 | ] 34 | 35 | dependencies = ["pynmeagps >= 1.0.50", "pyrtcm >= 1.1.7"] 36 | 37 | [project.urls] 38 | homepage = "https://github.com/semuconsulting/pyubx2" 39 | documentation = "https://www.semuconsulting.com/pyubx2/" 40 | repository = "https://github.com/semuconsulting/pyubx2" 41 | changelog = "https://github.com/semuconsulting/pyubx2/blob/master/RELEASE_NOTES.md" 42 | 43 | [project.optional-dependencies] 44 | deploy = ["build", "pip", "setuptools >= 75.0", "wheel"] 45 | test = [ 46 | "bandit", 47 | "black", 48 | "isort", 49 | "pylint", 50 | "pytest", 51 | "pytest-cov", 52 | "Sphinx", 53 | "sphinx-rtd-theme", 54 | ] 55 | 56 | [tool.setuptools.dynamic] 57 | version = { attr = "pyubx2._version.__version__" } 58 | 59 | [tool.black] 60 | target-version = ['py39'] 61 | 62 | [tool.isort] 63 | py_version = 39 64 | profile = "black" 65 | 66 | [tool.bandit] 67 | exclude_dirs = ["docs", "examples", "references", "tests"] 68 | skips = [] 69 | 70 | [tool.pylint] 71 | jobs = 0 72 | reports = "y" 73 | recursive = "y" 74 | py-version = "3.9" 75 | fail-under = "9.8" 76 | fail-on = "E,F" 77 | clear-cache-post-run = "y" 78 | good-names = "i,j,x,y" 79 | disable = """ 80 | raw-checker-failed, 81 | bad-inline-option, 82 | locally-disabled, 83 | file-ignored, 84 | suppressed-message, 85 | useless-suppression, 86 | deprecated-pragma, 87 | use-symbolic-message-instead, 88 | """ 89 | 90 | [tool.pytest.ini_options] 91 | minversion = "7.0" 92 | addopts = "--cov --cov-report html --cov-fail-under 99" 93 | pythonpath = ["src"] 94 | testpaths = ["tests"] 95 | 96 | [tool.coverage.run] 97 | source = ["src"] 98 | -------------------------------------------------------------------------------- /src/pyubx2/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on 27 Sep 2020 3 | 4 | :author: semuadmin 5 | :copyright: SEMU Consulting © 2020 6 | :license: BSD 3-Clause 7 | """ 8 | 9 | from pynmeagps import ( 10 | SocketWrapper, 11 | bearing, 12 | ecef2llh, 13 | haversine, 14 | latlon2dmm, 15 | latlon2dms, 16 | llh2ecef, 17 | llh2iso6709, 18 | planar, 19 | ) 20 | 21 | from pyubx2._version import __version__ 22 | from pyubx2.exceptions import ( 23 | GNSSStreamError, 24 | ParameterError, 25 | UBXMessageError, 26 | UBXParseError, 27 | UBXStreamError, 28 | UBXTypeError, 29 | ) 30 | from pyubx2.ubxhelpers import * 31 | from pyubx2.ubxmessage import UBXMessage 32 | from pyubx2.ubxreader import UBXReader 33 | from pyubx2.ubxtypes_configdb import * 34 | from pyubx2.ubxtypes_core import * 35 | from pyubx2.ubxtypes_decodes import * 36 | from pyubx2.ubxtypes_get import * 37 | from pyubx2.ubxtypes_poll import * 38 | from pyubx2.ubxtypes_set import * 39 | 40 | version = __version__ # pylint: disable=invalid-name 41 | -------------------------------------------------------------------------------- /src/pyubx2/_version.py: -------------------------------------------------------------------------------- 1 | """ 2 | Release Version. 3 | 4 | Created on 2 Oct 2020 5 | 6 | :author: semuadmin 7 | :copyright: SEMU Consulting © 2020 8 | :license: BSD 3-Clause 9 | """ 10 | 11 | __version__ = "1.2.53" 12 | -------------------------------------------------------------------------------- /src/pyubx2/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | UBX Custom Exception Types. 3 | 4 | Created on 27 Sep 2020 5 | 6 | :author: semuadmin 7 | :copyright: SEMU Consulting © 2020 8 | :license: BSD 3-Clause 9 | """ 10 | 11 | 12 | class ParameterError(Exception): 13 | """Parameter Error Class.""" 14 | 15 | 16 | class GNSSStreamError(Exception): 17 | """Generic Stream Error Class.""" 18 | 19 | 20 | class UBXParseError(Exception): 21 | """ 22 | UBX Parsing error. 23 | """ 24 | 25 | 26 | class UBXStreamError(Exception): 27 | """ 28 | UBX Streaming error. 29 | """ 30 | 31 | 32 | class UBXMessageError(Exception): 33 | """ 34 | UBX Undefined message class/id. 35 | Essentially a prompt to add missing payload types to UBX_PAYLOADS. 36 | """ 37 | 38 | 39 | class UBXTypeError(Exception): 40 | """ 41 | UBX Undefined payload attribute type. 42 | Essentially a prompt to fix incorrect payload definitions to UBX_PAYLOADS. 43 | """ 44 | -------------------------------------------------------------------------------- /src/pyubx2/ubxtypes_poll.py: -------------------------------------------------------------------------------- 1 | """ 2 | UBX Protocol POLL payload definitions. 3 | 4 | THESE ARE THE PAYLOAD DEFINITIONS FOR _POLL_ MESSAGES _TO_ THE RECEIVER 5 | (e.g. query configuration; request monitoring, receiver management, 6 | logging or sensor fusion status). 7 | 8 | Response payloads are defined in UBX_PAYLOADS_GET. 9 | 10 | NB: Attribute names must be unique within each message class/id 11 | 12 | Created on 27 Sep 2020 13 | 14 | Information sourced from public domain u-blox Interface Specifications © 2013-2021, u-blox AG 15 | 16 | :author: semuadmin 17 | """ 18 | 19 | from pyubx2.ubxtypes_core import U1, U2, U4 20 | 21 | UBX_PAYLOADS_POLL = { 22 | # AID messages are deprecated in favour of MGA messages in >=Gen8 23 | "AID-ALM": { 24 | "group": ( 25 | "None", 26 | { 27 | "svid": U1, 28 | }, 29 | ), 30 | }, 31 | "AID-AOP": { 32 | "group": ( 33 | "None", 34 | { 35 | "svid": U1, 36 | }, 37 | ), 38 | }, 39 | "AID-EPH": { 40 | "group": ( 41 | "None", 42 | { 43 | "svid": U1, 44 | }, 45 | ), 46 | }, 47 | "AID-DATA": {}, 48 | "AID-HUI": {}, 49 | "AID-INI": {}, 50 | "AID-REQ": {}, 51 | # ************************************************* 52 | "CFG-ANT": {}, 53 | "CFG-BATCH": {}, 54 | "CFG-DAT": {}, 55 | "CFG-DGNSS": {}, 56 | "CFG-DOSC": {}, 57 | "CFG-DYNSEED": {}, 58 | "CFG-EKF": {}, 59 | "CFG-ESFA": {}, 60 | "CFG-ESFALG": {}, 61 | "CFG-ESFG": {}, 62 | "CFG-ESFGWT": {}, 63 | "CFG-ESFWT": {}, 64 | "CFG-ESRC": {}, 65 | "CFG-FIXSEED": {}, 66 | "CFG-FXN": {}, 67 | "CFG-GEOFENCE": {}, 68 | "CFG-GNSS": {}, 69 | "CFG-HNR": {}, 70 | "CFG-INF": {"protocolID": U1}, 71 | "CFG-ITFM": {}, 72 | "CFG-LOGFILTER": {}, 73 | "CFG-MSG": {"msgClass": U1, "msgID": U1}, 74 | "CFG-NAV5": {}, 75 | "CFG-NAVX5": {}, 76 | "CFG-NMEA": {}, 77 | "CFG-NVS": {}, 78 | "CFG-ODO": {}, 79 | "CFG-PM2": {}, 80 | "CFG-PM": {}, 81 | "CFG-PMS": {}, 82 | "CFG-PRT": {"portID": U1}, 83 | "CFG-PWR": {}, 84 | "CFG-RATE": {}, 85 | "CFG-RINV": {}, 86 | "CFG-RXM": {}, 87 | "CFG-SBAS": {}, 88 | "CFG-SENIF": {}, 89 | "CFG-SLAS": {}, 90 | "CFG-SMGR": {}, 91 | "CFG-SPT": {}, 92 | "CFG-TMODE": {}, 93 | "CFG-TMODE2": {}, 94 | "CFG-TMODE3": {}, 95 | "CFG-TP": {}, 96 | "CFG-TP5": {}, # used if no payload keyword specified 97 | "CFG-TP5-TPX": {"tpIdx": U1}, # used with payload keyword 98 | "CFG-TXSLOT": {}, 99 | "CFG-USB": {}, 100 | "CFG-VALGET": { 101 | "version": U1, 102 | "layer": U1, 103 | "position": U2, 104 | "group": ("None", {"keys": U4}), # repeating group 105 | }, 106 | # ************************************************* 107 | "ESF-ALG": {}, 108 | "ESF-CAL": {}, 109 | "ESF-INS": {}, 110 | "ESF-STATUS": {}, 111 | # ************************************************* 112 | "LOG-BATCH": {}, 113 | "LOG-INFO": {}, 114 | # ************************************************* 115 | "MGA-DBD": {}, 116 | # ************************************************* 117 | "MON-COMMS": {}, 118 | "MON-GNSS": {}, 119 | "MON-HW": {}, 120 | "MON-HW2": {}, 121 | "MON-HW3": {}, 122 | "MON-IO": {}, 123 | "MON-MSGPP": {}, 124 | "MON-PATCH": {}, 125 | "MON-RF": {}, 126 | "MON-RXBUF": {}, 127 | "MON-SMGR": {}, 128 | "MON-SPAN": {}, 129 | "MON-TXBUF": {}, 130 | "MON-VER": {}, 131 | # ************************************************* 132 | "NAV-AOPSTATUS": {}, 133 | "NAV-ATT": {}, 134 | "NAV-CLOCK": {}, 135 | "NAV-COV": {}, 136 | "NAV-DGPS": {}, 137 | "NAV-DOP": {}, 138 | "NAV-EELL": {}, 139 | "NAV-EKFSTATUS": {}, 140 | "NAV-EOE": {}, 141 | "NAV-GEOFENCE": {}, 142 | "NAV-HPPOSECEF": {}, 143 | "NAV-HPPOSLLH": {}, 144 | "NAV-NMI": {}, 145 | "NAV-ODO": {}, 146 | "NAV-ORB": {}, 147 | "NAV-PL": {}, 148 | "NAV-POSECEF": {}, 149 | "NAV-POSLLH": {}, 150 | "NAV-PVT": {}, 151 | "NAV-RELPOSNED": {}, 152 | "NAV-RESETODO": {}, 153 | "NAV-SAT": {}, 154 | "NAV-SBAS": {}, 155 | "NAV-SIG": {}, 156 | "NAV-SLAS": {}, 157 | "NAV-SOL": {}, 158 | "NAV-STATUS": {}, 159 | "NAV-SVINFO": {}, 160 | "NAV-SVIN": {}, 161 | "NAV-TIMEBDS": {}, 162 | "NAV-TIMEGAL": {}, 163 | "NAV-TIMEGLO": {}, 164 | "NAV-TIMEGPS": {}, 165 | "NAV-TIMELS": {}, 166 | "NAV-TIMENAVIC": {}, 167 | "NAV-TIMEQZSS": {}, 168 | "NAV-TIMEUTC": {}, 169 | "NAV-VELECEF": {}, 170 | "NAV-VELNED": {}, 171 | # ************************************************* 172 | "NAV2-CLOCK": {}, 173 | "NAV2-COV": {}, 174 | "NAV2-DOP": {}, 175 | "NAV2-EOE": {}, 176 | "NAV2-ODO": {}, 177 | "NAV2-POSECEF": {}, 178 | "NAV2-POSLLH": {}, 179 | "NAV2-PVT": {}, 180 | "NAV2-SAT": {}, 181 | "NAV2-SBAS": {}, 182 | "NAV2-SIG": {}, 183 | "NAV2-STATUS": {}, 184 | "NAV2-TIMEBDS": {}, 185 | "NAV2-TIMEGAL": {}, 186 | "NAV2-TIMEGLO": {}, 187 | "NAV2-TIMEGPS": {}, 188 | "NAV2-TIMELS": {}, 189 | "NAV2-TIMENAVIC": {}, 190 | "NAV2-TIMEUTC": {}, 191 | "NAV2-VELECEF": {}, 192 | "NAV2-VELNED": {}, 193 | # ************************************************* 194 | "RXM-ALM": {}, 195 | "RXM-COR": {}, 196 | "RXM-EPH": {}, 197 | "RXM-IMES": {}, 198 | "RXM-MEAS20": {}, 199 | "RXM-MEAS50": {}, 200 | "RXM-MEASC12": {}, 201 | "RXM-MEASD12": {}, 202 | "RXM-MEASX": {}, 203 | "RXM-POSREQ": {}, 204 | "RXM-RAW": {}, 205 | "RXM-RAWX": {}, 206 | "RXM-RLM": {}, 207 | "RXM-RTCM": {}, 208 | "RXM-SFRB": {}, 209 | "RXM-SFRBX": {}, 210 | "RXM-SPARTN": {}, 211 | "RXM-SPARTN-KEY": {}, 212 | "RXM-SVSI": {}, 213 | "RXM-TM": {}, 214 | # ************************************************* 215 | "SEC-OSNMA": {}, 216 | "SEC-SIG": {}, 217 | "SEC-SIGLOG": {}, 218 | "SEC-SIGN": {}, 219 | "SEC-UNIQID": {}, 220 | # ************************************************* 221 | "TIM-DOSC": {}, 222 | "TIM-FCHG": {}, 223 | "TIM-SMEAS": {}, 224 | "TIM-SVIN": {}, 225 | "TIM-TM2": {}, 226 | "TIM-TOS": {}, 227 | "TIM-TP": {}, 228 | "TIM-VCOCAL": {}, 229 | "TIM-VRFY": {}, 230 | # ************************************************* 231 | "UPD-SOS": {}, 232 | } 233 | -------------------------------------------------------------------------------- /src/pyubx2/ubxvariants.py: -------------------------------------------------------------------------------- 1 | """ 2 | ubxvariants.py 3 | 4 | Various routines to get payload dictionaries for message 5 | types which exist in multiple variants for the same 6 | message class, id and mode. 7 | 8 | Created on 20 May 2024 9 | 10 | :author: semuadmin 11 | :copyright: SEMU Consulting © 2020 12 | :license: BSD 3-Clause 13 | """ 14 | 15 | from pyubx2.exceptions import UBXMessageError 16 | from pyubx2.ubxhelpers import val2bytes 17 | from pyubx2.ubxtypes_core import GET, POLL, SET, U1, UBX_MSGIDS 18 | from pyubx2.ubxtypes_get import UBX_PAYLOADS_GET 19 | from pyubx2.ubxtypes_poll import UBX_PAYLOADS_POLL 20 | from pyubx2.ubxtypes_set import UBX_PAYLOADS_SET 21 | 22 | 23 | def get_cfgtp5_dict(**kwargs) -> dict: 24 | """ 25 | Select appropriate CFG-TP5 POLL payload definition by checking 26 | presence of tpIdx or payload argument. 27 | 28 | :param kwargs: optional payload key/value pairs 29 | :return: dictionary representing payload definition 30 | :rtype: dict 31 | 32 | """ 33 | 34 | lp = 0 35 | if "payload" in kwargs: 36 | lp = len(kwargs["payload"]) 37 | elif "tpIdx" in kwargs: 38 | lp = 1 39 | print(f"DEBUG TP5 dict {kwargs} len payload {lp}") 40 | if lp == 1: 41 | return UBX_PAYLOADS_POLL["CFG-TP5-TPX"] 42 | return UBX_PAYLOADS_POLL["CFG-TP5"] # pragma: no cover 43 | 44 | 45 | def get_mga_dict(msg: bytes, mode: int, **kwargs) -> dict: 46 | """ 47 | Select appropriate MGA payload definition by checking 48 | value of 'type' attribute (1st byte of payload). 49 | 50 | :param str mode: mode (0=GET, 1=SET, 2=POLL) 51 | :param kwargs: optional payload key/value pairs 52 | :return: dictionary representing payload definition 53 | :rtype: dict 54 | :raises: UBXMessageError 55 | 56 | """ 57 | 58 | if "type" in kwargs: 59 | typ = val2bytes(kwargs["type"], U1) 60 | elif "payload" in kwargs: 61 | typ = kwargs["payload"][0:1] 62 | else: 63 | raise UBXMessageError( 64 | "MGA message definitions must include type or payload keyword" 65 | ) 66 | identity = UBX_MSGIDS[msg + typ] 67 | if mode == SET: 68 | return UBX_PAYLOADS_SET[identity] 69 | return UBX_PAYLOADS_GET[identity] 70 | 71 | 72 | def get_rxmpmreq_dict(**kwargs) -> dict: 73 | """ 74 | Select appropriate RXM-PMREQ payload definition by checking 75 | the 'version' keyword or payload length. 76 | 77 | :param kwargs: optional payload key/value pairs 78 | :return: dictionary representing payload definition 79 | :rtype: dict 80 | :raises: UBXMessageError 81 | 82 | """ 83 | 84 | lpd = 0 85 | if "version" in kwargs: # assume longer version 86 | lpd = 16 87 | elif "payload" in kwargs: 88 | lpd = len(kwargs["payload"]) 89 | else: 90 | raise UBXMessageError( 91 | "RXM-PMREQ message definitions must include version or payload keyword" 92 | ) 93 | if lpd == 16: 94 | return UBX_PAYLOADS_SET["RXM-PMREQ"] # long 95 | return UBX_PAYLOADS_SET["RXM-PMREQ-S"] # short 96 | 97 | 98 | def get_rxmpmp_dict(**kwargs) -> dict: 99 | """ 100 | Select appropriate RXM-PMP payload definition by checking 101 | value of 'version' attribute (1st byte of payload). 102 | 103 | :param kwargs: optional payload key/value pairs 104 | :return: dictionary representing payload definition 105 | :rtype: dict 106 | :raises: UBXMessageError 107 | 108 | """ 109 | 110 | if "version" in kwargs: 111 | ver = val2bytes(kwargs["version"], U1) 112 | elif "payload" in kwargs: 113 | ver = kwargs["payload"][0:1] 114 | else: 115 | raise UBXMessageError( 116 | "RXM-PMP message definitions must include version or payload keyword" 117 | ) 118 | if ver == b"\x00": 119 | return UBX_PAYLOADS_SET["RXM-PMP-V0"] 120 | return UBX_PAYLOADS_SET["RXM-PMP-V1"] 121 | 122 | 123 | def get_rxmrlm_dict(**kwargs) -> dict: 124 | """ 125 | Select appropriate RXM-RLM payload definition by checking 126 | value of 'type' attribute (2nd byte of payload). 127 | 128 | :param kwargs: optional payload key/value pairs 129 | :return: dictionary representing payload definition 130 | :rtype: dict 131 | :raises: UBXMessageError 132 | 133 | """ 134 | 135 | if "type" in kwargs: 136 | typ = val2bytes(kwargs["type"], U1) 137 | elif "payload" in kwargs: 138 | typ = kwargs["payload"][1:2] 139 | else: 140 | raise UBXMessageError( 141 | "RXM-RLM message definitions must include type or payload keyword" 142 | ) 143 | if typ == b"\x01": 144 | return UBX_PAYLOADS_GET["RXM-RLM-S"] # short 145 | return UBX_PAYLOADS_GET["RXM-RLM-L"] # long 146 | 147 | 148 | def get_cfgnmea_dict(**kwargs) -> dict: 149 | """ 150 | Select appropriate payload definition version for older 151 | generations of CFG-NMEA message by checking payload length. 152 | 153 | :param kwargs: optional payload key/value pairs 154 | :return: dictionary representing payload definition 155 | :rtype: dict 156 | :raises: UBXMessageError 157 | 158 | """ 159 | 160 | if "payload" in kwargs: 161 | lpd = len(kwargs["payload"]) 162 | else: 163 | raise UBXMessageError( 164 | "CFG-NMEA message definitions must include payload keyword" 165 | ) 166 | if lpd == 4: 167 | return UBX_PAYLOADS_GET["CFG-NMEAvX"] 168 | if lpd == 12: 169 | return UBX_PAYLOADS_GET["CFG-NMEAv0"] 170 | return UBX_PAYLOADS_GET["CFG-NMEA"] 171 | 172 | 173 | def get_aopstatus_dict(**kwargs) -> dict: 174 | """ 175 | Select appropriate payload definition version for older 176 | generations of NAV-AOPSTATUS message by checking payload length. 177 | 178 | :param kwargs: optional payload key/value pairs 179 | :return: dictionary representing payload definition 180 | :rtype: dict 181 | :raises: UBXMessageError 182 | 183 | """ 184 | 185 | if "payload" in kwargs: 186 | lpd = len(kwargs["payload"]) 187 | else: 188 | raise UBXMessageError( 189 | "NAV-AOPSTATUS message definitions must include payload keyword" 190 | ) 191 | if lpd == 20: 192 | return UBX_PAYLOADS_GET["NAV-AOPSTATUS-L"] 193 | return UBX_PAYLOADS_GET["NAV-AOPSTATUS"] 194 | 195 | 196 | def get_relposned_dict(**kwargs) -> dict: 197 | """ 198 | Select appropriate NAV-RELPOSNED payload definition by checking 199 | value of 'version' attribute (1st byte of payload). 200 | 201 | :param kwargs: optional payload key/value pairs 202 | :return: dictionary representing payload definition 203 | :rtype: dict 204 | :raises: UBXMessageError 205 | 206 | """ 207 | 208 | if "version" in kwargs: 209 | ver = val2bytes(kwargs["version"], U1) 210 | elif "payload" in kwargs: 211 | ver = kwargs["payload"][0:1] 212 | else: 213 | raise UBXMessageError( 214 | "NAV-RELPOSNED message definitions must include version or payload keyword" 215 | ) 216 | if ver == b"\x00": 217 | return UBX_PAYLOADS_GET["NAV-RELPOSNED-V0"] 218 | return UBX_PAYLOADS_GET["NAV-RELPOSNED"] 219 | 220 | 221 | def get_timvcocal_dict(**kwargs) -> dict: 222 | """ 223 | Select appropriate TIM-VCOCAL SET payload definition by checking 224 | the payload length. 225 | 226 | :param kwargs: optional payload key/value pairs 227 | :return: dictionary representing payload definition 228 | :rtype: dict 229 | :raises: UBXMessageError 230 | 231 | """ 232 | 233 | lpd = 1 234 | typ = 0 235 | if "type" in kwargs: 236 | typ = kwargs["type"] 237 | elif "payload" in kwargs: 238 | lpd = len(kwargs["payload"]) 239 | else: 240 | raise UBXMessageError( 241 | "TIM-VCOCAL SET message definitions must include type or payload keyword" 242 | ) 243 | if lpd == 1 and typ == 0: 244 | return UBX_PAYLOADS_SET["TIM-VCOCAL-V0"] # stop cal 245 | return UBX_PAYLOADS_SET["TIM-VCOCAL"] # cal 246 | 247 | 248 | def get_cfgdat_dict(**kwargs) -> dict: 249 | """ 250 | Select appropriate CFG-DAT SET payload definition by checking 251 | presence of datumNum keyword or payload length of 2 bytes. 252 | 253 | :param kwargs: optional payload key/value pairs 254 | :return: dictionary representing payload definition 255 | :rtype: dict 256 | 257 | """ 258 | 259 | lpd = 0 260 | if "payload" in kwargs: 261 | lpd = len(kwargs["payload"]) 262 | if lpd == 2 or "datumNum" in kwargs: 263 | return UBX_PAYLOADS_SET["CFG-DAT-NUM"] # datum num set 264 | return UBX_PAYLOADS_SET["CFG-DAT"] # manual datum set 265 | 266 | 267 | def get_secsig_dict(**kwargs) -> dict: 268 | """ 269 | Select appropriate SEC-SIG GET payload definition by checking 270 | value of 'version' attribute (1st byte of payload). 271 | 272 | :param kwargs: optional payload key/value pairs 273 | :return: dictionary representing payload definition 274 | :rtype: dict 275 | 276 | """ 277 | 278 | if "version" in kwargs: 279 | ver = val2bytes(kwargs["version"], U1) 280 | elif "payload" in kwargs: 281 | ver = kwargs["payload"][0:1] 282 | else: 283 | raise UBXMessageError( 284 | "SEC-SIG message definitions must include version or payload keyword" 285 | ) 286 | if ver == b"\x01": 287 | return UBX_PAYLOADS_GET["SEC-SIG-V1"] 288 | return UBX_PAYLOADS_GET["SEC-SIG-V2"] 289 | 290 | 291 | def get_alpsrv_dict(**kwargs) -> dict: 292 | """ 293 | Select appropriate AID-ALPSRV GET payload definition by checking 294 | value of 'type' attribute (2nd byte of payload). 295 | 296 | :param kwargs: optional payload key/value pairs 297 | :return: dictionary representing payload definition 298 | :rtype: dict 299 | 300 | """ 301 | 302 | if "type" in kwargs: 303 | typ = val2bytes(kwargs["type"], U1) 304 | elif "payload" in kwargs: 305 | typ = kwargs["payload"][1:2] 306 | else: 307 | raise UBXMessageError( 308 | "AID-ALPSRV GET message definitions must include type or payload keyword" 309 | ) 310 | if typ == b"\xff": 311 | return UBX_PAYLOADS_GET["AID-ALPSRV-SEND"] 312 | return UBX_PAYLOADS_GET["AID-ALPSRV-REQ"] 313 | 314 | 315 | VARIANTS = { 316 | POLL: {b"\x06\x31": get_cfgtp5_dict}, # CFG-TP5 317 | SET: { 318 | b"\x13\x00": get_mga_dict, # MGA GPS 319 | b"\x13\x02": get_mga_dict, # MGA GAL 320 | b"\x13\x03": get_mga_dict, # MGA BDS 321 | b"\x13\x05": get_mga_dict, # MGA QZSS 322 | b"\x13\x06": get_mga_dict, # MGA GLO 323 | b"\x13\x21": get_mga_dict, # MGA FLASH 324 | b"\x13\x40": get_mga_dict, # MGA INI 325 | b"\x02\x72": get_rxmpmp_dict, # RXM-PMP 326 | b"\x02\x41": get_rxmpmreq_dict, # RXM-PMREQ 327 | b"\x0d\x15": get_timvcocal_dict, # TIM-VCOCAL 328 | b"\x06\x06": get_cfgdat_dict, # CFG-DAT 329 | }, 330 | GET: { 331 | b"\x0b\x32": get_alpsrv_dict, # AID-ALPSRV 332 | b"\x13\x21": get_mga_dict, # MGA FLASH 333 | b"\x13\x60": get_mga_dict, # MGA ACK NAK 334 | b"\x02\x72": get_rxmpmp_dict, # RXM-PMP 335 | b"\x02\x59": get_rxmrlm_dict, # RXM-RLM 336 | b"\x06\x17": get_cfgnmea_dict, # CFG-NMEA 337 | b"\x01\x60": get_aopstatus_dict, # NAV-AOPSTATUS 338 | b"\x01\x3c": get_relposned_dict, # NAV-RELPOSNED 339 | b"\x27\x09": get_secsig_dict, # SEC-SIG 340 | }, 341 | } 342 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on 27 Sep 2020 3 | 4 | @author: semuadmin 5 | """ 6 | -------------------------------------------------------------------------------- /tests/assistnow.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/pyubx2/43e9a9d4a2f4cad0191ec42c1468efcec744cbab/tests/assistnow.log -------------------------------------------------------------------------------- /tests/pygpsdata-ALL.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/pyubx2/43e9a9d4a2f4cad0191ec42c1468efcec744cbab/tests/pygpsdata-ALL.log -------------------------------------------------------------------------------- /tests/pygpsdata-BADCK2.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/pyubx2/43e9a9d4a2f4cad0191ec42c1468efcec744cbab/tests/pygpsdata-BADCK2.log -------------------------------------------------------------------------------- /tests/pygpsdata-BADEOF1.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/pyubx2/43e9a9d4a2f4cad0191ec42c1468efcec744cbab/tests/pygpsdata-BADEOF1.log -------------------------------------------------------------------------------- /tests/pygpsdata-BADEOF2.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/pyubx2/43e9a9d4a2f4cad0191ec42c1468efcec744cbab/tests/pygpsdata-BADEOF2.log -------------------------------------------------------------------------------- /tests/pygpsdata-BADEOF3.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/pyubx2/43e9a9d4a2f4cad0191ec42c1468efcec744cbab/tests/pygpsdata-BADEOF3.log -------------------------------------------------------------------------------- /tests/pygpsdata-BADHDR.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/pyubx2/43e9a9d4a2f4cad0191ec42c1468efcec744cbab/tests/pygpsdata-BADHDR.log -------------------------------------------------------------------------------- /tests/pygpsdata-BADNMEAEOF.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/pyubx2/43e9a9d4a2f4cad0191ec42c1468efcec744cbab/tests/pygpsdata-BADNMEAEOF.log -------------------------------------------------------------------------------- /tests/pygpsdata-CFG.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/pyubx2/43e9a9d4a2f4cad0191ec42c1468efcec744cbab/tests/pygpsdata-CFG.log -------------------------------------------------------------------------------- /tests/pygpsdata-ESF.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/pyubx2/43e9a9d4a2f4cad0191ec42c1468efcec744cbab/tests/pygpsdata-ESF.log -------------------------------------------------------------------------------- /tests/pygpsdata-HNR.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/pyubx2/43e9a9d4a2f4cad0191ec42c1468efcec744cbab/tests/pygpsdata-HNR.log -------------------------------------------------------------------------------- /tests/pygpsdata-INF.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/pyubx2/43e9a9d4a2f4cad0191ec42c1468efcec744cbab/tests/pygpsdata-INF.log -------------------------------------------------------------------------------- /tests/pygpsdata-ITER.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/pyubx2/43e9a9d4a2f4cad0191ec42c1468efcec744cbab/tests/pygpsdata-ITER.log -------------------------------------------------------------------------------- /tests/pygpsdata-MIXED-RTCM3.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/pyubx2/43e9a9d4a2f4cad0191ec42c1468efcec744cbab/tests/pygpsdata-MIXED-RTCM3.log -------------------------------------------------------------------------------- /tests/pygpsdata-MIXED-RTCM3BADCRC.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/pyubx2/43e9a9d4a2f4cad0191ec42c1468efcec744cbab/tests/pygpsdata-MIXED-RTCM3BADCRC.log -------------------------------------------------------------------------------- /tests/pygpsdata-MIXED.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/pyubx2/43e9a9d4a2f4cad0191ec42c1468efcec744cbab/tests/pygpsdata-MIXED.log -------------------------------------------------------------------------------- /tests/pygpsdata-MIXED2.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/pyubx2/43e9a9d4a2f4cad0191ec42c1468efcec744cbab/tests/pygpsdata-MIXED2.log -------------------------------------------------------------------------------- /tests/pygpsdata-MIXED3.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/pyubx2/43e9a9d4a2f4cad0191ec42c1468efcec744cbab/tests/pygpsdata-MIXED3.log -------------------------------------------------------------------------------- /tests/pygpsdata-MIXED3BADCK.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/pyubx2/43e9a9d4a2f4cad0191ec42c1468efcec744cbab/tests/pygpsdata-MIXED3BADCK.log -------------------------------------------------------------------------------- /tests/pygpsdata-MON.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/pyubx2/43e9a9d4a2f4cad0191ec42c1468efcec744cbab/tests/pygpsdata-MON.log -------------------------------------------------------------------------------- /tests/pygpsdata-NAV.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/pyubx2/43e9a9d4a2f4cad0191ec42c1468efcec744cbab/tests/pygpsdata-NAV.log -------------------------------------------------------------------------------- /tests/pygpsdata-NAVHPPOS.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/pyubx2/43e9a9d4a2f4cad0191ec42c1468efcec744cbab/tests/pygpsdata-NAVHPPOS.log -------------------------------------------------------------------------------- /tests/pygpsdata-NMEA.log: -------------------------------------------------------------------------------- 1 | $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 2 | $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 3 | $PUBX,04,103607.00,060321,556567.00,2147,18,-384839,-53.623,16*2C 4 | $GPWPL,4917.16,N,12310.64,W,003*65 5 | $GPRMA,A,5327.03942,N,11214.42462,W,,,23.1,23,14.8,W*58 6 | $GPRMB,A,0.66,L,003,004,4917.24,N,12309.57,W,001.3,052.5,000.5,V*20 7 | $PGRME,15.0,M,45.0,M,25.0,M*1C 8 | $PGRMM,NAD27 Canada*2F 9 | $PGRMZ,246,f,3*1B 10 | $GPXTE,A,A,4.07,L,N*6D 11 | $GPVBW,12.3,0.07,A,11.78,0.12,A*6F 12 | $GPSTN,34*75 13 | $GPBWC,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM*21 14 | $GPBOD,097.0,T,103.2,M,POINTB,POINTA*4A 15 | $GPBOD,099.3,T,105.6,M,POINTB,*48 16 | $GPAAM,A,A,0.10,N,WPTNME*32 17 | $GPAPB,A,A,0.10,R,N,V,V,011,M,DEST,011,M,011,M*3C 18 | $GPMSK,318.0,A,100,M,2*45 19 | $GPMSS,55,27,318.0,100,*66 20 | $GBGSV,2,2,06,14,55,175,46,40,29,043,18,B*06 21 | $INGGA,103607.00,5327.03942,N,00214.42462,W,1,06,5.88,56.0,M,48.5,M,,*6A 22 | -------------------------------------------------------------------------------- /tests/pygpsdata-NMEABADEND.log: -------------------------------------------------------------------------------- 1 | $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 2 | $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 3 | $PUBX,04,103607.00,060321,556567.00,2147,18,-384839,-53.623,16*2C 4 | $GPWPL,4917.16,N,12310.64,W,003*65 5 | $GPRMA,A,5327.03942,N,11214.42462,W,,,23.1,23,14.8,W*58 6 | $GPRMB,A,0.66,L,003,004,4917.24,N,12309.57,W,001.3,052.5,000.5,V*20 7 | $PGRME,15.0,M,45.0,M,25.0,M*1C 8 | $PGRMM,NAD27 Canada*2F 9 | $PGRMZ,246,f,3*1B 10 | $GPXTE,A,A,4.07,L,N*6D 11 | $GPVBW,12.3,0.07,A,11.78,0.12,A*6F 12 | $GPSTN,34*75 13 | $GPBWC,220516,5130.02,N,00046.34,W,213.8,T,218.0,M,0004.6,N,EGLM*21 14 | $GPBOD,097.0,T,103.2,M,POINTB,POINTA*4A 15 | $GPBOD,099.3,T,105.6,M,POINTB,*48 16 | $GPAAM,A,A,0.10,N,WPTNME*32 17 | $GPAPB,A,A,0.10,R,N,V,V,011,M,DE -------------------------------------------------------------------------------- /tests/pygpsdata-RXM.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/pyubx2/43e9a9d4a2f4cad0191ec42c1468efcec744cbab/tests/pygpsdata-RXM.log -------------------------------------------------------------------------------- /tests/pygpsdata-RXMRAWX.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/pyubx2/43e9a9d4a2f4cad0191ec42c1468efcec744cbab/tests/pygpsdata-RXMRAWX.log -------------------------------------------------------------------------------- /tests/pygpsdata-SEC.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/pyubx2/43e9a9d4a2f4cad0191ec42c1468efcec744cbab/tests/pygpsdata-SEC.log -------------------------------------------------------------------------------- /tests/test_assistnow.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test MGA-GPS-ALM and MGA-GPS-EPH message payloads created by u-blox uCenter AssistNow Utility 3 | 4 | Created on 23 Nov 2020 5 | 6 | @author: semuadmin 7 | """ 8 | 9 | # pylint: disable=line-too-long, invalid-name, missing-docstring, no-member 10 | 11 | import unittest 12 | 13 | from pyubx2 import UBXMessage, SET 14 | 15 | 16 | class AssistNowTest(unittest.TestCase): 17 | def setUp(self): 18 | self.maxDiff = None 19 | self.mga_gps_alm_payloads = [ # created using uCenter AssistNow Offline 20 | b"\x02\x00\x01\x00\x0D\x53\x55\x39\x31\x1A5F\xFD\x37\x0D\xA1\x00\x08\x9D\xF8\xFF\x12\xA7\x21\x00\xF3\xB207\x00\x4B\x03\xFF\xFF\x00\x00\x00\x00", 21 | b"\x02\x00\x02\x00\xAC\xA4\x55\x39\x64\x0C52\xFD\x1D\x0C\xA1\x00\xE7\x5A\xF5\xFF\x3F\x83\xBF\xFF\xE1\xC910\x00\xC3\xFD\xFF\xFF\x00\x00\x00\x00", 22 | b"\x02\x00\x03\x00\xAF\x1A\x55\x39\x26\x1043\xFD\x2F\x0D\xA1\x00\xB1\xEF\x22\x00\xE7\xA7\x24\x00\xBC\x07D7\xFF\xF3\xFF\xFD\xFF\x00\x00\x00\x00", 23 | b"\x02\x00\x04\x00\xAA\x07\x55\x39\x57\x0B57\xFD\x7E\x0C\xA1\x00\xC1\xF1\x4E\x00\x98\x8B\x83\xFF\x1A\xD54C\x00\x5A\xFF\xFF\xFF\x00\x00\x00\x00", 24 | b"\x02\x00\x05\x00\x9F\x31\x55\x39\xA5\x0735\xFD\xB0\x0D\xA1\x00\x12\x89\x21\x00\x23\x74\x24\x00\xD3\x0D7C\x00\xE4\xFF\x00\x00\x00\x00\x00\x00", 25 | ] 26 | 27 | self.mga_gps_eph_payloads = [ # created using uCenter AssistNow Online 28 | b"\x01\x00\x01\x00\x00\x01\x00\x0B\x4F\x00A4\x1F\x00\x00\xDB\xFF\x50\x5C\x1A\x00\x3A\x03\x4F\x2D\x19\x5867\x9E\x80\x02\xE4\x0D\x8D\x2E\x30\x05\xA2\x8E\x0D\xA1\xA4\x1F46\x00\x69\x76\xA5\xF8\xB9\xFF\xC7\x20\x14\x1D\x09\x28\xF4\xD3AA\x21\x2B\xAA\xFF\xFF\xAA\x04\x00\x00\x20\xF1", 29 | b"\x01\x00\x02\x00\x00\x00\x00\xDA\x42\x00A4\x1F\x00\x00\xD7\xFF\x96\x1B\xEE\xFF\x02\x03\xB4\x30\x65\x7179\xA7\x2A\x03\xA4\x0C\x25\xFB\x49\x0A\xFC\x82\x0C\xA1\xA4\x1F7F\x00\x49\x9C\x63\xF5\x62\x00\x42\x20\xD5\x5E\x2C\x27\x29\x1E7F\xBF\xA6\xAA\xFF\xFF\x16\x04\x00\x00\xBD\xE1", 30 | b"\x01\x00\x03\x00\x00\x00\x00\x04\x14\x00A4\x1F\x00\x00\xA8\xFF\x3C\xA0\xFF\xFF\x35\xF9\x80\x32\xCF\xAAAC\x6D\xEA\xF9\x35\x0B\x7C\xC7\xAA\x01\xEB\x9F\x0D\xA1\xA4\x1FF0\xFF\x75\xA1\xF8\x22\xEF\xFF\xB2\x22\xC3\xB5\x68\x27\x93\x77BA\x24\xE9\xA6\xFF\xFF\xF8\xFE\x00\x00\x88\x8B", 31 | b"\x01\x00\x04\x00\x00\x00\x00\xF7\x3E\x02A4\x1F\x00\x00\xE1\xFF\xAB\xD2\xFA\xFF\x1F\xFB\xC7\x2F\xB8\xF560\xE3\x94\xFB\x3B\x16\x65\x55\x7A\x00\x59\xEC\x0C\xA1\xA4\x1F15\x00\xB3\x6D\xFA\x4E\xF7\xFF\xDD\x15\xFC\x36\x1C\x27\x7F\x7AAF\x83\xDB\xA9\xFF\xFF\x73\xFE\x00\x00\x6D\x49", 32 | b"\x01\x00\x05\x00\x00\x00\x00\xE8\x07\x00A4\x1F\x00\x00\xF8\xFF\xE6\x20\xFF\xFF\xFA\xF9\x71\x36\xB4\x04C4\x12\x28\xFB\x12\x0B\x32\x3A\x1A\x03\x84\x26\x0E\xA1\xA4\x1FF7\xFF\x03\x2F\x92\x21\x0C\x00\x25\x22\xAE\x95\xE0\x26\xCA\x457B\x24\xC4\xA3\xFF\xFF\x2D\xFF\x00\x00\xCD\x1F", 33 | ] 34 | 35 | def tearDown(self): 36 | pass 37 | 38 | def testAssistNowALM(self): 39 | EXPECTED_RESULTS = [ 40 | "", 41 | "", 42 | "", 43 | "", 44 | "", 45 | ] 46 | for i, pld in enumerate(self.mga_gps_alm_payloads): 47 | res = UBXMessage("MGA", "MGA-GPS-ALM", SET, payload=pld) 48 | # print(res) 49 | self.assertEqual(str(res), EXPECTED_RESULTS[i]) 50 | 51 | def testAssistNowEPH(self): 52 | EXPECTED_RESULTS = [ 53 | "", 54 | "", 55 | "", 56 | "", 57 | "", 58 | ] 59 | for i, pld in enumerate(self.mga_gps_eph_payloads): 60 | res = UBXMessage("MGA", "MGA-GPS-EPH", SET, payload=pld) 61 | # print(f'"{res}",') 62 | self.assertEqual(str(res), EXPECTED_RESULTS[i]) 63 | 64 | 65 | if __name__ == "__main__": 66 | # import sys;sys.argv = ['', 'Test.testName'] 67 | unittest.main() 68 | -------------------------------------------------------------------------------- /tests/test_bitfields.py: -------------------------------------------------------------------------------- 1 | """ 2 | Bitfield parse method tests for pyubx2.UBXMessage 3 | 4 | Created on 16 Oct 2021 5 | 6 | *** NB: must be saved in UTF-8 format *** 7 | 8 | @author: semuadmin 9 | """ 10 | # pylint: disable=line-too-long, invalid-name, missing-docstring, no-member 11 | 12 | import unittest 13 | 14 | from pyubx2 import UBXMessage, UBXReader, GET 15 | 16 | 17 | class ParseTest(unittest.TestCase): 18 | def setUp(self): 19 | self.maxDiff = None 20 | self.nav_sat2 = b"\xb5b\x015\x14\x00\x00\x00\x00\x00\x01\x01\x00\x00\x00\x00+*\xf0\x00\xfd\xff\x07\x07\x00\x00\x9bt" # qualityInd = 7, orbitSource = 7 21 | self.nav_sat3 = b"\xb5b\x015\x14\x00\x00\x00\x00\x00\x01\x01\x00\x00\x00\x00+*\xf0\x00\xfd\xff\x03\x04\x02\x00\x96_" # rtcmCorrUsed = 1 22 | self.nav_sat4 = b"\xb5b\x015\x14\x00\x00\x00\x00\x00\x01\x01\x00\x00\x00\x00+*\xf0\x00\xfd\xff\x0a\x15\x5f\x00\x0bh" # svUsed = 1, almUsed = 1 23 | 24 | def tearDown(self): 25 | pass 26 | 27 | def testNavSat2( 28 | self, 29 | ): # check X4 bitfield correctly parsed (remember little-endian) 30 | res = UBXReader.parse(self.nav_sat2) 31 | self.assertEqual( 32 | str(res), 33 | "", 34 | ) 35 | 36 | def testNavSat3( 37 | self, 38 | ): # check X4 bitfield correctly parsed (remember little-endian) 39 | res = UBXReader.parse(self.nav_sat3) 40 | self.assertEqual( 41 | str(res), 42 | "", 43 | ) 44 | 45 | def testNavSat4( 46 | self, 47 | ): # check X4 bitfield correctly parsed (remember little-endian) 48 | res = UBXReader.parse(self.nav_sat4) 49 | self.assertEqual( 50 | str(res), 51 | "", 52 | ) 53 | 54 | def testNavSat5(self): # check message bytes match original byte stream 55 | res = UBXReader.parse(self.nav_sat4) 56 | self.assertEqual(res.serialize(), self.nav_sat4) 57 | 58 | def testNavSat6( 59 | self, 60 | ): # check message correctly constructed from individual bit flags 61 | res = UBXMessage( 62 | "NAV", 63 | "NAV-SAT", 64 | GET, 65 | version=1, 66 | numSvs=1, 67 | gnssId_01=0, 68 | svId_01=0, 69 | cno_01=43, 70 | elev_01=42, 71 | azim_01=240, 72 | prRes_01=-0.30000000000000004, 73 | qualityInd_01=2, 74 | svUsed_01=1, 75 | health_01=0, 76 | diffCorr_01=0, 77 | smoothed_01=0, 78 | orbitSource_01=5, 79 | ephAvail_01=0, 80 | almAvail_01=1, 81 | anoAvail_01=0, 82 | aopAvail_01=0, 83 | sbasCorrUsed_01=1, 84 | rtcmCorrUsed_01=1, 85 | slasCorrUsed_01=1, 86 | spartnCorrUsed_01=1, 87 | prCorrUsed_01=1, 88 | crCorrUsed_01=0, 89 | doCorrUsed_01=1, 90 | ) 91 | self.assertEqual( 92 | str(res), 93 | "", 94 | ) 95 | self.assertEqual(res.serialize(), self.nav_sat4) 96 | 97 | 98 | if __name__ == "__main__": 99 | # import sys;sys.argv = ['', 'Test.testName'] 100 | unittest.main() 101 | -------------------------------------------------------------------------------- /tests/test_configdb.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sanity check of configdb key definitions 3 | 4 | Does a simple comparison between the active ubxtypes_configdb key definitions and a 5 | baselined copy to check for inadvertant corruption. 6 | 7 | Created on 19 Apr 2021 8 | 9 | @author: semuadmin 10 | """ 11 | 12 | import unittest 13 | 14 | from pyubx2 import UBXMessage, SET, POLL, SET_LAYER_FLASH, TXN_NONE 15 | from pyubx2.ubxtypes_configdb import UBX_CONFIG_DATABASE 16 | from tests.configdb_baseline import UBX_CONFIG_DATABASE_BASELINE 17 | 18 | 19 | class ConfigTest(unittest.TestCase): 20 | def setUp(self): 21 | self.maxDiff = None 22 | 23 | def tearDown(self): 24 | pass 25 | 26 | def testConfigDB(self): # sanity check against baselined configdb definitions 27 | for keyname, keytuple in UBX_CONFIG_DATABASE.items(): 28 | keyid, _ = keytuple 29 | try: 30 | keytuple2 = UBX_CONFIG_DATABASE_BASELINE[keyname] 31 | keyid2, _ = keytuple2 32 | self.assertEqual(keyid, keyid2) 33 | except KeyError: 34 | pass # ignore any keys added since baseline 35 | 36 | def testFill_CFGVALGET(self): # test CFG-VALGET POLL constructor 37 | EXPECTED_RESULT = "" 38 | res = UBXMessage( 39 | "CFG", 40 | "CFG-VALGET", 41 | POLL, 42 | payload=b"\x00\x01\x00\x00\x01\x00\x52\x40\x01\x00\x53\x40", 43 | ) 44 | self.assertEqual(str(res), EXPECTED_RESULT) 45 | 46 | def testFill_CFGVALDEL(self): # test CFG-VALDEL SET constructor 47 | EXPECTED_RESULT = "" 48 | res = UBXMessage( 49 | "CFG", 50 | "CFG-VALDEL", 51 | SET, 52 | payload=b"\x00\x03\x00\x00\x01\x00\x52\x40\x40\x53\x00\x01", 53 | ) 54 | self.assertEqual(str(res), EXPECTED_RESULT) 55 | 56 | def testFill_CFGVALSET(self): # test CFG-VALSET SET constructor 57 | EXPECTED_RESULT = "" 58 | res = UBXMessage( 59 | "CFG", 60 | "CFG-VALSET", 61 | SET, 62 | payload=b"\x00\x03\x00\x00\x01\x00\x52\x40\x80\x25\x00\x00", 63 | ) 64 | self.assertEqual(str(res), EXPECTED_RESULT) 65 | 66 | def testGOODConfigSet(self): 67 | EXPECTED_RESULT = "" 68 | msg = UBXMessage.config_set( 69 | layers=SET_LAYER_FLASH, 70 | transaction=TXN_NONE, 71 | cfgData=[(0x40110069, 0), (0x40110068, 0.1)], 72 | ) 73 | # print(msg) 74 | self.assertEqual(str(msg), EXPECTED_RESULT) 75 | 76 | 77 | if __name__ == "__main__": 78 | # import sys;sys.argv = ['', 'Test.testName'] 79 | unittest.main() 80 | -------------------------------------------------------------------------------- /tests/test_decodes.py: -------------------------------------------------------------------------------- 1 | """ 2 | test pyubx2.ubxtypes_enums.py 3 | 4 | Created on 3 Oct 2020 5 | 6 | *** NB: must be saved in UTF-8 format *** 7 | 8 | @author: semuadmin 9 | """ 10 | 11 | import os 12 | import unittest 13 | 14 | from pyubx2.ubxtypes_decodes import * 15 | 16 | 17 | class StaticTest(unittest.TestCase): 18 | def setUp(self): 19 | self.maxDiff = None 20 | dirname = os.path.dirname(__file__) 21 | 22 | def tearDown(self): 23 | pass 24 | 25 | def testenums(self): 26 | for enum in ( 27 | DGNSMODE, 28 | CONFLVL, 29 | SIGCFMASK, 30 | PROTOCOLID, 31 | DYNMODEL, 32 | FIXMODE, 33 | NMEAVERSION, 34 | SVNUMBERING, 35 | MAINTALKERID, 36 | GSVTALKERID, 37 | ODOPROFILE, 38 | CHARLEN, 39 | PARITY, 40 | NSTOPBITS, 41 | POL, 42 | SPIMODE, 43 | STATE, 44 | TIMEREF, 45 | NAVBBRMASK, 46 | RESETMODE, 47 | MODE, 48 | GRIDUTCGNSS, 49 | PROTIDS, 50 | ASTATUS, 51 | APOWER, 52 | JAMMINGSTATE, 53 | BOOTTYPE, 54 | GEOFENCE_STATUS, 55 | COMBSTATE, 56 | HEALTH, 57 | VISIBILITY, 58 | PLPOSFRAME, 59 | PLVELFRAME, 60 | GPSFIX, 61 | FIXTYPE, 62 | PSMSTATE, 63 | CARRSOLN, 64 | QUALITYIND, 65 | ORBITSOURCE, 66 | SBASMODE, 67 | SBASSYS, 68 | SBASINTEGRITYUSED, 69 | CORRSOURCE, 70 | IONOMODEL, 71 | SIGID, 72 | SOURCEOFCURLS, 73 | SRCOFLSCHANGE, 74 | SPOOFDETSTATE, 75 | PSMSTATUS, 76 | UTCSTANDARD, 77 | ): 78 | for key, val in enum.items(): 79 | # print(key, val) 80 | pass # test is just for test completion stats 81 | 82 | 83 | if __name__ == "__main__": 84 | # import sys;sys.argv = ['', 'Test.testName'] 85 | unittest.main() 86 | -------------------------------------------------------------------------------- /tests/test_socket.py: -------------------------------------------------------------------------------- 1 | """ 2 | Socket reader tests for pyubx2 - uses dummy socket class 3 | to achieve 99% test coverage of SocketWrapper. 4 | 5 | Created on 11 May 2022 6 | 7 | *** NB: must be saved in UTF-8 format *** 8 | 9 | :author: semuadmin 10 | """ 11 | 12 | import unittest 13 | from socket import socket 14 | from pyubx2 import UBXReader, UBXMessage, POLL 15 | 16 | 17 | class DummySocket(socket): 18 | """ 19 | Dummy socket class which simulates recv() and send() methods 20 | and TimeoutError. 21 | """ 22 | 23 | def __init__(self, *args, **kwargs): 24 | self._timeout = False 25 | if "timeout" in kwargs: 26 | self._timeout = kwargs["timeout"] 27 | kwargs.pop("timeout") 28 | 29 | super().__init__(*args, **kwargs) 30 | 31 | pool = ( 32 | b"\xb5b\x06\x8b\x0c\x00\x00\x00\x00\x00\x68\x00\x11\x40\xb6\xf3\x9d\x3f\xdb\x3d" 33 | + b"\xb5b\x10\x02\x1c\x00\x6d\xd8\x07\x00\x18\x20\x00\x00\xcd\x06\x00\x0e\xe4\xfe\xff\x0d\x03\xfa\xff\x05\x09\x0b\x00\x0c\x6d\xd8\x07\x00\xee\x51" 34 | + b"\xb5b\x10\x02\x18\x00\x72\xd8\x07\x00\x18\x18\x00\x00\x4b\xfd\xff\x10\x40\x02\x00\x11\x23\x28\x00\x12\x72\xd8\x07\x00\x03\x9c" 35 | + b"$GNDTM,W84,,0.0,N,0.0,E,0.0,W84*71\r\n" 36 | + b"$GNRMC,103607.00,A,5327.03942,N,10214.42462,W,0.046,,060321,,,A,V*0E\r\n" 37 | + b"$GPRTE,2,1,c,0,PBRCPK,PBRTO,PTELGR,PPLAND,PYAMBU,PPFAIR,PWARRN,PMORTL,PLISMR*73\r\n" 38 | + b"\xD3\x00\x13\x3E\xD7\xD3\x02\x02\x98\x0E\xDE\xEF\x34\xB4\xBD\x62\xAC\x09\x41\x98\x6F\x33\x36\x0B\x98" 39 | + b"\xd3\x00\x13>\xd0\x00\x03\x8aX\xd9I<\x87/4\x10\x9d\x07\xd6\xafH Z\xd7\xf7" 40 | + b"\xd3\x00\x12B\x91\x81\xc9\x84\x00\x04B\xb8\x88\x008\x80\t\xd0F\x00(\xf0kf" 41 | ) 42 | self._stream = pool * round(4096 / len(pool)) 43 | self._buffer = self._stream 44 | 45 | def recv(self, num: int) -> bytes: 46 | if self._timeout: 47 | raise TimeoutError 48 | if len(self._buffer) < num: 49 | self._buffer = self._buffer + self._stream 50 | buff = self._buffer[:num] 51 | self._buffer = self._buffer[num:] 52 | return buff 53 | 54 | def send(self, data: bytes): 55 | if self._timeout: 56 | raise TimeoutError 57 | return None 58 | 59 | 60 | class SocketTest(unittest.TestCase): 61 | def setUp(self): 62 | self.maxDiff = None 63 | 64 | def tearDown(self): 65 | pass 66 | 67 | # ******************************************* 68 | # Helper methods 69 | # ******************************************* 70 | 71 | def testSocketStub(self): 72 | EXPECTED_RESULTS = ( 73 | "", 74 | "", 75 | "", 76 | "", 77 | "", 78 | "", 79 | "", 80 | "", 81 | "", 82 | "", 83 | "", 84 | "", 85 | ) 86 | raw = None 87 | stream = DummySocket() 88 | ubr = UBXReader(stream, bufsize=1024, protfilter=7) 89 | buff = ubr._stream.buffer # test buffer getter method 90 | i = 0 91 | for raw, parsed in ubr: 92 | if raw is not None: 93 | # print(f'"{parsed}",') 94 | self.assertEqual(str(parsed), EXPECTED_RESULTS[i]) 95 | i += 1 96 | if i >= 12: 97 | break 98 | self.assertEqual(i, 12) 99 | 100 | def testSocketSend(self): 101 | stream = DummySocket() 102 | ubr = UBXReader(stream, bufsize=1024, protfilter=7) 103 | msg = UBXMessage("CFG", "CFG-PRT", POLL) 104 | res = ubr.datastream.write(msg.serialize()) 105 | self.assertEqual(res, None) 106 | 107 | def testSocketIter(self): # test for extended stream 108 | raw = None 109 | stream = DummySocket() 110 | ubr = UBXReader(stream) 111 | i = 0 112 | for raw, parsed in ubr: 113 | if raw is None: 114 | raise EOFError 115 | i += 1 116 | if i >= 123: 117 | break 118 | self.assertEqual(i, 123) 119 | 120 | def testSocketError(self): # test for simulated socket timeout 121 | raw = None 122 | stream = DummySocket(timeout=True) 123 | ubr = UBXReader(stream) 124 | i = 0 125 | for raw, parsed in ubr: 126 | i += 1 127 | if i >= 12: 128 | break 129 | self.assertEqual(i, 0) 130 | 131 | 132 | if __name__ == "__main__": 133 | # import sys;sys.argv = ['', 'Test.testName'] 134 | unittest.main() 135 | -------------------------------------------------------------------------------- /tests/test_static.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper, Property and Static method tests for pyubx2.UBXMessage 3 | 4 | Created on 3 Oct 2020 5 | 6 | *** NB: must be saved in UTF-8 format *** 7 | 8 | @author: semuadmin 9 | """ 10 | 11 | # pylint: disable=line-too-long, invalid-name, missing-docstring, no-member 12 | 13 | import os 14 | import unittest 15 | from datetime import datetime 16 | 17 | import pyubx2.ubxtypes_core as ubt 18 | from pyubx2 import POLL, SET, UBX_CLASSES, UBXMessage, UBXReader 19 | from pyubx2.ubxhelpers import ( 20 | attsiz, 21 | att2idx, 22 | att2name, 23 | bytes2val, 24 | calc_checksum, 25 | cel2cart, 26 | cfgkey2name, 27 | cfgname2key, 28 | dop2str, 29 | escapeall, 30 | get_bits, 31 | getinputmode, 32 | gnss2str, 33 | gpsfix2str, 34 | hextable, 35 | isvalid_checksum, 36 | itow2utc, 37 | key_from_val, 38 | msgstr2bytes, 39 | process_monver, 40 | protocol, 41 | utc2itow, 42 | val2bytes, 43 | val2sphp, 44 | val2twoscomp, 45 | val2signmag, 46 | ) 47 | 48 | 49 | class StaticTest(unittest.TestCase): 50 | def setUp(self): 51 | self.maxDiff = None 52 | dirname = os.path.dirname(__file__) 53 | self.streamNAV = open(os.path.join(dirname, "pygpsdata-NAV.log"), "rb") 54 | 55 | def tearDown(self): 56 | self.streamNAV.close() 57 | 58 | # def testDefinitions(self): # DEBUG test for possible missing payload definitions 59 | # for msg in ubt.UBX_MSGIDS.values(): 60 | # if ( 61 | # msg not in (ubp.UBX_PAYLOADS_POLL) 62 | # and msg not in (ubg.UBX_PAYLOADS_GET) 63 | # and msg not in (ubs.UBX_PAYLOADS_SET) 64 | # ): 65 | # print(f"Possible missing payload definition {msg}") 66 | # for msg in ubg.UBX_PAYLOADS_GET: 67 | # if msg not in ubt.UBX_MSGIDS.values(): 68 | # print(f"Possible missing core definition {msg} GET") 69 | # for msg in ubs.UBX_PAYLOADS_SET: 70 | # if msg not in ubt.UBX_MSGIDS.values(): 71 | # print(f"Possible missing core definition {msg} SET") 72 | # for msg in ubp.UBX_PAYLOADS_POLL: 73 | # if msg not in ubt.UBX_MSGIDS.values(): 74 | # print(f"Possible missing core definition {msg} POLL") 75 | 76 | def testFill_CFGMSG2(self): # test msg_cls in bytes property 77 | EXPECTED_RESULT = "b'\\x06'" 78 | res = UBXMessage("CFG", "CFG-MSG", POLL, msgClass=240, msgID=5) 79 | self.assertEqual(str(res.msg_cls), EXPECTED_RESULT) 80 | 81 | def testFill_CFGMSG3(self): # test msg_id in bytes property 82 | EXPECTED_RESULT = "b'\\x01'" 83 | res = UBXMessage("CFG", "CFG-MSG", POLL, msgClass=240, msgID=5) 84 | self.assertEqual(str(res.msg_id), EXPECTED_RESULT) 85 | 86 | def testFill_CFGMSG4(self): # test msg length property 87 | # EXPECTED_RESULT = "b'\\x02\\x00'" 88 | EXPECTED_RESULT = 2 89 | res = UBXMessage("CFG", "CFG-MSG", POLL, msgClass=240, msgID=5) 90 | self.assertEqual(res.length, EXPECTED_RESULT) 91 | 92 | def testVal2Bytes(self): # test conversion of value to bytes 93 | INPUTS = [ 94 | (2345, ubt.U2), 95 | (2345, ubt.E2), 96 | (1, ubt.L), 97 | (-2346789, ubt.I4), 98 | (b"\x44\x55", ubt.X2), 99 | (23.12345678, ubt.R4), 100 | (-23.12345678912345, ubt.R8), 101 | ([1, 2, 3, 4, 5], "A005"), 102 | ] 103 | EXPECTED_RESULTS = [ 104 | b"\x29\x09", 105 | b"\x29\x09", 106 | b"\x01", 107 | b"\xdb\x30\xdc\xff", 108 | b"\x44\x55", 109 | b"\xd7\xfc\xb8\x41", 110 | b"\x1f\xc1\x37\xdd\x9a\x1f\x37\xc0", 111 | b"\x01\x02\x03\x04\x05", 112 | ] 113 | for i, inp in enumerate(INPUTS): 114 | (val, att) = inp 115 | res = val2bytes(val, att) 116 | self.assertEqual(res, EXPECTED_RESULTS[i]) 117 | 118 | def testBytes2Val(self): # test conversion of bytes to value 119 | INPUTS = [ 120 | (b"\x29\x09", ubt.U2), 121 | (b"\x29\x09", ubt.E2), 122 | (b"\x01", ubt.L), 123 | (b"\xdb\x30\xdc\xff", ubt.I4), 124 | (b"\x44\x55", ubt.X2), 125 | (b"\xd7\xfc\xb8\x41", ubt.R4), 126 | (b"\x1f\xc1\x37\xdd\x9a\x1f\x37\xc0", ubt.R8), 127 | (b"\x01\x02\x03\x04\x05", "A005"), 128 | ] 129 | EXPECTED_RESULTS = [ 130 | 2345, 131 | 2345, 132 | 1, 133 | -2346789, 134 | b"\x44\x55", 135 | 23.12345678, 136 | -23.12345678912345, 137 | [1, 2, 3, 4, 5], 138 | ] 139 | for i, inp in enumerate(INPUTS): 140 | (valb, att) = inp 141 | res = bytes2val(valb, att) 142 | if att == ubt.R4: 143 | self.assertAlmostEqual(res, EXPECTED_RESULTS[i], 6) 144 | elif att == ubt.R8: 145 | self.assertAlmostEqual(res, EXPECTED_RESULTS[i], 14) 146 | else: 147 | self.assertEqual(res, EXPECTED_RESULTS[i]) 148 | 149 | def testUBX2Bytes(self): 150 | res = msgstr2bytes("CFG", "CFG-MSG") 151 | self.assertEqual(res, (b"\x06", b"\x01")) 152 | 153 | def testKeyfromVal(self): 154 | res = key_from_val(UBX_CLASSES, "MON") 155 | self.assertEqual(res, (b"\x0A")) 156 | 157 | def testCalcChecksum(self): 158 | res = calc_checksum(b"\x06\x01\x02\x00\xf0\x05") 159 | self.assertEqual(res, b"\xfe\x16") 160 | 161 | def testGoodChecksum(self): 162 | res = isvalid_checksum(b"\xb5b\x06\x01\x02\x00\xf0\x05\xfe\x16") 163 | self.assertTrue(res) 164 | 165 | def testBadChecksum(self): 166 | res = isvalid_checksum(b"\xb5b\x06\x01\x02\x00\xf0\x05\xfe\x15") 167 | self.assertFalse(res) 168 | 169 | def testitow2utc(self): 170 | res = str(itow2utc(387092000)) 171 | self.assertEqual(res, "11:31:14") 172 | 173 | def testutc2itow(self): 174 | dt = datetime(2024, 2, 8, 11, 31, 14) 175 | res = utc2itow(dt) 176 | self.assertEqual(res, (2300, 387092000)) 177 | 178 | def testgnss2str(self): 179 | GNSS = { 180 | 0: "GPS", 181 | 1: "SBAS", 182 | 2: "Galileo", 183 | 3: "BeiDou", 184 | 4: "IMES", 185 | 5: "QZSS", 186 | 6: "GLONASS", 187 | 7: "NAVIC", 188 | 8: "8", 189 | } 190 | for i in range(0, 9): 191 | res = gnss2str(i) 192 | self.assertEqual(res, GNSS[i]) 193 | 194 | def testgps2str(self): 195 | fixs = ["NO FIX", "DR", "2D", "3D", "GPS + DR", "TIME ONLY", "6"] 196 | for i, fix in enumerate(range(0, 7)): 197 | res = gpsfix2str(fix) 198 | self.assertEqual(res, fixs[i]) 199 | 200 | def testdop2str(self): 201 | dops = ["Ideal", "Excellent", "Good", "Moderate", "Fair", "Poor"] 202 | i = 0 203 | for dop in (1, 2, 5, 10, 20, 30): 204 | res = dop2str(dop) 205 | self.assertEqual(res, dops[i]) 206 | i += 1 207 | 208 | def testcfgname2key(self): 209 | (key, typ) = cfgname2key("CFG_NMEA_PROTVER") 210 | self.assertEqual(key, 0x20930001) 211 | self.assertEqual(typ, ubt.E1) 212 | (key, typ) = cfgname2key("CFG_UART1_BAUDRATE") 213 | self.assertEqual(key, 0x40520001) 214 | self.assertEqual(typ, ubt.U4) 215 | 216 | def testcfgkey2type(self): 217 | (key, typ) = cfgkey2name(0x20510001) 218 | self.assertEqual(key, "CFG_I2C_ADDRESS") 219 | self.assertEqual(typ, ubt.U1) 220 | 221 | def testgetbits(self): 222 | INPUTS = [ 223 | (b"\x89", 192), 224 | (b"\xc9", 3), 225 | (b"\x89", 9), 226 | (b"\xc9", 9), 227 | (b"\x18\x18", 8), 228 | (b"\x18\x20", 8), 229 | ] 230 | EXPECTED_RESULTS = [2, 1, 9, 9, 1, 0] 231 | for i, (vb, mask) in enumerate(INPUTS): 232 | vi = get_bits(vb, mask) 233 | self.assertEqual(vi, EXPECTED_RESULTS[i]) 234 | 235 | def testgetmsgmode(self): # test msgmode getter 236 | EXPECTED_RESULT = 2 237 | res = UBXMessage("CFG", "CFG-MSG", POLL, msgClass=240, msgID=5) 238 | self.assertEqual(res.msgmode, EXPECTED_RESULT) 239 | 240 | def testdatastream(self): # test datastream getter 241 | EXPECTED_RESULT = "" 242 | res = str(type(UBXReader(self.streamNAV).datastream)) 243 | self.assertEqual(res, EXPECTED_RESULT) 244 | 245 | def testprotocol(self): # test protocol() method 246 | res = protocol(b"\xb5b\x06\x01\x02\x00\xf0\x05\xfe\x16") 247 | self.assertEqual(res, ubt.UBX_PROTOCOL) 248 | res = protocol(b"$GNGLL,5327.04319,S,00214.41396,E,223232.00,A,A*68\r\n") 249 | self.assertEqual(res, ubt.NMEA_PROTOCOL) 250 | res = protocol(b"$PGRMM,WGS84*26\r\n") 251 | self.assertEqual(res, ubt.NMEA_PROTOCOL) 252 | res = protocol(b"\xd3\x00\x04L\xe0\x00\x80\xed\xed\xd6") 253 | self.assertEqual(res, ubt.RTCM3_PROTOCOL) 254 | res = protocol(b"aPiLeOfGarBage") 255 | self.assertEqual(res, 0) 256 | 257 | def testhextable(self): # test hextable*( method) 258 | EXPECTED_RESULT = "000: 2447 4e47 4c4c 2c35 3332 372e 3034 3331 | b'$GNGLL,5327.0431' |\n016: 392c 532c 3030 3231 342e 3431 3339 362c | b'9,S,00214.41396,' |\n032: 452c 3232 3332 3332 2e30 302c 412c 412a | b'E,223232.00,A,A*' |\n048: 3638 0d0a | b'68\\r\\n' |\n" 259 | res = hextable(b"$GNGLL,5327.04319,S,00214.41396,E,223232.00,A,A*68\r\n", 8) 260 | self.assertEqual(res, EXPECTED_RESULT) 261 | 262 | def testattsiz(self): # test attsiz 263 | self.assertEqual(attsiz("CH"), -1) 264 | self.assertEqual(attsiz("C032"), 32) 265 | 266 | def testatt2idx(self): # test att2idx 267 | EXPECTED_RESULT = [4, 16, 101, 0, (3, 6), 0] 268 | atts = ["svid_04", "gnssId_16", "cno_101", "gmsLon", "gnod_03_06", "dodgy_xx"] 269 | for i, att in enumerate(atts): 270 | res = att2idx(att) 271 | # print(res) 272 | self.assertEqual(res, EXPECTED_RESULT[i]) 273 | 274 | def testatt2name(self): # test att2name 275 | EXPECTED_RESULT = ["svid", "gnssId", "cno", "gmsLon"] 276 | atts = ["svid_04", "gnssId_16", "cno_101", "gmsLon"] 277 | for i, att in enumerate(atts): 278 | res = att2name(att) 279 | # print(res) 280 | self.assertEqual(res, EXPECTED_RESULT[i]) 281 | 282 | def testcel2cart(self): 283 | (elev, azim) = cel2cart(34, 128) 284 | self.assertAlmostEqual(elev, -0.510406, 5) 285 | self.assertAlmostEqual(azim, 0.653290, 5) 286 | (elev, azim) = cel2cart("xxx", 128) 287 | self.assertEqual(elev, 0) 288 | 289 | def testescapeall(self): 290 | EXPECTED_RESULT = "b'\\x68\\x65\\x72\\x65\\x61\\x72\\x65\\x73\\x6f\\x6d\\x65\\x63\\x68\\x61\\x72\\x73'" 291 | val = b"herearesomechars" 292 | res = escapeall(val) 293 | print(res) 294 | self.assertEqual(res, EXPECTED_RESULT) 295 | 296 | def testval2sphp(self): 297 | res = val2sphp(100.123456789) 298 | self.assertEqual(res, (1001234567, 89)) 299 | res = val2sphp(-13.987654321) 300 | self.assertEqual(res, (-139876543, -21)) 301 | res = val2sphp(5.9876543) 302 | self.assertEqual(res, (59876543, 0)) 303 | 304 | def testgetinputmode(self): 305 | res = getinputmode(UBXMessage("CFG", "CFG-ODO", POLL).serialize()) 306 | self.assertEqual(res, POLL) 307 | res = getinputmode( 308 | UBXMessage.config_poll(0, 0, ["CFG_UART1_BAUDRATE", 0x40530001]).serialize() 309 | ) 310 | self.assertEqual(res, POLL) 311 | res = getinputmode( 312 | UBXMessage.config_set( 313 | 0, 0, [("CFG_UART1_BAUDRATE", 9600), (0x40530001, 115200)] 314 | ).serialize() 315 | ) 316 | self.assertEqual(res, SET) 317 | res = getinputmode( 318 | UBXMessage.config_del(0, 0, ["CFG_UART1_BAUDRATE", 0x40530001]).serialize() 319 | ) 320 | self.assertEqual(res, SET) 321 | res = getinputmode(UBXMessage("CFG", "CFG-INF", POLL, protocolID=1).serialize()) 322 | self.assertEqual(res, POLL) 323 | res = getinputmode( 324 | UBXMessage( 325 | "CFG", "CFG-INF", SET, protocolID=1, infMsgMask_01=1, infMsgMask_02=1 326 | ).serialize() 327 | ) 328 | self.assertEqual(res, SET) 329 | 330 | def testprocess_monver(self): 331 | MONVER = b"\xb5\x62\x0a\x04\xdc\x00\x45\x58\x54\x20\x43\x4f\x52\x45\x20\x31\x2e\x30\x30\x20\x28\x66\x31\x37\x30\x36\x37\x29\x00\x00\x00\x00\x00\x00\x00\x00\x30\x30\x31\x39\x30\x30\x30\x30\x00\x00\x52\x4f\x4d\x20\x42\x41\x53\x45\x20\x30\x78\x31\x31\x38\x42\x32\x30\x36\x30\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x46\x57\x56\x45\x52\x3d\x48\x50\x47\x20\x31\x2e\x35\x30\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x50\x52\x4f\x54\x56\x45\x52\x3d\x32\x37\x2e\x35\x30\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x4d\x4f\x44\x3d\x5a\x45\x44\x2d\x46\x39\x50\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x47\x50\x53\x3b\x47\x4c\x4f\x3b\x47\x41\x4c\x3b\x42\x44\x53\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x53\x42\x41\x53\x3b\x51\x5a\x53\x53\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xce\x8b" 332 | msg = UBXReader.parse(MONVER) 333 | EXPECTED_RESULT = { 334 | "swversion": "Flash 1.00 (f17067)", 335 | "hwversion": "ZED-F9P 00190000", 336 | "fwversion": "HPG 1.50", 337 | "romversion": "27.50", 338 | "gnss": "GPS GLO GAL BDS SBAS QZSS ", 339 | } 340 | res = process_monver(msg) 341 | self.assertEqual(res, EXPECTED_RESULT) 342 | 343 | def testval2twoscomp(self): 344 | res = val2twoscomp(10, "U24") 345 | self.assertEqual(res, 0b0000000000000000000001010) 346 | res = val2twoscomp(-10, "U24") 347 | self.assertEqual(res, 0b111111111111111111110110) 348 | 349 | def testval2signmag(self): 350 | res = val2signmag(10, "U24") 351 | self.assertEqual(res, 0b0000000000000000000001010) 352 | res = val2signmag(-10, "U24") 353 | self.assertEqual(res, 0b1000000000000000000001010) 354 | 355 | 356 | if __name__ == "__main__": 357 | # import sys;sys.argv = ['', 'Test.testName'] 358 | unittest.main() 359 | -------------------------------------------------------------------------------- /tests/test_stream_no_parsing.py: -------------------------------------------------------------------------------- 1 | """ 2 | Stream method tests using actual receiver binary outputs for pyubx2.UBXReader 3 | 4 | Created on 3 Oct 2020 5 | 6 | *** NB: must be saved in UTF-8 format *** 7 | 8 | @author: semuadmin 9 | """ 10 | # pylint: disable=line-too-long, invalid-name, missing-docstring, no-member 11 | 12 | import sys 13 | import os 14 | import unittest 15 | 16 | from pyubx2 import UBXReader 17 | import pyubx2.ubxtypes_core as ubt 18 | 19 | DIRNAME = os.path.dirname(__file__) 20 | 21 | class StreamTest(unittest.TestCase): 22 | def setUp(self): 23 | self.maxDiff = None 24 | 25 | def tearDown(self): 26 | pass 27 | 28 | def catchio(self): 29 | """ 30 | Capture stdout as string. 31 | """ 32 | 33 | self._saved_stdout = sys.stdout 34 | self._strout = os.StringIO() 35 | sys.stdout = self._strout 36 | 37 | def restoreio(self) -> str: 38 | """ 39 | Return captured output and restore stdout. 40 | """ 41 | 42 | sys.stdout = self._saved_stdout 43 | return self._strout.getvalue().strip() 44 | 45 | def testMIXEDRTCM( 46 | self, 47 | ): # test mixed stream of NMEA, UBX & RTCM messages with protfilter = 7 48 | 49 | EXPECTED_RESULTS = ( 50 | b'$GNGLL,3203.94995,N,03446.42914,E,084158.00,A,D*77\r\n', 51 | b'\xd3\x00\x13>\xd0\x00\x03\x8aX\xd9I<\x87/4\x10\x9d\x07\xd6\xafH Z\xd7\xf7', 52 | b'\xd3\x00>\xfe\x80\x01\x00\x00\x00\x13\n\xb8\x8a@\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x01\xff\x9f\x00\x16\x02\x00\xfe\\\x00\x19\x02\x01\xfe\xdd\x00\x1d\x03\x00\x02\x86\x00\x13\x05\x00\x00\x00\x01\x90\x06\x00\x03\xf7\x00\x1a\x06\x01\x04%\x00\x1e\xd2O,', 53 | b'\xd3\x01\rCP\x000\xab\x88\xa6\x00\x00\x05GX\x02\x00\x00\x00\x00 \x00\x80\x00\x7f\x7fZZZ\x8aB\x1a\x82Z\x92Z8\x00\x00\x00\x00\x00\r\x11\xe1\xa4tf:f\xe3L,\xb1~\x9d\xf6\x87\xaf\xa0\xee\xff\x98\x14(B!A\xfc\xa9\xfaX\x96\n\x89K\x91\x971\x19c\xb6\x04\xa9\xe1F9l\xc3\x8ee\xd8\xe1\xaas\xa5\x1f?\xe9yc\x97\x98\xc6\x1f`)\xc9\xdck\xa5\x8e\xbcZ\x02SP\x82Yu\x06ex\x06Y\x00x\x10N\xf8T\x00\x05\xb0\xfa\x83\x90\xa2\x83\x89\xdc\xfc\xf1l|\xfeW~\\\xdb~h\x1c\x06\xc3\x82\x07#\x07\xfa\xe6pz\xf0\x03\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xa9:\xaa\xaa\xaa\xa0\x00\x0bB`\xac\'\t\xc2P\xb4.\x0b\x82p\x88-\t\x81\xf0\xb4.\nB\xdf\x8d\xc1k\xef\xf7\xde\xb7\xfa\xf0\x18\x13\'\xf5/\xea\xa2J\xe4\x99"T\x04\xb8\x19\xec\xb5Y\xdes\xbc\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa9t\xd0', 54 | b"\xd3\x00\xc3C\xf0\x00J\n\xbdf\x00\x00\x1c\x07\x01\x00\x00\x00\x00\x00 \x80\x00\x00\x7f\xfc\x8a\x80\x92\x98\x84\x8c\x9d\x9b\n\x0fTJ\xbe\x82'\xd0n\x9f\xc4\xfa\xce\x00\xe8T\x1e\xe1\xfeZ\t\xc0'\xa4\x15\xe6A\xd7_;\xc1\xf2\x85`.\xbe\x05\xa3'\xb6\xa6}\xb2y\xa4\xf5\x9dl\x84\x8a\x98KE\xfc!\xa6\x10W\xc8\x10oM\xfc\xd4\xe9\xfc\xa4<\x00\xbb\x0e\x01m\xcc\x1e\xd1\xb6\x1f\xc6\x0f\xe6\x98\xf1\xe7_4\x126\x18\x12\xe1\x05\xf0x\x14\xaa\xaa\xaa\xa2\xa8\xaa\xaa\xaa\xa2\xaa\xaa\xaa\xaa\xaa\xaa\xaa\x00\x02\xf0\xa0/\n\x82\xf0\x9c$\x08C\x00\xac0\n\x02\x90\xbf\xff\x80M\n\xda\x13S\x94\xa7#\xfb!\xf6\x11\xef%\xdd\xf8z\xa0\xf6\xb3\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00v'\xaf", 55 | b'\xd3\x00\x91D\x90\x000\xab\x88\xa6\x00\x00\x01\x80\x04\x12\x00\x00\x00\x00 \x01\x00\x00\x7f\xe9\xea\x8b)\xca`\x00\x00P +Z\xf8\x85~u\xef\xe04\xe0\x1f\xfd\x01\xf4\x19\x7f\x89\x81\xa5N:\xa52~\x15h6e\xdc\x18\xdd\xefY\xfb*\x9f\xf3?\xfd\x16Q\xfe$K\xe8\xe5;\xea\x9c\\\x1f\x97D \xd2\xc9\xf6\xfb\xf5\xf7\xb8\x19\xfe\xd1a\xff\xc8\xc2\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xa0\x05\xc1\x88R\x15\x85aXZ\x18\x85axiR\xd2s\x83\xd7\x07\xc9\xc4\xb3\x80\xc5g\x8a\xd4\xe1\xd2\xc3\x96\x00\x08y', 56 | b'\xd3\x01\rFp\x000\xaa\xad\xe4\x00\x00\x01`\t\x08\x84\x90\x00\x00 \x02\x00\x00/UT\x0c#\xf2Z\x8a\xa2rT\x12\xb0\x00\x00\x00\x00\x00\xf0\xf6\xa7\xb7;I$G\xaaT\xa1Y~\xfd\xfe7\xf5\xe0\x10|\xe4\r\xa7\xbe\xbf\xdf\xfe\x94\x02~h\x96\x0e\xe5\x89\xa7E\x19\xf4\xf7Q\x0e|\xe29\x81Q\x91s\xc6\xf9\x95\xf8C\xae\xcb\xf6\xf9\xa3\xbd\x83\xb5\xfb\x06\x9b"\x86~\xb7}C\xca\x7f4\xa1\x06\x0e\xb2\x84Y6\xfb\xe2\x95~\x0e6{*\xdc\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\x00-\nB\xa0\xb40\x0b\x82\xa0\xbc0\x0b\x02\xb0\xd3\xad\xa0c\xd4\xc7\xac\xc2\xed\x18\xdc\x03bo,\xd1S\x96\xcc\xbfP\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb2X\xbc', 57 | b'\xd3\x00\x04L\xe0\x00\x80\xed\xed\xd6', 58 | b'\xb5b\x01\x07\\\x00(\xe2*\x0c\xe6\x07\x02\x08\x08);7\x15\x00\x00\x00\xd0\x7f\x05\x00\x05\x03\xea\x1fN\x10\xba\x14\x95\xdb\x1c\x13\xc6\x19\x01\x00r\xd5\x00\x00\xad\x02\x00\x00\xe4\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>\xb7\xba\x01\n\x00\x00\x00\xcd\xc1\x1e\x00\x0f\'\x00\x00"\x9eE3\x00\x00\x00\x00\x00\x00\x00\x00\x12q', 59 | b'$GNRMC,084159.00,A,3203.94995,N,03446.42914,E,0.000,,080222,,,D,V*1F\r\n' 60 | ) 61 | 62 | i = 0 63 | raw = 0 64 | with open(os.path.join(DIRNAME, "pygpsdata-MIXED-RTCM3.log"), "rb") as stream: 65 | ubr = UBXReader(stream, parsing=False, protfilter=7, labelmsm=0, quitonerror=ubt.ERR_RAISE) 66 | # stdout_saved = sys.stdout 67 | # sys.stdout = open("output.txt", "w") 68 | while raw is not None: 69 | (raw, parsed) = ubr.read() 70 | if raw is not None: 71 | self.assertEqual(raw, EXPECTED_RESULTS[i]) 72 | self.assertIsNone(parsed) 73 | i += 1 74 | # sys.stdout = stdout_saved 75 | 76 | def testIterator( 77 | self, 78 | ): # test iterator function with UBX data stream & parsebitfield = False 79 | EXPECTED_RESULTS = ( 80 | b'$GNGLL,3203.94995,N,03446.42914,E,084158.00,A,D*77\r\n', 81 | b'\xd3\x00\x13>\xd0\x00\x03\x8aX\xd9I<\x87/4\x10\x9d\x07\xd6\xafH Z\xd7\xf7', 82 | b'\xd3\x00>\xfe\x80\x01\x00\x00\x00\x13\n\xb8\x8a@\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x01\xff\x9f\x00\x16\x02\x00\xfe\\\x00\x19\x02\x01\xfe\xdd\x00\x1d\x03\x00\x02\x86\x00\x13\x05\x00\x00\x00\x01\x90\x06\x00\x03\xf7\x00\x1a\x06\x01\x04%\x00\x1e\xd2O,', 83 | b'\xd3\x01\rCP\x000\xab\x88\xa6\x00\x00\x05GX\x02\x00\x00\x00\x00 \x00\x80\x00\x7f\x7fZZZ\x8aB\x1a\x82Z\x92Z8\x00\x00\x00\x00\x00\r\x11\xe1\xa4tf:f\xe3L,\xb1~\x9d\xf6\x87\xaf\xa0\xee\xff\x98\x14(B!A\xfc\xa9\xfaX\x96\n\x89K\x91\x971\x19c\xb6\x04\xa9\xe1F9l\xc3\x8ee\xd8\xe1\xaas\xa5\x1f?\xe9yc\x97\x98\xc6\x1f`)\xc9\xdck\xa5\x8e\xbcZ\x02SP\x82Yu\x06ex\x06Y\x00x\x10N\xf8T\x00\x05\xb0\xfa\x83\x90\xa2\x83\x89\xdc\xfc\xf1l|\xfeW~\\\xdb~h\x1c\x06\xc3\x82\x07#\x07\xfa\xe6pz\xf0\x03\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xa9:\xaa\xaa\xaa\xa0\x00\x0bB`\xac\'\t\xc2P\xb4.\x0b\x82p\x88-\t\x81\xf0\xb4.\nB\xdf\x8d\xc1k\xef\xf7\xde\xb7\xfa\xf0\x18\x13\'\xf5/\xea\xa2J\xe4\x99"T\x04\xb8\x19\xec\xb5Y\xdes\xbc\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa9t\xd0', 84 | b"\xd3\x00\xc3C\xf0\x00J\n\xbdf\x00\x00\x1c\x07\x01\x00\x00\x00\x00\x00 \x80\x00\x00\x7f\xfc\x8a\x80\x92\x98\x84\x8c\x9d\x9b\n\x0fTJ\xbe\x82'\xd0n\x9f\xc4\xfa\xce\x00\xe8T\x1e\xe1\xfeZ\t\xc0'\xa4\x15\xe6A\xd7_;\xc1\xf2\x85`.\xbe\x05\xa3'\xb6\xa6}\xb2y\xa4\xf5\x9dl\x84\x8a\x98KE\xfc!\xa6\x10W\xc8\x10oM\xfc\xd4\xe9\xfc\xa4<\x00\xbb\x0e\x01m\xcc\x1e\xd1\xb6\x1f\xc6\x0f\xe6\x98\xf1\xe7_4\x126\x18\x12\xe1\x05\xf0x\x14\xaa\xaa\xaa\xa2\xa8\xaa\xaa\xaa\xa2\xaa\xaa\xaa\xaa\xaa\xaa\xaa\x00\x02\xf0\xa0/\n\x82\xf0\x9c$\x08C\x00\xac0\n\x02\x90\xbf\xff\x80M\n\xda\x13S\x94\xa7#\xfb!\xf6\x11\xef%\xdd\xf8z\xa0\xf6\xb3\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00v'\xaf", 85 | b'\xd3\x00\x91D\x90\x000\xab\x88\xa6\x00\x00\x01\x80\x04\x12\x00\x00\x00\x00 \x01\x00\x00\x7f\xe9\xea\x8b)\xca`\x00\x00P +Z\xf8\x85~u\xef\xe04\xe0\x1f\xfd\x01\xf4\x19\x7f\x89\x81\xa5N:\xa52~\x15h6e\xdc\x18\xdd\xefY\xfb*\x9f\xf3?\xfd\x16Q\xfe$K\xe8\xe5;\xea\x9c\\\x1f\x97D \xd2\xc9\xf6\xfb\xf5\xf7\xb8\x19\xfe\xd1a\xff\xc8\xc2\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xa0\x05\xc1\x88R\x15\x85aXZ\x18\x85axiR\xd2s\x83\xd7\x07\xc9\xc4\xb3\x80\xc5g\x8a\xd4\xe1\xd2\xc3\x96\x00\x08y', 86 | b'\xd3\x01\rFp\x000\xaa\xad\xe4\x00\x00\x01`\t\x08\x84\x90\x00\x00 \x02\x00\x00/UT\x0c#\xf2Z\x8a\xa2rT\x12\xb0\x00\x00\x00\x00\x00\xf0\xf6\xa7\xb7;I$G\xaaT\xa1Y~\xfd\xfe7\xf5\xe0\x10|\xe4\r\xa7\xbe\xbf\xdf\xfe\x94\x02~h\x96\x0e\xe5\x89\xa7E\x19\xf4\xf7Q\x0e|\xe29\x81Q\x91s\xc6\xf9\x95\xf8C\xae\xcb\xf6\xf9\xa3\xbd\x83\xb5\xfb\x06\x9b"\x86~\xb7}C\xca\x7f4\xa1\x06\x0e\xb2\x84Y6\xfb\xe2\x95~\x0e6{*\xdc\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\x00-\nB\xa0\xb40\x0b\x82\xa0\xbc0\x0b\x02\xb0\xd3\xad\xa0c\xd4\xc7\xac\xc2\xed\x18\xdc\x03bo,\xd1S\x96\xcc\xbfP\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb2X\xbc', 87 | b'\xd3\x00\x04L\xe0\x00\x80\xed\xed\xd6', 88 | b'\xb5b\x01\x07\\\x00(\xe2*\x0c\xe6\x07\x02\x08\x08);7\x15\x00\x00\x00\xd0\x7f\x05\x00\x05\x03\xea\x1fN\x10\xba\x14\x95\xdb\x1c\x13\xc6\x19\x01\x00r\xd5\x00\x00\xad\x02\x00\x00\xe4\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>\xb7\xba\x01\n\x00\x00\x00\xcd\xc1\x1e\x00\x0f\'\x00\x00"\x9eE3\x00\x00\x00\x00\x00\x00\x00\x00\x12q', 89 | b'$GNRMC,084159.00,A,3203.94995,N,03446.42914,E,0.000,,080222,,,D,V*1F\r\n' 90 | ) 91 | i = 0 92 | with open(os.path.join(DIRNAME, "pygpsdata-MIXED-RTCM3.log"), "rb") as stream: 93 | ubr = UBXReader(stream, parsing=False, protfilter=7, parsebitfield=False) 94 | for raw, parsed in ubr: 95 | if raw is not None: 96 | # print(f'"{parsed}",') 97 | self.assertEqual(raw, EXPECTED_RESULTS[i]) 98 | self.assertIsNone(parsed) 99 | i += 1 100 | 101 | 102 | if __name__ == "__main__": 103 | # import sys;sys.argv = ['', 'Test.testName'] 104 | unittest.main() 105 | -------------------------------------------------------------------------------- /tests/ucenter-ZEDF9P-configdebug.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/pyubx2/43e9a9d4a2f4cad0191ec42c1468efcec744cbab/tests/ucenter-ZEDF9P-configdebug.log --------------------------------------------------------------------------------