├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── checkpr.yml │ └── main.yml ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── APPLAUNCH.md ├── CITATION.bib ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── RELEASE_NOTES.md ├── RTK-TIPS.md ├── SECURITY.md ├── docs ├── Makefile ├── conf.py ├── index.rst ├── make.bat ├── modules.rst └── pygpsclient.rst ├── examples ├── F9P_3D_printed_case.md ├── device-dummy123-1234-1234-1234-abcdefghijkl-ucenter-config.json ├── georef.py ├── mymap01.tif ├── mymap02.tif ├── mymap03.tif ├── pygpsclient_debian_install.sh ├── pygpsclient_macos_install.sh ├── python_compile.sh ├── socket_server.py ├── tcpclient.py ├── tcpserver_threaded.py ├── ttydata.log ├── ttypresets_examples.py ├── txt2ubx.py └── ubxsimulator.json ├── images ├── app.png ├── banner_widget.png ├── basestation_fixed.png ├── basestation_off.png ├── basestation_svin.png ├── chart_widget.png ├── console_widget.png ├── custommap.png ├── dgps_status.png ├── gpxviewer.png ├── graphview_widget.png ├── importcustommap.png ├── imu_widget.png ├── msgmode.png ├── nmeaconfig_widget.png ├── ntrip_consolelog.png ├── ntripconfig_widget.png ├── rover_widget.png ├── scatterplot_widget.png ├── skyview_widget.png ├── sourcetable_ggamarker.png ├── spartn_consolelog.png ├── spartnconfig_widget.png ├── spectrum_widget.png ├── staticmap.png ├── sysmon_widget.png ├── tty_console.png ├── tty_dialog.png ├── ubxconfig_widget.png └── webmap_widget.png ├── pygpsclient.desktop ├── pygpsclient.json ├── pyproject.toml ├── src └── pygpsclient │ ├── __init__.py │ ├── __main__.py │ ├── _version.py │ ├── about_dialog.py │ ├── app.py │ ├── banner_frame.py │ ├── chart_frame.py │ ├── configuration.py │ ├── confirm_box.py │ ├── console_frame.py │ ├── dialog_state.py │ ├── dynamic_config_frame.py │ ├── file_handler.py │ ├── globals.py │ ├── gnss_status.py │ ├── gpx_dialog.py │ ├── graphview_frame.py │ ├── hardware_info_frame.py │ ├── helpers.py │ ├── importmap_dialog.py │ ├── imu_frame.py │ ├── map_frame.py │ ├── mapquest.py │ ├── menu_bar.py │ ├── nmea_config_dialog.py │ ├── nmea_handler.py │ ├── nmea_preset_frame.py │ ├── ntrip_client_dialog.py │ ├── resources │ ├── app-128.png │ ├── binary-1-24.png │ ├── blank-1-24.png │ ├── bmc-full-logo-no-background.png │ ├── clear-1-24.png │ ├── ethernet-1-24.png │ ├── github-256.png │ ├── iconmonstr-antenna-3-24.png │ ├── iconmonstr-antenna-4-24.png │ ├── iconmonstr-arrow-12-24.png │ ├── iconmonstr-arrow-80-16.png │ ├── iconmonstr-check-mark-8-24.png │ ├── iconmonstr-door-6-24.png │ ├── iconmonstr-folder-18-24.png │ ├── iconmonstr-gear-2-24-brown.png │ ├── iconmonstr-gear-2-24.png │ ├── iconmonstr-location-1-24.png │ ├── iconmonstr-media-control-48-24.png │ ├── iconmonstr-media-control-50-24.png │ ├── iconmonstr-noclient-10-24.png │ ├── iconmonstr-plus-lined-24.png │ ├── iconmonstr-record-24.png │ ├── iconmonstr-refresh-6-16.png │ ├── iconmonstr-refresh-lined-24.png │ ├── iconmonstr-save-14-24.png │ ├── iconmonstr-stop-1-24.png │ ├── iconmonstr-time-6-24.png │ ├── iconmonstr-transmit-10-24.png │ ├── iconmonstr-trash-can-filled-24.png │ ├── iconmonstr-triangle-1-16.png │ ├── iconmonstr-undo-24.png │ ├── iconmonstr-warning-1-24.png │ ├── marker_end.png │ ├── marker_start.png │ ├── pygpsclient.ico │ ├── usbport-1-24.png │ └── world.png │ ├── rover_frame.py │ ├── rtcm3_handler.py │ ├── sbf_handler.py │ ├── scatter_frame.py │ ├── serialconfig_frame.py │ ├── serverconfig_frame.py │ ├── settings_frame.py │ ├── skyview_frame.py │ ├── socketconfig_frame.py │ ├── spartn_dialog.py │ ├── spartn_gnss_frame.py │ ├── spartn_json_config.py │ ├── spartn_lband_frame.py │ ├── spartn_mqtt_frame.py │ ├── spectrum_frame.py │ ├── status_frame.py │ ├── stream_handler.py │ ├── strings.py │ ├── sysmon_frame.py │ ├── tty_handler.py │ ├── tty_preset_dialog.py │ ├── ubx_cfgval_frame.py │ ├── ubx_config_dialog.py │ ├── ubx_handler.py │ ├── ubx_msgrate_frame.py │ ├── ubx_port_frame.py │ ├── ubx_preset_frame.py │ ├── ubx_recorder_frame.py │ ├── ubx_solrate_frame.py │ └── widget_state.py └── tests ├── __init__.py └── test_static.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | buy_me_a_coffee: semuconsulting 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: PyGPSClient 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 | # PyGPSClient Bug Report Template 14 | 15 | **NB**: Please raise any general queries in the [pygpsclient Discussions Channels](https://github.com/semuconsulting/pygpsclient/discussions) in the first instance. 16 | 17 | **Describe the bug** 18 | 19 | A clear and concise description of what the bug is. 20 | 21 | Please specify the pygpsclient version (`pygpsclient -V) and, where possible, include: 22 | - A screenshot of the error. 23 | - The error message and full traceback. 24 | - A binary / hexadecimal dump of the UBX data stream (e.g. from PuTTY or screen). 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 Ventura, Ubuntu Kinetic] 39 | - The version of Python you're using (e.g. Python 3.11.1) 40 | - The type of serial connection [e.g. USB, UART1] 41 | 42 | **GNSS/GPS Device (please complete the following information as best you can):** 43 | 44 | - Device Model/Generation: [e.g. u-blox ZED-F9P] 45 | - Firmware Version: [e.g. HPG 1.32] 46 | - Protocol: [e.g. 32.00] 47 | 48 | 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. 49 | 50 | **Additional context** 51 | 52 | Add any other context about the problem here. 53 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: pygpsclient Community Support 4 | url: https://github.com/semuconsulting/pygpsclient/discussions 5 | about: Please ask and answer questions here. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: PyGPSClient Feature request 3 | 4 | about: Suggest an idea for this project 5 | 6 | title: '' 7 | 8 | labels: '' 9 | 10 | assignees: semuadmin 11 | 12 | --- 13 | # PyGPSClient Feature Request Template 14 | 15 | **NB**: Please raise any feature requests or queries in the [pygpsclient Discussions Channels](https://github.com/semuconsulting/pygpsclient/discussions) in the first instance. 16 | 17 | **Is your feature request related to a problem? Please describe.** 18 | 19 | A clear and concise description of what the problem is e.g. I'd like to be able to do this [...] 20 | 21 | **Describe the solution you'd like** 22 | 23 | A clear and concise description of what you want to happen. 24 | 25 | **Describe alternatives you've considered** 26 | 27 | A clear and concise description of any alternative solutions or features you've considered. 28 | 29 | **Would you be willing to contribute a test device?** 30 | 31 | If the request relates to a specific u-blox device that is not currently supported, would you be 32 | willing to contribute a device to the project for testing purposes? 33 | 34 | **Additional context** 35 | 36 | Add any other context about the feature request here. 37 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # PyGPSClient 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. List any dependencies that are required for this change. 6 | 7 | Fixes # (issue) 8 | 9 | ## Testing 10 | 11 | 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. 12 | 13 | - [ ] Test A 14 | - [ ] Test B 15 | 16 | ## Checklist: 17 | 18 | - [ ] I agree to abide by the code of conduct (see [CODE_OF_CONDUCT.md](https://github.com/semuconsulting/pygpsclient/blob/master/CODE_OF_CONDUCT.md)). 19 | - [ ] My code follows the style guidelines of this project (see [CONTRIBUTING.MD](https://github.com/semuconsulting/pygpsclient/blob/master/CONTRIBUTING.md)). 20 | - [ ] I have performed a self-review of my own code. 21 | - [ ] I have commented my code, particularly in hard-to-understand areas. 22 | - [ ] I have made corresponding changes to the documentation. 23 | - [ ] (*if appropriate*) I have added test cases to the `tests/test_*.py` unittest suite to maintain test coverage. 24 | - [ ] I have tested my code against the full `tests/test_*.py` unittest suite. 25 | - [ ] My changes generate no new warnings. 26 | - [ ] Any dependent changes have been merged and published in downstream modules. 27 | - [ ] I have [signed](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits) my commits. 28 | - [ ] I understand and acknowledge that the code will be published under a BSD 3-Clause license. -------------------------------------------------------------------------------- /.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: pygpsclient 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 -lll . 36 | - name: Test with pytest 37 | run: | 38 | pytest 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dropbox 3 | .project 4 | .coverage 5 | .pydevproject 6 | *.code-workspace 7 | /.settings 8 | Pipfile*.* 9 | /htmlcov 10 | /build 11 | /dist 12 | /references 13 | /docs/_build 14 | /examples/temp* 15 | /examples/pointperfect 16 | /logs/*.log 17 | /logs/*.gpx 18 | /pygpsclient_* 19 | /pygpsclient-* 20 | src/pygpsclient.egg* 21 | /__pycache__ 22 | /.dbeaver 23 | /pygpsclient/__pycache__ 24 | mqapikey 25 | /.pytest_cache/ 26 | pylint_report.txt 27 | snapcraft.yaml 28 | /wiki 29 | *.pyc 30 | *.bin 31 | *.gpx -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: Module", 9 | "type": "debugpy", 10 | "request": "launch", 11 | "module": "pygpsclient" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.testing.pytestEnabled": true, 3 | "python.testing.unittestEnabled": false, 4 | "editor.formatOnSave": true, 5 | "modulename": "pygpsclient", 6 | "distname": "pygpsclient", 7 | "python.defaultInterpreterPath": "python3", 8 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | // These Python project tasks assume you have installed and configured: 5 | // build, wheel, black, pylint, pytest, pytest-cov, Sphinx, sphinx-rtd-theme 6 | // Use the Update Toolchain task to install the necessary packages. 7 | "version": "2.0.0", 8 | "tasks": [ 9 | { 10 | "label": "Install Dependencies", 11 | "type": "process", 12 | "command": "${config:python.defaultInterpreterPath}", 13 | "args": [ 14 | "-m", 15 | "pip", 16 | "install", 17 | "--upgrade", 18 | "." 19 | ], 20 | "problemMatcher": [] 21 | }, 22 | { 23 | "label": "Run Local Version", 24 | "type": "process", 25 | "command": "${config:python.defaultInterpreterPath}", 26 | "args": [ 27 | "-m", 28 | "pygpsclient", 29 | "--ntripcasteruser", 30 | "semuadmin", 31 | "--ntripcasterpassword", 32 | "testpassword", 33 | "--verbosity", 34 | "2" 35 | ], 36 | "options": { 37 | "cwd": "src" 38 | }, 39 | "problemMatcher": [] 40 | }, 41 | { 42 | "label": "Clean", 43 | "type": "shell", 44 | "command": "rm", 45 | "args": [ 46 | "-rfvd", 47 | "build", 48 | "dist", 49 | "htmlcov", 50 | "docs/_build", 51 | "${config:modulename}.egg-info" 52 | ], 53 | "windows": { 54 | "command": "Get-ChildItem", 55 | "args": [ 56 | "-Path", 57 | "build\\,", 58 | "dist\\,", 59 | "docs\\_build,", 60 | "${config:modulename}.egg-info", 61 | "-Recurse", 62 | "|", 63 | "Remove-Item", 64 | "-Recurse", 65 | "-Confirm:$false", 66 | "-Force" 67 | ] 68 | }, 69 | "options": { 70 | "cwd": "${workspaceFolder}" 71 | }, 72 | "problemMatcher": [] 73 | }, 74 | { 75 | "label": "Sort Imports", 76 | "type": "process", 77 | "command": "${config:python.defaultInterpreterPath}", 78 | "args": [ 79 | "-m", 80 | "isort", 81 | "src", 82 | "--jobs", 83 | "-1" 84 | ], 85 | "problemMatcher": [] 86 | }, 87 | { 88 | "label": "Format", 89 | "type": "process", 90 | "command": "${config:python.defaultInterpreterPath}", 91 | "args": [ 92 | "-m", 93 | "black", 94 | "src" 95 | ], 96 | "problemMatcher": [] 97 | }, 98 | { 99 | "label": "Pylint", 100 | "type": "process", 101 | "command": "${config:python.defaultInterpreterPath}", 102 | "args": [ 103 | "-m", 104 | "pylint", 105 | "src" 106 | ], 107 | "problemMatcher": [] 108 | }, 109 | { 110 | "label": "Security", 111 | "type": "process", 112 | "command": "${config:python.defaultInterpreterPath}", 113 | "args": [ 114 | "-m", 115 | "bandit", 116 | "-c", 117 | "pyproject.toml", 118 | "-r", 119 | "--exit-zero", 120 | "." 121 | ], 122 | "problemMatcher": [] 123 | }, 124 | { 125 | "label": "Build", 126 | "type": "process", 127 | "command": "${config:python.defaultInterpreterPath}", 128 | "args": [ 129 | "-m", 130 | "build", 131 | ".", 132 | "--wheel", 133 | "--sdist" 134 | ], 135 | "problemMatcher": [], 136 | "group": { 137 | "kind": "build", 138 | "isDefault": true 139 | } 140 | }, 141 | { 142 | "label": "Test", 143 | "type": "process", 144 | "command": "${config:python.defaultInterpreterPath}", 145 | "args": [ 146 | "-m", 147 | "pytest" 148 | ], 149 | "problemMatcher": [] 150 | }, 151 | { 152 | "label": "Sphinx", 153 | "type": "process", 154 | "command": "sphinx-apidoc", 155 | "args": [ 156 | "--ext-autodoc", 157 | "--ext-viewcode", 158 | "--templatedir=docs", 159 | "-f", 160 | "-o", 161 | "docs", 162 | "src/${config:modulename}" 163 | ], 164 | "problemMatcher": [] 165 | }, 166 | { 167 | "label": "Sphinx HTML", 168 | "type": "process", 169 | "command": "/usr/bin/make", 170 | "windows": { 171 | "command": "${workspaceFolder}/docs/make.bat" 172 | }, 173 | "args": [ 174 | "html" 175 | ], 176 | "options": { 177 | "cwd": "${workspaceFolder}/docs" 178 | }, 179 | "dependsOrder": "sequence", 180 | "dependsOn": [ 181 | "Sphinx" 182 | ], 183 | "problemMatcher": [] 184 | }, 185 | { 186 | "label": "Sphinx Deploy to S3", 187 | "type": "process", 188 | "command": "aws", 189 | "args": [ 190 | "s3", 191 | "cp", 192 | "${workspaceFolder}/docs/_build/html", 193 | "s3://www.semuconsulting.com/${config:modulename}/", 194 | "--recursive" 195 | ], 196 | "dependsOrder": "sequence", 197 | "dependsOn": [ 198 | "Sphinx HTML" 199 | ], 200 | "problemMatcher": [] 201 | }, 202 | { 203 | "label": "Install Wheel", 204 | "type": "shell", 205 | "command": "${config:python.defaultInterpreterPath}", 206 | "args": [ 207 | "-m", 208 | "pip", 209 | "install", 210 | "--user", 211 | "--force-reinstall", 212 | "*.whl" 213 | ], 214 | "options": { 215 | "cwd": "dist" 216 | }, 217 | "problemMatcher": [] 218 | }, 219 | { 220 | "label": "Install Locally", 221 | "type": "shell", 222 | "command": "${config:python.defaultInterpreterPath}", 223 | "args": [ 224 | "-m", 225 | "pip", 226 | "install", 227 | //"--user", 228 | "--force-reinstall", 229 | "*.whl" 230 | ], 231 | "dependsOrder": "sequence", 232 | "dependsOn": [ 233 | "Clean", 234 | "Security", 235 | "Sort Imports", 236 | "Format", 237 | "Pylint", 238 | "Test", 239 | "Build", 240 | "Sphinx HTML" 241 | ], 242 | "options": { 243 | "cwd": "dist" 244 | }, 245 | "problemMatcher": [] 246 | } 247 | ] 248 | } -------------------------------------------------------------------------------- /APPLAUNCH.md: -------------------------------------------------------------------------------- 1 | 2 | # Creating A Desktop Application Launcher 3 | 4 | The pip installation process does not automatically create a desktop application launcher for PyGPSClient, but this can be done manually: 5 | 6 | _In the examples below, substitute `Python3.11` for your environment's version of Python e.g. `Python3.12`, `Python3.13`, etc., and `myuser` for your user name._ 7 | 8 | ## Windows 9 | 10 | To create an application launcher for Windows, create a new Shortcut named `PyGPSClient` with the following properties (*adapted for your particular environment*): 11 | 12 | - Target type: Application 13 | - Target location: Scripts 14 | - Target: `C:\Users\myuser\pygpsclient\Scripts\pygpsclient.exe` 15 | - Start in: `C:\Users\myuser` 16 | - Run: Minimized 17 | 18 | and place this in the `C:\Users\myuser\AppData\Roaming\Microsoft\Windows\Start Menu\Programs` directory (*you may need Administrator privileges to do this*). To assign an icon to this shortcut, select Change Icon.. and Browse to the pygpsclient.ico file in the site_packages folder (e.g.`C:\Users\myuser\pygpsclient\Lib\site-packages\pygpsclient\resources\pygpsclient.ico`) 19 | 20 | ## MacOS 21 | 22 | To create an application launcher for MacOS, use MacOS's Automator tool to create a "Run Shell Script" application and save this as `PyGPSClient.app`, e.g. 23 | 24 | Shell: /bin/zsh 25 | ``` 26 | /Users/myuser/pygpsclient/bin/pygpsclient 27 | ``` 28 | 29 | To assign an icon to this shortcut, right-click on the `PyGPSClient` entry in the Applications folder, select "Get Info" and drag-and-drop the pygpsclient.ico image file from the site-packages folder (e.g. `/Users/myuser/pygpsclient/lib/python3.11/site-packages/pygpsclient/resources/pygpsclient.ico`) to the default application icon at the top left of the "Get Info" panel. 30 | 31 | ## Linux 32 | 33 | To create an application launcher for most Linux distributions, create a text file named `pygpsclient.desktop` with the following content (*adapted for your particular environment*) and copy this to the `/home/myuser/.local/share/applications` folder, e.g. 34 | 35 | ``` 36 | [Desktop Entry] 37 | Type=Application 38 | Terminal=false 39 | Name=PyGPSClient 40 | Icon=/home/myuser/pygpsclient/lib/python3.11/site-packages/pygpsclient/resources/pygpsclient.ico 41 | Exec=/home/myuser/pygpsclient/bin/pygpsclient 42 | ``` 43 | 44 | You will need to logout and login for the launcher to take effect. 45 | -------------------------------------------------------------------------------- /CITATION.bib: -------------------------------------------------------------------------------- 1 | @Misc{PyGPSClient, 2 | author = {{SEMU Consulting}}, 3 | howpublished = {GitHub repository}, 4 | note = {Viewed last: xxxx:xx:xx}, 5 | title = {Python Graphical GPS Client Application supporting NMEA, UBX, RTCM3, NTRIP & SPARTN Protocols}, 6 | year = {2022}, 7 | url = {https://github.com/semuconsulting/pygpsclient}, 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 | # PyGPSClient How to contribute 2 | 3 | PyGPSClient is a volunteer project and we appreciate any contribution, from fixing a grammar mistake in a comment to extending 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/pygpsclient/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 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 | ## Coding conventions 16 | 17 | * This is open source software. Code should be as simple and transparent as possible. Favour clarity over brevity. 18 | * The code should be compatible with Python >= 3.9 and tkinter >= 8.6. 19 | * Avoid external library dependencies unless there's a compelling reason not to. 20 | * 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. 21 | * Code should be documented in accordance with [Sphinx](https://www.sphinx-doc.org/en/master/) docstring conventions. 22 | * Code should formatted using [black](https://pypi.org/project/black/) (>= 25.0.0). 23 | * We use and recommend [pylint](https://pypi.org/project/pylint/) (>= 3.3.0) for code analysis. 24 | * We use and recommend [bandit](https://pypi.org/project/bandit/) (>= 1.8.0) for security vulnerability analysis. 25 | * Commits must be [signed](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits). 26 | 27 | ## Testing 28 | 29 | While we endeavour to test on as wide a variety of u-blox devices and host platforms 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.). 30 | 31 | We use Python's native unittest framework for local unit testing, complemented by the GitHub Actions automated build and testing workflow. 32 | 33 | Please write unittest examples for new code you create and add them to the `/tests` folder following the naming convention `test_*.py`. 34 | 35 | We test on the following host platforms using a variety of GNSS devices: 36 | 37 | * Windows 11 (Intel and Snapdragon) 38 | * MacOS (Sonoma & Sequoia, Intel and Apple Silicon) 39 | * Linux (Ubuntu 24.04 LTS Numbat & 25.02 Puffin) 40 | * Raspberry Pi OS (Bookworm 32-bit & 64-bit) 41 | 42 | ## Submitting changes 43 | 44 | Please send a [GitHub Pull Request to PyGPSClient](https://github.com/semuconsulting/PyGPSClient/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 (below) and make sure all of your commits are atomic (one feature per commit). 45 | 46 | Please use the supplied [Pull Request Template](https://github.com/semuconsulting/pygpsclient/blob/master/.github/pull_request_template.md). 47 | 48 | Please sign all commits - see [Signing GitHub Commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits) for instructions. 49 | 50 | Always write a clear log message for your commits. One-line messages are fine for small changes, but bigger changes should look like this: 51 | 52 | $ git commit -m "A brief summary of the commit 53 | > 54 | > A paragraph describing what changed and its impact." 55 | 56 | 57 | 58 | Thanks, 59 | 60 | 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 | include src/pygpsclient/resources/*.* 3 | global-exclude __pycache__ 4 | global-exclude *.pyc 5 | -------------------------------------------------------------------------------- /RTK-TIPS.md: -------------------------------------------------------------------------------- 1 | # RTK Tips - Achieving cm Level Accuracy using Real-time Kinematics 2 | 3 | This content has moved to [Achieving cm Level Accuracy using Real-time Kinematics](https://www.semuconsulting.com/gnsswiki/rtktips/). 4 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # PyGPSClient 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/PyGPSClient) 8 | 9 | ## Reporting a Vulnerability 10 | 11 | Please report any suspected security vulnerabilities via the supplied 12 | [Issue Template](https://github.com/semuconsulting/PyGPSClient/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 pygpsclient import version as VERSION 19 | 20 | # sys.path.insert(0, os.path.abspath('../pygpsclient')) 21 | 22 | # -- Project information ----------------------------------------------------- 23 | 24 | project = "PyGPSClient" 25 | copyright = "2021, SEMU Consulting" 26 | author = "SEMU Consulting" 27 | 28 | # The full version, including alpha/beta/rc tags 29 | release = VERSION 30 | version = VERSION 31 | 32 | # -- General configuration --------------------------------------------------- 33 | 34 | # Add any Sphinx extension module names here, as strings. They can be 35 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 36 | # ones. 37 | extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode"] 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ["_templates"] 41 | 42 | # List of patterns, relative to source directory, that match files and 43 | # directories to ignore when looking for source files. 44 | # This pattern also affects html_static_path and html_extra_path. 45 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 46 | 47 | # -- Options for HTML output ------------------------------------------------- 48 | 49 | # The theme to use for HTML and HTML Help pages. See the documentation for 50 | # a list of builtin themes. 51 | # 52 | html_theme = "sphinx_rtd_theme" 53 | html_title = " v documentation." 54 | 55 | # Add any paths that contain custom static files (such as style sheets) here, 56 | # relative to this directory. They are copied after the builtin static files, 57 | # so a file named "default.css" will overwrite the builtin "default.css". 58 | # html_static_path = ["_static"] 59 | html_last_updated_fmt = "%b %d %Y" 60 | 61 | autodoc_default_options = { 62 | "members": True, 63 | "member-order": "bysource", 64 | "special-members": "__init__", 65 | "undoc-members": True, 66 | "exclude-members": "__weakref__", 67 | } 68 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. pygpsclient 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 PyGPSClient's documentation! 7 | ======================================= 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | 14 | Indices and tables 15 | ================== 16 | 17 | * :ref:`genindex` 18 | * :ref:`modindex` 19 | * :ref:`search` 20 | -------------------------------------------------------------------------------- /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 | pygpsclient 2 | =========== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | pygpsclient 8 | -------------------------------------------------------------------------------- /examples/F9P_3D_printed_case.md: -------------------------------------------------------------------------------- 1 | ## Simple enclosure for SparkFun GPS-RTK-SMA ZED-F9P Breakout 2 | 3 | https://www.thingiverse.com/thing:6774791 4 | 5 | ![3D printed case](https://cdn.thingiverse.com/assets/2d/79/53/64/a3/large_display_f9pcase10.png) 6 | -------------------------------------------------------------------------------- /examples/device-dummy123-1234-1234-1234-abcdefghijkl-ucenter-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "MQTT": { 3 | "Connectivity": { 4 | "Name": "dummymqtt", 5 | "ClientID": "dummy123-1234-1234-1234-abcdefghijkl", 6 | "Protocol": "MQTT 3.1.1", 7 | "ServerURI": "ssl://pp.services.u-blox.com:8883", 8 | "ClientCredentials": { 9 | "Type": "X.509", 10 | "Key": "dummykey", 11 | "Cert": "dummycert", 12 | "RootCA": "dummyrootca" 13 | }, 14 | "CleanSession": true, 15 | "ConnectionTimeout": 30, 16 | "KeepAliveInterval": 3600, 17 | "SSL": { 18 | "Mode": "BASIC", 19 | "Protocol": "TLSv1.2" 20 | } 21 | }, 22 | "Subscriptions": { 23 | "Key": { 24 | "QoS": 1, 25 | "KeyTopics": [ 26 | "/pp/ubx/0236/ip" 27 | ] 28 | }, 29 | "AssistNow": { 30 | "QoS": 1, 31 | "AssistNowTopics": [ 32 | "/pp/ubx/mga" 33 | ] 34 | }, 35 | "Data": { 36 | "QoS": 0, 37 | "DataTopics": [ 38 | "/pp/ip/eu", 39 | "/pp/ip/eu/gad;/pp/ip/eu/hpac;/pp/ip/eu/ocb;/pp/ip/eu/clk", 40 | "/pp/ip/us", 41 | "/pp/ip/us/gad;/pp/ip/us/hpac;/pp/ip/us/ocb;/pp/ip/us/clk", 42 | "/pp/ip/kr", 43 | "/pp/ip/kr/gad;/pp/ip/kr/hpac;/pp/ip/kr/ocb;/pp/ip/kr/clk", 44 | "/pp/ip/au" 45 | ] 46 | } 47 | }, 48 | "dynamickeys": { 49 | "current": { 50 | "start": 1676159982000, 51 | "duration": 2419199999, 52 | "value": "abc123def456ghi789jkl901mno234pq" 53 | }, 54 | "next": { 55 | "start": 1678579182000, 56 | "duration": 3023999999, 57 | "value": "def456ghi789jkl901mno234pqr567st" 58 | } 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /examples/georef.py: -------------------------------------------------------------------------------- 1 | """ 2 | georef.py 3 | 4 | Support utility for the PyGPSClient custom offline map facility 5 | (requires PyGPSClient>=1.4.10 and rasterio>=1.3.6). 6 | 7 | Generates the necessary PyGPSClient json configuration settings 8 | from one or more georeferenced raster image files e.g. geoTIFF: 9 | 10 | Usage: 11 | 12 | python3 georef.py -I mymap01.tif,mymap02.tif,mymap03.tif 13 | 14 | "usermaps_l": [ 15 | [ 16 | "/home/myuser/maps/mymap01.tif", 17 | [52.48903090029343, 13.376796709434993, 52.428992219505325, 13.513273402895] 18 | ], 19 | [ 20 | "/home/myuser/maps/mymap02.tif", 21 | [52.43227595237385, 13.499564532600285, 52.37215985772205, 13.636041225870589] 22 | ], 23 | [ 24 | "/home/myuser/maps/mymap03.tif", 25 | [52.37776419675291, 13.625147570124257, 52.31757380344052, 13.761624263584265] 26 | ] 27 | ] 28 | 29 | To create a suitable geo-referenced map image, you can use the free open source QGIS 30 | software: 31 | 32 | 1. Download and install the latest version of QGIS for your platform (https://qgis.org). 33 | 2. Open the QGIS application. From the top menu bar, select Project...New. 34 | 3. From the source browser, select XYZ Tiles...OpenStreetMap. 35 | 4. Zoom into the area you wish to create a map image for. 36 | 5. From the top menu bar, select Project...Import/Export...Export Map to image. 37 | 6. From the pop-up Save Map as Image dialog, click Save and enter the required file name and format (TIF is recommended). 38 | 39 | NB: While Open Street Map is free to use, it is subject to copyright - see https://www.openstreetmap.org/copyright. 40 | 41 | Created on 21 Feb 2024 42 | 43 | @author: semuadmin 44 | """ 45 | 46 | from argparse import ArgumentParser 47 | from os.path import abspath 48 | 49 | from rasterio import open as openraster 50 | from rasterio.warp import transform_bounds 51 | 52 | 53 | def main(infiles: str): 54 | """ 55 | Generate config settings from georeferenced raster file. 56 | 57 | :param str infiles: comma-separated list of file paths 58 | """ 59 | 60 | files = infiles.split(",") 61 | print('"usermaps_l": [') 62 | for i, fl in enumerate(files): 63 | fl = fl.strip() 64 | ras = openraster(fl) 65 | lonmin, latmin, lonmax, latmax = transform_bounds( 66 | ras.crs.to_epsg(), 4326, *ras.bounds 67 | ) 68 | print( 69 | f' [\n "{abspath(fl)}",\n {[latmax, lonmin, latmin, lonmax]}\n ]{"," if i < len(files)-1 else ""}' 70 | ) 71 | print("]") 72 | 73 | 74 | if __name__ == "__main__": 75 | 76 | arp = ArgumentParser( 77 | description="Generates PyGPSClient json config settings for one or more georeferenced raster files e.g. geoTIFF" 78 | ) 79 | arp.add_argument( 80 | "-I", 81 | "--infiles", 82 | required=True, 83 | help="comma-separated list of file paths", 84 | ) 85 | 86 | kwargs = vars(arp.parse_args()) 87 | main(kwargs["infiles"]) 88 | -------------------------------------------------------------------------------- /examples/mymap01.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/PyGPSClient/93487c10d26414e47e5a61d05118777774d24215/examples/mymap01.tif -------------------------------------------------------------------------------- /examples/mymap02.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/PyGPSClient/93487c10d26414e47e5a61d05118777774d24215/examples/mymap02.tif -------------------------------------------------------------------------------- /examples/mymap03.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/PyGPSClient/93487c10d26414e47e5a61d05118777774d24215/examples/mymap03.tif -------------------------------------------------------------------------------- /examples/pygpsclient_debian_install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Bash shell script to install PyGPSClient on 64-bit Debian-based 4 | # Linux environments, including Raspberry Pi and Ubuntu. 5 | # 6 | # Change shebang /bin/bash to /bin/zsh if running from zsh shell. 7 | # NB: NOT for use on Windows or MacOS! 8 | # 9 | # Remember to run chmod +x pygpsclient_debian_install.sh to make this script executable. 10 | # 11 | # Full installation instructions: 12 | # https://github.com/semuconsulting/PyGPSClient 13 | # 14 | # Created by semuadmin on 20 Sep 2023. 15 | # 16 | # exit on error 17 | set -e 18 | 19 | PYVER="$(python3 -c 'import sys; print(".".join(map(str, sys.version_info[:2])))')" 20 | 21 | echo "Installed Python version is $PYVER" 22 | 23 | echo "PyGPSClient will be installed at $HOME/pygpsclient/bin" 24 | 25 | echo "Installing dependencies..." 26 | sudo apt install python3-pip python3-tk python3-pil python3-pil.imagetk \ 27 | libjpeg-dev zlib1g-dev tk-dev python3-rasterio 28 | 29 | echo "Setting user permissions..." 30 | sudo usermod -a -G tty $USER 31 | 32 | echo "Creating virtual environment..." 33 | cd $HOME 34 | python3 -m venv pygpsclient 35 | source pygpsclient/bin/activate 36 | python3 -m pip install --upgrade pip pygpsclient 37 | deactivate 38 | 39 | echo "Adding desktop launch icon..." 40 | cat > $HOME/.local/share/applications/pygpsclient.desktop <> $PROF 46 | source $PROF 47 | 48 | echo "Installation complete" 49 | -------------------------------------------------------------------------------- /examples/python_compile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # bash shell script to compile and install specified Python 3 version 4 | # on Debian Linux platforms e.g. Ubuntu or Raspberry Pi OS. 5 | # 6 | # Remember to run chmod +x python_compile.sh to make this script executable. 7 | # 8 | # Source code: 9 | # https://www.python.org/downloads/source/ 10 | # https://www.python.org/ftp/python/ 11 | # 12 | # Build instructions: 13 | # https://devguide.python.org/getting-started/setup-building/index.html#install-dependencies 14 | # https://docs.python.org/3/using/unix.html#building-python 15 | # 16 | # Created by semuadmin on 20 Sep 2020. 17 | # 18 | # exit on error 19 | set -e 20 | 21 | # set required Python major and minor version e.g. 3.10.10 22 | PYVER="3.13.0" 23 | # NB: uncomment this line to install this version alongside existing versions 24 | # ALTINSTALL=1 25 | 26 | # download and unzip source code 27 | sudo apt install vim wget screen -y 28 | wget https://www.python.org/ftp/python/${PYVER}/Python-${PYVER}.tgz 29 | tar zvxf Python-${PYVER}.tgz 30 | 31 | # enable the Debian source repos 32 | SRCDEB="/etc/apt/sources.list" 33 | SRCUBU="/etc/apt/sources.list.d/ubuntu.sources" 34 | # Debian, including Raspberry Pi OS: 35 | if test -f $SRCDEB 36 | then 37 | sudo sed -i -e 's/#deb-src/deb-src/g' $SRCDEB 38 | fi 39 | # Ubuntu, including 24.04 LTS: 40 | if test -f $SRCUBU 41 | then 42 | sudo sed -i 's/^Types: deb$/Types: deb deb-src/' $SRCUBU 43 | fi 44 | 45 | # install build dependencies 46 | sudo apt update 47 | sudo apt build-dep python3 48 | sudo apt install build-essential gdb lcov pkg-config \ 49 | libbz2-dev libffi-dev libgdbm-dev libgdbm-compat-dev liblzma-dev \ 50 | libncurses5-dev libreadline6-dev libsqlite3-dev libssl-dev \ 51 | lzma lzma-dev tk-dev uuid-dev zlib1g-dev -y 52 | 53 | # compile, install and verify installed version 54 | cd Python-${PYVER} 55 | ./configure --enable-optimizations 56 | make 57 | # make test 58 | if [ -z ${ALTINSTALL+x} ] 59 | then 60 | sudo make install 61 | python3 -V 62 | else 63 | sudo make altinstall 64 | python${PYVER%.*} -V 65 | fi 66 | -------------------------------------------------------------------------------- /examples/socket_server.py: -------------------------------------------------------------------------------- 1 | """ 2 | FOR TESTING ONLY 3 | 4 | Threaded GNSS TCP socket server. 5 | 6 | Reads serial stream from GNSS receiver and outputs 7 | raw binary data to multiple TCP socket clients. 8 | 9 | Created on 26 Apr 2022 10 | 11 | :author: semuadmin 12 | :copyright: 2020 SEMU Consulting 13 | :license: BSD 3-Clause 14 | """ 15 | 16 | from io import BufferedReader 17 | from queue import Queue 18 | from socketserver import StreamRequestHandler, ThreadingTCPServer 19 | from threading import Event, Thread 20 | 21 | from pynmeagps import NMEAMessageError, NMEAParseError 22 | from pyrtcm import RTCMMessageError, RTCMParseError 23 | from pyubx2 import ( 24 | ERR_IGNORE, 25 | NMEA_PROTOCOL, 26 | RTCM3_PROTOCOL, 27 | UBX_PROTOCOL, 28 | UBXMessageError, 29 | UBXParseError, 30 | UBXReader, 31 | ) 32 | from serial import Serial, SerialException, SerialTimeoutException 33 | 34 | from pygpsclient.globals import DEFAULT_BUFSIZE 35 | 36 | 37 | class SockServer(ThreadingTCPServer): 38 | """ 39 | Socket server class. 40 | """ 41 | 42 | def __init__(self, maxclients: int, msgqueue: Queue, *args, **kwargs): 43 | """ 44 | Constructor. 45 | 46 | :param int maxclients: max no of clients allowed 47 | :param Queue msgqueue: queue containing raw GNSS messages 48 | """ 49 | 50 | self.maxclients = maxclients 51 | self._gnss_inqueue = msgqueue 52 | self.connections = 0 53 | self._stopevent = Event() 54 | self.clientqueues = [] 55 | # set up pool of client queues 56 | for _ in range(self.maxclients): 57 | self.clientqueues.append({"client": None, "queue": Queue()}) 58 | super().__init__(*args, **kwargs) 59 | 60 | # start GNSS message reader 61 | while not self._gnss_inqueue.empty(): # flush queue 62 | self._gnss_inqueue.get() 63 | self._stopevent.clear() 64 | self._stream_thread = Thread( 65 | target=self._read_thread, 66 | args=(self._stopevent, self._gnss_inqueue, self.clientqueues), 67 | daemon=True, 68 | ) 69 | self._stream_thread.start() 70 | 71 | def _read_thread(self, stopevent: Event, msgqueue: Queue, clientqueues: dict): 72 | """ 73 | Read from main GNSS message queue and place 74 | raw data on array of socket client queues. 75 | 76 | :param Event stopevent: stop event 77 | :param Queue msgqueue: message queue 78 | :param Dict clientqueues: pool of queues for used by clients 79 | """ 80 | 81 | while not stopevent.is_set(): 82 | raw, _ = msgqueue.get() 83 | for i in range(self.maxclients): 84 | clientqueues[i]["queue"].put(raw) 85 | 86 | 87 | class ClientHandler(StreamRequestHandler): 88 | """ 89 | Threaded TCP client connection handler class. 90 | """ 91 | 92 | def __init__(self, *args, **kwargs): 93 | """ 94 | Constructor. 95 | """ 96 | 97 | self._qidx = None 98 | self._gnss_inqueue = None 99 | self._allowed = False 100 | super().__init__(*args, **kwargs) 101 | 102 | def setup(self, *args, **kwargs): 103 | """ 104 | Overidden client handler setup routine. 105 | """ 106 | 107 | # find next unused client queue in pool... 108 | for i, clq in enumerate(self.server.clientqueues): 109 | if clq["client"] is None: 110 | self.server.clientqueues[i]["client"] = self.client_address[1] 111 | self._gnss_inqueue = clq["queue"] 112 | while not self._gnss_inqueue.empty(): # flush queue 113 | self._gnss_inqueue.get() 114 | self._qidx = i 115 | self._allowed = True 116 | break 117 | if self._qidx is None: # no available client queues in pool 118 | print( 119 | f"Connection rejected - maximum {self.server.maxclients} clients allowed" 120 | ) 121 | return 122 | 123 | if self._allowed: 124 | self.server.connections += 1 125 | print( 126 | f"Client connected {self.client_address[0]}:{self.client_address[1]}", 127 | f"; number of clients: {self.server.connections}", 128 | ) 129 | super().setup(*args, **kwargs) 130 | 131 | def finish(self, *args, **kwargs): 132 | """ 133 | Overidden client handler finish routine. 134 | """ 135 | 136 | if self._qidx is not None: 137 | self.server.clientqueues[self._qidx]["client"] = None 138 | 139 | if self._allowed: 140 | self.server.connections -= 1 141 | print( 142 | f"Client disconnected {self.client_address[0]}:{self.client_address[1]}", 143 | f"; number of clients: {self.server.connections}", 144 | ) 145 | super().finish(*args, **kwargs) 146 | 147 | def handle(self): 148 | """ 149 | Overridden main client handler. 150 | """ 151 | 152 | while self._allowed: 153 | try: 154 | raw = self._gnss_inqueue.get() 155 | if raw is not None: 156 | self.wfile.write(raw) 157 | self.wfile.flush() 158 | except (ConnectionAbortedError, ConnectionResetError, BrokenPipeError): 159 | break 160 | 161 | 162 | class StreamHandler: 163 | """ 164 | Stream handler class. 165 | """ 166 | 167 | def __init__(self, serport, baud, timeout): 168 | """ 169 | Constructor. 170 | """ 171 | 172 | self._serport = serport 173 | self._baud = baud 174 | self._timeout = timeout 175 | self._serial_object = None 176 | self._serial_buffer = None 177 | self._stream_thread = None 178 | self._socket = None 179 | self._gnss_inqueue = Queue() 180 | self._stopevent = Event() 181 | self._maxclients = 10 182 | 183 | @property 184 | def msgqueue(self) -> Queue: 185 | """ 186 | Getter for main message queue. 187 | 188 | :return: message queue 189 | :rtype: Queue 190 | """ 191 | 192 | return self._gnss_inqueue 193 | 194 | def start_read_thread(self): 195 | """ 196 | Start the stream read thread. 197 | """ 198 | 199 | self._stopevent.clear() 200 | self._stream_thread = Thread( 201 | target=self._read_thread, 202 | args=(self._stopevent, self.msgqueue), 203 | daemon=True, 204 | ) 205 | self._stream_thread.start() 206 | 207 | def stop_read_thread(self): 208 | """ 209 | Stop serial reader thread. 210 | """ 211 | 212 | self._stopevent.set() 213 | self._stream_thread = None 214 | 215 | def _read_thread(self, stopevent: Event, msgqueue: Queue): 216 | """ 217 | THREADED PROCESS 218 | 219 | Connects to selected data stream and starts read loop. 220 | 221 | :param Event stopevent: thread stop event 222 | :param Queue msgqueue: message queue 223 | """ 224 | 225 | try: 226 | with Serial( 227 | self._serport, self._baud, timeout=self._timeout 228 | ) as self._serial_object: 229 | stream = BufferedReader(self._serial_object) 230 | self._readloop(stopevent, msgqueue, stream) 231 | 232 | except (SerialException, SerialTimeoutException) as err: 233 | print(err) 234 | 235 | def _readloop(self, stopevent: Event, msgqueue: Queue, stream: object): 236 | """ 237 | Read stream continously until stop event or stream error. 238 | 239 | :param Event stopevent: thread stop event 240 | :param Queue msgqueue: message queue 241 | :param object stream: data stream 242 | """ 243 | # pylint: disable=no-self-use 244 | 245 | ubr = UBXReader( 246 | stream, 247 | protfilter=NMEA_PROTOCOL | UBX_PROTOCOL | RTCM3_PROTOCOL, 248 | quitonerror=ERR_IGNORE, 249 | bufsize=DEFAULT_BUFSIZE, 250 | ) 251 | 252 | raw_data = None 253 | parsed_data = None 254 | while not stopevent.is_set(): 255 | try: 256 | raw_data, parsed_data = ubr.read() 257 | if raw_data is not None: 258 | # print(parsed_data) 259 | msgqueue.put((raw_data, parsed_data)) 260 | except ( 261 | UBXMessageError, 262 | UBXParseError, 263 | NMEAMessageError, 264 | NMEAParseError, 265 | RTCMMessageError, 266 | RTCMParseError, 267 | ) as err: 268 | print(err) 269 | continue 270 | 271 | 272 | if __name__ == "__main__": 273 | SERIAL = "/dev/tty.usbmodem14101" 274 | BAUD = 9600 275 | TIMEOUT = 0.1 276 | HOST = "localhost" 277 | PORT = 50007 278 | MAXCLIENTS = 5 279 | 280 | print(f"Creating Serial streamer on {SERIAL}@{BAUD}") 281 | streamer = StreamHandler(SERIAL, BAUD, TIMEOUT) 282 | print(f"Creating Socket server on {HOST}:{PORT}") 283 | server = SockServer(MAXCLIENTS, streamer.msgqueue, (HOST, PORT), ClientHandler) 284 | 285 | try: 286 | print("starting serial read thread...") 287 | streamer.start_read_thread() 288 | print("Starting TCP server, waiting for client connections...") 289 | server.serve_forever() 290 | except KeyboardInterrupt: 291 | streamer.stop_read_thread() 292 | print("Socket server terminated by user") 293 | -------------------------------------------------------------------------------- /examples/tcpclient.py: -------------------------------------------------------------------------------- 1 | """ 2 | FOR TESTING ONLY 3 | 4 | TCP socket client test harness. 5 | 6 | Receives and parses arbitrary UBX messages from TCP server. 7 | 8 | Created on 26 Apr 2022 9 | 10 | :author: semuadmin 11 | :copyright: 2020 SEMU Consulting 12 | :license: BSD 3-Clause 13 | """ 14 | 15 | import socket 16 | 17 | import pyubx2.ubxtypes_core as ubt 18 | from pynmeagps import NMEAReader 19 | from pyrtcm import RTCMReader 20 | from pyubx2 import UBXReader 21 | 22 | HOST = "localhost" # The remote host 23 | PORT = 50007 # The same port as used by the server 24 | BUFLEN = 4096 25 | 26 | 27 | class TCPClient: 28 | """ 29 | TCP Client class. 30 | """ 31 | 32 | def __init__(self, host, port): 33 | """ 34 | Constructor. 35 | """ 36 | 37 | self._host = host 38 | self._port = port 39 | 40 | def run(self): 41 | """ 42 | Start client connection. 43 | """ 44 | 45 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: 46 | sock.connect((self._host, self._port)) 47 | buf = bytearray() 48 | data = "init" 49 | try: 50 | while data: 51 | data = sock.recv(BUFLEN) 52 | print(f"Bytes received {len(data)}") 53 | buf += data 54 | while True: 55 | raw, buf = self.parse_buffer(buf) 56 | if raw is None: 57 | break 58 | except KeyboardInterrupt: 59 | print("TCP Client terminated by user") 60 | 61 | def parse_buffer(self, buf: bytearray) -> tuple: 62 | """ 63 | Read buffer and parse the first complete UBX, NMEA or RTCM2 message. 64 | 65 | :param bytearray buf: buffer 66 | :return: tuple of (raw_data, buf_remain) 67 | :rtype: tuple 68 | """ 69 | 70 | raw_data = None 71 | parsed_data = None 72 | buf_remain = buf 73 | start = 0 74 | 75 | while start < len(buf): 76 | try: 77 | byte1 = self._read_bytes(buf, start, 1) 78 | # if not NMEA, UBX or RTCM3, skip and continue 79 | if byte1 not in (b"\x24", b"\xb5", b"\xd3"): 80 | start += 1 81 | continue 82 | byte2 = self._read_bytes(buf, start + 1, 1) 83 | bytehdr = byte1 + byte2 84 | if bytehdr == ubt.UBX_HDR: # UBX 85 | raw_data, parsed_data = self.parse_ubx(buf, start, bytehdr) 86 | elif bytehdr in ubt.NMEA_HDR: # NMEA 87 | raw_data, parsed_data = self.parse_nmea(buf, start, bytehdr) 88 | elif byte1 == b"\xd3" and (byte2[0] & ~0x03) == 0: # RTCM3 89 | raw_data, parsed_data = self.parse_rtcm(buf, start, bytehdr) 90 | else: 91 | start += 2 92 | continue 93 | 94 | print(parsed_data) 95 | lnr = len(raw_data) 96 | buf_remain = buf[start + lnr :] 97 | break 98 | 99 | except EOFError: 100 | break 101 | 102 | return (raw_data, buf_remain) 103 | 104 | def parse_ubx(self, buf: bytearray, start: int, hdr: bytes) -> bytes: 105 | """ 106 | Parse UBX Message. 107 | """ 108 | 109 | byten = self._read_bytes(buf, start + 2, 4) 110 | lenb = int.from_bytes(byten[2:4], "little", signed=False) 111 | byten += self._read_bytes(buf, start + 6, lenb + 2) 112 | raw_data = bytes(hdr + byten) 113 | parsed_data = UBXReader.parse(raw_data) 114 | 115 | return raw_data, parsed_data 116 | 117 | def parse_nmea(self, buf: bytearray, start: int, hdr: bytes) -> bytes: 118 | """ 119 | Parse NMEA Message. 120 | """ 121 | 122 | i = 1 123 | # read buffer until CRLF - equivalent to readline() 124 | while True: 125 | byten = self._read_bytes(buf, start + 2, i) 126 | if byten[-2:] == b"\x0d\x0a": # CRLF 127 | raw_data = bytes(hdr + byten) 128 | parsed_data = NMEAReader.parse(raw_data) 129 | break 130 | i += 1 131 | 132 | return raw_data, parsed_data 133 | 134 | def parse_rtcm(self, buf: bytearray, start: int, hdr: bytes) -> bytes: 135 | """ 136 | Parse RTCM3 Message. 137 | """ 138 | 139 | hdr3 = self._read_bytes(buf, start + 2, 1) 140 | lenb = hdr3[0] | (hdr[1] << 8) 141 | byten = self._read_bytes(buf, start + 3, lenb + 3) 142 | raw_data = bytes(hdr + hdr3 + byten) 143 | parsed_data = RTCMReader.parse(raw_data) 144 | 145 | return raw_data, parsed_data 146 | 147 | @staticmethod 148 | def _read_bytes(buf: bytearray, start: int, num: int) -> bytes: 149 | """ 150 | Read specified number of bytes from buffer. 151 | 152 | :param bytearray buf: buffer 153 | :param int start: start index 154 | :param int num: number of bytes to read 155 | :return: bytes read 156 | :rtype: bytes 157 | :raises: EOFError 158 | """ 159 | 160 | if len(buf) < start + num: 161 | raise EOFError 162 | return buf[start : start + num] 163 | 164 | 165 | if __name__ == "__main__": 166 | print(f"Creating TCP client on {HOST}:{PORT}") 167 | client = TCPClient(HOST, PORT) 168 | 169 | print("Starting TCP client ...") 170 | client.run() 171 | -------------------------------------------------------------------------------- /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: 2020 SEMU Consulting 12 | :license: BSD 3-Clause 13 | """ 14 | 15 | import random 16 | 17 | # from serial import Serial 18 | from datetime import datetime 19 | from socketserver import StreamRequestHandler, ThreadingTCPServer 20 | from time import sleep 21 | 22 | from pynmeagps import NMEAMessage 23 | from pyrtcm import RTCMMessage 24 | from pyubx2 import GET, UBXMessage # , UBXReader 25 | 26 | SERPORT = "/dev/tty.usbmodem141101" 27 | BAUD = 9600 28 | TIMEOUT = 3 29 | AFAM = "IPv4" 30 | HOST = "::1" if AFAM == "IPv6" else "localhost" 31 | PORT = 50007 # Arbitrary non-privileged port 32 | DELAY = 0.1 33 | 34 | 35 | class GNSSServer(StreamRequestHandler): 36 | """ 37 | Threaded TCP client connection handler class. 38 | """ 39 | 40 | @staticmethod 41 | def create_unknownUBX_msg() -> UBXMessage: 42 | """ 43 | Create unknown UBX message to test error handling. 44 | """ 45 | 46 | return b"\xb5b\x06\x99\x08\x00\xf0\x01\x00\x01\x00\x01\x00\x00\x9a\xba" 47 | 48 | @staticmethod 49 | def create_UBX_msg() -> UBXMessage: 50 | """ 51 | Create arbitrary UBX message. 52 | """ 53 | # pylint: disable=invalid-name 54 | 55 | dat = datetime.now() 56 | msg = UBXMessage( 57 | "NAV", 58 | "NAV-PVT", 59 | GET, 60 | year=dat.year, 61 | month=dat.month, 62 | day=dat.day, 63 | hour=dat.hour, 64 | min=dat.minute, 65 | second=dat.second, 66 | validDate=1, 67 | validTime=1, 68 | fixType=3, 69 | lat=random.uniform(-90.0, 90.0), 70 | lon=random.uniform(-180.0, 180.0), 71 | hMSL=random.randint(0, 100000), 72 | numSV=random.randint(1, 26), 73 | ) 74 | return msg.serialize() 75 | # or use live stream from receiver (clunky) ... 76 | # with Serial(SERPORT, BAUD, timeout=TIMEOUT) as stream: 77 | # ubr = UBXReader(stream, protfilter=7) 78 | # raw, _ = ubr.read() 79 | # return raw 80 | 81 | @staticmethod 82 | def create_NMEA_msg() -> NMEAMessage: 83 | """ 84 | Create arbitrary NMEA message. 85 | """ 86 | # pylint: disable=invalid-name 87 | 88 | lat = random.uniform(-90.0, 90.0) 89 | lon = random.uniform(-180.0, 180.0) 90 | msg = NMEAMessage( 91 | "GN", 92 | "GLL", 93 | GET, 94 | lat=lat, 95 | lon=lon, 96 | NS="N" if lat > 0 else "S", 97 | EW="E" if lon > 0 else "W", 98 | status="A", 99 | posMode="A", 100 | ) 101 | return msg.serialize() 102 | 103 | @staticmethod 104 | def create_RTCM3_msg() -> RTCMMessage: 105 | """ 106 | Create arbitrary RTCM3 message. 107 | """ 108 | # pylint: disable=invalid-name 109 | 110 | msg = RTCMMessage( 111 | payload=b">\xd0\x00\x03\x8aX\xd9I<\x87/4\x10\x9d\x07\xd6\xafH " 112 | ) 113 | return msg.serialize() 114 | 115 | def handle(self): 116 | """ 117 | Handle client connection. 118 | """ 119 | 120 | print(f"Client connected: {self.client_address[0]}:{self.client_address[1]}") 121 | while True: 122 | try: 123 | # put multiple random msgs on buffer, mixed in with junk 124 | # to exercise the clients' parsing routine 125 | data = bytearray() 126 | n = random.randint(1, 5) 127 | for _ in range(n): 128 | r = random.randint(1, 7) 129 | if r in (1, 2, 3): 130 | data += self.create_NMEA_msg() + b"\x04\x05\x06" 131 | elif r == 4: 132 | data += self.create_RTCM3_msg() + b"\x07\x08\x09" 133 | elif r == 5: 134 | data += self.create_unknownUBX_msg() + b"\x03\x04\x05" 135 | else: 136 | data += self.create_UBX_msg() + b"\x01\x02\x03" 137 | # data = self.create_UBX_msg() 138 | if data is not None: 139 | self.wfile.write(data) 140 | self.wfile.flush() 141 | sleep(DELAY) 142 | except (ConnectionAbortedError, BrokenPipeError): 143 | print( 144 | f"Client disconnected: {self.client_address[0]}:{self.client_address[1]}" 145 | ) 146 | break 147 | 148 | 149 | if __name__ == "__main__": 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/ttydata.log: -------------------------------------------------------------------------------- 1 | AT+NAVI_OUTPUT=UART1,ON 2 | AT+LEVER_ARM=0,0,0 3 | AT+CLUB_VECTOR=0,0,1.855 4 | AT+GNSS_CARD=HEMI 5 | AT+WORK_MODE=152 6 | AT+SAVE_ALL 7 | 8 | OK 9 | 10 | 11 | 12 | 13 | Error 14 | 15 | 16 | 17 | AT+NAVI_OUTPUT=UART1,ON 18 | AT+LEVER_ARM=0,0,0 19 | AT+CLUB_VECTOR=0,0,1.855 20 | AT+GNSS_CARD=HEMI 21 | AT+WORK_MODE=152 22 | AT+SAVE_ALL 23 | 24 | OK 25 | 26 | 27 | 28 | 29 | Error 30 | 31 | 32 | 33 | AT+NAVI_OUTPUT=UART1,ON 34 | AT+LEVER_ARM=0,0,0 35 | AT+CLUB_VECTOR=0,0,1.855 36 | AT+GNSS_CARD=HEMI 37 | AT+WORK_MODE=152 38 | AT+SAVE_ALL 39 | 40 | OK 41 | 42 | 43 | 44 | 45 | Error 46 | 47 | 48 | 49 | AT+NAVI_OUTPUT=UART1,ON 50 | AT+LEVER_ARM=0,0,0 51 | AT+CLUB_VECTOR=0,0,1.855 52 | AT+GNSS_CARD=HEMI 53 | AT+WORK_MODE=152 54 | AT+SAVE_ALL 55 | 56 | OK 57 | 58 | 59 | 60 | 61 | Error 62 | 63 | 64 | AT+NAVI_OUTPUT=UART1,ON 65 | AT+LEVER_ARM=0,0,0 66 | AT+CLUB_VECTOR=0,0,1.855 67 | AT+GNSS_CARD=HEMI 68 | AT+WORK_MODE=152 69 | AT+SAVE_ALL 70 | 71 | OK 72 | 73 | 74 | 75 | 76 | Error 77 | 78 | 79 | 80 | AT+NAVI_OUTPUT=UART1,ON 81 | AT+LEVER_ARM=0,0,0 82 | AT+CLUB_VECTOR=0,0,1.855 83 | AT+GNSS_CARD=HEMI 84 | AT+WORK_MODE=152 85 | AT+SAVE_ALL 86 | 87 | OK 88 | 89 | 90 | 91 | 92 | Error 93 | 94 | 95 | 96 | AT+NAVI_OUTPUT=UART1,ON 97 | AT+LEVER_ARM=0,0,0 98 | AT+CLUB_VECTOR=0,0,1.855 99 | AT+GNSS_CARD=HEMI 100 | AT+WORK_MODE=152 101 | AT+SAVE_ALL 102 | 103 | OK 104 | 105 | 106 | 107 | 108 | Error 109 | 110 | 111 | 112 | AT+NAVI_OUTPUT=UART1,ON 113 | AT+LEVER_ARM=0,0,0 114 | AT+CLUB_VECTOR=0,0,1.855 115 | AT+GNSS_CARD=HEMI 116 | AT+WORK_MODE=152 117 | AT+SAVE_ALL 118 | 119 | OK 120 | 121 | 122 | 123 | 124 | Error 125 | 126 | 127 | AT+NAVI_OUTPUT=UART1,ON 128 | AT+LEVER_ARM=0,0,0 129 | AT+CLUB_VECTOR=0,0,1.855 130 | AT+GNSS_CARD=HEMI 131 | AT+WORK_MODE=152 132 | AT+SAVE_ALL 133 | 134 | OK 135 | 136 | 137 | 138 | 139 | Error 140 | 141 | 142 | 143 | AT+NAVI_OUTPUT=UART1,ON 144 | AT+LEVER_ARM=0,0,0 145 | AT+CLUB_VECTOR=0,0,1.855 146 | AT+GNSS_CARD=HEMI 147 | AT+WORK_MODE=152 148 | AT+SAVE_ALL 149 | 150 | OK 151 | 152 | 153 | 154 | 155 | Error 156 | 157 | 158 | 159 | AT+NAVI_OUTPUT=UART1,ON 160 | AT+LEVER_ARM=0,0,0 161 | AT+CLUB_VECTOR=0,0,1.855 162 | AT+GNSS_CARD=HEMI 163 | AT+WORK_MODE=152 164 | AT+SAVE_ALL 165 | 166 | OK 167 | 168 | 169 | 170 | 171 | Error 172 | 173 | 174 | 175 | AT+NAVI_OUTPUT=UART1,ON 176 | AT+LEVER_ARM=0,0,0 177 | AT+CLUB_VECTOR=0,0,1.855 178 | AT+GNSS_CARD=HEMI 179 | AT+WORK_MODE=152 180 | AT+SAVE_ALL 181 | 182 | OK 183 | 184 | 185 | 186 | 187 | Error 188 | 189 | 190 | AT+NAVI_OUTPUT=UART1,ON 191 | AT+LEVER_ARM=0,0,0 192 | AT+CLUB_VECTOR=0,0,1.855 193 | AT+GNSS_CARD=HEMI 194 | AT+WORK_MODE=152 195 | AT+SAVE_ALL 196 | 197 | OK 198 | 199 | 200 | 201 | 202 | Error 203 | 204 | 205 | 206 | AT+NAVI_OUTPUT=UART1,ON 207 | AT+LEVER_ARM=0,0,0 208 | AT+CLUB_VECTOR=0,0,1.855 209 | AT+GNSS_CARD=HEMI 210 | AT+WORK_MODE=152 211 | AT+SAVE_ALL 212 | 213 | OK 214 | 215 | 216 | 217 | 218 | Error 219 | 220 | 221 | 222 | AT+NAVI_OUTPUT=UART1,ON 223 | AT+LEVER_ARM=0,0,0 224 | AT+CLUB_VECTOR=0,0,1.855 225 | AT+GNSS_CARD=HEMI 226 | AT+WORK_MODE=152 227 | AT+SAVE_ALL 228 | 229 | OK 230 | 231 | 232 | 233 | 234 | Error 235 | 236 | 237 | 238 | AT+NAVI_OUTPUT=UART1,ON 239 | AT+LEVER_ARM=0,0,0 240 | AT+CLUB_VECTOR=0,0,1.855 241 | AT+GNSS_CARD=HEMI 242 | AT+WORK_MODE=152 243 | AT+SAVE_ALL 244 | 245 | OK 246 | 247 | 248 | 249 | 250 | Error 251 | 252 | 253 | AT+NAVI_OUTPUT=UART1,ON 254 | AT+LEVER_ARM=0,0,0 255 | AT+CLUB_VECTOR=0,0,1.855 256 | AT+GNSS_CARD=HEMI 257 | AT+WORK_MODE=152 258 | AT+SAVE_ALL 259 | 260 | OK 261 | 262 | 263 | 264 | 265 | Error 266 | 267 | 268 | 269 | AT+NAVI_OUTPUT=UART1,ON 270 | AT+LEVER_ARM=0,0,0 271 | AT+CLUB_VECTOR=0,0,1.855 272 | AT+GNSS_CARD=HEMI 273 | AT+WORK_MODE=152 274 | AT+SAVE_ALL 275 | 276 | OK 277 | 278 | 279 | 280 | 281 | Error 282 | 283 | 284 | 285 | AT+NAVI_OUTPUT=UART1,ON 286 | AT+LEVER_ARM=0,0,0 287 | AT+CLUB_VECTOR=0,0,1.855 288 | AT+GNSS_CARD=HEMI 289 | AT+WORK_MODE=152 290 | AT+SAVE_ALL 291 | 292 | OK 293 | 294 | 295 | 296 | 297 | Error 298 | 299 | 300 | 301 | AT+NAVI_OUTPUT=UART1,ON 302 | AT+LEVER_ARM=0,0,0 303 | AT+CLUB_VECTOR=0,0,1.855 304 | AT+GNSS_CARD=HEMI 305 | AT+WORK_MODE=152 306 | AT+SAVE_ALL 307 | 308 | OK 309 | 310 | 311 | 312 | 313 | Error 314 | 315 | 316 | -------------------------------------------------------------------------------- /examples/ttypresets_examples.py: -------------------------------------------------------------------------------- 1 | """ 2 | Examples of TTY command presets for a variety of GNSS and related devices 3 | 4 | Copy those required to your *.json configuration file and invoke them 5 | via the TTY Command dialog. 6 | 7 | Created on 30 May 2025 8 | 9 | :author: semuadmin 10 | :copyright: 2025 SEMU Consulting 11 | :license: BSD 3-Clause 12 | """ 13 | 14 | # ****************************************** 15 | # Septentrio Mosaic X5 Receiver 16 | # 17 | # Send an "Initialise Command Mode" string 18 | # before sending further commands. 19 | # 20 | # Full details in https://www.septentrio.com/resources/mosaic-X5/mosaic-X5+Firmware+v4.14.10.1+Reference+Guide.pdf 21 | # ****************************************** 22 | { 23 | "ttypresets_l": [ 24 | "Initialise Command Mode; SSSSSSSSSS", 25 | "Display Help for all available commands; help, Overview", 26 | "Display Help for getReceiverCapabilities command; help, getReceiverCapabilities", 27 | "Display Help for getReceiverCapabilities command; help, grc", 28 | "List contents of current configuration file; lcf, Current", 29 | "Save current configuration to boot file; eccf, Current, Boot", 30 | "List receiver capabilities; grc", 31 | "Get command line interface version; gri", 32 | "List Current NMEA outputs; gno", 33 | "Enable NMEA messages; sno, Stream1, COM1, GGA+GLL+GSV+RMC, sec1", 34 | "Disable NMEA messages; sno, Stream1, none, none, off", 35 | "Enable Group stream; ssgp, Group1, MeasEpoch+PVTCartesian+DOP; sso, Stream2, COM1, Group1, sec1", 36 | "Disable Group stream; sso, Stream2, none, none, off", 37 | "Output next Measurement Epoch; esoc, COM1, MeasEpoch", 38 | "Enable PVTGeod stream; sso, Stream2, COM1, PVTGeod, sec1", 39 | "Enable Status stream; sso, Stream2, COM1, Status, sec1", 40 | "Enable RTCM3 messages on COM2 in base station mode;sr3o, COM2, RTCM1001+RTCM1002+RTCM1005+RTCM1006; sdio, COM2, , RTCMv3", 41 | "List Known Antenna Phase Centres; lai, Overview", 42 | "Turn ethernet interface on; seth, on", 43 | ], 44 | } 45 | 46 | # ****************************************** 47 | # Feyman IM19 IMU with Tilt Compensation 48 | # 49 | # Full details in http://www.feymani.com/en/uploadfile/2023/1008/20231008072903360.pdf 50 | # ****************************************** 51 | { 52 | "ttypresets_l": [ 53 | "Tilt Survey Setup; AT+LOAD_DEFAULT; AT+GNSS_PORT=PHYSICAL_UART2; AT+NASC_OUTPUT=UART1,ON; AT+LEVER_ARM2=0.0057,-0.0732,-0.0645; AT+CLUB_VECTOR=0,0,1.865; AT+INSTALL_ANGLE=0,180,0; AT+GNSS_CARD=OEM; AT+WORK_MODE=408; AT+CORRECT_HOLDER=ENABLE; AT+SET_PPS_EDGE=RISING; AT+AHRS=ENABLE; AT+MAG_AUTO_SAVE=ENABLE; AT+SAVE_ALL", 54 | "System reset CONFIRM; AT+SYSTEM_RESET", 55 | "Save the parameters CONFIRM AT+SAVE_ALL", 56 | "Update module firmware, see attachment for protocols; AT+UPDATE_APP", 57 | "Update Bootloader, see attachment for protocols; AT+UPDATE_BOOT", 58 | "Set the GNSS RTK receiver type; AT+GNSS_CARD=OEM", 59 | "Read parameters (SYSTEM/ALL); AT+READ_PARA=SYSTEM/ALL", 60 | "Loading default parameters; AT+LOAD_DEFAULT", 61 | "Installation angle estimation in tilt measurement applications; AT+AUTO_FIX=ENABLE/DISABLE", 62 | "Set the RTK pole vector to map the position to the end of the RTK pole; AT+CLUB_VECTOR=X,Y,Z", 63 | "Binary NAVI positioning output; AT+NAVI_OUTPUT=UART1,ON/OFF", 64 | "Ascii type NAVI positioning output; AT+NASC_OUTPUT=UART1,ON/OFF", 65 | "MEMS raw output; AT+MEMS_OUTPUT=UART1,ON/OFF", 66 | "GNSS raw output; AT+GNSS_OUTPUT=UART1,ON/OFF", 67 | "Set the lever arm; AT+LEVER_ARM=X,Y,Z", 68 | "Query whether time is synchronized between MEMS and GNSS; AT+CHECK_SYNC", 69 | "High-rate mode setting; AT+HIGH_RATE=ENABLE/DISABLE", 70 | "Module activation; AT+ACTIVATE_KEY=KEY", 71 | "Set the initial alignment speed threshold; AT+ALIGN_VEL=1.0", 72 | "Query the Firmware version; AT+VERSION", 73 | "Set GNSS serial port; AT+GNSS_PORT=PHYSICAL_UART2", 74 | "Set the module working mode; AT+WORK_MODE=X", 75 | "Set the module installation angle; AT+INSTALL_ANGLE=X,Y,Z", 76 | "Query the serial port number; AT+THIS_PORT", 77 | "Causes the filter to enter or exit stop mode; AT+FILTER_STOP=ENABLE/DISABLE", 78 | "UART n enters or exits the loopback mode; AT+LOOP_BACK=UARTn/NONE", 79 | "Filter Reset; AT+FILTER_RESET", 80 | "Check firmware CRC, N=firmware size; AT+CHECK_CRC=N", 81 | "Turn on or off RTK pole length compensation; AT+CORRECT_HOLDER=ENABLE/DISABLE", 82 | "Disable the output of all messages over the serial port x; AT+DISABLE_OUTPUT=UARTx", 83 | "Factory calibration command; AT+CALIBRATE_MODE2=STEP1/STEP2", 84 | ], 85 | } 86 | -------------------------------------------------------------------------------- /examples/txt2ubx.py: -------------------------------------------------------------------------------- 1 | """ 2 | txt2ubx.py 3 | 4 | Utility which converts u-center *.txt configuration files to binary *.ubx files. 5 | 6 | Two output files are produced: 7 | 8 | *.get.ubx - contains GET (MON-VER and CFG-VALGET) messages mirroring the input file 9 | *.set.ubx - contains SET (converted CFG-VALSET) messages which can be used to set configuration 10 | 11 | The *.set.ubx file can be loaded into PyGPSClient's UBX Configuration Load/Save/record facility 12 | and uploaded to the receiver. 13 | 14 | Created on 27 Apr 2023 15 | 16 | @author: semuadmin 17 | """ 18 | 19 | from pyubx2 import UBXMessage, GET, SET 20 | 21 | def txt2ubx(fname: str): 22 | """ 23 | Convert txt configuration file to ubx 24 | 25 | :param fname str: txt config file name 26 | """ 27 | 28 | with open(fname, "r", encoding="utf-8") as infile: 29 | with open(fname + ".get.ubx", "wb") as outfile_get: 30 | with open(fname + ".set.ubx", "wb") as outfile_set: 31 | read = 0 32 | write = 0 33 | errors = 0 34 | for line in infile: 35 | try: 36 | read += 1 37 | parts = line.replace(" ", "").split("-") 38 | data = bytes.fromhex(parts[-1]) 39 | cls = data[0:1] 40 | mid = data[1:2] 41 | # lenb = data[2:4] 42 | version = data[4:5] 43 | layer = data[5:6] 44 | position = data[6:8] 45 | cfgdata = data[8:] 46 | payload = version + layer + position + cfgdata 47 | ubx = UBXMessage(cls, mid, GET, payload=payload) 48 | outfile_get.write(ubx.serialize()) 49 | # only convert CFG-VALGET 50 | if not (cls == b"\x06" and mid == b"\x8b"): 51 | continue 52 | layers = b"\x01" # bbr only 53 | transaction = b"\x00" # not transactional 54 | reserved0 = b"\x00" 55 | payload = version + layers + transaction + reserved0 + cfgdata 56 | # create a CFG-VALSET message from the input CFG-VALGET 57 | ubx = UBXMessage(b"\x06", b"\x8a", SET, payload=payload) 58 | outfile_set.write(ubx.serialize()) 59 | write += 1 60 | except Exception as err: # pylint: disable=broad-exception-caught 61 | print(err) 62 | errors += 1 63 | continue 64 | 65 | print(f"{read} GET messages read, {write} SET messages written, {errors} errors") 66 | 67 | txt2ubx("simpleRTK2B_FW132_Base-00.txt") 68 | -------------------------------------------------------------------------------- /examples/ubxsimulator.json: -------------------------------------------------------------------------------- 1 | { 2 | "interval": 1, 3 | "timeout": 3, 4 | "logfile": "/home/myuser/ubxsimulator.log", 5 | "simVector": 1, 6 | "global": { 7 | "lat": 52.477311, 8 | "lon": 13.391426, 9 | "alt": 50.034, 10 | "height": 47494, 11 | "sep": 2.54, 12 | "hMSL": 50034, 13 | "NS": "N", 14 | "EW": "E", 15 | "gSpeed": 100000, 16 | "headMot": 126, 17 | "spd": 194.3844, 18 | "cog": 126 19 | }, 20 | "ubxmessages": [ 21 | { 22 | "msgCls": 1, 23 | "msgId": 7, 24 | "rate": 1, 25 | "attrs": { 26 | "validDate": 1, 27 | "validTime": 1, 28 | "validMag": 1, 29 | "fixType": 3, 30 | "numSV": 12, 31 | "gnssFixOk": 1, 32 | "pDOP": 0.843, 33 | "hAcc": 585, 34 | "vAcc": 543 35 | } 36 | }, 37 | { 38 | "msgCls": 1, 39 | "msgId": 4, 40 | "rate": 1, 41 | "attrs": { 42 | "gDOP": 0.45, 43 | "pDOP": 0.356, 44 | "tDOP": 0.3864, 45 | "vDOP": 0.98733, 46 | "hDOP": 0.96763, 47 | "nDOP": 0.9873476, 48 | "eDOP": 0.834 49 | } 50 | }, 51 | { 52 | "msgCls": 1, 53 | "msgId": 53, 54 | "rate": 4, 55 | "attrs": { 56 | "version": 1, 57 | "numSvs": 13, 58 | "reserved0": 0, 59 | "gnssId_01": 0, 60 | "svId_01": 4, 61 | "cno_01": 0, 62 | "elev_01": 16, 63 | "azim_01": 302, 64 | "prRes_01": 0.0, 65 | "qualityInd_01": 1, 66 | "svUsed_01": 0, 67 | "health_01": 1, 68 | "diffCorr_01": 0, 69 | "smoothed_01": 0, 70 | "orbitSource_01": 2, 71 | "ephAvail_01": 0, 72 | "almAvail_01": 1, 73 | "gnssId_02": 0, 74 | "svId_02": 5, 75 | "cno_02": 48, 76 | "elev_02": 15, 77 | "azim_02": 73, 78 | "prRes_02": 0.0, 79 | "qualityInd_02": 4, 80 | "svUsed_02": 0, 81 | "health_02": 1, 82 | "diffCorr_02": 0, 83 | "smoothed_02": 0, 84 | "orbitSource_02": 2, 85 | "ephAvail_02": 0, 86 | "almAvail_02": 1, 87 | "gnssId_03": 0, 88 | "svId_03": 14, 89 | "cno_03": 0, 90 | "elev_03": -91, 91 | "azim_03": 0, 92 | "prRes_03": 0.0, 93 | "qualityInd_03": 1, 94 | "svUsed_03": 0, 95 | "health_03": 1, 96 | "diffCorr_03": 0, 97 | "smoothed_03": 0, 98 | "orbitSource_03": 0, 99 | "gnssId_04": 0, 100 | "svId_04": 18, 101 | "cno_04": 50, 102 | "elev_04": -91, 103 | "azim_04": 0, 104 | "prRes_04": 0.0, 105 | "qualityInd_04": 4, 106 | "svUsed_04": 0, 107 | "health_04": 1, 108 | "diffCorr_04": 0, 109 | "smoothed_04": 0, 110 | "orbitSource_04": 0, 111 | "gnssId_05": 0, 112 | "svId_05": 25, 113 | "cno_05": 0, 114 | "elev_05": 19, 115 | "azim_05": 116, 116 | "prRes_05": 0.0, 117 | "qualityInd_05": 1, 118 | "svUsed_05": 0, 119 | "health_05": 1, 120 | "diffCorr_05": 0, 121 | "smoothed_05": 0, 122 | "orbitSource_05": 1, 123 | "ephAvail_05": 1, 124 | "almAvail_05": 1, 125 | "gnssId_06": 0, 126 | "svId_06": 26, 127 | "cno_06": 45, 128 | "elev_06": 61, 129 | "azim_06": 285, 130 | "prRes_06": 3.5, 131 | "qualityInd_06": 7, 132 | "svUsed_06": 1, 133 | "health_06": 1, 134 | "diffCorr_06": 0, 135 | "smoothed_06": 0, 136 | "orbitSource_06": 1, 137 | "ephAvail_06": 1, 138 | "almAvail_06": 1, 139 | "gnssId_07": 0, 140 | "svId_07": 27, 141 | "cno_07": 0, 142 | "elev_07": 4, 143 | "azim_07": 245, 144 | "prRes_07": 0.0, 145 | "qualityInd_07": 1, 146 | "svUsed_07": 0, 147 | "health_07": 1, 148 | "diffCorr_07": 0, 149 | "smoothed_07": 0, 150 | "orbitSource_07": 2, 151 | "ephAvail_07": 0, 152 | "almAvail_07": 1, 153 | "gnssId_08": 0, 154 | "svId_08": 28, 155 | "cno_08": 52, 156 | "elev_08": 39, 157 | "azim_08": 196, 158 | "prRes_08": 2.1, 159 | "qualityInd_08": 7, 160 | "svUsed_08": 1, 161 | "health_08": 1, 162 | "diffCorr_08": 0, 163 | "smoothed_08": 0, 164 | "orbitSource_08": 1, 165 | "ephAvail_08": 1, 166 | "almAvail_08": 1, 167 | "gnssId_09": 0, 168 | "svId_09": 29, 169 | "cno_09": 43, 170 | "elev_09": 56, 171 | "azim_09": 67, 172 | "prRes_09": 1.8, 173 | "qualityInd_09": 7, 174 | "svUsed_09": 1, 175 | "health_09": 1, 176 | "diffCorr_09": 0, 177 | "smoothed_09": 0, 178 | "orbitSource_09": 1, 179 | "ephAvail_09": 1, 180 | "almAvail_09": 1, 181 | "gnssId_10": 0, 182 | "svId_10": 31, 183 | "cno_10": 54, 184 | "elev_10": 55, 185 | "azim_10": 230, 186 | "prRes_10": 0.3, 187 | "qualityInd_10": 7, 188 | "svUsed_10": 1, 189 | "health_10": 1, 190 | "diffCorr_10": 0, 191 | "smoothed_10": 0, 192 | "orbitSource_10": 1, 193 | "ephAvail_10": 1, 194 | "gnssId_11": 2, 195 | "svId_11": 120, 196 | "cno_11": 0, 197 | "elev_11": 28, 198 | "azim_11": 196, 199 | "prRes_11": 0.0, 200 | "qualityInd_11": 1, 201 | "svUsed_11": 0, 202 | "health_11": 0, 203 | "diffCorr_11": 0, 204 | "smoothed_11": 0, 205 | "orbitSource_11": 7, 206 | "gnssId_12": 2, 207 | "svId_12": 123, 208 | "cno_12": 0, 209 | "elev_12": 22, 210 | "azim_12": 140, 211 | "prRes_12": 0.0, 212 | "qualityInd_12": 1, 213 | "svUsed_12": 0, 214 | "health_12": 0, 215 | "diffCorr_12": 0, 216 | "smoothed_12": 0, 217 | "orbitSource_12": 7, 218 | "gnssId_13": 2, 219 | "svId_13": 136, 220 | "cno_13": 0, 221 | "elev_13": 29, 222 | "azim_13": 171, 223 | "prRes_13": 0.0, 224 | "qualityInd_13": 1, 225 | "svUsed_13": 0, 226 | "health_13": 0, 227 | "diffCorr_13": 0, 228 | "smoothed_13": 0, 229 | "orbitSource_13": 7 230 | } 231 | } 232 | ], 233 | "nmeamessages": [ 234 | { 235 | "talker": "GN", 236 | "msgId": "GGA", 237 | "rate": 1, 238 | "attrs": { 239 | "status": "A", 240 | "posMode": "A", 241 | "navStatus": "V", 242 | "sepUnit": "M", 243 | "altUnit": "M", 244 | "quality": 2, 245 | "numSV": 12, 246 | "HDOP": 0.9873476 247 | } 248 | }, 249 | { 250 | "talker": "GN", 251 | "msgId": "RMC", 252 | "rate": 1, 253 | "attrs": { 254 | "status": "A", 255 | "posMode": "A", 256 | "navStatus": "V" 257 | } 258 | } 259 | ] 260 | } -------------------------------------------------------------------------------- /images/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/PyGPSClient/93487c10d26414e47e5a61d05118777774d24215/images/app.png -------------------------------------------------------------------------------- /images/banner_widget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/PyGPSClient/93487c10d26414e47e5a61d05118777774d24215/images/banner_widget.png -------------------------------------------------------------------------------- /images/basestation_fixed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/PyGPSClient/93487c10d26414e47e5a61d05118777774d24215/images/basestation_fixed.png -------------------------------------------------------------------------------- /images/basestation_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/PyGPSClient/93487c10d26414e47e5a61d05118777774d24215/images/basestation_off.png -------------------------------------------------------------------------------- /images/basestation_svin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/PyGPSClient/93487c10d26414e47e5a61d05118777774d24215/images/basestation_svin.png -------------------------------------------------------------------------------- /images/chart_widget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/PyGPSClient/93487c10d26414e47e5a61d05118777774d24215/images/chart_widget.png -------------------------------------------------------------------------------- /images/console_widget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/PyGPSClient/93487c10d26414e47e5a61d05118777774d24215/images/console_widget.png -------------------------------------------------------------------------------- /images/custommap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/PyGPSClient/93487c10d26414e47e5a61d05118777774d24215/images/custommap.png -------------------------------------------------------------------------------- /images/dgps_status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/PyGPSClient/93487c10d26414e47e5a61d05118777774d24215/images/dgps_status.png -------------------------------------------------------------------------------- /images/gpxviewer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/PyGPSClient/93487c10d26414e47e5a61d05118777774d24215/images/gpxviewer.png -------------------------------------------------------------------------------- /images/graphview_widget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/PyGPSClient/93487c10d26414e47e5a61d05118777774d24215/images/graphview_widget.png -------------------------------------------------------------------------------- /images/importcustommap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/PyGPSClient/93487c10d26414e47e5a61d05118777774d24215/images/importcustommap.png -------------------------------------------------------------------------------- /images/imu_widget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/PyGPSClient/93487c10d26414e47e5a61d05118777774d24215/images/imu_widget.png -------------------------------------------------------------------------------- /images/msgmode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/PyGPSClient/93487c10d26414e47e5a61d05118777774d24215/images/msgmode.png -------------------------------------------------------------------------------- /images/nmeaconfig_widget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/PyGPSClient/93487c10d26414e47e5a61d05118777774d24215/images/nmeaconfig_widget.png -------------------------------------------------------------------------------- /images/ntrip_consolelog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/PyGPSClient/93487c10d26414e47e5a61d05118777774d24215/images/ntrip_consolelog.png -------------------------------------------------------------------------------- /images/ntripconfig_widget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/PyGPSClient/93487c10d26414e47e5a61d05118777774d24215/images/ntripconfig_widget.png -------------------------------------------------------------------------------- /images/rover_widget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/PyGPSClient/93487c10d26414e47e5a61d05118777774d24215/images/rover_widget.png -------------------------------------------------------------------------------- /images/scatterplot_widget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/PyGPSClient/93487c10d26414e47e5a61d05118777774d24215/images/scatterplot_widget.png -------------------------------------------------------------------------------- /images/skyview_widget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/PyGPSClient/93487c10d26414e47e5a61d05118777774d24215/images/skyview_widget.png -------------------------------------------------------------------------------- /images/sourcetable_ggamarker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/PyGPSClient/93487c10d26414e47e5a61d05118777774d24215/images/sourcetable_ggamarker.png -------------------------------------------------------------------------------- /images/spartn_consolelog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/PyGPSClient/93487c10d26414e47e5a61d05118777774d24215/images/spartn_consolelog.png -------------------------------------------------------------------------------- /images/spartnconfig_widget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/PyGPSClient/93487c10d26414e47e5a61d05118777774d24215/images/spartnconfig_widget.png -------------------------------------------------------------------------------- /images/spectrum_widget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/PyGPSClient/93487c10d26414e47e5a61d05118777774d24215/images/spectrum_widget.png -------------------------------------------------------------------------------- /images/staticmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/PyGPSClient/93487c10d26414e47e5a61d05118777774d24215/images/staticmap.png -------------------------------------------------------------------------------- /images/sysmon_widget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/PyGPSClient/93487c10d26414e47e5a61d05118777774d24215/images/sysmon_widget.png -------------------------------------------------------------------------------- /images/tty_console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/PyGPSClient/93487c10d26414e47e5a61d05118777774d24215/images/tty_console.png -------------------------------------------------------------------------------- /images/tty_dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/PyGPSClient/93487c10d26414e47e5a61d05118777774d24215/images/tty_dialog.png -------------------------------------------------------------------------------- /images/ubxconfig_widget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/PyGPSClient/93487c10d26414e47e5a61d05118777774d24215/images/ubxconfig_widget.png -------------------------------------------------------------------------------- /images/webmap_widget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/semuconsulting/PyGPSClient/93487c10d26414e47e5a61d05118777774d24215/images/webmap_widget.png -------------------------------------------------------------------------------- /pygpsclient.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Terminal=false 4 | Name=PyGPSClient 5 | Icon=/home/user/.local/lib/python3.9/site-packages/pygpsclient/resources/pygpsclient.ico 6 | Exec=/home/user/.local/bin/pygpsclient 7 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=75.0.0", "wheel"] 3 | build-backend = "setuptools.build_meta:__legacy__" 4 | 5 | [project] 6 | name = "pygpsclient" 7 | dynamic = ["version"] 8 | authors = [{ name = "semuadmin", email = "semuadmin@semuconsulting.com" }] 9 | maintainers = [{ name = "semuadmin", email = "semuadmin@semuconsulting.com" }] 10 | description = "GNSS Diagnostic and UBX Configuration GUI Application" 11 | license-files = ["LICENSE"] 12 | keywords = [ 13 | "PyGPSClient", 14 | "GNSS", 15 | "GPS", 16 | "GALILEO", 17 | "GLONASS", 18 | "BEIDOU", 19 | "NMEA", 20 | "UBX", 21 | "RTCM", 22 | "NTRIP", 23 | "SPARTN", 24 | "RTK", 25 | "DGPS", 26 | "u-center", 27 | ] 28 | readme = "README.md" 29 | requires-python = ">=3.9" 30 | classifiers = [ 31 | "Operating System :: OS Independent", 32 | "Development Status :: 5 - Production/Stable", 33 | "Environment :: MacOS X", 34 | "Environment :: Win32 (MS Windows)", 35 | "Environment :: X11 Applications", 36 | "Environment :: Console", 37 | "Intended Audience :: Developers", 38 | "Intended Audience :: Science/Research", 39 | "Intended Audience :: End Users/Desktop", 40 | "Programming Language :: Python :: 3", 41 | "Programming Language :: Python :: 3.9", 42 | "Programming Language :: Python :: 3.10", 43 | "Programming Language :: Python :: 3.11", 44 | "Programming Language :: Python :: 3.12", 45 | "Programming Language :: Python :: 3.13", 46 | "Topic :: Utilities", 47 | "Topic :: Software Development :: Libraries :: Python Modules", 48 | "Topic :: Scientific/Engineering :: GIS", 49 | ] 50 | 51 | dependencies = [ 52 | "requests>=2.28.0", 53 | "Pillow>=9.0.0", 54 | "pygnssutils>=1.1.14", 55 | "pyubx2>=1.2.52", 56 | "pyserial>=3.5", 57 | "pyubxutils>=1.0.3", 58 | ] 59 | 60 | [project.scripts] 61 | pygpsclient = "pygpsclient.__main__:main" 62 | 63 | [project.urls] 64 | homepage = "https://github.com/semuconsulting/PyGPSClient" 65 | documentation = "https://www.semuconsulting.com/pygpsclient/" 66 | repository = "https://github.com/semuconsulting/PyGPSClient" 67 | changelog = "https://github.com/semuconsulting/PyGPSClient/blob/master/RELEASE_NOTES.md" 68 | 69 | [project.optional-dependencies] 70 | deploy = [ 71 | "build", 72 | "packaging>=24.2", 73 | "pip", 74 | "setuptools>=75.0.0", 75 | "twine>=6.1.0", 76 | "wheel", 77 | ] 78 | test = [ 79 | "bandit", 80 | "black", 81 | "certifi", 82 | "isort", 83 | "pylint", 84 | "pytest", 85 | "pytest-cov", 86 | "Sphinx", 87 | "sphinx-rtd-theme", 88 | ] 89 | 90 | [tool.setuptools.dynamic] 91 | version = { attr = "pygpsclient._version.__version__" } 92 | 93 | [tool.black] 94 | target-version = ['py39'] 95 | 96 | [tool.isort] 97 | py_version = 39 98 | profile = "black" 99 | 100 | [tool.bandit] 101 | exclude_dirs = ["docs", "examples", "tests"] 102 | skips = [ 103 | "B104", 104 | "B311", 105 | "B318", 106 | "B404", 107 | "B408", 108 | "B603", 109 | ] # bind 0.0.0.0; randrange; minidom; subroutine 110 | 111 | [tool.pylint] 112 | jobs = 0 113 | reports = "y" 114 | recursive = "y" 115 | py-version = "3.9" 116 | fail-under = "9.8" 117 | fail-on = "E,F" 118 | clear-cache-post-run = "y" 119 | good-names = "i,j,x,y" 120 | disable = """ 121 | raw-checker-failed, 122 | bad-inline-option, 123 | locally-disabled, 124 | file-ignored, 125 | suppressed-message, 126 | useless-suppression, 127 | deprecated-pragma, 128 | use-symbolic-message-instead, 129 | too-many-instance-attributes, 130 | unused-private-member, 131 | too-few-public-methods, 132 | too-many-public-methods, 133 | too-many-locals, 134 | invalid-name, 135 | logging-fstring-interpolation 136 | """ 137 | 138 | [tool.pytest.ini_options] 139 | minversion = "7.0" 140 | addopts = "--cov --cov-report html --cov-fail-under 15" 141 | pythonpath = ["src"] 142 | testpaths = ["tests"] 143 | 144 | [tool.coverage.run] 145 | source = ["src"] 146 | -------------------------------------------------------------------------------- /src/pygpsclient/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on 27 Sep 2020 3 | 4 | :author: semuadmin 5 | :copyright: 2020 SEMU Consulting 6 | :license: BSD 3-Clause 7 | """ 8 | 9 | # pylint: disable=invalid-name 10 | 11 | from pygpsclient._version import __version__ 12 | from pygpsclient.helpers import nmea2preset, ubx2preset 13 | 14 | version = __version__ 15 | -------------------------------------------------------------------------------- /src/pygpsclient/__main__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Entry point for PyGPSClient Application. 3 | 4 | Created on 12 Sep 2020 5 | 6 | :author: semuadmin 7 | :copyright: 2020 SEMU Consulting 8 | :license: BSD 3-Clause 9 | """ 10 | 11 | import sys 12 | from argparse import SUPPRESS, ArgumentDefaultsHelpFormatter, ArgumentParser 13 | from logging import getLogger 14 | from tkinter import Tk 15 | 16 | from pygnssutils import ( 17 | VERBOSITY_CRITICAL, 18 | VERBOSITY_DEBUG, 19 | VERBOSITY_HIGH, 20 | VERBOSITY_LOW, 21 | VERBOSITY_MEDIUM, 22 | set_logging, 23 | ) 24 | 25 | from pygpsclient._version import __version__ as VERSION 26 | from pygpsclient.app import App 27 | from pygpsclient.globals import ( 28 | APPNAME, 29 | CONFIGFILE, 30 | SPARTN_BASEDATE_CURRENT, 31 | SPARTN_BASEDATE_DATASTREAM, 32 | ) 33 | from pygpsclient.strings import EPILOG 34 | 35 | 36 | def main(): 37 | """The main tkinter loop.""" 38 | 39 | ap = ArgumentParser( 40 | epilog=EPILOG, 41 | formatter_class=ArgumentDefaultsHelpFormatter, 42 | description="Command line arguments will override configuration file", 43 | ) 44 | ap.add_argument("-V", "--version", action="version", version="%(prog)s " + VERSION) 45 | ap.add_argument( 46 | "-C", 47 | "--config", 48 | help="Fully-qualified path to configuration file", 49 | default=CONFIGFILE, 50 | ) 51 | ap.add_argument( 52 | "-U", 53 | "--userport", 54 | help="User-defined GNSS receiver port", 55 | default=SUPPRESS, 56 | ) 57 | ap.add_argument( 58 | "-S", 59 | "--spartnport", 60 | help="User-defined SPARTN receiver port", 61 | default=SUPPRESS, 62 | ) 63 | ap.add_argument( 64 | "--mqapikey", 65 | help="MapQuest API Key", 66 | default=SUPPRESS, 67 | ) 68 | ap.add_argument( 69 | "--mqttclientid", 70 | help="MQTT Client ID", 71 | default=SUPPRESS, 72 | ) 73 | ap.add_argument( 74 | "--mqttclientregion", 75 | help="MQTT Client Region", 76 | default=SUPPRESS, 77 | ) 78 | ap.add_argument( 79 | "--mqttclientmode", 80 | help="MQTT Client Mode (0 - IP, 1 - L-Band)", 81 | default=SUPPRESS, 82 | ) 83 | ap.add_argument( 84 | "--ntripcasteruser", 85 | help="NTRIP Caster authentication user", 86 | default=SUPPRESS, 87 | ) 88 | ap.add_argument( 89 | "--ntripcasterpassword", 90 | help="NTRIP Caster authentication password", 91 | default=SUPPRESS, 92 | ) 93 | ap.add_argument( 94 | "--spartnkey", 95 | help="SPARTN message decryption key", 96 | default=SUPPRESS, 97 | ) 98 | ap.add_argument( 99 | "--spartnbasedate", 100 | help=f"SPARTN message decryption timetag ({SPARTN_BASEDATE_CURRENT} = \ 101 | current datetime, {SPARTN_BASEDATE_DATASTREAM} = use timetags from data stream)", 102 | type=int, 103 | default=SUPPRESS, 104 | ) 105 | ap.add_argument( 106 | "--verbosity", 107 | help=( 108 | f"Log message verbosity " 109 | f"{VERBOSITY_CRITICAL} = critical, " 110 | f"{VERBOSITY_LOW} = low (error), " 111 | f"{VERBOSITY_MEDIUM} = medium (warning), " 112 | f"{VERBOSITY_HIGH} = high (info), {VERBOSITY_DEBUG} = debug, " 113 | f"default = {VERBOSITY_CRITICAL}" 114 | ), 115 | type=int, 116 | choices=[ 117 | VERBOSITY_LOW, 118 | VERBOSITY_MEDIUM, 119 | VERBOSITY_HIGH, 120 | VERBOSITY_DEBUG, 121 | VERBOSITY_CRITICAL, 122 | ], 123 | default=VERBOSITY_CRITICAL, 124 | ) 125 | ap.add_argument( 126 | "--logtofile", 127 | help="fully qualified log file name, or '' for no log file", 128 | default="", 129 | ) 130 | kwargs = vars(ap.parse_args()) 131 | 132 | # set up global logging configuration 133 | verbosity = int(kwargs.pop("verbosity", VERBOSITY_CRITICAL)) 134 | logtofile = kwargs.pop("logtofile", "") 135 | logger = getLogger(APPNAME) # "pygpsclient" 136 | logger_utils = getLogger("pygnssutils") 137 | logger_pyubx2 = getLogger("pyubx2") 138 | for logr in (logger, logger_utils, logger_pyubx2): 139 | set_logging(logr, verbosity, logtofile) 140 | 141 | root = Tk() 142 | App(root, **kwargs) 143 | root.mainloop() 144 | sys.exit() 145 | 146 | 147 | if __name__ == "__main__": 148 | main() 149 | -------------------------------------------------------------------------------- /src/pygpsclient/_version.py: -------------------------------------------------------------------------------- 1 | """ 2 | Application Version. 3 | 4 | Created on 12 Sep 2020 5 | 6 | :author: semuadmin 7 | :copyright: 2020 SEMU Consulting 8 | :license: BSD 3-Clause 9 | """ 10 | 11 | __version__ = "1.5.9" 12 | -------------------------------------------------------------------------------- /src/pygpsclient/confirm_box.py: -------------------------------------------------------------------------------- 1 | """ 2 | confirm_box.py 3 | 4 | Confirm action dialog class. 5 | Provides better consistency across different OS platforms 6 | than using messagebox.askyesno() 7 | 8 | Created on 17 Apr 2021 9 | 10 | :author: semuadmin 11 | :copyright: 2020 SEMU Consulting 12 | :license: BSD 3-Clause 13 | 14 | """ 15 | 16 | from tkinter import Button, Label, Toplevel, W 17 | 18 | 19 | class ConfirmBox(Toplevel): 20 | """ 21 | Confirm action dialog class. 22 | Provides better consistency across different OS platforms 23 | than using messagebox.askyesno() 24 | 25 | Returns True if OK, False if Cancel 26 | """ 27 | 28 | def __init__(self, parent, title, prompt): 29 | """ 30 | Constructor 31 | 32 | :param parent: parent dialog 33 | :param string title: title 34 | :param string prompt: prompt to be displayed 35 | """ 36 | 37 | self.__master = parent 38 | Toplevel.__init__(self, parent) 39 | self.title(title) # pylint: disable=E1102 40 | self.resizable(False, False) 41 | Label(self, text=prompt, anchor=W).grid( 42 | row=0, column=0, columnspan=2, padx=3, pady=5 43 | ) 44 | Button(self, command=self._on_ok, text="OK", width=8).grid( 45 | row=1, column=0, padx=3, pady=3 46 | ) 47 | Button(self, command=self._on_cancel, text="Cancel", width=8).grid( 48 | row=1, column=1, padx=3, pady=3 49 | ) 50 | self.lift() # Put on top of 51 | self.grab_set() # Make modal 52 | self._rc = False 53 | 54 | self._centre() 55 | 56 | def _on_ok(self, event=None): # pylint: disable=unused-argument 57 | """ 58 | OK button handler 59 | """ 60 | 61 | self._rc = True 62 | self.destroy() 63 | 64 | def _on_cancel(self, event=None): # pylint: disable=unused-argument 65 | """ 66 | Cancel button handler 67 | """ 68 | 69 | self._rc = False 70 | self.destroy() 71 | 72 | def _centre(self): 73 | """ 74 | Centre dialog in parent 75 | """ 76 | 77 | # self.update_idletasks() 78 | dw = self.winfo_width() 79 | dh = self.winfo_height() 80 | mx = self.__master.winfo_x() 81 | my = self.__master.winfo_y() 82 | mw = self.__master.winfo_width() 83 | mh = self.__master.winfo_height() 84 | self.geometry(f"+{int(mx + (mw/2 - dw/2))}+{int(my + (mh/2 - dh/2))}") 85 | 86 | def show(self): 87 | """ 88 | Show dialog 89 | 90 | :return: True (OK) or False (Cancel) 91 | :rtype: bool 92 | """ 93 | 94 | self.wm_deiconify() 95 | self.wait_window() 96 | return self._rc 97 | -------------------------------------------------------------------------------- /src/pygpsclient/console_frame.py: -------------------------------------------------------------------------------- 1 | """ 2 | console_frame.py 3 | 4 | Console frame class for PyGPSClient application. 5 | 6 | This handles a scrollable text box into which the serial data is printed. 7 | 8 | *** Remember that tcl indices look like floats but they're not! *** 9 | ("1.0:, "2.0") signifies "from the first character in 10 | line 1 (inclusive) to the first character in line 2 (exclusive)" 11 | i.e. the first line 12 | 13 | Created on 12 Sep 2020 14 | 15 | :author: semuadmin 16 | :copyright: 2020 SEMU Consulting 17 | :license: BSD 3-Clause 18 | """ 19 | 20 | from tkinter import END, HORIZONTAL, NONE, VERTICAL, E, Frame, N, S, Scrollbar, Text, W 21 | 22 | from pyubx2 import hextable 23 | 24 | from pygpsclient.globals import ( 25 | BGCOL, 26 | DISCONNECTED, 27 | ERRCOL, 28 | FGCOL, 29 | FONT_FIXED, 30 | FONT_TEXT, 31 | FORMAT_BINARY, 32 | FORMAT_BOTH, 33 | FORMAT_HEXSTR, 34 | FORMAT_HEXTAB, 35 | WIDGETU3, 36 | ) 37 | from pygpsclient.strings import HALTTAGWARN 38 | 39 | HALT = "HALT" 40 | CONSOLELINES = 20 41 | 42 | 43 | class ConsoleFrame(Frame): 44 | """ 45 | Console frame class. 46 | """ 47 | 48 | def __init__(self, app, *args, **kwargs): 49 | """ 50 | Constructor. 51 | 52 | :param Frame app: reference to main tkinter application 53 | :param args: optional args to pass to Frame parent class 54 | :param kwargs: optional kwargs to pass to Frame parent class 55 | """ 56 | 57 | self.__app = app # Reference to main application class 58 | self.__master = self.__app.appmaster # Reference to root class (Tk) 59 | 60 | Frame.__init__(self, self.__master, *args, **kwargs) 61 | 62 | def_w, def_h = WIDGETU3 63 | self.width = kwargs.get("width", def_w) 64 | self.height = kwargs.get("height", def_h) 65 | self._colortags = self.__app.configuration.get("colortags_l") 66 | self._body() 67 | self._do_layout() 68 | self._attach_events() 69 | self._halt = "" 70 | 71 | def _body(self): 72 | """ 73 | Set up frame and widgets. 74 | """ 75 | 76 | self.option_add("*Font", self.__app.font_sm) 77 | self._console_fg = FGCOL 78 | self._console_bg = BGCOL 79 | self.grid_columnconfigure(0, weight=1) 80 | self.grid_rowconfigure(0, weight=1) 81 | self.grid_columnconfigure(1, weight=0) 82 | self.grid_rowconfigure(1, weight=0) 83 | self.sblogv = Scrollbar(self, orient=VERTICAL) 84 | self.sblogh = Scrollbar(self, orient=HORIZONTAL) 85 | self.txt_console = Text( 86 | self, 87 | bg=self._console_bg, 88 | fg=self._console_fg, 89 | yscrollcommand=self.sblogv.set, 90 | xscrollcommand=self.sblogh.set, 91 | wrap=NONE, 92 | height=15, 93 | ) 94 | self.sblogh.config(command=self.txt_console.xview) 95 | self.sblogv.config(command=self.txt_console.yview) 96 | 97 | # making the textbox read only and fixed width font 98 | self.txt_console.configure(font=FONT_FIXED, state="disabled") 99 | 100 | # set up color tagging 101 | for match, color in self._colortags: 102 | if color == HALT: 103 | color = ERRCOL 104 | match = HALT 105 | self.txt_console.tag_config(match, foreground=color) 106 | 107 | def _do_layout(self): 108 | """ 109 | Set position of widgets in frame 110 | """ 111 | 112 | self.txt_console.grid(column=0, row=0, pady=1, padx=1, sticky=(N, S, E, W)) 113 | self.sblogv.grid(column=1, row=0, sticky=(N, S, E)) 114 | self.sblogh.grid(column=0, row=1, sticky=(S, E, W)) 115 | 116 | def _attach_events(self): 117 | """ 118 | Bind events to frame 119 | """ 120 | 121 | self.bind("", self._on_resize) 122 | self.txt_console.bind("", self._on_clipboard) 123 | self.txt_console.bind("", self._on_clipboard) 124 | self.txt_console.bind("", self._on_clipboard) 125 | # self.txt_console.tag_bind(HALT, "<1>", self._on_halt) # doesn't seem to work on MacOS 126 | 127 | def update_frame(self, consoledata: list): 128 | """ 129 | Print the latest data stream to the console in raw (NMEA) or 130 | parsed (key,value pair) format. 131 | 132 | 'maxlines' defines the maximum number of scrollable lines that are 133 | retained in the text box on a FIFO basis. 134 | 135 | :param list consoledata: list of tuples (raw, parsed, marker) \ 136 | accumulated since last console update 137 | """ 138 | 139 | con = self.txt_console 140 | consoleformat = self.__app.configuration.get("consoleformat_s") 141 | colortagging = self.__app.configuration.get("colortag_b") 142 | maxlines = self.__app.configuration.get("maxlines_n") 143 | autoscroll = self.__app.configuration.get("autoscroll_b") 144 | self._halt = "" 145 | con.configure(font=FONT_FIXED) 146 | 147 | consolestr = "" 148 | for raw_data, parsed_data, marker in consoledata: 149 | if consoleformat == FORMAT_BINARY: 150 | data = f"{marker}{raw_data}".strip("\n") 151 | elif consoleformat == FORMAT_HEXSTR: 152 | data = f"{marker}{raw_data.hex()}" 153 | elif consoleformat == FORMAT_HEXTAB: 154 | data = hextable(raw_data) 155 | elif consoleformat == FORMAT_BOTH: 156 | data = f"{marker}{parsed_data}\n{hextable(raw_data)}" 157 | else: 158 | con.configure(font=FONT_TEXT) 159 | data = f"{marker}{parsed_data}" 160 | consolestr += data + "\n" 161 | 162 | numlinesbefore = self.numlines 163 | con.configure(state="normal") 164 | con.insert(END, consolestr) 165 | 166 | if colortagging: 167 | self._tag_line(con, numlinesbefore, self.numlines) 168 | if self._halt != "": 169 | self._on_halt(None) 170 | 171 | while self.numlines > maxlines: 172 | con.delete("1.0", "2.0") # delete top line 173 | 174 | if autoscroll: 175 | con.see("end") 176 | con.configure(state="disabled") 177 | self.update_idletasks() 178 | 179 | def _tag_line(self, con, startline: int, endline: int): 180 | """ 181 | Highlights any occurrence of tags in line - each tag 182 | must be a tuple of (search term, highlight color) 183 | 184 | :param object con: console textbox 185 | :param int startline: starting line 186 | :param int endline: ending line 187 | :param str string: string in console 188 | 189 | """ 190 | 191 | for lineidx in range(startline - 1, endline): 192 | for match, color in self._colortags: 193 | line = con.get(f"{lineidx}.0", END) 194 | start = line.find(match) 195 | end = start + len(match) 196 | if start != -1: # If search string found in line 197 | if color.upper() == HALT: # "HALT" tag terminates stream 198 | self._halt = match 199 | match = HALT 200 | con.tag_add(match, f"{lineidx}.{start}", f"{lineidx}.{end}") 201 | 202 | @property 203 | def numlines(self) -> int: 204 | """ 205 | Get number of lines in console. 206 | 207 | :return: nmber of lines 208 | :type: int 209 | """ 210 | 211 | return int(self.txt_console.index("end-1c").split(".", 1)[0]) 212 | 213 | def _on_halt(self, event): # pylint: disable=unused-argument 214 | """ 215 | Halt streaming. 216 | 217 | :param event event: HALT event 218 | """ 219 | 220 | self.__app.stream_handler.stop_read_thread() 221 | self.__app.set_status(HALTTAGWARN.format(self._halt), ERRCOL) 222 | self.__app.conn_status = DISCONNECTED 223 | 224 | def _on_clipboard(self, event): # pylint: disable=unused-argument 225 | """ 226 | Copy console content to clipboard. 227 | 228 | :param event event: double click event 229 | """ 230 | 231 | self.__master.clipboard_clear() 232 | self.__master.clipboard_append(self.txt_console.get("1.0", END)) 233 | self.__master.update() 234 | 235 | def _on_resize(self, event): # pylint: disable=unused-argument 236 | """ 237 | Resize frame 238 | 239 | :param event event: resize event 240 | """ 241 | 242 | self.width, self.height = self.get_size() 243 | 244 | def get_size(self): 245 | """ 246 | Get current object size. 247 | 248 | :return: window size (width, height) 249 | :rtype: tuple 250 | """ 251 | 252 | self.update_idletasks() # Make sure we know about any resizing 253 | return self.winfo_width(), self.winfo_height() 254 | -------------------------------------------------------------------------------- /src/pygpsclient/dialog_state.py: -------------------------------------------------------------------------------- 1 | """ 2 | dialog_state.py 3 | 4 | Class holding global constants, strings and dictionaries 5 | used to maintain the state of the various threaded dialogs. 6 | 7 | Created on 16 Aug 2023 8 | 9 | :author: semuadmin 10 | :copyright: 2020 SEMU Consulting 11 | :license: BSD 3-Clause 12 | """ 13 | 14 | from pygpsclient.about_dialog import AboutDialog 15 | from pygpsclient.globals import CFG, CLASS, THD 16 | from pygpsclient.gpx_dialog import GPXViewerDialog 17 | from pygpsclient.importmap_dialog import ImportMapDialog 18 | from pygpsclient.nmea_config_dialog import NMEAConfigDialog 19 | from pygpsclient.ntrip_client_dialog import NTRIPConfigDialog 20 | from pygpsclient.spartn_dialog import SPARTNConfigDialog 21 | from pygpsclient.strings import ( 22 | DLG, 23 | DLGTABOUT, 24 | DLGTGPX, 25 | DLGTIMPORTMAP, 26 | DLGTNMEA, 27 | DLGTNTRIP, 28 | DLGTSPARTN, 29 | DLGTTTY, 30 | DLGTUBX, 31 | ) 32 | from pygpsclient.tty_preset_dialog import TTYPresetDialog 33 | from pygpsclient.ubx_config_dialog import UBXConfigDialog 34 | 35 | 36 | class DialogState: 37 | """ 38 | Class holding current state of PyGPSClient dialogs. 39 | """ 40 | 41 | def __init__(self): 42 | """ 43 | Constructor. 44 | """ 45 | 46 | self.state = { 47 | DLGTABOUT: {CLASS: AboutDialog, THD: None, DLG: None, CFG: False}, 48 | DLGTUBX: {CLASS: UBXConfigDialog, THD: None, DLG: None, CFG: True}, 49 | DLGTNMEA: {CLASS: NMEAConfigDialog, THD: None, DLG: None, CFG: True}, 50 | DLGTNTRIP: {CLASS: NTRIPConfigDialog, THD: None, DLG: None, CFG: True}, 51 | DLGTSPARTN: {CLASS: SPARTNConfigDialog, THD: None, DLG: None, CFG: True}, 52 | DLGTGPX: {CLASS: GPXViewerDialog, THD: None, DLG: None, CFG: True}, 53 | DLGTIMPORTMAP: {CLASS: ImportMapDialog, THD: None, DLG: None, CFG: True}, 54 | DLGTTTY: {CLASS: TTYPresetDialog, THD: None, DLG: None, CFG: True}, 55 | # add any new dialogs here 56 | } 57 | -------------------------------------------------------------------------------- /src/pygpsclient/gnss_status.py: -------------------------------------------------------------------------------- 1 | """ 2 | gnss_status.py 3 | 4 | GNSS Status class. 5 | 6 | Container for the latest readings from the GNSS receiver. 7 | 8 | Created on 07 Apr 2022 9 | 10 | :author: semuadmin 11 | :copyright: 2020 SEMU Consulting 12 | :license: BSD 3-Clause 13 | """ 14 | 15 | from datetime import datetime, timezone 16 | 17 | 18 | class GNSSStatus: 19 | """ 20 | GNSS Status class. 21 | Container for the latest readings from the GNSS receiver. 22 | """ 23 | 24 | def __init__(self): 25 | """ 26 | Constructor. 27 | """ 28 | 29 | self.utc = datetime.now(timezone.utc).time().replace(microsecond=0) # UTC time 30 | self.lat = 0.0 # latitude as decimal 31 | self.lon = 0.0 # longitude as decimal 32 | self.alt = 0.0 # height above sea level m 33 | self.hae = 0.0 # height above ellipsoid m 34 | self.speed = 0.0 # speed m/s 35 | self.track = 0.0 # track degrees 36 | self.fix = "NO FIX" # fix type e.g. "3D" 37 | self.siv = 0 # satellites in view 38 | self.sip = 0 # satellites in position solution 39 | self.pdop = 0.0 # dilution of precision DOP 40 | self.hdop = 0.0 # horizontal DOP 41 | self.vdop = 0.0 # vertical DOP 42 | self.hacc = 0.0 # horizontal accuracy m 43 | self.vacc = 0.0 # vertical accuracy m 44 | self.diff_corr = 0 # DGPS correction status True/False 45 | self.diff_age = 0 # DGPS correction age seconds 46 | self.diff_station = "N/A" # DGPS station id 47 | self.rel_pos_heading = 0.0 # rover relative position heading 48 | self.rel_pos_length = 0.0 # rover relative position distance 49 | self.acc_heading = 0.0 # rover relative position heading accuracy 50 | self.acc_length = 0.0 # rover relative position distance accuracy 51 | self.rel_pos_flags = [] # rover relative position flags 52 | self.gsv_data = {} # list of satellite tuples (gnssId, svid, elev, azim, cno) 53 | self.version_data = {} # dict of hardware, firmware and software versions 54 | self.sysmon_data = {} # dict of system monitor data (cpu and memory load, etc.) 55 | self.spectrum_data = [] # list of spectrum data (spec, spn, res, ctr, pga) 56 | self.comms_data = {} # dict of comms port utilisation (tx and rx loads) 57 | self.imu_data = {} # dict of imu data (roll, pitch, yaw, status) 58 | -------------------------------------------------------------------------------- /src/pygpsclient/graphview_frame.py: -------------------------------------------------------------------------------- 1 | """ 2 | graphview_frame.py 3 | 4 | Graphview frame class for PyGPSClient application. 5 | 6 | This handles a frame containing a graph of current satellite reception. 7 | 8 | Created on 14 Sep 2020 9 | 10 | :author: semuadmin 11 | :copyright: 2020 SEMU Consulting 12 | :license: BSD 3-Clause 13 | """ 14 | 15 | from tkinter import ALL, BOTH, YES, Canvas, E, Frame 16 | 17 | from pygpsclient.globals import ( 18 | AXISCOL, 19 | BGCOL, 20 | FGCOL, 21 | GNSS_LIST, 22 | GRIDCOL, 23 | MAX_SNR, 24 | WIDGETU2, 25 | ) 26 | from pygpsclient.helpers import fontheight, scale_font, snr2col 27 | 28 | # Relative offsets of graph axes and legend 29 | AXIS_XL = 19 30 | AXIS_XR = 10 31 | AXIS_Y = 22 32 | OL_WID = 2 33 | LEG_XOFF = AXIS_XL + 10 34 | LEG_YOFF = 5 35 | LEG_GAP = 5 36 | 37 | 38 | class GraphviewFrame(Frame): 39 | """ 40 | Graphview frame class. 41 | """ 42 | 43 | def __init__(self, app, *args, **kwargs): 44 | """ 45 | Constructor. 46 | 47 | :param Frame app: reference to main tkinter application 48 | :param args: optional args to pass to Frame parent class 49 | :param kwargs: optional kwargs to pass to Frame parent class 50 | """ 51 | 52 | self.__app = app # Reference to main application class 53 | self.__master = self.__app.appmaster # Reference to root class (Tk) 54 | 55 | Frame.__init__(self, self.__master, *args, **kwargs) 56 | 57 | def_w, def_h = WIDGETU2 58 | self.width = kwargs.get("width", def_w) 59 | self.height = kwargs.get("height", def_h) 60 | self._font = self.__app.font_vsm 61 | self._fonth = fontheight(self._font) 62 | self._body() 63 | self._attach_events() 64 | 65 | def _body(self): 66 | """ 67 | Set up frame and widgets. 68 | """ 69 | 70 | self.grid_columnconfigure(0, weight=1) 71 | self.grid_rowconfigure(0, weight=1) 72 | self.can_graphview = Canvas( 73 | self, width=self.width, height=self.height, bg=BGCOL 74 | ) 75 | self.can_graphview.pack(fill=BOTH, expand=YES) 76 | 77 | def _attach_events(self): 78 | """ 79 | Bind events to frame. 80 | """ 81 | 82 | self.bind("", self._on_resize) 83 | self.can_graphview.bind("", self._on_legend) 84 | 85 | def _on_legend(self, event): # pylint: disable=unused-argument 86 | """ 87 | On double-click - toggle legend on/off. 88 | 89 | :param event: event 90 | """ 91 | 92 | self.__app.configuration.set( 93 | "legend_b", not self.__app.configuration.get("legend_b") 94 | ) 95 | 96 | def init_frame(self): 97 | """ 98 | Initialise graph view 99 | """ 100 | 101 | w, h = self.width, self.height 102 | ticks = int(MAX_SNR / 10) 103 | self.can_graphview.delete(ALL) 104 | for i in range(ticks, 0, -1): 105 | y = (h - AXIS_Y) * i / ticks 106 | self.can_graphview.create_line( 107 | AXIS_XL, y, w - AXIS_XR + 2, y, fill=AXISCOL if i == ticks else GRIDCOL 108 | ) 109 | self.can_graphview.create_text( 110 | 10, 111 | y, 112 | text=str(MAX_SNR - (i * 10)), 113 | angle=90, 114 | fill=FGCOL, 115 | font=self._font, 116 | ) 117 | self.can_graphview.create_line(AXIS_XL, 5, AXIS_XL, h - AXIS_Y, fill=AXISCOL) 118 | self.can_graphview.create_line( 119 | w - AXIS_XR + 2, 5, w - AXIS_XR + 2, h - AXIS_Y, fill=AXISCOL 120 | ) 121 | self.can_graphview.create_text( 122 | AXIS_XR, 123 | 5, 124 | text="CN₀ dB", 125 | angle=90, 126 | fill=FGCOL, 127 | anchor=E, 128 | font=self._font, 129 | ) 130 | 131 | if self.__app.configuration.get("legend_b"): 132 | self._draw_legend() 133 | 134 | def _draw_legend(self): 135 | """ 136 | Draw GNSS color code legend 137 | """ 138 | 139 | w = self.width / 10 140 | h = self.height / 15 141 | 142 | for i, (_, (gnssName, gnssCol)) in enumerate(GNSS_LIST.items()): 143 | x = LEG_XOFF + w * i 144 | self.can_graphview.create_rectangle( 145 | x, 146 | LEG_YOFF, 147 | x + w - LEG_GAP, 148 | LEG_YOFF + h, 149 | outline=gnssCol, 150 | fill=BGCOL, 151 | width=OL_WID, 152 | ) 153 | self.can_graphview.create_text( 154 | (x + x + w - LEG_GAP) / 2, 155 | LEG_YOFF + h / 2, 156 | text=gnssName, 157 | fill=FGCOL, 158 | font=self._font, 159 | ) 160 | 161 | def update_frame(self): 162 | """ 163 | Plot satellites' signal-to-noise ratio (cno). 164 | Automatically adjust y axis according to number of satellites in view. 165 | """ 166 | 167 | data = self.__app.gnss_status.gsv_data 168 | siv = len(self.__app.gnss_status.gsv_data) 169 | 170 | if siv == 0: 171 | return 172 | 173 | w, h = self.width, self.height 174 | self.init_frame() 175 | 176 | offset = AXIS_XL + 2 177 | colwidth = (w - AXIS_XL - AXIS_XR + 1) / siv 178 | # scale x axis label according to siv 179 | svfont, _ = scale_font(self.width, 6, siv, 14) 180 | for d in sorted(data.values()): # sort by ascending gnssid, svid 181 | gnssId, prn, _, _, snr = d 182 | if snr in ("", "0", 0): 183 | snr = 1 # show 'place marker' in graph 184 | else: 185 | snr = int(snr) 186 | snr_y = int(snr) * (h - AXIS_Y - 1) / MAX_SNR 187 | (_, ol_col) = GNSS_LIST[gnssId] 188 | prn = f"{int(prn):02}" 189 | self.can_graphview.create_rectangle( 190 | offset, 191 | h - AXIS_Y - 1, 192 | offset + colwidth - OL_WID, 193 | h - AXIS_Y - 1 - snr_y, 194 | outline=ol_col, 195 | fill=snr2col(snr), 196 | width=OL_WID, 197 | ) 198 | self.can_graphview.create_text( 199 | offset + colwidth / 2, 200 | h - 10, 201 | text=prn, 202 | fill=FGCOL, 203 | font=svfont, 204 | angle=35, 205 | ) 206 | offset += colwidth 207 | 208 | self.can_graphview.update_idletasks() 209 | 210 | def _on_resize(self, event): # pylint: disable=unused-argument 211 | """ 212 | Resize frame 213 | 214 | :param event event: resize event 215 | """ 216 | 217 | self.width, self.height = self.get_size() 218 | self._font, self._fonth = scale_font(self.width, 8, 25, 16) 219 | 220 | def get_size(self): 221 | """ 222 | Get current canvas size. 223 | 224 | :return: window size (width, height) 225 | :rtype: tuple 226 | """ 227 | 228 | self.update_idletasks() # Make sure we know about any resizing 229 | return self.can_graphview.winfo_width(), self.can_graphview.winfo_height() 230 | -------------------------------------------------------------------------------- /src/pygpsclient/hardware_info_frame.py: -------------------------------------------------------------------------------- 1 | """ 2 | hardware_info_frame.py 3 | 4 | Hardware Information Dialog for NMEA and UBX Configuration panels. 5 | 6 | Created on 22 Dec 2020 7 | 8 | :author: semuadmin 9 | :copyright: 2020 SEMU Consulting 10 | :license: BSD 3-Clause 11 | """ 12 | 13 | from tkinter import Frame, Label, W 14 | 15 | from PIL import Image, ImageTk 16 | from pynmeagps import POLL, NMEAMessage 17 | from pyubx2 import UBXMessage 18 | 19 | from pygpsclient.globals import ( 20 | CONNECTED, 21 | CONNECTED_SIMULATOR, 22 | ICON_CONFIRMED, 23 | ICON_PENDING, 24 | ICON_SEND, 25 | ICON_WARNING, 26 | NMEA_MONHW, 27 | OKCOL, 28 | UBX_MONVER, 29 | ) 30 | from pygpsclient.strings import NA 31 | 32 | 33 | class Hardware_Info_Frame(Frame): 34 | """ 35 | Hardware & firmware information panel. 36 | """ 37 | 38 | def __init__(self, app, container, *args, **kwargs): 39 | """ 40 | Constructor. 41 | 42 | :param Frame app: reference to main tkinter application 43 | :param Frame container: reference to container frame (config-dialog) 44 | :param args: optional args to pass to Frame parent class 45 | :param kwargs: optional kwargs to pass to Frame parent class 46 | """ 47 | 48 | self.__app = app # Reference to main application class 49 | self.__master = self.__app.appmaster # Reference to root class (Tk) 50 | self.__container = container 51 | self._protocol = kwargs.pop("protocol", "UBX") 52 | 53 | Frame.__init__(self, self.__container.container, *args, **kwargs) 54 | 55 | self._img_send = ImageTk.PhotoImage(Image.open(ICON_SEND)) 56 | self._img_pending = ImageTk.PhotoImage(Image.open(ICON_PENDING)) 57 | self._img_confirmed = ImageTk.PhotoImage(Image.open(ICON_CONFIRMED)) 58 | self._img_warn = ImageTk.PhotoImage(Image.open(ICON_WARNING)) 59 | 60 | self._body() 61 | self._do_layout() 62 | self._attach_events() 63 | 64 | def _body(self): 65 | """ 66 | Set up frame and widgets. 67 | """ 68 | 69 | self._lbl_hwverl = Label(self, text="Hardware") 70 | self._lbl_hwver = Label(self) 71 | self._lbl_swverl = Label(self, text="Software") 72 | self._lbl_swver = Label(self) 73 | self._lbl_fwverl = Label(self, text="Firmware") 74 | self._lbl_fwver = Label(self) 75 | self._lbl_romverl = Label(self, text="Protocol") 76 | self._lbl_romver = Label(self) 77 | self._lbl_gnssl = Label(self, text="GNSS/AS") 78 | self._lbl_gnss = Label(self) 79 | 80 | def _do_layout(self): 81 | """ 82 | Layout widgets. 83 | """ 84 | 85 | self._lbl_hwverl.grid(column=0, row=0, padx=2, sticky=W) 86 | self._lbl_hwver.grid(column=1, row=0, columnspan=2, padx=2, sticky=W) 87 | self._lbl_swverl.grid(column=3, row=0, padx=2, sticky=W) 88 | self._lbl_swver.grid(column=4, row=0, columnspan=2, padx=2, sticky=W) 89 | self._lbl_fwverl.grid(column=0, row=1, padx=2, sticky=W) 90 | self._lbl_fwver.grid(column=1, row=1, columnspan=2, padx=2, sticky=W) 91 | self._lbl_romverl.grid(column=3, row=1, padx=2, sticky=W) 92 | self._lbl_romver.grid(column=4, row=1, columnspan=2, padx=2, sticky=W) 93 | self._lbl_gnssl.grid(column=0, row=2, columnspan=1, padx=2, sticky=W) 94 | self._lbl_gnss.grid(column=1, row=2, columnspan=4, padx=2, sticky=W) 95 | 96 | (cols, rows) = self.grid_size() 97 | for i in range(cols): 98 | self.grid_columnconfigure(i, weight=1) 99 | for i in range(rows): 100 | self.grid_rowconfigure(i, weight=1) 101 | self.option_add("*Font", self.__app.font_sm) 102 | 103 | def _attach_events(self): 104 | """ 105 | Bind events to widget. 106 | """ 107 | 108 | # click mouse button to refresh information 109 | self.bind("