diff --git a/.github/workflows/checkpr.yml b/.github/workflows/checkpr.yml index 8ccdfe85..79c71f9d 100644 --- a/.github/workflows/checkpr.yml +++ b/.github/workflows/checkpr.yml @@ -17,9 +17,7 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Install deploy dependencies - run: pip install .[deploy] - - name: Install test dependencies - run: pip install .[test] + run: pip install --group deploy - name: Install code dependencies run: pip install . - name: Lint with pylint diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f4850b9b..34b061a3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,20 +19,12 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Install deploy dependencies - run: | - pip install .[deploy] - - name: Install test dependencies - run: | - pip install .[test] + run: pip install --group deploy - name: Install code dependencies - run: | - pip install . + run: pip install . - name: Lint with pylint - run: | - pylint -E src + run: pylint -E src - name: Security vulnerability analysis with bandit - run: | - bandit -c pyproject.toml -r -lll . + run: bandit -c pyproject.toml -r -lll . - name: Test with pytest - run: | - pytest + run: pytest diff --git a/.vscode/settings.json b/.vscode/settings.json index 1fd93d61..77c63bcf 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,8 +1,20 @@ { "python.testing.pytestEnabled": true, - "python.testing.unittestEnabled": false, + "python.terminal.activateEnvironment": true, "editor.formatOnSave": true, - "modulename": "pygpsclient", - "distname": "pygpsclient", - "python.defaultInterpreterPath": "python3", + "modulename": "${workspaceFolderBasename}", + "distname": "${workspaceFolderBasename}", + //"venv": "${env:UserProfile}/pygpsclient", + "venv": "${env:HOME}/pygpsclient", + "python.testing.pytestArgs": [ + "tests" + ], + "C_Cpp.copilotHover": "disabled", + "chat.agent.enabled": false, + "chat.commandCenter.enabled": false, + "chat.notifyWindowOnConfirmation": false, + "telemetry.feedback.enabled": false, + "python.analysis.addHoverSummaries": false, + "python-envs.defaultEnvManager": "ms-python.python:venv", + "python-envs.pythonProjects": [], } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 07ab4e6c..fe46fe31 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,31 +1,29 @@ { // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format - // These Python project tasks assume you have installed and configured: - // build, wheel, black, pylint, pytest, pytest-cov, Sphinx, sphinx-rtd-theme - // Use the Update Toolchain task to install the necessary packages. + // + // This VSCode development workflow is intended to work on + // MacOS, Linux and Windows (with Powershell>=5.1). + // + // Use the Install Deploy Dependencies tasks to install the necessary + // build and test packages into the system environment. + // + // Use the Create Venv task to create a virtual environment in the + // designated directory. Select this environment using Select + // Interpreter to auto-activate it via New Terminal. + // + // Remember to include any global Python bin (Scripts on Windows) in PATH. "version": "2.0.0", "tasks": [ { - "label": "Install Dependencies", + "label": "Create Venv", "type": "process", "command": "${config:python.defaultInterpreterPath}", "args": [ "-m", - "pip", - "install", - "--upgrade", - "setuptools", - "build", - "wheel", - "black", - "pylint", - "pytest", - "pytest-cov", - "isort", - "bandit", - "Sphinx", - "sphinx-rtd-theme" + "venv", + "${config:venv}", + //"--system-site-packages" ], "problemMatcher": [] }, @@ -48,6 +46,19 @@ }, "problemMatcher": [] }, + { + "label": "Install Deploy Dependencies", + "type": "process", + "command": "${config:python.defaultInterpreterPath}", + "args": [ + "-m", + "pip", + "install", + "--group", + "deploy" + ], + "problemMatcher": [] + }, { "label": "Clean", "type": "shell", @@ -58,22 +69,16 @@ "dist", "htmlcov", "docs/_build", - "${config:modulename}.egg-info" + "${config:modulename}.egg-info", ], "windows": { - "command": "Get-ChildItem", + "command": "rm", "args": [ + "-R", "-Path", - "build\\,", - "dist\\,", - "docs\\_build,", - "${config:modulename}.egg-info", - "-Recurse", - "|", - "Remove-Item", - "-Recurse", - "-Confirm:$false", - "-Force" + "'src/${config:modulename}.egg-info','build','dist','htmlcov','docs/_build'", + "-ErrorAction", + "SilentlyContinue" // doesn't work! - stops on exit code 1 anyway ] }, "options": { @@ -126,37 +131,49 @@ "-c", "pyproject.toml", "-r", - "--exit-zero", "." ], "problemMatcher": [] }, { - "label": "Build", + "label": "Test", "type": "process", "command": "${config:python.defaultInterpreterPath}", "args": [ "-m", - "build", - ".", - "--wheel", - "--sdist" + "pytest" ], "problemMatcher": [], "group": { - "kind": "build", + "kind": "test", "isDefault": true } }, { - "label": "Test", + "label": "Build", "type": "process", "command": "${config:python.defaultInterpreterPath}", "args": [ "-m", - "pytest" + "build", + ".", + "--wheel", + "--sdist", ], - "problemMatcher": [] + "problemMatcher": [], + "dependsOrder": "sequence", + "dependsOn": [ + "Clean", + "Security", + "Sort Imports", + "Format", + "Pylint", + "Test", + ], + "group": { + "kind": "build", + "isDefault": true + } }, { "label": "Sphinx", @@ -193,7 +210,7 @@ "problemMatcher": [] }, { - "label": "Sphinx Deploy to S3", + "label": "Sphinx Deploy to S3", // needs AWS credentials "type": "process", "command": "aws", "args": [ @@ -210,48 +227,28 @@ "problemMatcher": [] }, { - "label": "Install Wheel", - "type": "shell", + "label": "Install Locally", + "type": "process", "command": "${config:python.defaultInterpreterPath}", "args": [ "-m", "pip", "install", - "--user", + "--upgrade", "--force-reinstall", - "*.whl" + "--find-links=${workspaceFolder}/dist", + "${workspaceFolderBasename}", + // "--target", + // "${config:venv}/Lib/site-packages" ], "options": { "cwd": "dist" }, - "problemMatcher": [] - }, - { - "label": "Install Locally", - "type": "shell", - "command": "${config:python.defaultInterpreterPath}", - "args": [ - "-m", - "pip", - "install", - //"--user", - "--force-reinstall", - "*.whl" - ], "dependsOrder": "sequence", "dependsOn": [ - "Clean", - "Security", - "Sort Imports", - "Format", - "Pylint", - "Test", "Build", "Sphinx HTML" ], - "options": { - "cwd": "dist" - }, "problemMatcher": [] } ] diff --git a/README.md b/README.md index da9c7651..3e0f78d3 100644 --- a/README.md +++ b/README.md @@ -418,6 +418,8 @@ The facility can be accessed by clicking ![SPARTN Client button](https://github. 2. L-BAND Correction (D9* Receiver): + **NB** Note that u-blox [discontinued their PointPerfect SPARTN L-Band service](https://portal.u-blox.com/s/question/0D5Oj00000uB53GKAS/suspension-of-european-pointperfect-lband-spartn-service) in the European region in March 2025 and Worldwide in June 2025. The SPARTN L-Band configuration panel is now disabled by default, though the panel can still be used for other generic L-Band modem configuration purposes and can be re-enabled by setting json configuration parameter `lband_enabled_b` to `1`. + - SPARTN L-Band correction receiver e.g. u-blox NEO-D9S. - [Suitable Inmarsat L-band antenna](https://www.amazon.com/RTL-SDR-Blog-1525-1637-Inmarsat-Iridium/dp/B07WGWZS1D) and good satellite reception on regional frequency (NB: standard GNSS antenna may not be suitable). - Subscription to L-Band location service e.g. u-blox / Thingstream PointPerfect, which should provide the following details: @@ -453,6 +455,7 @@ The facility can be accessed by clicking ![SPARTN Client button](https://github. ### L-Band Correction Configuration (D9*) +1. **NOTE** This panel is only available if json configuration setting `lband_enabled_b` is set to `1`. 1. To connect to the Correction receiver, select the receiver's port from the SPARTN dialog's Serial Port listbox and click ![connect icon](https://github.com/semuconsulting/PyGPSClient/blob/master/src/pygpsclient/resources/usbport-1-24.png?raw=true). To disconnect, click ![disconnect icon](https://github.com/semuconsulting/PyGPSClient/blob/master/src/pygpsclient/resources/iconmonstr-media-control-50-24.png?raw=true). 1. Select the required Output Port - this is the port used to connect the Correction receiver to the GNSS receiver e.g. UART2 or I2C. 1. If both Correction and GNSS receivers are connected to the same PyGPSClient workstation (e.g. via separate USB ports), it is possible to run the utility in Output Port = 'Passthough' mode, whereby the output data from the Correction receiver (UBX `RXM-PMP` messages) will be automatically passed through to the GNSS receiver by PyGPSClient, without the need to connect the two externally. @@ -634,7 +637,9 @@ For further details, refer to the `pygnssutils` homepage at [https://github.com/ sudo apt-get install build-essential libssl-dev libffi-dev python3-dev pkg-config ``` -2. The [latest official Python 3.13 installers](https://docs.python.org/3/howto/free-threading-python.html) include the option to disable the standard [Python Global Interpreter Lock (GIL)](https://realpython.com/python-gil/) and allow threads to run concurrently. Whilst the core PyGPSClient packages run fine in this mode (*and some marginal performance improvements may be seen on multi-core machines*), installing PyGPSClient using pip under the "No-GIL" `python3.13t` executable currently fails due to missing cryptography dependencies (*Warning: CPython 3.13t at /Users/user/pygpsclient_nogil/bin/python3 does not yet support abi3 so the build artifacts will be version-specific* *warning: openssl-sys@0.9.108: Could not find directory of OpenSSL installation*). +2. The [latest official Python 3.13 installers](https://docs.python.org/3/howto/free-threading-python.html) include the option to disable the standard [Python Global Interpreter Lock (GIL)](https://realpython.com/python-gil/) and allow threads to run concurrently. Whilst the core PyGPSClient packages run fine in this mode (*and some marginal performance improvements may be seen on multi-core machines*), installing PyGPSClient using pip under the "No-GIL" `python3.13t` executable currently fails due to missing cryptography dependencies (*Warning: CPython 3.13t at /Users/user/pygpsclient_nogil/bin/python3 does not yet support abi3 so the build artifacts will be version-specific* *warning: openssl-sys@0.9.108: Could not find directory of OpenSSL installation*). + +3. u-blox [discontinued their PointPerfect SPARTN L-Band service](https://portal.u-blox.com/s/question/0D5Oj00000uB53GKAS/suspension-of-european-pointperfect-lband-spartn-service) in the European region in March 2025. As a result, PyGPSClient maintainers based in this region are no longer able to test this functionality against live data. --- ## License diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index c3a7fb39..ba55f303 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,20 @@ # PyGPSClient Release Notes +### RELEASE 1.5.11 + +FIXES: + +1. Clarify interpretation of SPARTN decryption basedate integer values in *.json configuration file; `-1` signifies 'use current datetime' (`basedate=None`); `0` signifies 'use gnssTimeTag from incoming SPARTN data stream' (`basedate=pyspartn.TIMEBASE`); any other integer value represents an explicit gnssTimeTag value. +1. Fix minor bugs in GPX Viewer custom map handling. + +ENHANCEMENTS: + +1. Make all Toplevel dialogs scrollable and resizeable, depending on effective screen resolution. Applies to: UBX Config, NMEA Config, NTRIP Client, SPARTN Client, Display GPX Track, Import Custom Map, TTY Commands. This allows the dialogs to be usable on low resolution screens( 600 <= width <= 1024 pixels). +1. Make SPARTN L-Band configuration panel optional - disabled by default (*ublox PointPerfect SPARTN L-Band service was discontinued in May 2025, though the panel can still be used for other generic L-Band modem configuration purposes*). Panel can be re-enabled by setting lband_enabled_b to 1. +1. Make `cryptography` library dependency optional (*it is only required to decrypt encrypted MQTT SPARTN payloads*). If the `cryptography` library is not installed, the "Decode SPARTN in console" option will be greyed out in the SPARTN MQTT Client dialog. +1. Allow datalogging and track recording to be enabled or disabled while connected (previously only available while disconnected). +1. Update minimum pyubx2, pysbf2 and pygnssutils versions to take onboard latest fixes and enhancements. + ### RELEASE 1.5.10 FIXES: @@ -632,7 +647,7 @@ ENHANCEMENTS: 1. New CFG-* Other Configuration command panel added to UBX Configuration panel. Provides structured inputs for a range of legacy CFG commands. **NB:** For Generation 9+ devices, legacy CFG commands are deprecated in favour of the CFG-VALGET/SET/DEL Configuration Interface commands in the adjacent panel. 2. When a legacy CFG command is selected from the CFG-* listbox, a POLL request is sent to the device to retrieve the current settings; these are then used to populate a series of dynamically generated Entry widgets. The user can amend the values as required and send the updated set of values as a SET message to the device. After sending, the current values will be polled again to confirm the update has taken place. **NB:** this mechanism is dependent on receiving timely POLL responses. Note caveats in README re. optimising POLL response performance. 3. For the time being, there are a few constraints with regard to updating certain CFG types, but these will hopefully be addressed in a future update as and when time permits. The `pyubx2` library which underpins`PyGPSClient` fully supports *ALL* CFG-* commands. -4. The new panel can be enabled or disabled using the `ENABLE_CFG_OTHER` boolean in `globals.py`. +4. The new panel can be enabled or disabled using the `ENABLE_CFG_LEGACY` boolean in `globals.py`. ### RELEASE v1.3.7 diff --git a/docs/modules.rst b/docs/modules.rst index 48a58d79..667e2a2e 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -1,7 +1,7 @@ -pygpsclient +PyGPSClient =========== .. toctree:: :maxdepth: 4 - pygpsclient + PyGPSClient diff --git a/docs/pygpsclient.rst b/docs/pygpsclient.rst index 712aac72..c7014b9a 100644 --- a/docs/pygpsclient.rst +++ b/docs/pygpsclient.rst @@ -1,445 +1,453 @@ -pygpsclient package +PyGPSClient package =================== Submodules ---------- -pygpsclient.about\_dialog module +PyGPSClient.about\_dialog module -------------------------------- -.. automodule:: pygpsclient.about_dialog +.. automodule:: PyGPSClient.about_dialog :members: :show-inheritance: :undoc-members: -pygpsclient.app module +PyGPSClient.app module ---------------------- -.. automodule:: pygpsclient.app +.. automodule:: PyGPSClient.app :members: :show-inheritance: :undoc-members: -pygpsclient.banner\_frame module +PyGPSClient.banner\_frame module -------------------------------- -.. automodule:: pygpsclient.banner_frame +.. automodule:: PyGPSClient.banner_frame :members: :show-inheritance: :undoc-members: -pygpsclient.chart\_frame module +PyGPSClient.chart\_frame module ------------------------------- -.. automodule:: pygpsclient.chart_frame +.. automodule:: PyGPSClient.chart_frame :members: :show-inheritance: :undoc-members: -pygpsclient.configuration module +PyGPSClient.configuration module -------------------------------- -.. automodule:: pygpsclient.configuration +.. automodule:: PyGPSClient.configuration :members: :show-inheritance: :undoc-members: -pygpsclient.confirm\_box module +PyGPSClient.confirm\_box module ------------------------------- -.. automodule:: pygpsclient.confirm_box +.. automodule:: PyGPSClient.confirm_box :members: :show-inheritance: :undoc-members: -pygpsclient.console\_frame module +PyGPSClient.console\_frame module --------------------------------- -.. automodule:: pygpsclient.console_frame +.. automodule:: PyGPSClient.console_frame :members: :show-inheritance: :undoc-members: -pygpsclient.dialog\_state module +PyGPSClient.dialog\_state module -------------------------------- -.. automodule:: pygpsclient.dialog_state +.. automodule:: PyGPSClient.dialog_state :members: :show-inheritance: :undoc-members: -pygpsclient.dynamic\_config\_frame module +PyGPSClient.dynamic\_config\_frame module ----------------------------------------- -.. automodule:: pygpsclient.dynamic_config_frame +.. automodule:: PyGPSClient.dynamic_config_frame :members: :show-inheritance: :undoc-members: -pygpsclient.file\_handler module +PyGPSClient.file\_handler module -------------------------------- -.. automodule:: pygpsclient.file_handler +.. automodule:: PyGPSClient.file_handler :members: :show-inheritance: :undoc-members: -pygpsclient.globals module +PyGPSClient.globals module -------------------------- -.. automodule:: pygpsclient.globals +.. automodule:: PyGPSClient.globals :members: :show-inheritance: :undoc-members: -pygpsclient.gnss\_status module +PyGPSClient.gnss\_status module ------------------------------- -.. automodule:: pygpsclient.gnss_status +.. automodule:: PyGPSClient.gnss_status :members: :show-inheritance: :undoc-members: -pygpsclient.gpx\_dialog module +PyGPSClient.gpx\_dialog module ------------------------------ -.. automodule:: pygpsclient.gpx_dialog +.. automodule:: PyGPSClient.gpx_dialog :members: :show-inheritance: :undoc-members: -pygpsclient.graphview\_frame module +PyGPSClient.graphview\_frame module ----------------------------------- -.. automodule:: pygpsclient.graphview_frame +.. automodule:: PyGPSClient.graphview_frame :members: :show-inheritance: :undoc-members: -pygpsclient.hardware\_info\_frame module +PyGPSClient.hardware\_info\_frame module ---------------------------------------- -.. automodule:: pygpsclient.hardware_info_frame +.. automodule:: PyGPSClient.hardware_info_frame :members: :show-inheritance: :undoc-members: -pygpsclient.helpers module +PyGPSClient.helpers module -------------------------- -.. automodule:: pygpsclient.helpers +.. automodule:: PyGPSClient.helpers :members: :show-inheritance: :undoc-members: -pygpsclient.importmap\_dialog module +PyGPSClient.importmap\_dialog module ------------------------------------ -.. automodule:: pygpsclient.importmap_dialog +.. automodule:: PyGPSClient.importmap_dialog :members: :show-inheritance: :undoc-members: -pygpsclient.imu\_frame module +PyGPSClient.imu\_frame module ----------------------------- -.. automodule:: pygpsclient.imu_frame +.. automodule:: PyGPSClient.imu_frame :members: :show-inheritance: :undoc-members: -pygpsclient.map\_frame module +PyGPSClient.map\_frame module ----------------------------- -.. automodule:: pygpsclient.map_frame +.. automodule:: PyGPSClient.map_frame :members: :show-inheritance: :undoc-members: -pygpsclient.mapquest module +PyGPSClient.mapquest module --------------------------- -.. automodule:: pygpsclient.mapquest +.. automodule:: PyGPSClient.mapquest :members: :show-inheritance: :undoc-members: -pygpsclient.menu\_bar module +PyGPSClient.menu\_bar module ---------------------------- -.. automodule:: pygpsclient.menu_bar +.. automodule:: PyGPSClient.menu_bar :members: :show-inheritance: :undoc-members: -pygpsclient.nmea\_config\_dialog module +PyGPSClient.nmea\_config\_dialog module --------------------------------------- -.. automodule:: pygpsclient.nmea_config_dialog +.. automodule:: PyGPSClient.nmea_config_dialog :members: :show-inheritance: :undoc-members: -pygpsclient.nmea\_handler module +PyGPSClient.nmea\_handler module -------------------------------- -.. automodule:: pygpsclient.nmea_handler +.. automodule:: PyGPSClient.nmea_handler :members: :show-inheritance: :undoc-members: -pygpsclient.nmea\_preset\_frame module +PyGPSClient.nmea\_preset\_frame module -------------------------------------- -.. automodule:: pygpsclient.nmea_preset_frame +.. automodule:: PyGPSClient.nmea_preset_frame :members: :show-inheritance: :undoc-members: -pygpsclient.ntrip\_client\_dialog module +PyGPSClient.ntrip\_client\_dialog module ---------------------------------------- -.. automodule:: pygpsclient.ntrip_client_dialog +.. automodule:: PyGPSClient.ntrip_client_dialog :members: :show-inheritance: :undoc-members: -pygpsclient.rover\_frame module +PyGPSClient.rover\_frame module ------------------------------- -.. automodule:: pygpsclient.rover_frame +.. automodule:: PyGPSClient.rover_frame :members: :show-inheritance: :undoc-members: -pygpsclient.rtcm3\_handler module +PyGPSClient.rtcm3\_handler module --------------------------------- -.. automodule:: pygpsclient.rtcm3_handler +.. automodule:: PyGPSClient.rtcm3_handler :members: :show-inheritance: :undoc-members: -pygpsclient.sbf\_handler module +PyGPSClient.sbf\_handler module ------------------------------- -.. automodule:: pygpsclient.sbf_handler +.. automodule:: PyGPSClient.sbf_handler :members: :show-inheritance: :undoc-members: -pygpsclient.scatter\_frame module +PyGPSClient.scatter\_frame module --------------------------------- -.. automodule:: pygpsclient.scatter_frame +.. automodule:: PyGPSClient.scatter_frame :members: :show-inheritance: :undoc-members: -pygpsclient.serialconfig\_frame module +PyGPSClient.serialconfig\_frame module -------------------------------------- -.. automodule:: pygpsclient.serialconfig_frame +.. automodule:: PyGPSClient.serialconfig_frame :members: :show-inheritance: :undoc-members: -pygpsclient.serverconfig\_frame module +PyGPSClient.serverconfig\_frame module -------------------------------------- -.. automodule:: pygpsclient.serverconfig_frame +.. automodule:: PyGPSClient.serverconfig_frame :members: :show-inheritance: :undoc-members: -pygpsclient.settings\_frame module +PyGPSClient.settings\_frame module ---------------------------------- -.. automodule:: pygpsclient.settings_frame +.. automodule:: PyGPSClient.settings_frame :members: :show-inheritance: :undoc-members: -pygpsclient.skyview\_frame module +PyGPSClient.skyview\_frame module --------------------------------- -.. automodule:: pygpsclient.skyview_frame +.. automodule:: PyGPSClient.skyview_frame :members: :show-inheritance: :undoc-members: -pygpsclient.socketconfig\_frame module +PyGPSClient.socketconfig\_frame module -------------------------------------- -.. automodule:: pygpsclient.socketconfig_frame +.. automodule:: PyGPSClient.socketconfig_frame :members: :show-inheritance: :undoc-members: -pygpsclient.spartn\_dialog module +PyGPSClient.spartn\_dialog module --------------------------------- -.. automodule:: pygpsclient.spartn_dialog +.. automodule:: PyGPSClient.spartn_dialog :members: :show-inheritance: :undoc-members: -pygpsclient.spartn\_gnss\_frame module +PyGPSClient.spartn\_gnss\_frame module -------------------------------------- -.. automodule:: pygpsclient.spartn_gnss_frame +.. automodule:: PyGPSClient.spartn_gnss_frame :members: :show-inheritance: :undoc-members: -pygpsclient.spartn\_json\_config module +PyGPSClient.spartn\_json\_config module --------------------------------------- -.. automodule:: pygpsclient.spartn_json_config +.. automodule:: PyGPSClient.spartn_json_config :members: :show-inheritance: :undoc-members: -pygpsclient.spartn\_lband\_frame module +PyGPSClient.spartn\_lband\_frame module --------------------------------------- -.. automodule:: pygpsclient.spartn_lband_frame +.. automodule:: PyGPSClient.spartn_lband_frame :members: :show-inheritance: :undoc-members: -pygpsclient.spartn\_mqtt\_frame module +PyGPSClient.spartn\_mqtt\_frame module -------------------------------------- -.. automodule:: pygpsclient.spartn_mqtt_frame +.. automodule:: PyGPSClient.spartn_mqtt_frame :members: :show-inheritance: :undoc-members: -pygpsclient.spectrum\_frame module +PyGPSClient.spectrum\_frame module ---------------------------------- -.. automodule:: pygpsclient.spectrum_frame +.. automodule:: PyGPSClient.spectrum_frame :members: :show-inheritance: :undoc-members: -pygpsclient.status\_frame module +PyGPSClient.status\_frame module -------------------------------- -.. automodule:: pygpsclient.status_frame +.. automodule:: PyGPSClient.status_frame :members: :show-inheritance: :undoc-members: -pygpsclient.stream\_handler module +PyGPSClient.stream\_handler module ---------------------------------- -.. automodule:: pygpsclient.stream_handler +.. automodule:: PyGPSClient.stream_handler :members: :show-inheritance: :undoc-members: -pygpsclient.strings module +PyGPSClient.strings module -------------------------- -.. automodule:: pygpsclient.strings +.. automodule:: PyGPSClient.strings :members: :show-inheritance: :undoc-members: -pygpsclient.sysmon\_frame module +PyGPSClient.sysmon\_frame module -------------------------------- -.. automodule:: pygpsclient.sysmon_frame +.. automodule:: PyGPSClient.sysmon_frame :members: :show-inheritance: :undoc-members: -pygpsclient.tty\_handler module +PyGPSClient.toplevel\_dialog module +----------------------------------- + +.. automodule:: PyGPSClient.toplevel_dialog + :members: + :show-inheritance: + :undoc-members: + +PyGPSClient.tty\_handler module ------------------------------- -.. automodule:: pygpsclient.tty_handler +.. automodule:: PyGPSClient.tty_handler :members: :show-inheritance: :undoc-members: -pygpsclient.tty\_preset\_dialog module +PyGPSClient.tty\_preset\_dialog module -------------------------------------- -.. automodule:: pygpsclient.tty_preset_dialog +.. automodule:: PyGPSClient.tty_preset_dialog :members: :show-inheritance: :undoc-members: -pygpsclient.ubx\_cfgval\_frame module +PyGPSClient.ubx\_cfgval\_frame module ------------------------------------- -.. automodule:: pygpsclient.ubx_cfgval_frame +.. automodule:: PyGPSClient.ubx_cfgval_frame :members: :show-inheritance: :undoc-members: -pygpsclient.ubx\_config\_dialog module +PyGPSClient.ubx\_config\_dialog module -------------------------------------- -.. automodule:: pygpsclient.ubx_config_dialog +.. automodule:: PyGPSClient.ubx_config_dialog :members: :show-inheritance: :undoc-members: -pygpsclient.ubx\_handler module +PyGPSClient.ubx\_handler module ------------------------------- -.. automodule:: pygpsclient.ubx_handler +.. automodule:: PyGPSClient.ubx_handler :members: :show-inheritance: :undoc-members: -pygpsclient.ubx\_msgrate\_frame module +PyGPSClient.ubx\_msgrate\_frame module -------------------------------------- -.. automodule:: pygpsclient.ubx_msgrate_frame +.. automodule:: PyGPSClient.ubx_msgrate_frame :members: :show-inheritance: :undoc-members: -pygpsclient.ubx\_port\_frame module +PyGPSClient.ubx\_port\_frame module ----------------------------------- -.. automodule:: pygpsclient.ubx_port_frame +.. automodule:: PyGPSClient.ubx_port_frame :members: :show-inheritance: :undoc-members: -pygpsclient.ubx\_preset\_frame module +PyGPSClient.ubx\_preset\_frame module ------------------------------------- -.. automodule:: pygpsclient.ubx_preset_frame +.. automodule:: PyGPSClient.ubx_preset_frame :members: :show-inheritance: :undoc-members: -pygpsclient.ubx\_recorder\_frame module +PyGPSClient.ubx\_recorder\_frame module --------------------------------------- -.. automodule:: pygpsclient.ubx_recorder_frame +.. automodule:: PyGPSClient.ubx_recorder_frame :members: :show-inheritance: :undoc-members: -pygpsclient.ubx\_solrate\_frame module +PyGPSClient.ubx\_solrate\_frame module -------------------------------------- -.. automodule:: pygpsclient.ubx_solrate_frame +.. automodule:: PyGPSClient.ubx_solrate_frame :members: :show-inheritance: :undoc-members: -pygpsclient.widget\_state module +PyGPSClient.widget\_state module -------------------------------- -.. automodule:: pygpsclient.widget_state +.. automodule:: PyGPSClient.widget_state :members: :show-inheritance: :undoc-members: @@ -447,7 +455,7 @@ pygpsclient.widget\_state module Module contents --------------- -.. automodule:: pygpsclient +.. automodule:: PyGPSClient :members: :show-inheritance: :undoc-members: diff --git a/examples/ttypresets_examples.py b/examples/ttypresets_examples.py index 85bdcb0a..07dda383 100644 --- a/examples/ttypresets_examples.py +++ b/examples/ttypresets_examples.py @@ -14,128 +14,230 @@ # ****************************************** # Septentrio Mosaic X5 Receiver # -# Send an "Initialise Command Mode" string +# Send an 'Initialise Command Mode' string # before sending further commands. # -# NOT AN EXHAUSTIVE LIST +# NOT NECESSARILY AN EXHAUSTIVE LIST # Full details in https://www.septentrio.com/resources/mosaic-X5/mosaic-X5+Firmware+v4.14.10.1+Reference+Guide.pdf # ****************************************** { - "ttypresets_l": [ - "Initialise Command Mode; SSSSSSSSSS", - "Display Help for all available commands; help, Overview", - "Display Help for getReceiverCapabilities command; help, getReceiverCapabilities", - "Display Help for getReceiverCapabilities command; help, grc", - "List contents of current configuration file; lcf, Current", - "Save current configuration to boot file; eccf, Current, Boot", - "List receiver capabilities; grc", - "Get command line interface version; gri", - "List Current NMEA outputs; gno", - "Enable NMEA messages; sno, Stream1, COM1, GGA+GSA+GLL+GSV+RMC+VTG, sec1", - "Disable NMEA messages; sno, Stream1, none, none, off", - "Enable Group stream; ssgp, Group1, MeasEpoch+PVTCartesian+DOP; sso, Stream2, COM1, Group1, sec1", - "Disable Group stream; sso, Stream2, none, none, off", - "Output next Measurement Epoch; esoc, COM1, MeasEpoch", - "Enable PVTGeod stream; sso, Stream2, COM1, PVTGeod, sec1", - "Enable Status stream; sso, Stream2, COM1, Status, sec1", - "Enable RTCM3 messages on COM2 in base station mode;sr3o, COM2, RTCM1001+RTCM1002+RTCM1005+RTCM1006; sdio, COM2, , RTCMv3", - "List Known Antenna Phase Centres; lai, Overview", - "Turn ethernet interface on; seth, on", - "Set Fixed Base Station Mode;setDataInOut,COM1, ,RTCMv3;setRTCMv3Formatting,1234;setStaticPosGeodetic,Geodetic1,37.23345,-115.81513,15;setPVTMode,Static, ,Geodetic1", - "Set Survey-In Base Station Mode;setDataInOut,COM1, ,RTCMv3;setRTCMv3Formatting,1234;setPVTMode,Static, ,auto", - "Stop RTCM output;setDataInOut,COM1, ,none", - "Soft Reset to Factory Defaults;erst,soft,config", - "List antenna information; lai, Overview", - "List specific antenna information; lai, 'AERAT2775_159 SPKE'", - "List command help; help, Overview", - "Help getReceiverCapabilities; help, getReceiverCapabilities", - "Help getReceiverCapabilities; help, grc", - "Description; smp, TestMarker", - "List configuration file; lcf, Current", - "Execute copy configuration file; eccf, Current, Boot", - "Execute copy configuration file; eccf, User1, Current", - "Set ethernet mode; seth, on", - "Execute FTP upgrade; efup, myftp.com, /tst.suf, user, password", - "Set GPIO functionality; sgpf, GP2, Output, , LevelHigh", - "List internal file; lif, Permissions", - "Set LED mode; slm, DIFFCORLED", - "List MIB description generic; lmd, Overview", - "List MIB description getReceiverCapabilities; lmd, grc", - "Get receiver capabilities; grc", - "Get receiver interface; gri", - "Execute registered applications; era, com1, MyApp", - "Execute reset receiver; erst, soft, none", - "Set USB internet access; suia, on", - "Execute power mode; epwm, Standby", - "List current user; lcu", + 'ttypresets_l': [ + "Initialise Command Mode; SSSSSSSSSS", + "List Antenna Info; lai, Overview", + "List Antenna Info; lai, 'AERAT2775_159 SPKE'", + "List Command Help; help, Overview", + "List Command Help; help, getReceiverCapabilities", + "List Command Help; help, grc", + "Set Marker Parameters; smp, TestMarker", + "List Config File; lcf, Current", + "Execute Copy Config File; eccf, Current, Boot", + "Execute Copy Config File; eccf, User1, Current", + "Set Ethernet Mode; seth, on", + "Execute FTP Upgrade; efup, myftp.com, /tst.suf, user, password", + "Set GPIO Functionality; sgpf, GP2, Output, , LevelHigh", + "List Internal File; lif, Permissions", + "Set LED Mode; slm, DIFFCORLED", + "List MIB Description; lmd, Overview", + "List MIB Description; lmd, grc", + "Get Receiver Capabilities; grc", + "Get Receiver Interface; gri", + "Execute Registered Applications; era, com1, MyApp", + "Execute Reset Receiver; erst, soft, none", + "Set USB Internet Access; suia, on", + "Execute Power Mode; epwm, Standby", + "List Current User; lcu", + "login; login,admin, admin", + "List Current User; lcu", + "Set Default Access Level; sdal, User, none, User, none, User", "Login; login, admin, admin", + "Set SBF Output; sso, Stream1, COM1, MeasEpoch, sec1", + "Set SBF Output; sso, Stream1, COM1, PVTCartesian, sec1", "Logout; logout", - "Set default access level; sdal, User, none, User, none, User", - "Get user access level; sua1, User3, Mildred, mypwd, Viewer, AAAAE2VjZHa9YSdPMw", - "Set calibration common delay; scco, 65.4", - "Set calibration signal delay; scsi, GPSL5+GALE5a, -2.56", - "Set channel allocation; sca, Ch05, G01", - "Get channel allocation; gca, Ch05", - "Set CN0 mask; scm, GEOL1, 30", - "Get CN0 mask; gcm, GEOL1", - "Set multipath mitigation; smm, on, off", - "Get multipath mitigation; gmm", - "Set satellite tracking GPS; sst, GPS", - "Set satellite tracking all SBAS; sst, +SBAS", - "Set satellite tracking remove SBAS S120; sst, -S120", - "Set signal tracking; snt, GPSL1CA+GEOL1", - "Get signal tracking; gnt", - "Set smoothing interval; ssi, GPSL1CA, 300", - "Get smoothing interval; gsi, GPSL1CA", - "Set tracking loop parameters; stlp, GPSL1CA, 0.20, 12, , , off", - "Get tracking loop parameters; gtlp, GPSL1CA", - "Set AGC mode; sam, all, manual, 30", - "Get AGC mode; gam, all", - "Set baseband sampling mode; sbbs, BeforeIM", - "Get baseband sampling mode; gbbs", - "Set notch filtering; snf, Notch1, manual, 1227.0, 30", - "Get notch filtering; gnf, Notch1", - "Set wideband interference mitigation; swbi, on", - "Get wideband interference mitigation; gwbi", - "Set antenna offset; sao, Main, 0.1, 0.0, 1.3, 'AERAT2775_159 SPKE', 5684, 0", - "Get antenna offset; gao, Main", - "Set differential correction max age; sdca, 10", - "Get differential correction max age; gdca", - "Set differential correction usage; sdcu, LowLatency, 5.0, manual, 1011, off, 1, 10000", - "Get differential correction usage; gdcu", - "Set elevation mask; sem, PVT, 15", - "Get elevation mask; gem, PVT", - "Set geoid undulation; sgu, manual, 25.3", - "Get geoid undulation; ggu", - "Set health mask; shm, Tracking, off", - "Get health mask; ghm", - "Set ionosphere model; sim, off", - "Get ionosphere model; gim", - "Set magnetic variance; smv, manual, 1.1", - "Get magnetic variance; gmv", - "Set network RTK config; snrc, VRS", - "Get network RTK config; gnrc", - "Set PVT mode rover; spm, Rover, StandAlone+RTK", - "Get PVT mode; gpm", - "Set static position geodesic; sspg, Geodetic1, 50.5209, 4.4245, 113.3", - "Set PVT mode static; spm, Static, , Geodetic1", - "Set RAIM levels; srl, on, -4, -4, -6", - "Get RAIM levels; grl", - "Set RAIM off; srl, off", - "Set receiver dynamics; srd, High, Automotive", - "Get receiver dynamics; grd", - "Execute receiver nav filter; ernf, PVT", - "Get receiver nav filter; grnf", - "Set satellite usage GPS; ssu, GPS", - "Get satellite usage; gsu", - "Set satellite usage all SBAS; ssu, +SBAS", - "Set satellite usage remove SBAS S120; ssu, -S120", - "Set SBAS corrections; ssbc, S122, Test", - "Get SBAS corrections; gsbc", - "Set signal usage; snu, GPSL1CA, GPSL1CA", - "Get signal usage; gnu", - "Set static position cartesian; sspc, Cartesian1, 4019952.028, 331452.954, 4924307.458", - "Get static position cartesian; gspc, Cartesian1", + "Set User Access Level; sual, User3, Mildred, mypwd, Viewer, AAAAE2VjZH ...", + "Set Calibration Common Delay; scco, 65.4", + "Set Calibration Signal Delay; scsi, GPSL5+GALE5a, -2.56", + "Set Channel Allocation; sca, Ch05, G01", + "Get Channel Allocation; gca, Ch05", + "Set CN0 Mask; scm, GEOL1, 30", + "Get CN0 Mask; gcm, GEOL1", + "Set Multipath Mitigation; smm, on, off", + "Get Multipath Mitigation; gmm", + "Set Satellite Tracking; sst, GPS", + "Set Satellite Tracking; sst, +SBAS", + "Set Satellite Tracking; sst, -S120", + "Set Signal Tracking; snt, GPSL1CA+GEOL1", + "Get Signal Tracking; gnt", + "Set Smoothing Interval; ssi, GPSL1CA, 300", + "Get Smoothing Interval; gsi, GPSL1CA", + "Set Tracking Loop Parameters; stlp, GPSL1CA, 0.20, 12, , , off", + "Set AGC Mode; sam, all, manual, 30", + "Set BB Sampling Mode; sbbs, BeforeIM", + "Set Notch Filtering; snf, Notch1, manual, 1227.0, 30", + "Set WBI Mitigation; swbi, on", + "Set Antenna Offset; sao, Main, 0.1, 0.0, 1.3, 'AERAT2775_159 SPKE', 5684, 0", + "Set Differential Correction Max Age; sdca, 10", + "Set Differential Correction Usage; sdcu, LowLatency, 5.0, manual, 1011, off, 1, 10000", + "Set Elevation Mask; sem, PVT, 15", + "Set Geoid Undulation; sgu, manual, 25.3", + "Get Geoid Undulation; ggu", + "Set Health Mask; shm, Tracking, off", + "Get Health Mask; ghm", + "Set Ionosphere Model; sim, off", + "Get Ionosphere Model; gim", + "Set Magnetic Variance; smv, manual, 1.1", + "Get Magnetic Variance; gmv", + "Set Network RTK Config; snrc, VRS", + "Set PVT Mode; spm, Rover, StandAlone+RTK", + "Set Static Pos Geodetic; sspg, Geodetic1, 50.5209, 4.4245, 113.3", + "Set PVT Mode; spm, Static, , Geodetic1", + "Set RAIM Levels; srl, on, -4, -4, -6", + "Set RAIM Levels; srl, off", + "Set Receiver Dynamics; srd, High, Automotive", + "Execute Reset Nav Filter; ernf, PVT", + "Set Satellite Usage; ssu, GPS", + "Set Satellite Usage; ssu, +SBAS", + "Set Satellite Usage; ssu, -S120", + "Set SBAS Corrections; ssbc, S122, Test", + "Set Signal Usage; snu, GPSL1CA, GPSL1CA", + "Set Static Pos Cartesian; sspc, Cartesian1, 4019952.028, 331452.954, 4924307.458", + "Set PVT Mode; spm, Static, , Cartesian1", + "Set Static Pos Geodetic; sspg, Geodetic1, 50.86696443, 4.71347657, 114.880", + "Set PVT Mode; spm, Static, , Geodetic1", + "Set Troposphere Model; stm, MOPS, MOPS", + "Get Troposphere Model; gtm", + "Set Troposphere Parameters; stp, 25, 1013, 60", + "List Gal OSNMA Public Keys; lopk", + "Set Galileo OSNMA Public Keys; sopk, Key2,", + "Set Galileo OSNMA Usage; sou, strict", + "Set Galileo OSNMA Usage; sou, loose,", + "Set Antenna Location; sal, Base, manual, 0, -1, 0.1", + "Set Attitude Offset; sto, 93.2, -0.4", + "Set GNSS Attitude; sga, MovingBase", + "Set Geodetic Datum; sgd, ETRS89", + "Set User Datum; sud, User1, 52.1, 49.3, -58.5, 0.891, 5.390, -8.712,", + "Set User Datum Vel; sudv, User1, 0.1, 0.1, -1.8, 0.081, 0.49, -0.792, 0.08,", + "Set User Ellipsoid; sue, User1, 6378388, 297", + "Set ENH Transform Horizontal; smth, lt1, 10.904, 10.904, 156.341, 1.3, 1.34, 1.34, 1.34,", + "Set ENH Transform Vertical; smtv, lt1, 10.904, 156.341, 1.3, 1.34, 1.34", + "Set Local Coord Operation; slco, NONE, lt1", + "List Local Coord Operations; llc, Overview", + "Set Clock Sync Threshold; scst, msec1, off", + "Set Event Parameters; sep, EventA, High2Low, 10", + "Set Ntp Client; snc, on, pool.ntp.org", + "Set NTP Server; sntp, on", + "Set PPS Parameters; spps, sec1, Low2High, 23.40, GPS, 60, 0.1", + "Set PTP Server; sptp, on", + "Set Time Sync Source; stss, EventB", + "Set Timing System; sts, GPS", + "Set Marker Parameters; smp, Test, 356, GEODETIC, TST1, 0, 0, BEL", + "Set Observer Comment; soc, 'Data taken with choke ring antenna'", + "Get Observer Comment; goc", + "Set Observer Parameters; sop, TestObserver, TestAgency", + "Get Observer Parameters; gop", + "Set Check Internet Availability; scia, on", + "Set COM Settings; scs, COM1, baud19200, bits8, No, bit1, RTS|CTS", + "Set Cross Domain Web Access; scda, on", + "Set Daisy Chain Mode; sdcm, DC1, ASCII", + "Set Data In Out; sdio, COM1, CMD", + "Set Data In Out; sdio, COM2, DC1, DC2", + "Set Data In Out; sdio, COM3, DC2, DC1", + "Set Dynamic DNS; sdds, dyndns.org, Bart, MyPwd, rx1.dyndns-free.com, auto", + "Execute Echo Message; eecm, COM2, 'A:Hello world!', none", + "Execute Echo Message; eecm, COM2, 'H:48 65 6C 6C 6F 20 77 6F 72 6C 64 21', none", + "Set Https Settings; shs, HTTP", + "Set IP Filtering; sipf, on, 192.168.0.7 192.168.2.0/24", + "Set IP Keep Alive; sipk, on, 20, 20, 20", + "Set IP Port Settings; sipp, 12345, 21", + "Set IP Receive Settings; sirs, IPR1, 28785, TCP2Way, 192.168.10.5", + "Set IP Server Settings; siss, IPS1, 28785, UDP, 255.255.255.255", + "Set IP Settings; sips, Static, 192.168.1.123, 255.255.252.0, 192.168.1.255,", + "Set Periodic Echo; spe, COM2, 'A:Hello!''CR''LF', sec60", + "Set Periodic Echo; spe, COM2, 'H:48 65 6C 6C 6F 21 0D 0A', sec60", + "Set Periodic Echo; spe, COM2, 'A:Hello!''CR''LF', once", + "Execute Copy Config File; eccf, Current, Boot", + "Set Point To Point; sp2p, P2PP1, Off, COM1, 255.255.255.255, 255.255.255.255,", + "Set Port Firewall; spfw, Ethernet, PortList, '21 80 28784'", + "Set NTRIP Caster Mount Points; snmp, MP1, on, MyMP, Yes, MyUser, MyPwd, basic", + "Set NTRIP Caster MP Format; smpf, MP1, manual, RAW''CMNMEA, 'SBF (1s)''CM NMEA (5s)'", + "Set NTRIP Caster Settings; sncs, on, 2101, default, 2102", + "Set NTRIP Caster Users; sncu, User1, MyUser, MyPwd, all, 1", + "Set NTRIP Settings; snts, NTR1, Client, ntrip.com, 2101, USER, PWD, MP1, v2,", + "List NTRIP Source Table; lnst, ntripcaster", + "Set NTRIP Tls Settings; sntt, NTR1, on, ''", + "Set NTRIP Tls Settings; sntt, NTR1, on, Aa:Bb:56:78:90:12: ... 78:90:12:34", + "Set NTRIP Tls Settings; sntt, NTR1, on, 'Aa Bb 56 78 90 12 ... 78 90 12 34'", + "Set NTRIP Tls Settings; sntt, NTR1, on, AaBb56789012 ... 78901234", + "Execute NMEA Once; enoc, COM1, GGA", + "Set NMEA Output; sno, Stream1, COM1, GGA, sec1", + "Set NMEA Output; sno, Stream2, COM1, RMC, msec100", + "Get NMEA Output; gno", + "Set NMEA Precision; snp, 2, Mode2, off, 0.05", + "Set NMEA Talker ID; snti, GP", + "Set NMEA Version; snv, v4x", + "Set Meas3 Max Ref Interval; smrf, OnlyRef", + "Set SBF Groups; ssgp, Group1, MeasEpoch+PVTCartesian+DOP", + "Set SBF Output; sso, Stream1, COM1, Group1, sec1", + "Execute SBF Once; esoc, COM1, MeasEpoch", + "Set SBF Output; sso, Stream1, COM1, MeasEpoch, msec100", + "Set SBF Output; sso, Stream2, COM1, PVTGeodetic, sec1", + "Set RTCMv2 Compatibility; sr2c, , Tb", + "Set RTCMv2 Ephemeris Holdoff; sr2h, 60, 0", + "Set RTCMv2 Formatting; sr2f, 345", + "Get RTCMv2 Formatting; gr2f", + "Set RTCMv2 Interval; sr2i, RTCM22, 15", + "Get RTCMv2 Interval; gr2i", + "Set RTCMv2 Interval Obs; sr2b, RTCM20|21, 2", + "Get RTCMv2 Interval Obs; gr2b", + "Set RTCMv2 Message16; sr2m, Hello", + "Set RTCMv2 Output; sr2o, COM2, RTCM16", + "Set Data In Out; sdio, COM2, , RTCMv2", + "Set RTCMv2 Output; sr2o, COM2, RTCM3+RTCM18|19+RTCM22", + "Set Data In Out; sdio, COM2, , RTCMv2", + "Set RTCMv2 Usage; sr2u, RTCM1+RTCM3", + "Set Differential Correction Usage; sdcu, , , manual, 1011", + "Set RTCMv3 CRS Transform; sr3t, manual, '4258'", + "Set RTCMv3 Interval; sr3i, RTCM1001|2, 10", + "Set RTCMv3 Delay; sr3d, 2", + "Set RTCMv3 Formatting; sr3f, 345,", + "Set RTCMv3 Interval; sr3i, RTCM1001|2, 2", + "Set RTCMv3 Message1029; sr3m, Hello", + "Set RTCMv3 Output; sr3o, COM2, RTCM1029", + "Set Data In Out; sdio, COM2, , RTCMv3", + "Set RTCMv3 Output; sr3o, COM2, RTCM1001+RTCM1002+RTCM1005+RTCM1006", + "Set Data In Out; sdio, COM2, , RTCMv3", + "Set RTCMv3 Usage; sr3u, RTCM1001+RTCM1002", + "Set Differential Correction Usage; sdcu, , , manual, 1011", + "Set CMRv2 Formatting; sc2f, 12", + "Get CMRv2 Formatting; gc2f", + "Set CMRv2 Interval; sc2i, CMR0, 2", + "Get CMRv2 Interval; gc2i", + "Set CMRv2 Message2; sc2m, Hello", + "Set CMRv2 Output; sc2o, COM2, CMR2", + "Set Data In Out; sdio, COM2, , CMRv2", + "Set CMRv2 Output; sc2o, COM2, CMR0", + "Set Data In Out; sdio, COM2, , CMRv2", + "Set CMRv2 Usage; sc2u, CMR0", + "Set Differential Correction Usage; sdcu, , , manual, 12", + "Set Disk Full Action; sdfa, DSK1, StopLogging", + "Get Disk Full Action; gdfa", + "List Disk Info; ldi, DSK1", + "Set File Naming; sfn, DSK1, FileName, mytest", + "Set Global File Naming Options; sfno, on", + "Execute Manage Disk; emd, DSK1, Format", + "List Recorded File; lrf, DSK1, log.sbf", + "List Recorded File; lrf, DSK1, log.sbf", + "Execute Remove File; erf, DSK1, 03298/ATRX2980.03_", + "Execute Remove File; erf, DSK1, all", + "Set RINEX Logging; srxl, DSK1, hour24, sec30, GPSL1CA", + "Set UMSD On Connect; suoc, off", + "Set FTP Push RINEX; sfpr, ftp.mydomain.com, mydata/'Y'm'd, myname, mypwd", + "Set FTP Push SBF; sfps, ftp.mydomain.com, mydata/'Y'm'd, myname, mypwd", + "Execute FTP Push Test; efpt, myftp.com, mydata/'Y'm'd, myname, mypwd", + "List L Band Beams; llbb", + "Set L Band Beams; slbb, User1, 1537460000, baud1200, 25East, E, Enabled", + "Set L Band Custom Service ID; slcs, A5A5, 0101, on", + "Set L Band NTRIP Delivery; slnd, NTR1", + "Set L Band Select Mode; slsm, manual, LBAS2, User1, User2", ], } @@ -145,37 +247,37 @@ # Full details in http://www.feymani.com/en/uploadfile/2023/1008/20231008072903360.pdf # ****************************************** { - "ttypresets_l": [ - "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", - "System reset CONFIRM; AT+SYSTEM_RESET", - "Save the parameters CONFIRM AT+SAVE_ALL", - "Update module firmware, see attachment for protocols; AT+UPDATE_APP", - "Update Bootloader, see attachment for protocols; AT+UPDATE_BOOT", - "Set the GNSS RTK receiver type; AT+GNSS_CARD=OEM", - "Read parameters (SYSTEM/ALL); AT+READ_PARA=SYSTEM/ALL", - "Loading default parameters; AT+LOAD_DEFAULT", - "Installation angle estimation in tilt measurement applications; AT+AUTO_FIX=ENABLE/DISABLE", - "Set the RTK pole vector to map the position to the end of the RTK pole; AT+CLUB_VECTOR=X,Y,Z", - "Binary NAVI positioning output; AT+NAVI_OUTPUT=UART1,ON/OFF", - "Ascii type NAVI positioning output; AT+NASC_OUTPUT=UART1,ON/OFF", - "MEMS raw output; AT+MEMS_OUTPUT=UART1,ON/OFF", - "GNSS raw output; AT+GNSS_OUTPUT=UART1,ON/OFF", - "Set the lever arm; AT+LEVER_ARM=X,Y,Z", - "Query whether time is synchronized between MEMS and GNSS; AT+CHECK_SYNC", - "High-rate mode setting; AT+HIGH_RATE=ENABLE/DISABLE", - "Module activation; AT+ACTIVATE_KEY=KEY", - "Set the initial alignment speed threshold; AT+ALIGN_VEL=1.0", - "Query the Firmware version; AT+VERSION", - "Set GNSS serial port; AT+GNSS_PORT=PHYSICAL_UART2", - "Set the module working mode; AT+WORK_MODE=X", - "Set the module installation angle; AT+INSTALL_ANGLE=X,Y,Z", - "Query the serial port number; AT+THIS_PORT", - "Causes the filter to enter or exit stop mode; AT+FILTER_STOP=ENABLE/DISABLE", - "UART n enters or exits the loopback mode; AT+LOOP_BACK=UARTn/NONE", - "Filter Reset; AT+FILTER_RESET", - "Check firmware CRC, N=firmware size; AT+CHECK_CRC=N", - "Turn on or off RTK pole length compensation; AT+CORRECT_HOLDER=ENABLE/DISABLE", - "Disable the output of all messages over the serial port x; AT+DISABLE_OUTPUT=UARTx", - "Factory calibration command; AT+CALIBRATE_MODE2=STEP1/STEP2", + 'ttypresets_l': [ + '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', + 'System reset CONFIRM; AT+SYSTEM_RESET', + 'Save the parameters CONFIRM AT+SAVE_ALL', + 'Update module firmware, see attachment for protocols; AT+UPDATE_APP', + 'Update Bootloader, see attachment for protocols; AT+UPDATE_BOOT', + 'Set the GNSS RTK receiver type; AT+GNSS_CARD=OEM', + 'Read parameters (SYSTEM/ALL); AT+READ_PARA=SYSTEM/ALL', + 'Loading default parameters; AT+LOAD_DEFAULT', + 'Installation angle estimation in tilt measurement applications; AT+AUTO_FIX=ENABLE/DISABLE', + 'Set the RTK pole vector to map the position to the end of the RTK pole; AT+CLUB_VECTOR=X,Y,Z', + 'Binary NAVI positioning output; AT+NAVI_OUTPUT=UART1,ON/OFF', + 'Ascii type NAVI positioning output; AT+NASC_OUTPUT=UART1,ON/OFF', + 'MEMS raw output; AT+MEMS_OUTPUT=UART1,ON/OFF', + 'GNSS raw output; AT+GNSS_OUTPUT=UART1,ON/OFF', + 'Set the lever arm; AT+LEVER_ARM=X,Y,Z', + 'Query whether time is synchronized between MEMS and GNSS; AT+CHECK_SYNC', + 'High-rate mode setting; AT+HIGH_RATE=ENABLE/DISABLE', + 'Module activation; AT+ACTIVATE_KEY=KEY', + 'Set the initial alignment speed threshold; AT+ALIGN_VEL=1.0', + 'Query the Firmware version; AT+VERSION', + 'Set GNSS serial port; AT+GNSS_PORT=PHYSICAL_UART2', + 'Set the module working mode; AT+WORK_MODE=X', + 'Set the module installation angle; AT+INSTALL_ANGLE=X,Y,Z', + 'Query the serial port number; AT+THIS_PORT', + 'Causes the filter to enter or exit stop mode; AT+FILTER_STOP=ENABLE/DISABLE', + 'UART n enters or exits the loopback mode; AT+LOOP_BACK=UARTn/NONE', + 'Filter Reset; AT+FILTER_RESET', + 'Check firmware CRC, N=firmware size; AT+CHECK_CRC=N', + 'Turn on or off RTK pole length compensation; AT+CORRECT_HOLDER=ENABLE/DISABLE', + 'Disable the output of all messages over the serial port x; AT+DISABLE_OUTPUT=UARTx', + 'Factory calibration command; AT+CALIBRATE_MODE2=STEP1/STEP2', ], } diff --git a/pygpsclient.json b/pygpsclient.json index addf4605..c53de291 100644 --- a/pygpsclient.json +++ b/pygpsclient.json @@ -108,6 +108,7 @@ "lbandclientxonxoff_b": 0, "lbandclienttimeout_f": 0.1, "lbandclientmsgmode_n": 0, + "lband_enabled_b": 0, "spartnport_s": "<== YOUR USER_DEFINED D9S SERIAL PORT ==>", "lbandclientfreq_n": 1556290000, "lbandclientschwin_n": 2200, @@ -261,14 +262,16 @@ "MOSAIC X5 Disable Group stream; sso, Stream2, none, none, off", "MOSAIC X5 Output next Measurement Epoch; esoc, COM1, MeasEpoch", "MOSAIC X5 Enable PVTGeod stream; sso, Stream2, COM1, PVTGeod, sec1", - "MOSAIC X5 Enable Status stream; sso, Stream2, COM1, Status, sec1", + "MOSAIC X5 Disable PVTGeod stream; sso, Stream2, COM1, none, none", + "MOSAIC X5 Enable Status stream; sso, Stream3, COM1, Status, sec1", + "MOSAIC X5 Disable Status stream; sso, Stream3, COM1, none, none", "MOSAIC X5 Set Fixed Base Station Mode; setDataInOut,COM1, ,RTCMv3;setRTCMv3Formatting,1234;setStaticPosGeodetic,Geodetic1,37.23345,-115.81513,15;setPVTMode,Static, ,Geodetic1", "MOSAIC X5 Set Survey-In Base Station Mode;setDataInOut,COM1, ,RTCMv3;setRTCMv3Formatting,1234;setPVTMode,Static, ,auto", "MOSAIC X5 Stop RTCM output;setDataInOut,COM1, ,none", "MOSAIC X5 Soft Reset to Factory Defaults;erst,soft,config", "IM19 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", "IM19 System reset CONFIRM; AT+SYSTEM_RESET", - "IM19 Save the parameters CONFIRM AT+SAVE_ALL", + "IM19 Save the parameters; CONFIRM AT+SAVE_ALL", "IM19 Update module firmware, see attachment for protocols; AT+UPDATE_APP", "IM19 Update Bootloader, see attachment for protocols; AT+UPDATE_BOOT", "IM19 Set the GNSS RTK receiver type; AT+GNSS_CARD=OEM", diff --git a/pyproject.toml b/pyproject.toml index ffd5e57c..92dee284 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,7 @@ dynamic = ["version"] authors = [{ name = "semuadmin", email = "semuadmin@semuconsulting.com" }] maintainers = [{ name = "semuadmin", email = "semuadmin@semuconsulting.com" }] description = "GNSS Diagnostic and UBX Configuration GUI Application" +license = "BSD-3-Clause" license-files = ["LICENSE"] keywords = [ "PyGPSClient", @@ -48,15 +49,7 @@ classifiers = [ "Topic :: Scientific/Engineering :: GIS", ] -dependencies = [ - "requests>=2.28.0", - "Pillow>=9.0.0", - "pygnssutils>=1.1.14", - "pyubx2>=1.2.52", - "pyserial>=3.5", - "pyubxutils>=1.0.3", - "pysbf2>=0.2.0", -] +dependencies = ["requests>=2.28.0", "Pillow>=9.0.0", "pygnssutils>=1.1.16"] [project.scripts] pygpsclient = "pygpsclient.__main__:main" @@ -67,8 +60,8 @@ documentation = "https://www.semuconsulting.com/pygpsclient/" repository = "https://github.com/semuconsulting/PyGPSClient" changelog = "https://github.com/semuconsulting/PyGPSClient/blob/master/RELEASE_NOTES.md" -[project.optional-dependencies] -deploy = [ +[dependency-groups] +build = [ "build", "packaging>=24.2", "pip", @@ -87,6 +80,7 @@ test = [ "Sphinx", "sphinx-rtd-theme", ] +deploy = [{ include-group = "build" }, { include-group = "test" }] [tool.setuptools.dynamic] version = { attr = "pygpsclient._version.__version__" } @@ -144,3 +138,12 @@ testpaths = ["tests"] [tool.coverage.run] source = ["src"] + +[tool.coverage.paths] +source = ["src"] + +[tool.coverage.report] +fail_under = 15 + +[tool.coverage.html] +directory = "htmlcov" diff --git a/src/pygpsclient/_version.py b/src/pygpsclient/_version.py index b3bad705..c95ac125 100644 --- a/src/pygpsclient/_version.py +++ b/src/pygpsclient/_version.py @@ -8,4 +8,4 @@ :license: BSD 3-Clause """ -__version__ = "1.5.10" +__version__ = "1.5.11" diff --git a/src/pygpsclient/configuration.py b/src/pygpsclient/configuration.py index 24acb567..9d80c4f9 100644 --- a/src/pygpsclient/configuration.py +++ b/src/pygpsclient/configuration.py @@ -171,6 +171,7 @@ def __init__(self, app): "mqttclienttlscrt_s": "<=== FULLY QUALIFIED PATH TO MQTT CRT FILE ===>", "mqttclienttlskey_s": "<=== FULLY QUALIFIED PATH TO MQTT KEY FILE ===>", # SPARTN L-Band client settings from SpartnLbandDialog if open + "lband_enabled_b": 0, # PointPerfect SPARTN L-Band service has been discontiued "spartnport_s": "", "spartndecode_b": 0, "spartnkey_s": SPARTN_DEFAULT_KEY, diff --git a/src/pygpsclient/dynamic_config_frame.py b/src/pygpsclient/dynamic_config_frame.py index bea4cab1..da595228 100644 --- a/src/pygpsclient/dynamic_config_frame.py +++ b/src/pygpsclient/dynamic_config_frame.py @@ -140,7 +140,7 @@ def __init__(self, app, container, *args, **kwargs): self.logger = logging.getLogger(__name__) self._protocol = kwargs.pop("protocol", "UBX") - Frame.__init__(self, self.__container.container, *args, **kwargs) + super().__init__(container.container, *args, **kwargs) self._img_send = ImageTk.PhotoImage(Image.open(ICON_SEND)) self._img_pending = ImageTk.PhotoImage(Image.open(ICON_PENDING)) @@ -171,7 +171,7 @@ def _body(self): self, border=2, relief="sunken", - height=15, + height=10, justify=LEFT, exportselection=False, ) diff --git a/src/pygpsclient/globals.py b/src/pygpsclient/globals.py index 18333453..5039f918 100644 --- a/src/pygpsclient/globals.py +++ b/src/pygpsclient/globals.py @@ -93,7 +93,7 @@ def create_circle(self, x, y, r, **kwargs): DMM = "DM.M" DMS = "D.M.S" ECEF = "ECEF" -ENABLE_CFG_OTHER = True # enable CFG=* Other Configuration command panel +ENABLE_CFG_LEGACY = True # enable CFG=* Other Configuration command panel ERRCOL = "salmon" # default invalid data entry field background color ERROR = "ERR!" FGCOL = "white" # default widget foreground color @@ -214,6 +214,8 @@ def create_circle(self, x, y, r, **kwargs): MAPAPI_URL = "https://developer.mapquest.com/user/login/sign-up" MQTTIPMODE = 0 MQTTLBANDMODE = 1 +MINHEIGHT = 600 +MINWIDTH = 800 CUSTOM = "custom" MAP = "map" SAT = "sat" @@ -249,6 +251,7 @@ def create_circle(self, x, y, r, **kwargs): RXMMSG = "RXM-SPARTN-KEY" SAT_EXPIRY = 10 # how long passed satellites are kept in the sky and graph view SBF_PROTOCOL = 64 +SCREENSCALE = 0.8 # screen resolution scaling factor SOCK_NTRIP = "NTRIP CASTER" SOCK_SERVER = "SOCKET SERVER" SOCKCLIENT_HOST = "localhost" diff --git a/src/pygpsclient/gpx_dialog.py b/src/pygpsclient/gpx_dialog.py index 103c71a3..2ea2a85c 100644 --- a/src/pygpsclient/gpx_dialog.py +++ b/src/pygpsclient/gpx_dialog.py @@ -18,11 +18,9 @@ from io import BytesIO from tkinter import ( ALL, - BOTH, CENTER, DISABLED, NW, - YES, Button, Canvas, E, @@ -33,7 +31,6 @@ S, Spinbox, StringVar, - Toplevel, W, font, ) @@ -49,11 +46,6 @@ CUSTOM, ERRCOL, HOME, - ICON_END, - ICON_EXIT, - ICON_LOAD, - ICON_REDRAW, - ICON_START, IMG_WORLD_CALIB, INFOCOL, KM2M, @@ -64,7 +56,6 @@ KPH2MPS, M2FT, MAP, - POPUP_TRANSIENT, READONLY, RPTDELAY, SAT, @@ -80,13 +71,12 @@ DLGGPXERROR, DLGGPXLOAD, DLGGPXNULL, - DLGGPXPROMPT, - DLGGPXVIEWER, DLGTGPX, MAPCONFIGERR, MAPOPENERR, OUTOFBOUNDS, ) +from pygpsclient.toplevel_dialog import ToplevelDialog # profile chart parameters: AXIS_XL = 35 # x axis left offset @@ -97,9 +87,10 @@ SPD_COL = INFOCOL # color of speed plot TRK_COL = "magenta" # color of track MD_LINES = 2 # number of lines of metadata +MINDIM = (567, 467) -class GPXViewerDialog(Toplevel): +class GPXViewerDialog(ToplevelDialog): """GPXViewerDialog class.""" def __init__(self, app, *args, **kwargs): @@ -108,24 +99,12 @@ def __init__(self, app, *args, **kwargs): self.__app = app self.logger = logging.getLogger(__name__) # self.__master = self.__app.appmaster # link to root Tk window - Toplevel.__init__(self, app) - if POPUP_TRANSIENT: - self.transient(self.__app) - self.resizable(True, True) - self.title(DLGGPXVIEWER) # pylint: disable=E1102 - self.protocol("WM_DELETE_WINDOW", self.on_exit) - self._img_load = ImageTk.PhotoImage(Image.open(ICON_LOAD)) - self._img_redraw = ImageTk.PhotoImage(Image.open(ICON_REDRAW)) - self._img_exit = ImageTk.PhotoImage(Image.open(ICON_EXIT)) - self._img_start = ImageTk.PhotoImage(Image.open(ICON_START)) - self._img_end = ImageTk.PhotoImage(Image.open(ICON_END)) - self.width = int(kwargs.get("width", 600)) - self.height = int(kwargs.get("height", 600)) - self.mheight = int(self.height * 0.75) - self.mwidth = self.width - self.pheight = int(self.height * 0.25) + super().__init__(app, DLGTGPX, MINDIM) self._zoom = IntVar() self._maptype = StringVar() + self.mheight = int(self.height * 0.75) + self.mwidth = int(self.width * 0.93) + self.pheight = int(self.mheight * 0.25) zoom = int(kwargs.get("zoom", 12)) self._zoom.set(zoom) self._info = [] @@ -142,23 +121,28 @@ def __init__(self, app, *args, **kwargs): self._do_layout() self._attach_events() self._reset() - - self._do_mapalert(DLGGPXPROMPT) + self._finalise() def _body(self): """ Create widgets. """ - self._frm_map = Frame(self, borderwidth=2, relief="groove", bg=BGCOL) - self._frm_profile = Frame(self, borderwidth=2, relief="groove", bg=BGCOL) - self._frm_info = Frame(self, borderwidth=2, relief="groove") - self._frm_controls = Frame(self, borderwidth=2, relief="groove") + self._frm_body = Frame(self.container, borderwidth=2, relief="groove") + self._frm_map = Frame(self._frm_body, borderwidth=2, relief="groove", bg=BGCOL) + self._frm_profile = Frame( + self._frm_body, borderwidth=2, relief="groove", bg=BGCOL + ) + self._frm_info = Frame(self._frm_body, borderwidth=2, relief="groove") + self._frm_controls = Frame(self._frm_body, borderwidth=2, relief="groove") self._can_mapview = Canvas( - self._frm_map, width=self.width, height=self.mheight, bg=BGCOL + self._frm_map, height=self.mheight, width=self.mwidth, bg=BGCOL ) self._can_profile = Canvas( - self._frm_profile, width=self.width, height=self.pheight, bg="#f0f0e8" + self._frm_profile, + height=self.pheight, + width=self.mwidth, + bg="#f0f0e8", ) self._lbl_info = [] for i in range(MD_LINES): @@ -167,7 +151,7 @@ def _body(self): ) self._btn_load = Button( self._frm_controls, - image=self._img_load, + image=self.img_load, width=40, command=self._on_load, ) @@ -196,28 +180,23 @@ def _body(self): ) self._btn_redraw = Button( self._frm_controls, - image=self._img_redraw, + image=self.img_redraw, width=40, command=self._on_redraw, ) - self._btn_exit = Button( - self._frm_controls, - image=self._img_exit, - width=40, - command=self.on_exit, - ) def _do_layout(self): """ Arrange widgets. """ + self._frm_body.grid(column=0, row=0, sticky=(N, S, E, W)) self._frm_map.grid(column=0, row=0, sticky=(N, S, E, W)) self._frm_profile.grid(column=0, row=1, sticky=(W, E)) self._frm_info.grid(column=0, row=2, sticky=(W, E)) self._frm_controls.grid(column=0, row=3, columnspan=7, sticky=(W, E)) - self._can_mapview.pack(fill=BOTH, expand=YES) - self._can_profile.pack(fill=BOTH, expand=YES) + self._can_mapview.grid(column=0, row=0, sticky=(N, S, E, W)) + self._can_profile.grid(column=0, row=0, sticky=(N, S, E, W)) for i in range(MD_LINES): self._lbl_info[i].grid(column=0, row=i, padx=3, pady=1, sticky=(W, E)) self._btn_load.grid(column=0, row=1, padx=3, pady=3) @@ -251,19 +230,22 @@ def _do_layout(self): padx=3, pady=3, ) - self._btn_exit.grid(column=6, row=1, padx=3, pady=3, sticky=E) - self.grid_columnconfigure(0, weight=1) - self.grid_rowconfigure(0, weight=3) - self.grid_rowconfigure(1, weight=1) + self._frm_body.grid_columnconfigure(0, weight=10) + self._frm_body.grid_rowconfigure(0, weight=10) + self._frm_map.grid_columnconfigure(0, weight=10) + self._frm_map.grid_rowconfigure(0, weight=10) + self._frm_profile.grid_columnconfigure(0, weight=10) + self._frm_profile.grid_rowconfigure(0, weight=10) + self._frm_body.grid_rowconfigure(1, weight=2) def _attach_events(self): """ Bind events to window. """ - self.bind("", self._on_resize) self._maptype.trace_add("write", self._on_maptype) + # self.bind("", self._on_resize) def _reset(self): """ @@ -275,14 +257,6 @@ def _reset(self): for i in range(MD_LINES): self._info[i].set("") - def on_exit(self, *args, **kwargs): - """ - Handle Exit button press. - """ - - self.__app.stop_dialog(DLGTGPX) - self.destroy() - def _on_redraw(self, *args, **kwargs): """ Handle redraw button press. @@ -337,29 +311,6 @@ def _on_maptype(self, var, index, mode): self._can_mapview.unbind("") self._can_mapview.unbind("") - def get_size(self): - """ - Get current frame size. - - :return: window size (width, height) - :rtype: tuple - """ - - self.update_idletasks() # Make sure we know about any resizing - return self.winfo_width(), self.winfo_height() - - def _on_resize(self, event): - """ - Resize frame - - :param event event: resize event - """ - - self.width, self.height = self.get_size() - self.mheight = self._can_mapview.winfo_height() - self.mwidth = self._can_mapview.winfo_width() - self.pheight = self._can_profile.winfo_height() - 5 - def _open_gpxfile(self) -> str: """ Open gpx file. @@ -379,15 +330,15 @@ def _on_load(self): if self._gpxfile is None: # user cancelled return - self._do_mapalert(DLGGPXLOAD) + self.set_status(DLGGPXLOAD, INFOCOL) with open(self._gpxfile, "r", encoding="utf-8") as gpx: try: parser = minidom.parse(gpx) trkpts = parser.getElementsByTagName("trkpt") self._process_track(trkpts) - except (TypeError, AttributeError, expat.ExpatError) as err: - self._do_mapalert(f"{DLGGPXERROR}\n{repr(err)}") + except (TypeError, expat.ExpatError) as err: # AttributeError, + self.set_status(f"{DLGGPXERROR}\n{repr(err)}", ERRCOL) def _process_track(self, trkpts: list): """ @@ -398,7 +349,7 @@ def _process_track(self, trkpts: list): rng = len(trkpts) if rng == 0: - self._do_mapalert(DLGGPXNULL) + self.set_status(DLGGPXNULL, ERRCOL) return minlat = minlon = 400 @@ -500,7 +451,8 @@ def _draw_offline_map(self, track: list): 0, 0, image=self._mapimg, anchor=NW, tags="image" ) else: - self._do_mapalert(err) + self.set_status(err, ERRCOL) + return # draw track with start and end icons i = 0 @@ -530,6 +482,7 @@ def _draw_online_map(self, track: list): """ # pylint: disable=unused-variable + self.set_status("") if track in ({}, None): return @@ -539,8 +492,8 @@ def _draw_online_map(self, track: list): url = format_mapquest_request( mqapikey, self._maptype.get(), - self.width, - self.mheight, + int(self.width), + int(self.mheight), self._zoom.get(), locations, ) @@ -548,9 +501,12 @@ def _draw_online_map(self, track: list): response.raise_for_status() # raise Exception on HTTP error self._mapimg = ImageTk.PhotoImage(Image.open(BytesIO(response.content))) except (ConnError, ConnectTimeout, RequestException, HTTPError): - self._do_mapalert( - f"MAPQUEST API ERROR: HTTP code {response.status_code} " - + f"{responses[response.status_code]}\n\n{response.text}" + self.set_status( + ( + f"MAPQUEST API ERROR: HTTP code {response.status_code} " + f"{responses[response.status_code]}\n\n{response.text}" + ), + ERRCOL, ) self._can_mapview.create_image( @@ -766,21 +722,6 @@ def _get_units(self) -> tuple: return (dst_u, dst_c, ele_u, ele_c, spd_u, spd_c) - def _do_mapalert(self, msg: str): - """ - Display alert on map canvas. - """ - - # self._reset() - self._can_mapview.create_text( - self.width / 2, - self.mheight / 2, - text=msg, - fill=ERRCOL, - tags="alert", - ) - self.update_idletasks() - @property def metadata(self) -> dict: """ diff --git a/src/pygpsclient/hardware_info_frame.py b/src/pygpsclient/hardware_info_frame.py index 5f3dcde8..863c67f8 100644 --- a/src/pygpsclient/hardware_info_frame.py +++ b/src/pygpsclient/hardware_info_frame.py @@ -47,11 +47,10 @@ def __init__(self, app, container, *args, **kwargs): """ self.__app = app # Reference to main application class - self.__master = self.__app.appmaster # Reference to root class (Tk) self.__container = container self._protocol = kwargs.pop("protocol", "UBX") - Frame.__init__(self, self.__container.container, *args, **kwargs) + super().__init__(container.container, *args, **kwargs) self._img_send = ImageTk.PhotoImage(Image.open(ICON_SEND)) self._img_pending = ImageTk.PhotoImage(Image.open(ICON_PENDING)) @@ -99,7 +98,7 @@ def _do_layout(self): self.grid_columnconfigure(i, weight=1) for i in range(rows): self.grid_rowconfigure(i, weight=1) - self.option_add("*Font", self.__app.font_sm) + # self.option_add("*Font", self.__app.font_sm) def _attach_events(self): """ diff --git a/src/pygpsclient/helpers.py b/src/pygpsclient/helpers.py index 4dfa9ef8..41aebeaa 100644 --- a/src/pygpsclient/helpers.py +++ b/src/pygpsclient/helpers.py @@ -18,7 +18,7 @@ from math import asin, atan, atan2, cos, degrees, pi, radians, sin, sqrt, trunc from socket import AF_INET, SOCK_DGRAM, socket from time import strftime -from tkinter import Entry +from tkinter import Entry, Tk from tkinter.font import Font from pynmeagps import WGS84_SMAJ_AXIS, haversine @@ -41,6 +41,7 @@ MAX_SNR, PUBLICIP_URL, ROMVER_NEW, + SCREENSCALE, TIME0, Area, AreaXY, @@ -70,6 +71,40 @@ POINTLIMIT = 500 # max number of shape points supported by MapQuest API +def screenres(master: Tk, scale: float = SCREENSCALE) -> tuple: + """ + Get effective screen resolution. + + :param tkinter.Tk master: reference to root + :param float scale: screen scaling factor + :return: adjusted screen resolution in pixels (height, width) + :rtype: tuple + """ + + return (master.winfo_screenheight() * scale, master.winfo_screenwidth() * scale) + + +def check_lowres(master: Tk, dim: tuple) -> tuple: + """ + Check if dialog dimensions exceed effective screen resolution. + + :param tkinter.Tk master: reference to root + :param tuple dim: dialog dimensions in pixels (height, width) + :return: low resolution yes/no and effective resolution + :rtype: tuple (boolean, (screen height/width)) + """ + + sh, sw = screenres(master) + dh, dw = dim + if sh < dh or sw < dw: + maxh, maxw = sh, sw + lowres = True + else: + maxh, maxw = dh, dw + lowres = False + return lowres, (maxh, maxw) + + def cel2cart(elevation: float, azimuth: float) -> tuple: """ Convert celestial coordinates (degrees) to Cartesian coordinates. diff --git a/src/pygpsclient/importmap_dialog.py b/src/pygpsclient/importmap_dialog.py index cde9b05d..f259a8f8 100644 --- a/src/pygpsclient/importmap_dialog.py +++ b/src/pygpsclient/importmap_dialog.py @@ -28,7 +28,6 @@ N, S, StringVar, - Toplevel, W, ) @@ -46,13 +45,10 @@ BGCOL, ERRCOL, HOME, - ICON_EXIT, - ICON_LOAD, - ICON_SEND, INFOCOL, - POPUP_TRANSIENT, ) from pygpsclient.strings import DLGTIMPORTMAP +from pygpsclient.toplevel_dialog import ToplevelDialog # profile chart parameters: AXIS_XL = 35 # x axis left offset @@ -62,9 +58,10 @@ ELE_COL = "palegreen3" # color of elevation plot SPD_COL = INFOCOL # color of speed plot MD_LINES = 2 # number of lines of metadata +MINDIM = (456, 418) -class ImportMapDialog(Toplevel): +class ImportMapDialog(ToplevelDialog): """ImportMapDialog class.""" def __init__(self, app, *args, **kwargs): @@ -72,15 +69,7 @@ def __init__(self, app, *args, **kwargs): self.__app = app # self.__master = self.__app.appmaster # link to root Tk window - Toplevel.__init__(self, app) - if POPUP_TRANSIENT: - self.transient(self.__app) - self.resizable(True, True) - self.title(DLGTIMPORTMAP) # pylint: disable=E1102 - self.protocol("WM_DELETE_WINDOW", self.on_exit) - self._img_load = ImageTk.PhotoImage(Image.open(ICON_LOAD)) - self._img_send = ImageTk.PhotoImage(Image.open(ICON_SEND)) - self._img_exit = ImageTk.PhotoImage(Image.open(ICON_EXIT)) + super().__init__(app, DLGTIMPORTMAP, MINDIM) self.width = int(kwargs.get("width", 400)) self.height = int(kwargs.get("height", 400)) self.mheight = int(self.height * 0.75) @@ -97,26 +86,28 @@ def __init__(self, app, *args, **kwargs): self._do_layout() self._attach_events() self._reset() + self._finalise() def _body(self): """ Create widgets. """ - self._frm_map = Frame(self, borderwidth=2, relief="groove", bg=BGCOL) - self._frm_controls = Frame(self, borderwidth=2, relief="groove") + self._frm_body = Frame(self.container, borderwidth=2, relief="groove") + self._frm_map = Frame(self._frm_body, borderwidth=2, relief="groove", bg=BGCOL) + self._frm_controls = Frame(self._frm_body, borderwidth=2, relief="groove") self._canvas_map = Canvas( self._frm_map, width=self.width, height=self.mheight, bg=BGCOL ) self._btn_load = Button( self._frm_controls, - image=self._img_load, + image=self.img_load, width=40, command=self._on_load, ) self._btn_import = Button( self._frm_controls, - image=self._img_send, + image=self.img_send, width=40, command=self._on_import, ) @@ -138,26 +129,17 @@ def _body(self): self._ent_maxlon = Entry( self._frm_controls, width=10, textvariable=self._lonmax ) - self._btn_exit = Button( - self._frm_controls, - image=self._img_exit, - width=40, - command=self.on_exit, - ) - self._lbl_status = Label(self._frm_controls, text="", anchor=W) def _do_layout(self): """ Arrange widgets. """ - + self._frm_body.grid(column=0, row=0, sticky=(N, S, E, W)) self._frm_map.grid(column=0, row=0, sticky=(N, S, E, W)) self._frm_controls.grid(column=0, row=1, sticky=(W, E)) self._canvas_map.pack(fill=BOTH, expand=YES) self._btn_load.grid(column=0, row=0, padx=3, pady=3) self._btn_import.grid(column=1, row=0, padx=3, pady=3) - self._btn_exit.grid(column=4, row=0, padx=3, pady=3, sticky=E) - self._lbl_min.grid(column=0, row=1, padx=3, pady=3, sticky=W) self._lbl_minlat.grid(column=1, row=1, padx=3, pady=3, sticky=E) self._ent_minlat.grid(column=2, row=1, padx=3, pady=3) @@ -168,20 +150,13 @@ def _do_layout(self): self._ent_maxlat.grid(column=2, row=2, padx=3, pady=3) self._lbl_maxlon.grid(column=3, row=2, padx=3, pady=3, sticky=E) self._ent_maxlon.grid(column=4, row=2, padx=3, pady=3) - self._lbl_status.grid( - column=0, row=3, columnspan=5, padx=3, pady=3, sticky=(W, E) - ) - - self.grid_columnconfigure(0, weight=1) - self.grid_rowconfigure(0, weight=3) - self.grid_rowconfigure(1, weight=1) def _attach_events(self): """ Bind events to window. """ - self.bind("", self._on_resize) + # self.bind("", self._on_resize) def _reset(self): """ @@ -191,39 +166,12 @@ def _reset(self): self._canvas_map.delete(ALL) self._btn_import.config(state=DISABLED) if not HASRASTERIO: - self._show_status( - "Warning: rasterio library is not installed - bounds must be entered manually" + self.set_status( + "Warning: rasterio library is not installed - bounds must be entered manually", + INFOCOL, ) else: - self._show_status() - - def on_exit(self, *args, **kwargs): - """ - Handle Exit button press. - """ - - self.__app.stop_dialog(DLGTIMPORTMAP) - self.destroy() - - def get_size(self): - """ - Get current frame size. - - :return: window size (width, height) - :rtype: tuple - """ - - return (self.winfo_width(), self.winfo_height()) - - def _on_resize(self, event): - """ - Resize frame - - :param event event: resize event - """ - - self.width, self.height = self.get_size() - self.mheight = self._frm_map.winfo_height() + self.set_status("") def _open_mapfile(self) -> str: """ @@ -240,7 +188,7 @@ def _on_load(self): Load custom map from file. """ - self._show_status() + self.set_status("") self._custommap = self._open_mapfile() if self._custommap is not None: self._get_bounds(self._custommap) @@ -262,8 +210,9 @@ def _get_bounds(self, mappath) -> tuple: ras.crs.to_epsg(), 4326, *ras.bounds ) except Exception: # pylint: disable=broad-exception-caught - self._show_status( - "Warning: image is not georeferenced - bounds must be entered manually" + self.set_status( + "Warning: image is not georeferenced - bounds must be entered manually", + ERRCOL, ) self._lonmin.set(round(lonmin, 8)) @@ -296,24 +245,13 @@ def _on_import(self): latmin = float(self._latmin.get()) latmax = float(self._latmax.get()) except ValueError: - self._show_status("Error: invalid bounds") + self.set_status("Error: invalid bounds", ERRCOL) return if lonmax + 180 <= lonmin + 180 or latmax + 90 <= latmin + 90: - self._show_status("Error: minimum must be less than maximum") + self.set_status("Error: minimum must be less than maximum", ERRCOL) else: usermaps = self.__app.configuration.get("usermaps_l") usermaps.append([self._custommap, [latmin, lonmin, latmax, lonmax]]) self.__app.configuration.set("usermaps_l", usermaps) - self._show_status("Custom map imported", INFOCOL) - - def _show_status(self, msg: str = "", col: str = ERRCOL): - """ - Show error message in status label - - :param str msg: error message - :param str col: text colour - """ - - self._lbl_status.config(text=msg, fg=col) - self.update_idletasks() + self.set_status("Custom map imported", INFOCOL) diff --git a/src/pygpsclient/nmea_config_dialog.py b/src/pygpsclient/nmea_config_dialog.py index bbb11dfc..7446d457 100644 --- a/src/pygpsclient/nmea_config_dialog.py +++ b/src/pygpsclient/nmea_config_dialog.py @@ -15,9 +15,8 @@ :license: BSD 3-Clause """ -from tkinter import Button, E, Frame, Label, N, S, StringVar, Toplevel, W +from tkinter import E, N, S, W -from PIL import Image, ImageTk from pynmeagps import NMEAMessage from pygpsclient.dynamic_config_frame import Dynamic_Config_Frame @@ -26,19 +25,20 @@ CONNECTED_SIMULATOR, CONNECTED_SOCKET, ERRCOL, - ICON_EXIT, NMEA_CFGOTHER, NMEA_MONHW, NMEA_PRESET, - POPUP_TRANSIENT, ) from pygpsclient.hardware_info_frame import Hardware_Info_Frame from pygpsclient.nmea_preset_frame import NMEA_PRESET_Frame -from pygpsclient.strings import DLGNMEACONFIG, DLGTNMEA +from pygpsclient.strings import DLGTNMEA +from pygpsclient.toplevel_dialog import ToplevelDialog +MINDIM = (541, 810) -class NMEAConfigDialog(Toplevel): - """, + +class NMEAConfigDialog(ToplevelDialog): + """ NMEAConfigDialog class. """ @@ -52,40 +52,23 @@ def __init__(self, app, *args, **kwargs): # pylint: disable=unused-argument """ self.__app = app # Reference to main application class - self.__master = self.__app.appmaster # Reference to root class (Tk) - - Toplevel.__init__(self, app) - if POPUP_TRANSIENT: - self.transient(self.__app) - self.resizable(True, True) # allow for MacOS resize glitches - self.title(DLGNMEACONFIG) # pylint: disable=E1102 - self.protocol("WM_DELETE_WINDOW", self.on_exit) - self._img_exit = ImageTk.PhotoImage(Image.open(ICON_EXIT)) + + super().__init__(app, DLGTNMEA, MINDIM) + self._cfg_msg_command = None self._pending_confs = {} - self._status = StringVar() - self._status_cfgmsg = StringVar() self._body() self._do_layout() self._reset() + self._attach_events() + self._finalise() def _body(self): """ Set up frame and widgets. """ - self._frm_container = Frame(self, borderwidth=2, relief="groove") - self._frm_status = Frame(self._frm_container, borderwidth=2, relief="groove") - self._lbl_status = Label(self._frm_status, textvariable=self._status, anchor=W) - self._btn_exit = Button( - self._frm_status, - image=self._img_exit, - width=50, - fg=ERRCOL, - command=self.on_exit, - font=self.__app.font_md, - ) # add configuration widgets self._frm_device_info = Hardware_Info_Frame( self.__app, self, borderwidth=2, relief="groove", protocol="NMEA" @@ -106,22 +89,11 @@ def _do_layout(self): """ # top of grid - col = colsp = 0 - row = rowsp = 0 - self._frm_container.grid( - column=col, - row=row, - columnspan=12, - rowspan=22, - padx=3, - pady=3, - ipadx=5, - ipady=5, - sticky=(N, S, W, E), - ) + col = 0 + row = 0 # left column of grid for frm in (self._frm_device_info, self._frm_preset): - (colsp, rowsp) = frm.grid_size() + colsp, rowsp = frm.grid_size() frm.grid( column=col, row=row, @@ -130,12 +102,11 @@ def _do_layout(self): sticky=(N, S, W, E), ) row += rowsp - maxrow = row # right column of grid row = 0 col += colsp for frm in (self._frm_config_dynamic,): - (colsp, rowsp) = frm.grid_size() + colsp, rowsp = frm.grid_size() frm.grid( column=col, row=row, @@ -144,25 +115,6 @@ def _do_layout(self): sticky=(N, S, W, E), ) row += rowsp - maxrow = max(maxrow, row) - # bottom of grid - col = 0 - row = maxrow - (colsp, rowsp) = self._frm_container.grid_size() - self._frm_status.grid(column=col, row=row, columnspan=colsp, sticky=(W, E)) - self._lbl_status.grid( - column=0, row=0, columnspan=colsp - 1, ipadx=3, ipady=3, sticky=(W, E) - ) - self._btn_exit.grid(column=colsp - 1, row=0, ipadx=3, ipady=3, sticky=E) - - for frm in (self._frm_container, self._frm_status): - for i in range(colsp): - frm.grid_columnconfigure(i, weight=1) - for i in range(rowsp): - frm.grid_rowconfigure(i, weight=1) - - self._frm_container.option_add("*Font", self.__app.font_sm) - self._frm_status.option_add("*Font", self.__app.font_sm) def _reset(self): """ @@ -178,6 +130,13 @@ def _reset(self): ): self.set_status("Device not connected", ERRCOL) + def _attach_events(self): + """ + Bind events to window. + """ + + # self.bind("", self._on_resize) + def set_pending(self, msgid: int, ubxfrm: int): """ Set pending confirmation flag for NMEA configuration frame to @@ -212,49 +171,6 @@ def update_pending(self, msg: NMEAMessage): if self._pending_confs.get(msgid, None) == nmeafrm: self._pending_confs.pop(msgid) - def set_status(self, message: str, color: str = ""): - """ - Set status message. - - :param str message: message to be displayed - :param str color: rgb color of text (blue) - """ - - message = (message[:120] + "..") if len(message) > 120 else message - if color != "": - self._lbl_status.config(fg=color) - self._status.set(" " + message) - - def on_exit(self, *args, **kwargs): # pylint: disable=unused-argument - """ - Handle Exit button press. - """ - - self.__app.stop_dialog(DLGTNMEA) - self.destroy() - - def get_size(self): - """ - Get current frame size. - - :return: window size (width, height) - :rtype: tuple - """ - - self.__master.update_idletasks() # Make sure we know about any resizing - return self.winfo_width(), self.winfo_height() - - @property - def container(self): - """ - Getter for container frame. - - :return: reference to container frame - :rtype: tkinter.Frame - """ - - return self._frm_container - def send_command(self, msg: NMEAMessage): """ Send command to receiver. diff --git a/src/pygpsclient/nmea_preset_frame.py b/src/pygpsclient/nmea_preset_frame.py index 102ba175..5c8b371b 100644 --- a/src/pygpsclient/nmea_preset_frame.py +++ b/src/pygpsclient/nmea_preset_frame.py @@ -94,7 +94,7 @@ def _body(self): self, border=2, relief="sunken", - height=35, + height=30, width=55, justify=LEFT, exportselection=False, diff --git a/src/pygpsclient/ntrip_client_dialog.py b/src/pygpsclient/ntrip_client_dialog.py index 2f66e086..fff59095 100644 --- a/src/pygpsclient/ntrip_client_dialog.py +++ b/src/pygpsclient/ntrip_client_dialog.py @@ -38,12 +38,10 @@ Spinbox, StringVar, TclError, - Toplevel, W, ttk, ) -from PIL import Image, ImageTk from pygnssutils import NOGGA from pygnssutils.helpers import find_mp_distance @@ -52,12 +50,8 @@ DISCONNECTED, ERRCOL, GGA_INTERVALS, - ICON_CONN, - ICON_DISCONN, - ICON_EXIT, INFOCOL, NTRIP, - POPUP_TRANSIENT, READONLY, RPTDELAY, UBX_CFGMSG, @@ -73,7 +67,6 @@ from pygpsclient.helpers import MAXALT, VALFLOAT, get_mp_info, valid_entry from pygpsclient.socketconfig_frame import SocketConfigFrame from pygpsclient.strings import ( - DLGNTRIPCONFIG, DLGTNTRIP, LBLGGAFIXED, LBLGGALIVE, @@ -84,6 +77,7 @@ LBLNTRIPUSER, LBLNTRIPVERSION, ) +from pygpsclient.toplevel_dialog import ToplevelDialog NTRIP_VERSIONS = ("2.0", "1.0") KM2MILES = 0.6213712 @@ -94,9 +88,10 @@ NTRIP_SPARTN = "ppntrip.services.u-blox.com" TCPIPV4 = "IPv4" TCPIPV6 = "IPv6" +MINDIM = (500, 505) -class NTRIPConfigDialog(Toplevel): +class NTRIPConfigDialog(ToplevelDialog): """, NTRIPConfigDialog class. """ @@ -114,15 +109,7 @@ def __init__(self, app, *args, **kwargs): # pylint: disable=unused-argument self.logger = getLogger(__name__) self.__master = self.__app.appmaster # Reference to root class (Tk) - Toplevel.__init__(self, app) - if POPUP_TRANSIENT: - self.transient(self.__app) - self.resizable(False, False) - self.title(DLGNTRIPCONFIG) # pylint: disable=E1102 - self.protocol("WM_DELETE_WINDOW", self.on_exit) - self._img_exit = ImageTk.PhotoImage(Image.open(ICON_EXIT)) - self._img_conn = ImageTk.PhotoImage(Image.open(ICON_CONN)) - self._img_disconn = ImageTk.PhotoImage(Image.open(ICON_DISCONN)) + super().__init__(app, DLGTNTRIP, MINDIM) self._cfg_msg_command = None self._pending_confs = { UBX_MONVER: (), @@ -133,7 +120,6 @@ def __init__(self, app, *args, **kwargs): # pylint: disable=unused-argument UBX_PRESET: (), UBX_CFGRATE: (), } - self._status = StringVar() self._ntrip_datatype = StringVar() self._ntrip_https = IntVar() self._ntrip_version = StringVar() @@ -155,6 +141,7 @@ def __init__(self, app, *args, **kwargs): # pylint: disable=unused-argument self._do_layout() self._reset() self._attach_events() + self._finalise() def _body(self): """ @@ -162,27 +149,17 @@ def _body(self): """ # pylint: disable=unnecessary-lambda - self._frm_container = Frame(self, borderwidth=2, relief="groove") + self._frm_body = Frame(self.container, borderwidth=2, relief="groove") self._frm_socket = SocketConfigFrame( self.__app, - self._frm_container, + self._frm_body, NTRIP, protocols=[TCPIPV4, TCPIPV6], server_callback=self._on_server, ) - self._frm_status = Frame(self._frm_container, borderwidth=2, relief="groove") - self._lbl_status = Label(self._frm_status, textvariable=self._status, anchor=W) - self._btn_exit = Button( - self._frm_status, - image=self._img_exit, - width=55, - fg=ERRCOL, - command=self.on_exit, - font=self.__app.font_md, - ) - self._lbl_mountpoint = Label(self._frm_container, text=LBLNTRIPMOUNT) + self._lbl_mountpoint = Label(self._frm_body, text=LBLNTRIPMOUNT) self._ent_mountpoint = Entry( - self._frm_container, + self._frm_body, textvariable=self._ntrip_mountpoint, state=NORMAL, relief="sunken", @@ -190,28 +167,28 @@ def _body(self): ) self._lbl_mpdist = Label( - self._frm_container, + self._frm_body, textvariable=self._ntrip_mpdist, width=30, anchor=W, ) - self._lbl_sourcetable = Label(self._frm_container, text=LBLNTRIPSTR) + self._lbl_sourcetable = Label(self._frm_body, text=LBLNTRIPSTR) self._lbx_sourcetable = Listbox( - self._frm_container, + self._frm_body, height=4, relief="sunken", width=55, ) - self._scr_sourcetablev = Scrollbar(self._frm_container, orient=VERTICAL) - self._scr_sourcetableh = Scrollbar(self._frm_container, orient=HORIZONTAL) + self._scr_sourcetablev = Scrollbar(self._frm_body, orient=VERTICAL) + self._scr_sourcetableh = Scrollbar(self._frm_body, orient=HORIZONTAL) self._lbx_sourcetable.config(yscrollcommand=self._scr_sourcetablev.set) self._lbx_sourcetable.config(xscrollcommand=self._scr_sourcetableh.set) self._scr_sourcetablev.config(command=self._lbx_sourcetable.yview) self._scr_sourcetableh.config(command=self._lbx_sourcetable.xview) - self._lbl_ntripversion = Label(self._frm_container, text=LBLNTRIPVERSION) + self._lbl_ntripversion = Label(self._frm_body, text=LBLNTRIPVERSION) self._spn_ntripversion = Spinbox( - self._frm_container, + self._frm_body, values=(NTRIP_VERSIONS), width=4, wrap=True, @@ -220,9 +197,9 @@ def _body(self): textvariable=self._ntrip_version, state=READONLY, ) - self._lbl_datatype = Label(self._frm_container, text="Data Type") + self._lbl_datatype = Label(self._frm_body, text="Data Type") self._spn_datatype = Spinbox( - self._frm_container, + self._frm_body, values=(RTCM, SPARTN), width=8, wrap=True, @@ -231,26 +208,26 @@ def _body(self): textvariable=self._ntrip_datatype, state=READONLY, ) - self._lbl_user = Label(self._frm_container, text=LBLNTRIPUSER) + self._lbl_user = Label(self._frm_body, text=LBLNTRIPUSER) self._ent_user = Entry( - self._frm_container, + self._frm_body, textvariable=self._ntrip_user, state=NORMAL, relief="sunken", width=50, ) - self._lbl_password = Label(self._frm_container, text=LBLNTRIPPWD) + self._lbl_password = Label(self._frm_body, text=LBLNTRIPPWD) self._ent_password = Entry( - self._frm_container, + self._frm_body, textvariable=self._ntrip_password, state=NORMAL, relief="sunken", width=20, show="*", ) - self._lbl_ntripggaint = Label(self._frm_container, text=LBLNTRIPGGAINT) + self._lbl_ntripggaint = Label(self._frm_body, text=LBLNTRIPGGAINT) self._spn_ntripggaint = Spinbox( - self._frm_container, + self._frm_body, values=(GGA_INTERVALS), width=5, wrap=True, @@ -260,41 +237,41 @@ def _body(self): state=READONLY, ) self._rad_ggalive = Radiobutton( - self._frm_container, text=LBLGGALIVE, variable=self._ntrip_gga_mode, value=0 + self._frm_body, text=LBLGGALIVE, variable=self._ntrip_gga_mode, value=0 ) self._rad_ggafixed = Radiobutton( - self._frm_container, + self._frm_body, text=LBLGGAFIXED, variable=self._ntrip_gga_mode, value=1, ) - self._lbl_lat = Label(self._frm_container, text="Ref Latitude") + self._lbl_lat = Label(self._frm_body, text="Ref Latitude") self._ent_lat = Entry( - self._frm_container, + self._frm_body, textvariable=self._ntrip_gga_lat, state=NORMAL, relief="sunken", width=15, ) - self._lbl_lon = Label(self._frm_container, text="Ref Longitude") + self._lbl_lon = Label(self._frm_body, text="Ref Longitude") self._ent_lon = Entry( - self._frm_container, + self._frm_body, textvariable=self._ntrip_gga_lon, state=NORMAL, relief="sunken", width=15, ) - self._lbl_alt = Label(self._frm_container, text="Ref Elevation m") + self._lbl_alt = Label(self._frm_body, text="Ref Elevation m") self._ent_alt = Entry( - self._frm_container, + self._frm_body, textvariable=self._ntrip_gga_alt, state=NORMAL, relief="sunken", width=15, ) - self._lbl_sep = Label(self._frm_container, text="Ref Separation m") + self._lbl_sep = Label(self._frm_body, text="Ref Separation m") self._ent_sep = Entry( - self._frm_container, + self._frm_body, textvariable=self._ntrip_gga_sep, state=NORMAL, relief="sunken", @@ -302,17 +279,17 @@ def _body(self): ) self._btn_connect = Button( - self._frm_container, + self._frm_body, width=45, height=35, - image=self._img_conn, + image=self.img_conn, command=lambda: self._connect(), ) self._btn_disconnect = Button( - self._frm_container, + self._frm_body, width=45, height=35, - image=self._img_disconn, + image=self.img_disconn, command=lambda: self._disconnect(), state=DISABLED, ) @@ -323,25 +300,13 @@ def _do_layout(self): """ # top of grid - col = 0 - row = 0 - self._frm_container.grid( - column=col, - row=row, - columnspan=5, - rowspan=22, - padx=3, - pady=3, - ipadx=5, - ipady=5, - sticky=(N, S, W, E), - ) + self._frm_body.grid(column=0, row=0, sticky=(N, S, E, W)) # body of grid self._frm_socket.grid( column=0, row=0, columnspan=3, rowspan=3, padx=3, pady=3, sticky=W ) - ttk.Separator(self._frm_container).grid( + ttk.Separator(self._frm_body).grid( column=0, row=3, columnspan=5, padx=3, pady=3, sticky=(W, E) ) self._lbl_mountpoint.grid(column=0, row=4, padx=3, pady=3, sticky=W) @@ -363,7 +328,7 @@ def _do_layout(self): self._ent_password.grid( column=1, row=13, columnspan=2, padx=3, pady=3, sticky=W ) - ttk.Separator(self._frm_container).grid( + ttk.Separator(self._frm_body).grid( column=0, row=14, columnspan=5, padx=3, pady=3, sticky=(W, E) ) self._lbl_ntripggaint.grid(column=0, row=15, padx=2, pady=3, sticky=W) @@ -378,31 +343,12 @@ def _do_layout(self): self._ent_alt.grid(column=1, row=18, columnspan=2, padx=3, pady=2, sticky=W) self._lbl_sep.grid(column=2, row=18, padx=3, pady=2, sticky=W) self._ent_sep.grid(column=3, row=18, columnspan=2, padx=3, pady=2, sticky=W) - ttk.Separator(self._frm_container).grid( + ttk.Separator(self._frm_body).grid( column=0, row=19, columnspan=5, padx=3, pady=3, sticky=(W, E) ) self._btn_connect.grid(column=0, row=20, padx=3, pady=3, sticky=W) self._btn_disconnect.grid(column=1, row=20, padx=3, pady=3, sticky=W) - # bottom of grid - row = 21 - col = 0 - (colsp, rowsp) = self._frm_container.grid_size() - self._frm_status.grid(column=col, row=row, columnspan=colsp, sticky=(W, E)) - self._lbl_status.grid( - column=0, row=0, columnspan=colsp - 1, ipadx=3, ipady=3, sticky=(W, E) - ) - self._btn_exit.grid(column=colsp - 1, row=0, ipadx=3, ipady=3, sticky=E) - - for frm in (self._frm_container, self._frm_status): - for i in range(colsp): - frm.grid_columnconfigure(i, weight=1) - for i in range(rowsp): - frm.grid_rowconfigure(i, weight=1) - - self._frm_container.option_add("*Font", self.__app.font_sm) - self._frm_status.option_add("*Font", self.__app.font_sm) - def _attach_events(self): """ Set up event listeners. @@ -426,6 +372,7 @@ def _attach_events(self): self._ntrip_gga_sep, ): setting.trace_add("write", self._on_update_config) + # self.bind("", self._on_resize) def _reset(self): """ @@ -595,25 +542,6 @@ def _on_server(self, var, index, mode): # pylint: disable=unused-argument except TclError: pass - def on_exit(self, *args, **kwargs): # pylint: disable=unused-argument - """ - Handle Exit button press. - """ - - self.__app.stop_dialog(DLGTNTRIP) - self.destroy() - - def get_size(self): - """ - Get current frame size. - - :return: window size (width, height) - :rtype: tuple - """ - - self.__master.update_idletasks() # Make sure we know about any resizing - return (self.winfo_width(), self.winfo_height()) - def _get_settings(self): """ Get settings from saved configuration or from the running instance of diff --git a/src/pygpsclient/settings_frame.py b/src/pygpsclient/settings_frame.py index 71dbeeef..4f946427 100644 --- a/src/pygpsclient/settings_frame.py +++ b/src/pygpsclient/settings_frame.py @@ -865,7 +865,8 @@ def _on_data_log(self): """ if self._datalog.get() == 1: - self.logpath = self.__app.file_handler.set_logfile_path(self.logpath) + if self.logpath in ("", None): + self.logpath = self.__app.file_handler.set_logfile_path(self.logpath) if self.logpath is not None: self.__app.configuration.set("datalog_b", 1) self.__app.configuration.set("logpath_s", self.logpath) @@ -874,11 +875,13 @@ def _on_data_log(self): else: self.logpath = "" self._datalog.set(False) + self._spn_datalog.config(state=DISABLED) else: self.__app.configuration.set("datalog_b", 0) self._datalog.set(False) self.__app.file_handler.close_logfile() self.__app.set_status("Data logging disabled") + self._spn_datalog.config(state=READONLY) def _on_record_track(self): """ @@ -923,8 +926,6 @@ def enable_controls(self, status: int): for ctl in ( self._btn_connect_socket, self._btn_connect_file, - self._chk_datalog, - self._chk_recordtrack, self._chk_tty, ): ctl.config( @@ -937,13 +938,6 @@ def enable_controls(self, status: int): self._btn_disconnect.config( state=(DISABLED if status in (DISCONNECTED,) else NORMAL) ) - self._spn_datalog.config( - state=( - DISABLED - if status in (CONNECTED, CONNECTED_SOCKET, CONNECTED_FILE) - else READONLY - ) - ) def get_size(self) -> tuple: """ diff --git a/src/pygpsclient/spartn_dialog.py b/src/pygpsclient/spartn_dialog.py index f922db43..73996b2a 100644 --- a/src/pygpsclient/spartn_dialog.py +++ b/src/pygpsclient/spartn_dialog.py @@ -16,21 +16,13 @@ :license: BSD 3-Clause """ -from tkinter import Button, E, Frame, Label, N, S, StringVar, Toplevel, W +from tkinter import E, N, S, W -from PIL import Image, ImageTk from pyubx2 import UBXMessage from pygpsclient.globals import ( CONNECTED_SPARTNIP, CONNECTED_SPARTNLB, - ERRCOL, - ICON_BLANK, - ICON_CONFIRMED, - ICON_EXIT, - ICON_PENDING, - ICON_WARNING, - POPUP_TRANSIENT, SPARTN_GNSS, SPARTN_LBAND, SPARTN_MQTT, @@ -38,14 +30,17 @@ from pygpsclient.spartn_gnss_frame import SPARTNGNSSDialog from pygpsclient.spartn_lband_frame import SpartnLbandDialog from pygpsclient.spartn_mqtt_frame import SPARTNMQTTDialog -from pygpsclient.strings import DLGSPARTNCONFIG, DLGTSPARTN +from pygpsclient.strings import DLGTSPARTN +from pygpsclient.toplevel_dialog import ToplevelDialog RXMMSG = "RXM-SPARTN-KEY" CFGSET = "CFG-VALGET/SET" CFGPOLL = "CFG-VALGET" +MINDIM = (408, 758) -class SPARTNConfigDialog(Toplevel): + +class SPARTNConfigDialog(ToplevelDialog): """, SPARTNConfigDialog class. """ @@ -62,23 +57,15 @@ def __init__(self, app, *args, **kwargs): # pylint: disable=unused-argument self.__app = app # Reference to main application class self.__master = self.__app.appmaster # Reference to root class (Tk) - Toplevel.__init__(self, app) - if POPUP_TRANSIENT: - self.transient(self.__app) - self.resizable(False, False) - self.title(DLGSPARTNCONFIG) # pylint: disable=E1102 - self.protocol("WM_DELETE_WINDOW", self.on_exit) - self._img_blank = ImageTk.PhotoImage(Image.open(ICON_BLANK)) - self._img_pending = ImageTk.PhotoImage(Image.open(ICON_PENDING)) - self._img_confirmed = ImageTk.PhotoImage(Image.open(ICON_CONFIRMED)) - self._img_warn = ImageTk.PhotoImage(Image.open(ICON_WARNING)) - self._img_exit = ImageTk.PhotoImage(Image.open(ICON_EXIT)) - self._status = StringVar() + super().__init__(app, DLGTSPARTN, MINDIM) self._pending_confs = {} + self._lband_enabled = self.__app.configuration.get("lband_enabled_b") self._body() self._do_layout() self._reset() + self._attach_events() + self._finalise() def _body(self): """ @@ -86,30 +73,19 @@ def _body(self): """ # pylint: disable=unnecessary-lambda - self._frm_container = Frame(self) - self._frm_status = Frame(self._frm_container, borderwidth=2, relief="groove") - self._lbl_status = Label(self._frm_status, textvariable=self._status, anchor=W) - self._btn_exit = Button( - self._frm_status, - image=self._img_exit, - width=55, - fg=ERRCOL, - command=self.on_exit, - font=self.__app.font_md, - ) - self.frm_corrip = SPARTNMQTTDialog( self.__app, self, borderwidth=2, relief="groove", ) - self.frm_corrlband = SpartnLbandDialog( - self.__app, - self, - borderwidth=2, - relief="groove", - ) + if self._lband_enabled: + self.frm_corrlband = SpartnLbandDialog( + self.__app, + self, + borderwidth=2, + relief="groove", + ) self.frm_gnss = SPARTNGNSSDialog( self.__app, self, borderwidth=2, relief="groove" ) @@ -126,54 +102,24 @@ def _do_layout(self): ipady=5, sticky=(N, S, W, E), ) - self.frm_corrlband.grid( - column=1, - row=0, - ipadx=5, - ipady=5, - sticky=(N, S, W, E), - ) + col = 1 + if self._lband_enabled: + self.frm_corrlband.grid( + column=col, + row=0, + ipadx=5, + ipady=5, + sticky=(N, S, W, E), + ) + col += 1 self.frm_gnss.grid( - column=2, + column=col, row=0, ipadx=5, ipady=5, sticky=(N, S, W, E), ) - # bottom of grid - self._frm_container.grid( - column=0, - row=0, - columnspan=3, - rowspan=2, - padx=3, - pady=3, - ipadx=5, - ipady=5, - sticky=(N, S, W, E), - ) - self._frm_status.grid( - column=0, - row=1, - columnspan=3, - ipadx=5, - ipady=5, - sticky=(W, E), - ) - self._lbl_status.grid(column=0, row=0, columnspan=2, sticky=W) - self._btn_exit.grid(column=2, row=0, sticky=E) - - (colsp, rowsp) = self._frm_container.grid_size() - for frm in (self._frm_container, self._frm_status): - for i in range(colsp): - frm.grid_columnconfigure(i, weight=1) - for i in range(rowsp): - frm.grid_rowconfigure(i, weight=1) - - self._frm_container.option_add("*Font", self.__app.font_sm) - self._frm_status.option_add("*Font", self.__app.font_sm) - def _reset(self): """ Reset configuration widgets. @@ -181,6 +127,13 @@ def _reset(self): self.set_status("") + def _attach_events(self): + """ + Bind events to window. + """ + + # self.bind("", self._on_resize) + def set_status(self, message: str, color: str = ""): """ Set status message. @@ -193,14 +146,6 @@ def set_status(self, message: str, color: str = ""): self._lbl_status.config(fg=color) self._status.set(" " + message) - def on_exit(self, *args, **kwargs): # pylint: disable=unused-argument - """ - Handle Exit button press. - """ - - self.__app.stop_dialog(DLGTSPARTN) - self.destroy() - def set_pending(self, msgid: int, spartnfrm: int): """ Set pending confirmation flag for UBX configuration frame to @@ -275,14 +220,6 @@ def disconnect_lband(self, msg: str = ""): self.frm_corrlband.on_disconnect(msg) - @property - def container(self): - """ - Getter for container. - """ - - return self._frm_container - @property def server(self) -> str: """ diff --git a/src/pygpsclient/spartn_mqtt_frame.py b/src/pygpsclient/spartn_mqtt_frame.py index c5a147fd..665a6eb0 100644 --- a/src/pygpsclient/spartn_mqtt_frame.py +++ b/src/pygpsclient/spartn_mqtt_frame.py @@ -18,7 +18,6 @@ :license: BSD 3-Clause """ -from datetime import datetime, timezone from os import path from pathlib import Path from tkinter import ( @@ -40,7 +39,12 @@ ) from PIL import Image, ImageTk -from pyspartn import date2timetag +from pyspartn import TIMEBASE + +try: + from pyspartn import HASCRYPTO +except ImportError: + HASCRYPTO = 1 from pyubx2 import UBXMessage from pygpsclient.globals import ( @@ -65,6 +69,7 @@ RPTDELAY, RXMMSG, SPARTN_BASEDATE_CURRENT, + SPARTN_BASEDATE_DATASTREAM, SPARTN_GNSS, SPARTN_OUTPORT, SPARTN_PPREGIONS, @@ -327,6 +332,10 @@ def _reset(self): else: self.set_controls(DISCONNECTED) + if not HASCRYPTO: + self._lbl_spartndecode.config(state=DISABLED) + self._chk_spartndecode.config(state=DISABLED) + def _reset_keypaths(self, clientid): """ Reset key and cert file paths. @@ -406,12 +415,16 @@ def _get_settings(self): self._settings["tlskey"] = cfg.get("mqttclienttlskey_s") self._settings["spartndecode"] = cfg.get("spartndecode_b") self._settings["spartnkey"] = cfg.get("spartnkey_s") - if self._settings["spartnkey"] == "": + if self._settings["spartnkey"] == "" or not HASCRYPTO: self._settings["spartndecode"] = 0 # if basedate is provided in config file, it must be an integer gnssTimetag basedate = cfg.get("spartnbasedate_n") if basedate == SPARTN_BASEDATE_CURRENT: - basedate = date2timetag(datetime.now(timezone.utc)) + # pyspartn will interpret 'None' as current datetime + basedate = None + elif basedate == SPARTN_BASEDATE_DATASTREAM: + # pyspartn will interpret 'TIMEBASE' as use gnssTimeTag in datastream + basedate = TIMEBASE self._settings["spartnbasedate"] = basedate self._mqtt_server.set(self._settings["server"]) @@ -443,6 +456,7 @@ def _set_settings(self): self._settings["topic_freq"] = self._mqtt_freqtopic.get() self._settings["tlscrt"] = self._mqtt_crt.get() self._settings["tlskey"] = self._mqtt_pem.get() + self._settings["spartndecode"] = self._spartndecode.get() self._settings["output"] = self._output def set_controls(self, status: int): @@ -551,6 +565,9 @@ def on_connect(self): topic_freq=self._settings["topic_freq"], tlscrt=self._settings["tlscrt"], tlskey=self._settings["tlskey"], + spartndecode=self._settings["spartndecode"], + spartnkey=self._settings["spartnkey"], + spartnbasedate=self._settings["spartnbasedate"], output=self._settings["output"], ) self.set_controls(CONNECTED_SPARTNIP) diff --git a/src/pygpsclient/strings.py b/src/pygpsclient/strings.py index cafa4bd2..86c284ca 100644 --- a/src/pygpsclient/strings.py +++ b/src/pygpsclient/strings.py @@ -158,23 +158,17 @@ DLGABOUT = TITLE DLGENABLEMONSPAN = "Enable or poll MON-SPAN message" DLGENABLEMONSYS = "Enable or poll MON-SYS/COMMS messages" -DLGGPXERROR = "GPX PARSING ERROR!" -DLGGPXLOAD = "LOADING GPX TRACK ..." -DLGGPXNULL = "NO TRACKPOINTS IN GPX FILE!" -DLGGPXPROMPT = "CLICK FOLDER ICON TO LOAD GPX FILE" -DLGGPXVIEWER = "GPX Track Viewer" +DLGGPXERROR = "GPX Parsing Error!" +DLGGPXLOAD = "Loading GPX Track ..." +DLGGPXNULL = "No Trackpoints in GPS File!" DLGHOWTO = f"How To Use {TITLE}" -DLGJSONERR = "ERROR! {}" +DLGJSONERR = "Error! {}" DLGJSONOK = "Keys loaded from {}" DLGNOMONSPAN = "This receiver does not appear to\nsupport the MON-SPAN messages" DLGNOMONSYS = "This receiver does not appear to support\nthe MON-SYS/COMMS messages" -DLGNTRIPCONFIG = "NTRIP Client Configuration" DLGACTION = "Confirm Command" DLGACTIONCONFIRM = "Are you sure?" -DLGSPARTNCONFIG = "SPARTN Client Configuration" DLGSPARTNWARN = "WARNING! Disconnect from {} client before using {} client" -DLGNMEACONFIG = "NMEA Configuration" -DLGUBXCONFIG = "UBX Configuration" DLGWAITMONSPAN = "Waiting for MON-SPAN message..." DLGWAITMONSYS = "Waiting for MON-SYS/COMMS messages..." DLGSTOPRTK = "WARNING! Stop all active connections before loading configuration" diff --git a/src/pygpsclient/toplevel_dialog.py b/src/pygpsclient/toplevel_dialog.py new file mode 100644 index 00000000..8c3d5d97 --- /dev/null +++ b/src/pygpsclient/toplevel_dialog.py @@ -0,0 +1,228 @@ +""" +toplevel_dialog.py + +Top Level container dialog which displays child frames +within a scrollable and resizeable canvas, primarily +to allow dialog to be usable on low resolution screens. + +Created on 19 Sep 2020 + +:author: semuadmin +:copyright: 2020 SEMU Consulting +:license: BSD 3-Clause +""" + +from tkinter import ( + ALL, + HORIZONTAL, + NW, + VERTICAL, + Button, + Canvas, + E, + Frame, + Label, + N, + S, + Scrollbar, + StringVar, + Toplevel, + W, +) + +from PIL import Image, ImageTk + +from pygpsclient.globals import ( + ERRCOL, + ICON_BLANK, + ICON_CONFIRMED, + ICON_CONN, + ICON_DISCONN, + ICON_END, + ICON_EXIT, + ICON_LOAD, + ICON_PENDING, + ICON_REDRAW, + ICON_SEND, + ICON_START, + ICON_WARNING, + MINHEIGHT, + MINWIDTH, + POPUP_TRANSIENT, +) +from pygpsclient.helpers import check_lowres + + +class ToplevelDialog(Toplevel): + """ + ToplevelDialog class. + """ + + def __init__(self, app, dlgname: str, dim: tuple = (MINHEIGHT, MINWIDTH)): + """ + Constructor. + + :param Frame app: reference to main tkinter application + :param str dlgname: dialog name + :param tuple dim: initial dimensions (height, width) + """ + + self.__app = app # Reference to main application class + self.__master = self.__app.appmaster # Reference to root class (Tk) + self._dlgname = dlgname + self.lowres, (self.height, self.width) = check_lowres(self.__master, dim) + + super().__init__() + + if POPUP_TRANSIENT: # keep dialog on top of main app window + self.transient(self.__app) + self.title(dlgname) # pylint: disable=E1102 + self.resizable(self.lowres, self.lowres) + self.protocol("WM_DELETE_WINDOW", self.on_exit) + self.img_none = ImageTk.PhotoImage(Image.open(ICON_BLANK)) + self.img_confirmed = ImageTk.PhotoImage(Image.open(ICON_CONFIRMED)) + self.img_conn = ImageTk.PhotoImage(Image.open(ICON_CONN)) + self.img_disconn = ImageTk.PhotoImage(Image.open(ICON_DISCONN)) + self.img_end = ImageTk.PhotoImage(Image.open(ICON_END)) + self.img_exit = ImageTk.PhotoImage(Image.open(ICON_EXIT)) + self.img_load = ImageTk.PhotoImage(Image.open(ICON_LOAD)) + self.img_pending = ImageTk.PhotoImage(Image.open(ICON_PENDING)) + self.img_redraw = ImageTk.PhotoImage(Image.open(ICON_REDRAW)) + self.img_send = ImageTk.PhotoImage(Image.open(ICON_SEND)) + self.img_start = ImageTk.PhotoImage(Image.open(ICON_START)) + self.img_warn = ImageTk.PhotoImage(Image.open(ICON_WARNING)) + self._status = StringVar() + + self._con_body() + + def on_expand(self): + """ + Automatically expand container canvas when sub-frames are resized. + """ + + self._can_container.event_generate("") + + def _con_body(self): + """ + Set up scrollable frame and widgets. + """ + + # create container frame + if self.lowres: + x_scrollbar = Scrollbar(self, orient=HORIZONTAL) + y_scrollbar = Scrollbar(self, orient=VERTICAL) + self._can_container = Canvas( + self, + width=self.width, + height=self.height, + xscrollcommand=x_scrollbar.set, + yscrollcommand=y_scrollbar.set, + ) + self._frm_container = Frame( + self._can_container, borderwidth=2, relief="groove" + ) + self._can_container.grid(column=0, row=0, sticky=(N, S, E, W)) + x_scrollbar.grid(column=0, row=1, sticky=(E, W)) + y_scrollbar.grid(column=1, row=0, sticky=(N, S)) + x_scrollbar.config(command=self._can_container.xview) + y_scrollbar.config(command=self._can_container.yview) + # ensure container canvas expands to accommodate child frames + self._can_container.create_window( + (0, 0), window=self._frm_container, anchor=NW + ) + self._can_container.bind( + "", + lambda e: self._can_container.config( + scrollregion=self._can_container.bbox(ALL) + ), + ) + else: # normal resolution + self._frm_container = Frame(self, borderwidth=2, relief="groove") + self._frm_container.grid(column=0, row=0, sticky=(N, S, E, W)) + + # create status frame + self._frm_status = Frame(self, borderwidth=2, relief="groove") + self._lbl_status = Label(self._frm_status, textvariable=self._status, anchor=W) + self._btn_exit = Button( + self._frm_status, + image=self.img_exit, + width=50, + fg=ERRCOL, + command=self.on_exit, + ) + self._frm_status.grid(column=0, row=2, sticky=(W, E)) + self._lbl_status.grid(column=0, row=0, sticky=(W, E)) + self._btn_exit.grid(column=1, row=0, sticky=E) + + # set column and row weights + # these govern the 'pack' behaviour of the frames on resize + self.grid_columnconfigure(0, weight=10) + self.grid_rowconfigure(0, weight=10) + self._frm_status.grid_columnconfigure(0, weight=10) + if self.lowres: + colsp, rowsp = self._can_container.grid_size() + else: + colsp, rowsp = self._frm_container.grid_size() + for i in range(colsp): + self._frm_status.grid_columnconfigure(i, weight=10) + for i in range(rowsp): + self._frm_status.grid_rowconfigure(i, weight=10) + + def _finalise(self): + """ + Finalise Toplevel window after child frames have been created. + """ + + # self.set_status(f"{self.height}, {self.width}") # testing only + + def set_status(self, message: str, color: str = ""): + """ + Set status message. + + :param str message: message to be displayed + :param str color: rgb color of text (blue) + """ + + message = (message[:120] + "..") if len(message) > 120 else message + if color != "": + self._lbl_status.config(fg=color) + self._status.set(" " + message) + + def on_exit(self, *args, **kwargs): # pylint: disable=unused-argument + """ + Handle Exit button press. + """ + + self.__app.stop_dialog(self._dlgname) + self.destroy() + + def _on_resize(self, event): # pylint: disable=unused-argument + """ + Resize frame. + + :param event event: resize event + """ + + self.width, self.height = self.get_size() + + def get_size(self): + """ + Get current frame size. + + :return: window size (width, height) + :rtype: tuple + """ + + self.__master.update_idletasks() # Make sure we know about any resizing + return self.winfo_width(), self.winfo_height() + + @property + def container(self): + """ + Getter for container frame. + + :return: reference to container frame + :rtype: tkinter.Frame + """ + + return self._frm_container diff --git a/src/pygpsclient/tty_preset_dialog.py b/src/pygpsclient/tty_preset_dialog.py index 7a3e369c..af5f14a6 100644 --- a/src/pygpsclient/tty_preset_dialog.py +++ b/src/pygpsclient/tty_preset_dialog.py @@ -26,28 +26,18 @@ S, Scrollbar, StringVar, - Toplevel, W, ttk, ) -from PIL import Image, ImageTk - from pygpsclient.confirm_box import ConfirmBox from pygpsclient.globals import ( ASCII, BSR, CRLF, ERRCOL, - ICON_BLANK, - ICON_CONFIRMED, - ICON_EXIT, - ICON_PENDING, - ICON_SEND, - ICON_WARNING, INFOCOL, OKCOL, - POPUP_TRANSIENT, TTY_EVENT, TTYERR, TTYOK, @@ -58,13 +48,15 @@ DLGACTIONCONFIRM, DLGTTTY, ) +from pygpsclient.toplevel_dialog import ToplevelDialog CANCELLED = 0 CONFIRMED = 1 NOMINAL = 2 +MINDIM = (463, 493) -class TTYPresetDialog(Toplevel): +class TTYPresetDialog(ToplevelDialog): """ TTY Preset and User-defined configuration command dialog. """ @@ -80,18 +72,7 @@ def __init__(self, app, **kwargs): # pylint: disable=unused-argument self.__app = app self.__master = self.__app.appmaster # Reference to root class (Tk) - Toplevel.__init__(self, app) - if POPUP_TRANSIENT: - self.transient(self.__app) - self.resizable(True, True) - self.title(DLGTTTY) # pylint: disable=E1102 - self.protocol("WM_DELETE_WINDOW", self.on_exit) - self._img_none = ImageTk.PhotoImage(Image.open(ICON_BLANK)) - self._img_send = ImageTk.PhotoImage(Image.open(ICON_SEND)) - self._img_pending = ImageTk.PhotoImage(Image.open(ICON_PENDING)) - self._img_confirmed = ImageTk.PhotoImage(Image.open(ICON_CONFIRMED)) - self._img_warn = ImageTk.PhotoImage(Image.open(ICON_WARNING)) - self._img_exit = ImageTk.PhotoImage(Image.open(ICON_EXIT)) + super().__init__(app, DLGTTTY, MINDIM) self._confirm = False self._command = StringVar() self._crlf = IntVar() @@ -101,43 +82,42 @@ def __init__(self, app, **kwargs): # pylint: disable=unused-argument self._do_layout() self.reset() self._attach_events() + self._finalise() def _body(self): """ Set up frame and widgets. """ - self._frm_container = Frame(self, borderwidth=2, relief="groove") + self._frm_body = Frame(self.container, borderwidth=2, relief="groove") self._lbl_command = Label( - self._frm_container, + self._frm_body, text="Command", ) self._ent_command = Entry( - self._frm_container, + self._frm_body, textvariable=self._command, relief="sunken", width=50, ) self._chk_crlf = Checkbutton( - self._frm_container, + self._frm_body, text="CRLF", variable=self._crlf, ) self._chk_echo = Checkbutton( - self._frm_container, + self._frm_body, text="Echo", variable=self._echo, ) self._chk_delay = Checkbutton( - self._frm_container, + self._frm_body, text="Delay", variable=self._delay, ) - self._lbl_presets = Label( - self._frm_container, text="Preset TTY Commands", anchor=W - ) + self._lbl_presets = Label(self._frm_body, text="Preset TTY Commands", anchor=W) self._lbx_preset = Listbox( - self._frm_container, + self._frm_body, border=2, relief="sunken", height=25, @@ -145,33 +125,26 @@ def _body(self): justify=LEFT, exportselection=False, ) - self._scr_presetv = Scrollbar(self._frm_container, orient=VERTICAL) - self._scr_preseth = Scrollbar(self._frm_container, orient=HORIZONTAL) + self._scr_presetv = Scrollbar(self._frm_body, orient=VERTICAL) + self._scr_preseth = Scrollbar(self._frm_body, orient=HORIZONTAL) self._lbx_preset.config(yscrollcommand=self._scr_presetv.set) self._lbx_preset.config(xscrollcommand=self._scr_preseth.set) self._scr_presetv.config(command=self._lbx_preset.yview) self._scr_preseth.config(command=self._lbx_preset.xview) - self._lbl_send_command = Label(self._frm_container) + self._lbl_send_command = Label(self._frm_body) self._btn_send_command = Button( - self._frm_container, - image=self._img_send, + self._frm_body, + image=self.img_send, width=50, command=self._on_send_command, ) - self._btn_exit = Button( - self._frm_container, - image=self._img_exit, - width=40, - command=self.on_exit, - ) - self._lbl_status = Label(self._frm_container, anchor=W) def _do_layout(self): """ Layout widgets. """ - self._frm_container.grid( + self._frm_body.grid( column=0, row=0, padx=5, pady=5, ipadx=5, ipady=5, sticky=(N, S, E, W) ) self._lbl_command.grid(column=0, row=0, padx=3, sticky=W) @@ -179,7 +152,7 @@ def _do_layout(self): self._chk_crlf.grid(column=0, row=1, padx=3, sticky=W) self._chk_echo.grid(column=1, row=1, padx=3, sticky=W) self._chk_delay.grid(column=2, row=1, padx=3, sticky=W) - ttk.Separator(self._frm_container).grid( + ttk.Separator(self._frm_body).grid( column=0, row=2, columnspan=4, padx=2, pady=2, sticky=(W, E) ) self._lbl_presets.grid(column=0, row=3, columnspan=3, padx=3, sticky=(W, E)) @@ -192,7 +165,7 @@ def _do_layout(self): pady=3, sticky=(W, E), ) - self._scr_presetv.grid(column=2, row=4, rowspan=20, sticky=(N, S, E)) + self._scr_presetv.grid(column=2, row=3, rowspan=21, sticky=(N, S, E)) self._scr_preseth.grid(column=0, row=24, columnspan=3, sticky=(W, E)) self._btn_send_command.grid( column=3, row=3, padx=3, ipadx=3, ipady=3, sticky=(N, E) @@ -200,21 +173,6 @@ def _do_layout(self): self._lbl_send_command.grid( column=3, row=4, padx=3, ipadx=3, ipady=3, sticky=(N, W, E) ) - ttk.Separator(self._frm_container).grid( - column=0, row=25, padx=2, columnspan=4, pady=2, sticky=(W, E) - ) - self._lbl_status.grid( - column=0, row=26, padx=3, ipadx=3, columnspan=3, ipady=3, sticky=(W, E) - ) - self._btn_exit.grid(column=3, row=26, padx=3, ipadx=3, ipady=3) - - # self._frm_container.grid_columnconfigure(0, weight=10) - # for col in range(1, 4): - # self._frm_container.grid_columnconfigure(col, weight=0) - # for row in range(3): - # self._frm_container.grid_rowconfigure(row, weight=0) - # self._frm_container.grid_rowconfigure(3, weight=10) - self._frm_container.option_add("*Font", self.__app.font_sm) def _attach_events(self): """ @@ -225,6 +183,7 @@ def _attach_events(self): for setting in (self._crlf, self._echo): setting.trace_add("write", self._on_update_settings) self._lbx_preset.bind("<>", self._on_select_preset) + # self.bind("", self._on_resize) def reset(self): """ @@ -244,7 +203,7 @@ def _on_update_command(self, var, index, mode): # pylint: disable=unused-argume Command has been updated. """ - self._lbl_send_command.config(image=self._img_none) + self._lbl_send_command.config(image=self.img_none) def _on_update_settings(self, var, index, mode): # pylint: disable=unused-argument """ @@ -289,7 +248,7 @@ def _on_send_command(self, *args, **kwargs): # pylint: disable=unused-argument self._parse_command(self._command.get()) status = CONFIRMED if status == CONFIRMED: - self._lbl_send_command.config(image=self._img_pending) + self._lbl_send_command.config(image=self.img_pending) self.set_status("Command(s) sent") elif status == CANCELLED: self.set_status("Command(s) cancelled") @@ -299,7 +258,7 @@ def _on_send_command(self, *args, **kwargs): # pylint: disable=unused-argument except Exception as err: # pylint: disable=broad-except self.set_status(f"Error {err}", ERRCOL) - self._lbl_send_command.config(image=self._img_warn) + self._lbl_send_command.config(image=self.img_warn) def _parse_command(self, command: str): """ @@ -324,7 +283,7 @@ def _parse_command(self, command: str): self.__master.event_generate(TTY_EVENT) except Exception as err: # pylint: disable=broad-except self.set_status(f"Error {err}", ERRCOL) - self._lbl_send_command.config(image=self._img_warn) + self._lbl_send_command.config(image=self.img_warn) def update_status(self, msg: bytes): """ @@ -336,29 +295,11 @@ def update_status(self, msg: bytes): msgstr = msg.decode(ASCII, errors=BSR).upper() for ack in TTYOK: if ack in msgstr: - self._lbl_send_command.config(image=self._img_confirmed) + self._lbl_send_command.config(image=self.img_confirmed) self.set_status("Command(s) acknowledged", OKCOL) return for nak in TTYERR: if nak in msgstr: - self._lbl_send_command.config(image=self._img_warn) + self._lbl_send_command.config(image=self.img_warn) self.set_status("Command(s) rejected", ERRCOL) break - - def set_status(self, msg: str, col: str = INFOCOL): - """ - Set status message. - - :param str msg: message - :param str col: color - """ - - self._lbl_status.configure(text=msg, fg=col) - - def on_exit(self, *args, **kwargs): # pylint: disable=unused-argument - """ - Handle Exit button press. - """ - - self.__app.stop_dialog(DLGTTTY) - self.destroy() diff --git a/src/pygpsclient/ubx_cfgval_frame.py b/src/pygpsclient/ubx_cfgval_frame.py index c7395ccc..ff315247 100644 --- a/src/pygpsclient/ubx_cfgval_frame.py +++ b/src/pygpsclient/ubx_cfgval_frame.py @@ -80,7 +80,7 @@ def __init__(self, app, container, *args, **kwargs): self.__master = self.__app.appmaster # Reference to root class (Tk) self.__container = container - Frame.__init__(self, self.__container.container, *args, **kwargs) + super().__init__(container.container, *args, **kwargs) self._img_send = ImageTk.PhotoImage(Image.open(ICON_SEND)) self._img_pending = ImageTk.PhotoImage(Image.open(ICON_PENDING)) diff --git a/src/pygpsclient/ubx_config_dialog.py b/src/pygpsclient/ubx_config_dialog.py index af203f88..d8ff5cd3 100644 --- a/src/pygpsclient/ubx_config_dialog.py +++ b/src/pygpsclient/ubx_config_dialog.py @@ -23,9 +23,8 @@ :license: BSD 3-Clause """ -from tkinter import Button, E, Frame, Label, N, S, StringVar, Toplevel, W +from tkinter import E, N, S, W -from PIL import Image, ImageTk from pyubx2 import UBXMessage from pygpsclient.dynamic_config_frame import Dynamic_Config_Frame @@ -33,10 +32,8 @@ CONNECTED, CONNECTED_SIMULATOR, CONNECTED_SOCKET, - ENABLE_CFG_OTHER, + ENABLE_CFG_LEGACY, ERRCOL, - ICON_EXIT, - POPUP_TRANSIENT, UBX_CFGMSG, UBX_CFGOTHER, UBX_CFGPRT, @@ -48,7 +45,8 @@ UBX_PRESET, ) from pygpsclient.hardware_info_frame import Hardware_Info_Frame -from pygpsclient.strings import DLGTUBX, DLGUBXCONFIG +from pygpsclient.strings import DLGTUBX +from pygpsclient.toplevel_dialog import ToplevelDialog from pygpsclient.ubx_cfgval_frame import UBX_CFGVAL_Frame from pygpsclient.ubx_msgrate_frame import UBX_MSGRATE_Frame from pygpsclient.ubx_port_frame import UBX_PORT_Frame @@ -56,8 +54,10 @@ from pygpsclient.ubx_recorder_frame import UBX_Recorder_Frame from pygpsclient.ubx_solrate_frame import UBX_RATE_Frame +MINDIM = (570, 1076) -class UBXConfigDialog(Toplevel): + +class UBXConfigDialog(ToplevelDialog): """, UBXConfigDialog class. """ @@ -72,41 +72,24 @@ def __init__(self, app, *args, **kwargs): # pylint: disable=unused-argument """ self.__app = app # Reference to main application class - self.__master = self.__app.appmaster # Reference to root class (Tk) - - Toplevel.__init__(self, app) - if POPUP_TRANSIENT: - self.transient(self.__app) - self.resizable(True, True) # allow for MacOS resize glitches - self.title(DLGUBXCONFIG) # pylint: disable=E1102 - self.protocol("WM_DELETE_WINDOW", self.on_exit) - self._img_exit = ImageTk.PhotoImage(Image.open(ICON_EXIT)) + + super().__init__(app, DLGTUBX, MINDIM) + self._cfg_msg_command = None self._pending_confs = {} - self._status = StringVar() - self._status_cfgmsg = StringVar() self._recordmode = False self._body() self._do_layout() self._reset() + self._attach_events() + self._finalise() def _body(self): """ Set up frame and widgets. """ - self._frm_container = Frame(self, borderwidth=2, relief="groove") - self._frm_status = Frame(self._frm_container, borderwidth=2, relief="groove") - self._lbl_status = Label(self._frm_status, textvariable=self._status, anchor=W) - self._btn_exit = Button( - self._frm_status, - image=self._img_exit, - width=50, - fg=ERRCOL, - command=self.on_exit, - font=self.__app.font_md, - ) # add configuration widgets self._frm_device_info = Hardware_Info_Frame( self.__app, self, borderwidth=2, relief="groove", protocol="UBX" @@ -144,18 +127,6 @@ def _do_layout(self): # top of grid col = 0 row = 0 - self._frm_container.grid( - column=col, - row=row, - columnspan=12, - rowspan=22, - padx=3, - pady=3, - ipadx=5, - ipady=5, - sticky=(N, S, W, E), - ) - # left column of grid for frm in ( self._frm_device_info, self._frm_recorder, @@ -163,7 +134,7 @@ def _do_layout(self): self._frm_config_rate, self._frm_config_msg, ): - (colsp, rowsp) = frm.grid_size() + colsp, rowsp = frm.grid_size() frm.grid( column=col, row=row, @@ -172,9 +143,21 @@ def _do_layout(self): sticky=(N, S, W, E), ) row += rowsp - maxrow = row # middle column of grid - if ENABLE_CFG_OTHER: + row = 0 + col += colsp + for frm in (self._frm_configdb, self._frm_preset): + colsp, rowsp = frm.grid_size() + frm.grid( + column=col, + row=row, + columnspan=colsp, + rowspan=rowsp, + sticky=(N, S, W, E), + ) + row += rowsp + # right column of grid + if ENABLE_CFG_LEGACY: row = 0 col += colsp for frm in (self._frm_config_dynamic,): @@ -187,39 +170,6 @@ def _do_layout(self): sticky=(N, S, W, E), ) row += rowsp - maxrow = max(maxrow, row) - # right column of grid - row = 0 - col += colsp - for frm in (self._frm_configdb, self._frm_preset): - (colsp, rowsp) = frm.grid_size() - frm.grid( - column=col, - row=row, - columnspan=colsp, - rowspan=rowsp, - sticky=(N, S, W, E), - ) - row += rowsp - maxrow = max(maxrow, row) - # bottom of grid - col = 0 - row = maxrow - (colsp, rowsp) = self._frm_container.grid_size() - self._frm_status.grid(column=col, row=row, columnspan=colsp, sticky=(W, E)) - self._lbl_status.grid( - column=0, row=0, columnspan=colsp - 1, ipadx=3, ipady=3, sticky=(W, E) - ) - self._btn_exit.grid(column=colsp - 1, row=0, ipadx=3, ipady=3, sticky=E) - - for frm in (self._frm_container, self._frm_status): - for i in range(colsp): - frm.grid_columnconfigure(i, weight=1) - for i in range(rowsp): - frm.grid_rowconfigure(i, weight=1) - - self._frm_container.option_add("*Font", self.__app.font_sm) - self._frm_status.option_add("*Font", self.__app.font_sm) def _reset(self): """ @@ -237,6 +187,13 @@ def _reset(self): ): self.set_status("Device not connected", ERRCOL) + def _attach_events(self): + """ + Bind events to window. + """ + + # self.bind("", self._on_resize) + def set_pending(self, msgid: int, ubxfrm: int): """ Set pending confirmation flag for UBX configuration frame to @@ -279,49 +236,6 @@ def update_pending(self, msg: UBXMessage): if self._pending_confs.get(msgid, None) == ubxfrm: self._pending_confs.pop(msgid) - def set_status(self, message: str, color: str = ""): - """ - Set status message. - - :param str message: message to be displayed - :param str color: rgb color of text (blue) - """ - - message = (message[:120] + "..") if len(message) > 120 else message - if color != "": - self._lbl_status.config(fg=color) - self._status.set(" " + message) - - def on_exit(self, *args, **kwargs): # pylint: disable=unused-argument - """ - Handle Exit button press. - """ - - self.__app.stop_dialog(DLGTUBX) - self.destroy() - - def get_size(self): - """ - Get current frame size. - - :return: window size (width, height) - :rtype: tuple - """ - - self.__master.update_idletasks() # Make sure we know about any resizing - return self.winfo_width(), self.winfo_height() - - @property - def container(self): - """ - Getter for container frame. - - :return: reference to container frame - :rtype: tkinter.Frame - """ - - return self._frm_container - @property def recordmode(self) -> bool: """ diff --git a/src/pygpsclient/ubx_msgrate_frame.py b/src/pygpsclient/ubx_msgrate_frame.py index de218ee5..681e3024 100644 --- a/src/pygpsclient/ubx_msgrate_frame.py +++ b/src/pygpsclient/ubx_msgrate_frame.py @@ -65,7 +65,7 @@ def __init__(self, app, container, *args, **kwargs): self.__master = self.__app.appmaster # Reference to root class (Tk) self.__container = container - Frame.__init__(self, self.__container.container, *args, **kwargs) + super().__init__(container.container, *args, **kwargs) self._img_send = ImageTk.PhotoImage(Image.open(ICON_SEND)) self._img_pending = ImageTk.PhotoImage(Image.open(ICON_PENDING)) diff --git a/src/pygpsclient/ubx_port_frame.py b/src/pygpsclient/ubx_port_frame.py index f6d29fad..87b13968 100644 --- a/src/pygpsclient/ubx_port_frame.py +++ b/src/pygpsclient/ubx_port_frame.py @@ -51,7 +51,7 @@ def __init__(self, app, container, *args, **kwargs): self.__master = self.__app.appmaster # Reference to root class (Tk) self.__container = container - Frame.__init__(self, self.__container.container, *args, **kwargs) + super().__init__(container.container, *args, **kwargs) self._img_send = ImageTk.PhotoImage(Image.open(ICON_SEND)) self._img_pending = ImageTk.PhotoImage(Image.open(ICON_PENDING)) diff --git a/src/pygpsclient/ubx_preset_frame.py b/src/pygpsclient/ubx_preset_frame.py index 00105d7b..f0c55ead 100644 --- a/src/pygpsclient/ubx_preset_frame.py +++ b/src/pygpsclient/ubx_preset_frame.py @@ -123,7 +123,7 @@ def __init__(self, app, container, *args, **kwargs): self.logger = logging.getLogger(__name__) self.__container = container - Frame.__init__(self, self.__container.container, *args, **kwargs) + super().__init__(container.container, *args, **kwargs) self._img_send = ImageTk.PhotoImage(Image.open(ICON_SEND)) self._img_pending = ImageTk.PhotoImage(Image.open(ICON_PENDING)) diff --git a/src/pygpsclient/ubx_recorder_frame.py b/src/pygpsclient/ubx_recorder_frame.py index 5d8ec702..76babe45 100644 --- a/src/pygpsclient/ubx_recorder_frame.py +++ b/src/pygpsclient/ubx_recorder_frame.py @@ -81,7 +81,7 @@ def __init__(self, app, container, *args, **kwargs): self.__master = self.__app.appmaster # Reference to root class (Tk) self.__container = container # Reference to UBX Configuration dialog - Frame.__init__(self, self.__container.container, *args, **kwargs) + super().__init__(container.container, *args, **kwargs) self._img_load = ImageTk.PhotoImage(Image.open(ICON_LOAD)) self._img_save = ImageTk.PhotoImage(Image.open(ICON_SAVE)) diff --git a/src/pygpsclient/ubx_solrate_frame.py b/src/pygpsclient/ubx_solrate_frame.py index 3646cf71..35b00c41 100644 --- a/src/pygpsclient/ubx_solrate_frame.py +++ b/src/pygpsclient/ubx_solrate_frame.py @@ -57,7 +57,7 @@ def __init__(self, app, container, *args, **kwargs): self.__master = self.__app.appmaster # Reference to root class (Tk) self.__container = container - Frame.__init__(self, self.__container.container, *args, **kwargs) + super().__init__(container.container, *args, **kwargs) self._img_send = ImageTk.PhotoImage(Image.open(ICON_SEND)) self._img_pending = ImageTk.PhotoImage(Image.open(ICON_PENDING)) diff --git a/tests/test_static.py b/tests/test_static.py index 968571e3..7f9f04a2 100644 --- a/tests/test_static.py +++ b/tests/test_static.py @@ -799,7 +799,7 @@ def testconfiguration(self): self.assertEqual(cfg.get("lbandclientdrat_n"), 2400) self.assertEqual(cfg.get("userport_s"), "") self.assertEqual(cfg.get("spartnport_s"), "") - self.assertEqual(len(cfg.settings), 135) + self.assertEqual(len(cfg.settings), 136) kwargs = {"userport": "/dev/ttyACM0", "spartnport": "/dev/ttyACM1"} cfg.loadcli(**kwargs) self.assertEqual(cfg.get("userport_s"), "/dev/ttyACM0") @@ -837,6 +837,7 @@ def testdop2str(self): self.assertEqual(res, dops[i]) i += 1 + if __name__ == "__main__": # import sys;sys.argv = ['', 'Test.testName'] unittest.main()