├── .github ├── release-drafter.yml └── workflows │ ├── ci.yml │ ├── matchers │ ├── codespell.json │ ├── flake8.json │ └── python.json │ ├── python-publish.yml │ └── release-drafter.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .vscode └── launch.json ├── LICENSE.TXT ├── README.md ├── Scripts └── pyG5DualStacked ├── assets ├── 23-12-13 19-58-23 1732.jpg ├── Screenshot 2024-06-13 at 09.10.52.png ├── Screenshot 2024-06-13 at 09.10.54.png ├── demoView.png ├── flightSimView.jpeg └── pyG5ViewTester.png ├── bootstrap.sh ├── pyG5 ├── __init__.py ├── pyG5Main.py ├── pyG5Network.py ├── pyG5View.py └── pyG5ViewTester.py ├── requirements.txt └── setup.py /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | template: | 2 | ## This release: 3 | 4 | $CHANGES -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | # yamllint disable-line rule:truthy 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: ~ 9 | 10 | env: 11 | CACHE_VERSION: 1 12 | DEFAULT_PYTHON: "3.11" 13 | PRE_COMMIT_HOME: ~/.cache/pre-commit 14 | 15 | jobs: 16 | # Separate job to pre-populate the base dependency cache 17 | # This prevent upcoming jobs to do the same individually 18 | prepare-base: 19 | name: Prepare base dependencies 20 | runs-on: ubuntu-latest 21 | strategy: 22 | matrix: 23 | python-version: ["3.11"] 24 | steps: 25 | - name: Check out code from GitHub 26 | uses: actions/checkout@v2 27 | - name: Set up Python ${{ matrix.python-version }} 28 | id: python 29 | uses: actions/setup-python@v2.1.4 30 | with: 31 | python-version: ${{ matrix.python-version }} 32 | - name: Restore base Python virtual environment 33 | id: cache-venv 34 | uses: actions/cache@v2 35 | with: 36 | path: venv 37 | key: >- 38 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ 39 | steps.python.outputs.python-version }}-${{ 40 | hashFiles('setup.py') }}-${{ 41 | hashFiles('requirements.txt') }} 42 | restore-keys: | 43 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}- 44 | - name: Create Python virtual environment 45 | if: steps.cache-venv.outputs.cache-hit != 'true' 46 | run: | 47 | python -m venv venv 48 | . venv/bin/activate 49 | pip install -U pip setuptools pre-commit 50 | pip install -r requirements.txt 51 | 52 | pre-commit: 53 | name: Prepare pre-commit environment 54 | runs-on: ubuntu-latest 55 | needs: prepare-base 56 | steps: 57 | - name: Check out code from GitHub 58 | uses: actions/checkout@v2 59 | - name: Set up Python ${{ env.DEFAULT_PYTHON }} 60 | uses: actions/setup-python@v2.1.4 61 | id: python 62 | with: 63 | python-version: ${{ env.DEFAULT_PYTHON }} 64 | - name: Restore base Python virtual environment 65 | id: cache-venv 66 | uses: actions/cache@v2 67 | with: 68 | path: venv 69 | key: >- 70 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ 71 | steps.python.outputs.python-version }}-${{ 72 | hashFiles('setup.py') }}-${{ 73 | hashFiles('requirements.txt') }} 74 | - name: Fail job if Python cache restore failed 75 | if: steps.cache-venv.outputs.cache-hit != 'true' 76 | run: | 77 | echo "Failed to restore Python virtual environment from cache" 78 | exit 1 79 | - name: Restore pre-commit environment from cache 80 | id: cache-precommit 81 | uses: actions/cache@v2 82 | with: 83 | path: ${{ env.PRE_COMMIT_HOME }} 84 | key: | 85 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} 86 | restore-keys: | 87 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit- 88 | - name: Install pre-commit dependencies 89 | if: steps.cache-precommit.outputs.cache-hit != 'true' 90 | run: | 91 | . venv/bin/activate 92 | pre-commit install-hooks 93 | 94 | lint-black: 95 | name: Check black 96 | runs-on: ubuntu-latest 97 | needs: pre-commit 98 | steps: 99 | - name: Check out code from GitHub 100 | uses: actions/checkout@v2 101 | - name: Set up Python ${{ env.DEFAULT_PYTHON }} 102 | uses: actions/setup-python@v2.1.4 103 | id: python 104 | with: 105 | python-version: ${{ env.DEFAULT_PYTHON }} 106 | - name: Restore base Python virtual environment 107 | id: cache-venv 108 | uses: actions/cache@v2 109 | with: 110 | path: venv 111 | key: >- 112 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ 113 | steps.python.outputs.python-version }}-${{ 114 | hashFiles('setup.py') }}-${{ 115 | hashFiles('requirements.txt') }} 116 | - name: Fail job if Python cache restore failed 117 | if: steps.cache-venv.outputs.cache-hit != 'true' 118 | run: | 119 | echo "Failed to restore Python virtual environment from cache" 120 | exit 1 121 | - name: Restore pre-commit environment from cache 122 | id: cache-precommit 123 | uses: actions/cache@v2 124 | with: 125 | path: ${{ env.PRE_COMMIT_HOME }} 126 | key: | 127 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} 128 | - name: Fail job if cache restore failed 129 | if: steps.cache-venv.outputs.cache-hit != 'true' 130 | run: | 131 | echo "Failed to restore Python virtual environment from cache" 132 | exit 1 133 | - name: Run black 134 | run: | 135 | . venv/bin/activate 136 | pre-commit run --hook-stage manual black --all-files --show-diff-on-failure 137 | 138 | lint-flake8: 139 | name: Check flake8 140 | runs-on: ubuntu-latest 141 | needs: pre-commit 142 | steps: 143 | - name: Check out code from GitHub 144 | uses: actions/checkout@v2 145 | - name: Set up Python ${{ env.DEFAULT_PYTHON }} 146 | uses: actions/setup-python@v2.1.4 147 | id: python 148 | with: 149 | python-version: ${{ env.DEFAULT_PYTHON }} 150 | - name: Restore base Python virtual environment 151 | id: cache-venv 152 | uses: actions/cache@v2 153 | with: 154 | path: venv 155 | key: >- 156 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ 157 | steps.python.outputs.python-version }}-${{ 158 | hashFiles('setup.py') }}-${{ 159 | hashFiles('requirements.txt') }} 160 | - name: Fail job if Python cache restore failed 161 | if: steps.cache-venv.outputs.cache-hit != 'true' 162 | run: | 163 | echo "Failed to restore Python virtual environment from cache" 164 | exit 1 165 | - name: Restore pre-commit environment from cache 166 | id: cache-precommit 167 | uses: actions/cache@v2 168 | with: 169 | path: ${{ env.PRE_COMMIT_HOME }} 170 | key: | 171 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} 172 | - name: Fail job if cache restore failed 173 | if: steps.cache-venv.outputs.cache-hit != 'true' 174 | run: | 175 | echo "Failed to restore Python virtual environment from cache" 176 | exit 1 177 | - name: Register flake8 problem matcher 178 | run: | 179 | echo "::add-matcher::.github/workflows/matchers/flake8.json" 180 | - name: Run flake8 181 | run: | 182 | . venv/bin/activate 183 | pre-commit run --hook-stage manual flake8 --all-files 184 | 185 | lint-codespell: 186 | name: Check codespell 187 | runs-on: ubuntu-latest 188 | needs: pre-commit 189 | steps: 190 | - name: Check out code from GitHub 191 | uses: actions/checkout@v2 192 | - name: Set up Python ${{ env.DEFAULT_PYTHON }} 193 | uses: actions/setup-python@v2.1.4 194 | id: python 195 | with: 196 | python-version: ${{ env.DEFAULT_PYTHON }} 197 | - name: Restore base Python virtual environment 198 | id: cache-venv 199 | uses: actions/cache@v2 200 | with: 201 | path: venv 202 | key: >- 203 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ 204 | steps.python.outputs.python-version }}-${{ 205 | hashFiles('setup.py') }}-${{ 206 | hashFiles('requirements.txt') }} 207 | - name: Fail job if Python cache restore failed 208 | if: steps.cache-venv.outputs.cache-hit != 'true' 209 | run: | 210 | echo "Failed to restore Python virtual environment from cache" 211 | exit 1 212 | - name: Restore pre-commit environment from cache 213 | id: cache-precommit 214 | uses: actions/cache@v2 215 | with: 216 | path: ${{ env.PRE_COMMIT_HOME }} 217 | key: | 218 | ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} 219 | - name: Fail job if cache restore failed 220 | if: steps.cache-venv.outputs.cache-hit != 'true' 221 | run: | 222 | echo "Failed to restore Python virtual environment from cache" 223 | exit 1 224 | - name: Register codespell problem matcher 225 | run: | 226 | echo "::add-matcher::.github/workflows/matchers/codespell.json" 227 | - name: Run codespell 228 | run: | 229 | . venv/bin/activate 230 | pre-commit run --hook-stage manual codespell --all-files --show-diff-on-failure 231 | -------------------------------------------------------------------------------- /.github/workflows/matchers/codespell.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "codespell", 5 | "severity": "warning", 6 | "pattern": [ 7 | { 8 | "regexp": "^(.+):(\\d+):\\s(.+)$", 9 | "file": 1, 10 | "line": 2, 11 | "message": 3 12 | } 13 | ] 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/matchers/flake8.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "flake8-error", 5 | "severity": "error", 6 | "pattern": [ 7 | { 8 | "regexp": "^(.*):(\\d+):(\\d+):\\s([EF]\\d{3}\\s.*)$", 9 | "file": 1, 10 | "line": 2, 11 | "column": 3, 12 | "message": 4 13 | } 14 | ] 15 | }, 16 | { 17 | "owner": "flake8-warning", 18 | "severity": "warning", 19 | "pattern": [ 20 | { 21 | "regexp": "^(.*):(\\d+):(\\d+):\\s([CDNW]\\d{3}\\s.*)$", 22 | "file": 1, 23 | "line": 2, 24 | "column": 3, 25 | "message": 4 26 | } 27 | ] 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/matchers/python.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "python", 5 | "pattern": [ 6 | { 7 | "regexp": "^\\s*File\\s\\\"(.*)\\\",\\sline\\s(\\d+),\\sin\\s(.*)$", 8 | "file": 1, 9 | "line": 2 10 | }, 11 | { 12 | "regexp": "^\\s*raise\\s(.*)\\(\\'(.*)\\'\\)$", 13 | "message": 2 14 | } 15 | ] 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [published] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: '3.x' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools wheel Twine 25 | pip install -r requirements.txt 26 | - name: Build and publish 27 | env: 28 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 29 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 30 | run: | 31 | python setup.py sdist bdist_wheel 32 | twine upload dist/* 33 | - name: Upload binaries to release 34 | uses: svenstaro/upload-release-action@v2 35 | with: 36 | repo_token: ${{ secrets.GITHUB_TOKEN }} 37 | file: ./dist/*.whl 38 | tag: ${{ github.ref }} 39 | overwrite: true 40 | file_glob: true 41 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | # branches to consider in the event; optional, defaults to all 6 | branches: 7 | - master 8 | # pull_request event is required only for autolabeler 9 | pull_request: 10 | # Only following types are handled by the action, but one can default to all as well 11 | types: [opened, reopened, synchronize] 12 | 13 | jobs: 14 | update_release_draft: 15 | runs-on: ubuntu-latest 16 | steps: 17 | # (Optional) GitHub Enterprise requires GHE_HOST variable set 18 | #- name: Set GHE_HOST 19 | # run: | 20 | # echo "GHE_HOST=${GITHUB_SERVER_URL##https:\/\/}" >> $GITHUB_ENV 21 | 22 | # Drafts your next Release notes as Pull Requests are merged into "master" 23 | - uses: release-drafter/release-drafter@v5.15.0 24 | # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml 25 | # with: 26 | # config-name: my-config.yml 27 | # disable-autolabeler: true 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | **/.DS_Store -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: 23.3.0 4 | hooks: 5 | - id: black 6 | args: 7 | - --safe 8 | - --quiet 9 | 10 | - repo: https://github.com/pycqa/flake8 11 | rev: 3.9.2 12 | hooks: 13 | - id: flake8 14 | additional_dependencies: 15 | - flake8-docstrings==1.5.0 16 | - pydocstyle==5.1.1 17 | args: ['--max-line-length=500',"--ignore=E203,W503"] 18 | 19 | - repo: https://github.com/codespell-project/codespell 20 | rev: v1.17.1 21 | hooks: 22 | - id: codespell 23 | args: 24 | - --ignore-words-list=dout,hass,hsi 25 | - --skip="./.*" 26 | - --quiet-level=2 -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "ViewTester", 9 | "type": "python", 10 | "request": "launch", 11 | "module": "pyG5.pyG5ViewTester", 12 | "justMyCode": true 13 | }, 14 | { 15 | "name": "App", 16 | "type": "python", 17 | "request": "launch", 18 | "module": "pyG5.pyG5Main", 19 | "args": ["-v"], 20 | "justMyCode": true 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE.TXT: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # Copyright (c) 2019 Ben Lauret 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 20 | # OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyG5 2 | 3 | ![CI](https://github.com/blauret/pyg5/workflows/CI/badge.svg?branch=main) 4 | 5 | ## Description 6 | 7 | This project aims at development a Garmin G5 view targeting a Raspberry Pi 7 inches display (640x480) . The intent is to provide a G5 Attitude indicator + G5 Horizontal Situation Indicator stacked on the display in vertical mode. The `pyG5` connects to X-Plane flight simulator. 8 | 9 | It does not require any plugin and use the standard DREF UDP interface from X-Plane. It should not require any configuration. Start it and 10 | it will connect to X-Plane and fetch the required data. 11 | 12 | This is currently developed on macOS with python 3.9 and testing on a Raspberry Pi 4 with Raspberry Pi OS and an official 7 inches display in vertical mode. 13 | 14 | Below is a view of the user interface. 15 | 16 | ![demoView](https://raw.githubusercontent.com/blauret/pyG5/main/assets/demoView.png?raw=true) 17 | 18 | And you can see it in its simulation environment 19 | 20 | ![flightSimView](https://raw.githubusercontent.com/blauret/pyG5/main/assets/23-12-13%2019-58-23%201732.jpg) 21 | 22 | ## Second Display 23 | 24 | It's also possible to run pyG5 with a secondary window. In my sim setup this contains all the missing items to fly a C172 without display. In thery you could do a complete IFR flight switcing of displays right after take-off down to minimums. 25 | 26 | The secondary window contains: 27 | - Transpoder control and status, works with a touch screen to input code 28 | - Fuel selector status 29 | - Carb heat and Fuel pump status 30 | - Advisory Panel 31 | - Flap indicator 32 | - Trim indicator 33 | 34 | The view: 35 | 36 | ![secondWidget](https://raw.githubusercontent.com/blauret/pyG5/main/assets/Screenshot%202024-06-13%20at%2009.10.52.png) 37 | 38 | With the XPDR expanded: 39 | 40 | ![secondWidget](https://raw.githubusercontent.com/blauret/pyG5/main/assets/Screenshot%202024-06-13%20at%2009.10.54.png) 41 | 42 | 43 | 44 | ## Maturity 45 | 46 | It's currently in pretty early phase. It's functional and should be easy to install but might suffer from issues here and there. 47 | 48 | Not all the features of the G5 are implemented. It's currently missing: 49 | 50 | * Glide scope 51 | * lateral guidance on the AI 52 | * Distance to next way point on the Horizontal Situation Indicator. 53 | 54 | ## Installation 55 | 56 | `pyG5` depends on `pySide6`. Due to failure to install `pySide6` from pip on Raspberry Pi OS it is not 57 | a dependency of the `pyG5`. As a result it needs to be installed manually. 58 | 59 | ```console 60 | > sudo pip3 install pyside6 61 | ``` 62 | 63 | The install `PyG5`: 64 | 65 | ```console 66 | > sudo pip3 install pyG5 67 | ``` 68 | 69 | ## Running 70 | 71 | ```console 72 | > pyG5DualStacked 73 | ``` 74 | 75 | Running on Raspberry Pi it is recommended to install FreeSans fonts in order to be consistent with the rendering on the current main development platform, ie. macOS. Most liked this is solved with: 76 | 77 | ```console 78 | > sudo apt-get install libfreetype6 79 | ``` 80 | 81 | ## Developers 82 | 83 | If you intend to develop based on this project. At a glance: 84 | 85 | * The application runs on PyQt5 event loop. 86 | * It's loosely implementing a Model View Controller coding style 87 | * The `pyG5Network` contains X-Plane network interface is monitoring the connection and feed data at 30Hz to a slot 88 | * The view is repainting the interface every time the data is received from the network interface 89 | * The `pyG5Widget` is derived twice into and Horizontal Situation Indicator and an AI. the `pyG5DualStack` instantiate both into a single widget. That means it's easy to build the view with just one of them. 90 | * The `pyG5Main` module contains the application and the main window class. 91 | 92 | ### Running from sources 93 | 94 | Clone the repository 95 | 96 | ```console 97 | > git clone 98 | ``` 99 | 100 | Initialize the virtual environment 101 | 102 | ```console 103 | > source bootstrap.sh 104 | ``` 105 | 106 | Start the Application 107 | 108 | ```console 109 | > python -m pyG5.pyG5Main 110 | ``` 111 | 112 | In order to evaluate the design without X-Plane running you can use: 113 | 114 | ```console 115 | > python -m pyG5.pyG5ViewTester 116 | ``` 117 | 118 | This will feed the data from the sliders in the UI instead of the X-Plane network interface: 119 | 120 | ![ViewTester](https://raw.githubusercontent.com/blauret/pyG5/main/assets/pyG5ViewTester.png) 121 | 122 | ## License 123 | 124 | [License files](LICENSE.TXT) 125 | -------------------------------------------------------------------------------- /Scripts/pyG5DualStacked: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Script added to path running the pyG5 in dual stack mode.""" 3 | 4 | import sys 5 | 6 | from pyG5.pyG5Main import pyG5App 7 | 8 | """Main application.""" 9 | a = pyG5App() 10 | 11 | sys.exit(a.exec_()) 12 | -------------------------------------------------------------------------------- /assets/23-12-13 19-58-23 1732.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blauret/pyG5/4f571b7f8c3e7b11babb33efdcf86f797a0f4e58/assets/23-12-13 19-58-23 1732.jpg -------------------------------------------------------------------------------- /assets/Screenshot 2024-06-13 at 09.10.52.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blauret/pyG5/4f571b7f8c3e7b11babb33efdcf86f797a0f4e58/assets/Screenshot 2024-06-13 at 09.10.52.png -------------------------------------------------------------------------------- /assets/Screenshot 2024-06-13 at 09.10.54.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blauret/pyG5/4f571b7f8c3e7b11babb33efdcf86f797a0f4e58/assets/Screenshot 2024-06-13 at 09.10.54.png -------------------------------------------------------------------------------- /assets/demoView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blauret/pyG5/4f571b7f8c3e7b11babb33efdcf86f797a0f4e58/assets/demoView.png -------------------------------------------------------------------------------- /assets/flightSimView.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blauret/pyG5/4f571b7f8c3e7b11babb33efdcf86f797a0f4e58/assets/flightSimView.jpeg -------------------------------------------------------------------------------- /assets/pyG5ViewTester.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blauret/pyG5/4f571b7f8c3e7b11babb33efdcf86f797a0f4e58/assets/pyG5ViewTester.png -------------------------------------------------------------------------------- /bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | WIN_ACTIVATE=".venv/Scripts/activate" 4 | UNIX_ACTIVATE=".venv/bin/activate" 5 | 6 | if ! [ -e $UNIX_ACTIVATE ] || ! [ -e $WIN_ACTIVE ] ; 7 | then 8 | python3 -m venv .venv 9 | fi 10 | 11 | if [ -e $UNIX_ACTIVATE ] 12 | then 13 | source $UNIX_ACTIVATE 14 | else 15 | source $WIN_ACTIVATE 16 | fi 17 | pip3 install wheel 18 | pip3 install -r requirements.txt -------------------------------------------------------------------------------- /pyG5/__init__.py: -------------------------------------------------------------------------------- 1 | """pyG5 package.""" 2 | -------------------------------------------------------------------------------- /pyG5/pyG5Main.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on 8 Aug 2021. 3 | 4 | @author: Ben Lauret 5 | """ 6 | 7 | __version__ = "0.0.4" 8 | __appName__ = "pyG5" 9 | 10 | import argparse 11 | import logging 12 | import sys 13 | import platform 14 | 15 | 16 | from PySide6.QtCore import ( 17 | Qt, 18 | QTimer, 19 | QCoreApplication, 20 | QSettings, 21 | Slot, 22 | QByteArray, 23 | Signal, 24 | QEvent, 25 | ) 26 | from PySide6.QtGui import QFont, QFontDatabase, QCloseEvent, QAction 27 | from PySide6.QtWidgets import ( 28 | QApplication, 29 | QMainWindow, 30 | ) 31 | 32 | from pyG5.pyG5Network import pyG5NetWorkManager 33 | from pyG5.pyG5View import pyG5DualStackFMA, pyG5SecondaryWidget 34 | 35 | 36 | class pyG5App(QApplication): 37 | """pyG5App PySide6 application. 38 | 39 | Args: 40 | sys.argv 41 | 42 | Returns: 43 | self 44 | """ 45 | 46 | def __init__(self): 47 | """g5Widget Constructor. 48 | 49 | Args: 50 | parent: Parent Widget 51 | 52 | Returns: 53 | self 54 | """ 55 | QApplication.__init__(self, sys.argv) 56 | 57 | QCoreApplication.setOrganizationName("pyG5") 58 | QCoreApplication.setOrganizationDomain("pyg5.org") 59 | QCoreApplication.setApplicationName("pyG5") 60 | self.settings = QSettings( 61 | QSettings.Format.IniFormat, 62 | QSettings.Scope.UserScope, 63 | QCoreApplication.organizationDomain(), 64 | "pyG5", 65 | ) 66 | 67 | # parse the command line arguments 68 | self.argument_parser() 69 | 70 | # set the verbosity 71 | if self.args.verbose: 72 | logging.basicConfig(level=logging.DEBUG) 73 | else: 74 | logging.basicConfig(level=logging.INFO) 75 | 76 | logging.info("{} v{}".format(self.__class__.__name__, __version__)) 77 | 78 | self.networkManager = pyG5NetWorkManager() 79 | 80 | self.paintTimer = QTimer() 81 | self.paintTimer.timeout.connect( 82 | self.painTimerCB 83 | ) # Let the interpreter run each 500 ms. 84 | self.paintTimer.start(25) # You may change this if you wish. 85 | 86 | # The QWidget widget is the base class of all user interface objects in PySide6. 87 | self.mainWindow = pyG5MainWindow() 88 | 89 | self.networkManager.drefUpdate.connect( 90 | self.mainWindow.pyG5DualStacked.pyG5AI.drefHandler 91 | ) 92 | self.networkManager.drefUpdate.connect( 93 | self.mainWindow.pyG5DualStacked.pyG5HSI.drefHandler 94 | ) 95 | 96 | self.networkManager.drefUpdate.connect( 97 | self.mainWindow.pyG5DualStacked.pyG5FMA.drefHandler 98 | ) 99 | 100 | # Show window 101 | self.mainWindow.loadSettings() 102 | 103 | if platform.machine() == "aarch64": 104 | self.mainWindow.setWindowFlags( 105 | self.mainWindow.windowFlags() | Qt.FramelessWindowHint 106 | ) 107 | self.mainWindow.setWindowState(Qt.WindowFullScreen) 108 | 109 | self.mainWindow.show() 110 | 111 | if self.args.mode == "full": 112 | self.secondaryWindow = pyG5SecondWindow() 113 | 114 | self.secondaryWindow.loadSettings() 115 | 116 | if platform.machine() == "aarch64": 117 | self.secondaryWindow.setWindowFlags( 118 | self.secondaryWindow.windowFlags() | Qt.FramelessWindowHint 119 | ) 120 | 121 | self.secondaryWindow.setWindowState(Qt.WindowFullScreen) 122 | 123 | # connect the value coming from the simulator 124 | self.networkManager.drefUpdate.connect( 125 | self.secondaryWindow.cWidget.drefHandler 126 | ) 127 | 128 | # connect the value to update to the simulator 129 | self.secondaryWindow.cWidget.xpdrCodeSignal.connect( 130 | self.send_transponder_code 131 | ) 132 | self.secondaryWindow.cWidget.xpdrModeSignal.connect( 133 | self.send_transponder_mode 134 | ) 135 | 136 | self.secondaryWindow.show() 137 | 138 | self.secondaryWindow.closed.connect(self.mainWindow.close) 139 | self.mainWindow.closed.connect(self.secondaryWindow.close) 140 | 141 | def send_transponder_code(self, code): 142 | """Trigger the xpdr transmission to xplane.""" 143 | self.networkManager.write_data_ref("sim/cockpit/radios/transponder_code", code) 144 | 145 | def send_transponder_mode(self, mode): 146 | """Trigger the xpdr transmission to xplane.""" 147 | self.networkManager.write_data_ref("sim/cockpit/radios/transponder_mode", mode) 148 | 149 | def painTimerCB(self): 150 | """Trigger update of all the widgets.""" 151 | self.mainWindow.pyG5DualStacked.pyG5HSI.update() 152 | self.mainWindow.pyG5DualStacked.update() 153 | if self.args.mode == "full": 154 | self.secondaryWindow.cWidget.update() 155 | 156 | def argument_parser(self): 157 | """Initialize the arguments passed from the command line.""" 158 | self.parser = argparse.ArgumentParser( 159 | description="{} Application v{}".format(__appName__, __version__) 160 | ) 161 | self.parser.add_argument( 162 | "-v", "--verbose", help="increase verbosity", action="store_true" 163 | ) 164 | self.parser.add_argument( 165 | "-m", 166 | "--mode", 167 | help="Define the operating mode", 168 | choices=[ 169 | "hsi", 170 | "full", 171 | ], 172 | default="hsi", 173 | ) 174 | 175 | self.args = self.parser.parse_args() 176 | 177 | 178 | class pyG5BaseWindow(QMainWindow): 179 | """pyG5App PySide6 application. 180 | 181 | Args: 182 | sys.argv 183 | 184 | Returns: 185 | self 186 | """ 187 | 188 | closed = Signal() 189 | 190 | def __init__(self, parent=None): 191 | """g5Widget Constructor. 192 | 193 | Args: 194 | parent: Parent Widget 195 | 196 | Returns: 197 | self 198 | """ 199 | QMainWindow.__init__(self, parent) 200 | 201 | self.settings = QCoreApplication.instance().settings 202 | 203 | self.setStyleSheet("background-color: black;") 204 | 205 | target = "FreeSans" 206 | 207 | if target in QFontDatabase.families(): 208 | font = QFont(target) 209 | self.setFont(font) 210 | 211 | self.setWindowTitle(__appName__) 212 | action = QAction("&Quit", self) 213 | action.setShortcut("Ctrl+w") 214 | action.triggered.connect(self.close) 215 | self.addAction(action) 216 | 217 | def changeEvent(self, event): 218 | """Window change event overload. 219 | 220 | Args: 221 | event 222 | 223 | Returns: 224 | self 225 | """ 226 | if QEvent.Type.ActivationChange == event.type(): 227 | self.settings.setValue( 228 | "{}/windowState".format(self.__class__.__name__), self.saveState() 229 | ) 230 | elif QEvent.Type.WindowStateChange == event.type(): 231 | if Qt.WindowMinimized != self.windowState(): 232 | try: 233 | self.restoreState( 234 | self.settings.value( 235 | "{}/windowState".format(self.__class__.__name__) 236 | ) 237 | ) 238 | except Exception as inst: 239 | logging.warning("State restore: {}".format(inst)) 240 | pass 241 | 242 | def loadSettings(self): 243 | """Load settings helper.""" 244 | try: 245 | self.restoreGeometry( 246 | self.settings.value( 247 | "{}/geometry".format(self.__class__.__name__), QByteArray() 248 | ) 249 | ) 250 | self.restoreState( 251 | self.settings.value("{}/windowState".format(self.__class__.__name__)) 252 | ) 253 | except Exception as inst: 254 | logging.warning("State restore: {}".format(inst)) 255 | pass 256 | 257 | @Slot(QCloseEvent) 258 | def closeEvent(self, event): 259 | """Close event overload. 260 | 261 | Args: 262 | event 263 | 264 | Returns: 265 | self 266 | """ 267 | self.settings.setValue( 268 | "{}/geometry".format(self.__class__.__name__), self.saveGeometry() 269 | ) 270 | self.settings.setValue( 271 | "{}/windowState".format(self.__class__.__name__), self.saveState() 272 | ) 273 | event.accept() 274 | self.closed.emit() 275 | QMainWindow.closeEvent(self, event) 276 | 277 | 278 | class pyG5MainWindow(pyG5BaseWindow): 279 | """pyG5App PySide6 application. 280 | 281 | Args: 282 | sys.argv 283 | 284 | Returns: 285 | self 286 | """ 287 | 288 | closed = Signal() 289 | 290 | def __init__(self, parent=None): 291 | """g5Widget Constructor. 292 | 293 | Args: 294 | parent: Parent Widget 295 | 296 | Returns: 297 | self 298 | """ 299 | pyG5BaseWindow.__init__(self, parent) 300 | 301 | self.pyG5DualStacked = pyG5DualStackFMA() 302 | 303 | self.setCentralWidget(self.pyG5DualStacked) 304 | 305 | 306 | class pyG5SecondWindow(pyG5BaseWindow): 307 | """pyG5App PyQt5 application. 308 | 309 | Args: 310 | sys.argv 311 | 312 | Returns: 313 | self 314 | """ 315 | 316 | closed = Signal() 317 | 318 | def __init__(self, parent=None): 319 | """g5Widget Constructor. 320 | 321 | Args: 322 | parent: Parent Widget 323 | 324 | Returns: 325 | self 326 | """ 327 | pyG5BaseWindow.__init__(self, parent) 328 | 329 | self.cWidget = pyG5SecondaryWidget() 330 | 331 | self.setCentralWidget(self.cWidget) 332 | 333 | 334 | if __name__ == "__main__": 335 | """Main application.""" 336 | a = pyG5App() 337 | 338 | sys.exit(a.exec()) 339 | 340 | pass 341 | -------------------------------------------------------------------------------- /pyG5/pyG5Network.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on 8 Aug 2021. 3 | 4 | @author: Ben Lauret 5 | """ 6 | 7 | import platform 8 | import logging 9 | import struct 10 | import binascii 11 | import os 12 | from datetime import datetime as datetime_, timedelta 13 | 14 | from PySide6.QtCore import QObject, Slot, Signal, QTimer 15 | 16 | from PySide6.QtNetwork import QUdpSocket, QHostAddress, QAbstractSocket 17 | 18 | from PySide6 import QtGui 19 | 20 | 21 | class pyG5NetWorkManager(QObject): 22 | """pyG5NetWorkManager Object. 23 | 24 | This object listen on the XPlane multicast group 25 | and emit on the xpInstance signal the host address 26 | and port of the Xplane on the network 27 | 28 | Args: 29 | parent: Parent Widget 30 | 31 | Returns: 32 | self 33 | """ 34 | 35 | drefUpdate = Signal(object) 36 | 37 | def __init__(self, parent=None): 38 | """Object constructor. 39 | 40 | Args: 41 | parent: Parent Widget 42 | 43 | Returns: 44 | self 45 | """ 46 | QObject.__init__(self, parent) 47 | 48 | self.xpHost = None 49 | # list the datarefs to request 50 | self.datarefs = [ 51 | # ( dataref, frequency, unit, description, num decimals to display in formatted output ) 52 | ( 53 | "sim/cockpit/radios/nav1_dme_dist_m", 54 | 30, 55 | "kt", 56 | "dme Range anv1", 57 | 0, 58 | "_nav1dme", 59 | ), 60 | ( 61 | "sim/cockpit/radios/nav2_dme_dist_m", 62 | 30, 63 | "kt", 64 | "dme Range nav2", 65 | 0, 66 | "_nav2dme", 67 | ), 68 | ( 69 | "sim/cockpit2/radios/indicators/nav1_bearing_deg_mag", 70 | 30, 71 | "degrees", 72 | "Nav bearing", 73 | 0, 74 | "_nav1bearing", 75 | ), 76 | ( 77 | "sim/cockpit2/radios/indicators/nav2_bearing_deg_mag", 78 | 30, 79 | "degrees", 80 | "Nav bearing", 81 | 0, 82 | "_nav2bearing", 83 | ), 84 | ( 85 | "sim/cockpit2/autopilot/altitude_hold_ft", 86 | 20, 87 | "ft", 88 | "Altitude Hold", 89 | 0, 90 | "_altitudeHold", 91 | ), 92 | ( 93 | "sim/cockpit2/autopilot/altitude_vnav_ft", 94 | 20, 95 | "ft", 96 | "Altitude VNAV", 97 | 0, 98 | "_altitudeVNAV", 99 | ), 100 | ( 101 | "sim/cockpit2/radios/indicators/nav_src_ref", 102 | 20, 103 | "enum", 104 | "NAV source", 105 | 0, 106 | "_navSrc", 107 | ), 108 | ( 109 | "sim/cockpit/autopilot/altitude", 110 | 20, 111 | "feet", 112 | "AP altitude selected", 113 | 0, 114 | "_apAltitude", 115 | ), 116 | ( 117 | "sim/cockpit/autopilot/vertical_velocity", 118 | 20, 119 | "fpm", 120 | "NAV source", 121 | 0, 122 | "_apVS", 123 | ), 124 | ( 125 | "sim/cockpit/autopilot/airspeed", 126 | 20, 127 | "kt", 128 | "AP air speed", 129 | 0, 130 | "_apAirSpeed", 131 | ), 132 | ( 133 | "sim/cockpit/autopilot/autopilot_mode", 134 | 20, 135 | "enum", 136 | "AP mode", 137 | 0, 138 | "_apMode", 139 | ), 140 | ( 141 | "sim/cockpit/autopilot/autopilot_state", 142 | 20, 143 | "enum", 144 | "AP state", 145 | 0, 146 | "_apState", 147 | ), 148 | ( 149 | "sim/flightmodel/controls/parkbrake", 150 | 1, 151 | "onoff", 152 | "Parking brake set", 153 | 0, 154 | "_parkBrake", 155 | ), 156 | ( 157 | "sim/cockpit/warnings/annunciators/fuel_quantity", 158 | 1, 159 | "onoff", 160 | "fuel selector", 161 | 0, 162 | "_lowFuel", 163 | ), 164 | ( 165 | "sim/cockpit/warnings/annunciators/oil_pressure_low[0]", 166 | 1, 167 | "onoff", 168 | "fuel selector", 169 | 0, 170 | "_oilPres", 171 | ), 172 | ( 173 | "sim/cockpit/warnings/annunciators/fuel_pressure_low[0]", 174 | 1, 175 | "onoff", 176 | "fuel selector", 177 | 0, 178 | "_fuelPress", 179 | ), 180 | ( 181 | "sim/cockpit/warnings/annunciators/low_vacuum", 182 | 1, 183 | "onoff", 184 | "fuel selector", 185 | 0, 186 | "_lowVacuum", 187 | ), 188 | ( 189 | "sim/cockpit/warnings/annunciators/low_voltage", 190 | 1, 191 | "onoff", 192 | "fuel selector", 193 | 0, 194 | "_lowVolts", 195 | ), 196 | ( 197 | "sim/cockpit2/fuel/fuel_tank_selector", 198 | 30, 199 | "onoff", 200 | "fuel selector", 201 | 0, 202 | "_fuelSel", 203 | ), 204 | ( 205 | "sim/cockpit2/engine/actuators/carb_heat_ratio[0]", 206 | 30, 207 | "onoff", 208 | "fuel pump on", 209 | 0, 210 | "_carbheat", 211 | ), 212 | ( 213 | "sim/cockpit/engine/fuel_pump_on[0]", 214 | 10, 215 | "onoff", 216 | "fuel pump on", 217 | 0, 218 | "_fuelpump", 219 | ), 220 | ( 221 | "sim/flightmodel/controls/elv_trim", 222 | 30, 223 | "mode", 224 | "Transponder mode", 225 | 0, 226 | "_trims", 227 | ), 228 | ( 229 | "sim/flightmodel/controls/flaprat", 230 | 30, 231 | "mode", 232 | "Transponder mode", 233 | 0, 234 | "_flaps", 235 | ), 236 | ( 237 | "sim/cockpit/radios/transponder_mode", 238 | 5, 239 | "mode", 240 | "Transponder mode", 241 | 0, 242 | "_xpdrMode", 243 | ), 244 | ( 245 | "sim/cockpit/radios/transponder_code", 246 | 5, 247 | "code", 248 | "Transponder code", 249 | 0, 250 | "_xpdrCode", 251 | ), 252 | ( 253 | "sim/cockpit/radios/gps_dme_dist_m", 254 | 1, 255 | "Gs", 256 | "GPS GS available", 257 | 0, 258 | "_gpsdmedist", 259 | ), 260 | ( 261 | "sim/cockpit2/radios/indicators/fms_fpta_pilot", 262 | 1, 263 | "Gs", 264 | "GPS GS available", 265 | 0, 266 | "_gpsvnavavailable", 267 | ), 268 | ( 269 | # int n enum GPS CDI sensitivity: 0=OCN, 1=ENR, 2=TERM, 3=DPRT, 4=MAPR, 5=APR, 6=RNPAR, 7=LNAV, 8=LNAV+V, 9=L/VNAV, 10=LP, 11=LPV, 12=LP+V, 13=GLS 270 | "sim/cockpit/radios/gps_cdi_sensitivity", 271 | 1, 272 | "index", 273 | "GPS Horizontal Situation Indicator sensitivity mode", 274 | 0, 275 | "_gpshsisens", 276 | ), 277 | ( 278 | "sim/cockpit/radios/gps_has_glideslope", 279 | 1, 280 | "Gs", 281 | "GPS GS available", 282 | 0, 283 | "_gpsgsavailable", 284 | ), 285 | ( 286 | "sim/cockpit/radios/gps_gp_mtr_per_dot", 287 | 1, 288 | "boolean", 289 | "Avionics powered on", 290 | 0, 291 | "_gpsvsens", 292 | ), 293 | ( 294 | "sim/cockpit/radios/nav_type[0]", 295 | 1, 296 | "boolean", 297 | "Avionics powered on", 298 | 0, 299 | "_nav1type", 300 | ), 301 | ( 302 | "sim/cockpit/radios/nav_type[1]", 303 | 1, 304 | "boolean", 305 | "Avionics powered on", 306 | 0, 307 | "_nav2type", 308 | ), 309 | ( 310 | "sim/cockpit/gps/destination_type", 311 | 1, 312 | "boolean", 313 | "Avionics powered on", 314 | 0, 315 | "_gpstype", 316 | ), 317 | ( 318 | "sim/cockpit/electrical/avionics_on", 319 | 1, 320 | "boolean", 321 | "Avionics powered on", 322 | 0, 323 | "_avionicson", 324 | ), 325 | ( 326 | "sim/cockpit/radios/nav1_vdef_dot", 327 | 30, 328 | "Dots", 329 | "NAV1 Vertical deviation in dots", 330 | 0, 331 | "_nav1gs", 332 | ), 333 | ( 334 | "sim/cockpit/radios/nav2_vdef_dot", 335 | 30, 336 | "Dots", 337 | "NAV2 Vertical deviation in dots", 338 | 0, 339 | "_nav2gs", 340 | ), 341 | ( 342 | "sim/cockpit/radios/gps_vdef_dot", 343 | 30, 344 | "Dots", 345 | "GPS Vertical deviation in dots", 346 | 0, 347 | "_gpsgs", 348 | ), 349 | ( 350 | "sim/cockpit/radios/nav1_CDI", 351 | 30, 352 | "Gs", 353 | "Nav 1 GS available", 354 | 0, 355 | "_nav1gsavailable", 356 | ), 357 | ( 358 | "sim/cockpit/radios/nav2_CDI", 359 | 30, 360 | "Gs", 361 | "Nav 2 GS available", 362 | 0, 363 | "_nav2gsavailable", 364 | ), 365 | ( 366 | "sim/cockpit2/gauges/indicators/airspeed_acceleration_kts_sec_pilot", 367 | 30, 368 | "Gs", 369 | "GPS CRS", 370 | 0, 371 | "_kiasDelta", 372 | ), 373 | ( 374 | "sim/cockpit2/radios/actuators/HSI_source_select_pilot", 375 | 30, 376 | "°", 377 | "GPS CRS", 378 | 0, 379 | "_hsiSource", 380 | ), 381 | ( 382 | "sim/cockpit2/radios/indicators/nav1_flag_from_to_pilot", 383 | 30, 384 | "°", 385 | "NAV1 CRS", 386 | 0, 387 | "_nav1fromto", 388 | ), 389 | ( 390 | "sim/cockpit2/radios/indicators/nav2_flag_from_to_pilot", 391 | 30, 392 | "°", 393 | "NAV2 CRS", 394 | 0, 395 | "_nav2fromto", 396 | ), 397 | ( 398 | "sim/cockpit/radios/gps_fromto", 399 | 30, 400 | "°", 401 | "NAV2 CRS", 402 | 0, 403 | "_gpsfromto", 404 | ), 405 | ( 406 | "sim/cockpit/radios/nav1_obs_degm", 407 | 30, 408 | "°", 409 | "NAV1 CRS", 410 | 0, 411 | "_nav1crs", 412 | ), 413 | ( 414 | "sim/cockpit/radios/nav2_obs_degm", 415 | 30, 416 | "°", 417 | "NAV2 CRS", 418 | 0, 419 | "_nav2crs", 420 | ), 421 | ( 422 | "sim/cockpit/radios/gps_course_degtm", 423 | 30, 424 | "°", 425 | "GPS CRS", 426 | 0, 427 | "_gpscrs", 428 | ), 429 | ( 430 | "sim/cockpit/radios/gps_course_degtm", 431 | 30, 432 | "°", 433 | "GPS CRS", 434 | 0, 435 | "_nav1dev", 436 | ), 437 | ( 438 | "sim/cockpit/radios/nav1_hdef_dot", 439 | 30, 440 | "°", 441 | "NAV1 VOR coursedeflection", 442 | 0, 443 | "_nav1dft", 444 | ), 445 | ( 446 | "sim/cockpit/radios/nav2_hdef_dot", 447 | 30, 448 | "°", 449 | "NAV1 VOR course deflection", 450 | 0, 451 | "_nav2dft", 452 | ), 453 | ( 454 | "sim/cockpit/radios/gps_hdef_dot", 455 | 30, 456 | "°", 457 | "GPS course deflection", 458 | 0, 459 | "_gpsdft", 460 | ), 461 | ( 462 | "sim/flightmodel/position/magnetic_variation", 463 | 30, 464 | "°", 465 | "Ground track heading", 466 | 0, 467 | "_magneticVariation", 468 | ), 469 | ( 470 | "sim/cockpit2/gauges/indicators/ground_track_mag_pilot", 471 | 30, 472 | "°", 473 | "Ground track heading", 474 | 0, 475 | "_groundTrack", 476 | ), 477 | ( 478 | "sim/cockpit/autopilot/heading_mag", 479 | 30, 480 | "°", 481 | "Horizontal Situation Indicator bug", 482 | 0, 483 | "_headingBug", 484 | ), 485 | ( 486 | "sim/weather/wind_direction_degt", 487 | 30, 488 | "°", 489 | "The effective direction of the wind at the plane's location", 490 | 0, 491 | "_windDirection", 492 | ), 493 | ( 494 | "sim/weather/wind_speed_kt", 495 | 30, 496 | "kt", 497 | "The effective speed of the wind at the plane's location.", 498 | 0, 499 | "_windSpeed", 500 | ), 501 | ( 502 | "sim/flightmodel/position/mag_psi", 503 | 30, 504 | "°", 505 | "Magnetic heading of the aircraft", 506 | 0, 507 | "_magHeading", 508 | ), 509 | ( 510 | "sim/flightmodel/position/phi", 511 | 30, 512 | "°", 513 | "Roll of the aircraft", 514 | 0, 515 | "_rollAngle", 516 | ), 517 | ( 518 | "sim/flightmodel/position/theta", 519 | 30, 520 | "°", 521 | "Pitch of the aircraft", 522 | 0, 523 | "_pitchAngle", 524 | ), 525 | ( 526 | "sim/flightmodel/position/indicated_airspeed", 527 | 30, 528 | "kt", 529 | "Indicated airpseed", 530 | 0, 531 | "_kias", 532 | ), 533 | ( 534 | "sim/cockpit2/gauges/indicators/true_airspeed_kts_pilot", 535 | 30, 536 | "kt", 537 | "Indicated airpseed", 538 | 0, 539 | "_ktas", 540 | ), 541 | ( 542 | "sim/flightmodel/position/groundspeed", 543 | 30, 544 | "kt", 545 | "Indicated airpseed", 546 | 0, 547 | "_gs", 548 | ), 549 | ( 550 | "sim/cockpit2/gauges/indicators/altitude_ft_pilot", 551 | 30, 552 | "feet", 553 | "Altitude", 554 | 0, 555 | "_altitude", 556 | ), 557 | ( 558 | "sim/cockpit2/autopilot/altitude_dial_ft", 559 | 30, 560 | "feet", 561 | "Altitude", 562 | 0, 563 | "_altitudeSel", 564 | ), 565 | ( 566 | "sim/cockpit2/gauges/actuators/barometer_setting_in_hg_pilot", 567 | 30, 568 | "feet", 569 | "Altimeter setting", 570 | 0, 571 | "_alt_setting", 572 | ), 573 | ( 574 | "sim/physics/metric_press", 575 | 1, 576 | "feet", 577 | "Altimeter setting", 578 | 0, 579 | "_alt_setting_metric", 580 | ), 581 | ( 582 | "sim/cockpit2/gauges/indicators/slip_deg", 583 | 30, 584 | "°", 585 | "Slip angle", 586 | 0, 587 | "_slip", 588 | ), 589 | ( 590 | "sim/cockpit2/gauges/indicators/turn_rate_heading_deg_pilot", 591 | 30, 592 | "°", 593 | "Turn Rate", 594 | 0, 595 | "_turnRate", 596 | ), 597 | ( 598 | "sim/flightmodel/position/vh_ind_fpm", 599 | 30, 600 | "kt", 601 | "Indicated airpseed", 602 | 0, 603 | "_vh_ind_fpm", 604 | ), 605 | ( 606 | "sim/aircraft/view/acf_Vso", 607 | 1, 608 | "kt", 609 | "stall speed", 610 | 0, 611 | "_vs0", 612 | ), 613 | ( 614 | "sim/aircraft/view/acf_Vs", 615 | 1, 616 | "kt", 617 | "stall in Landing configuration speed", 618 | 0, 619 | "_vs", 620 | ), 621 | ( 622 | "sim/aircraft/view/acf_Vfe", 623 | 1, 624 | "kt", 625 | "flap extended speed", 626 | 0, 627 | "_vfe", 628 | ), 629 | ( 630 | "sim/aircraft/view/acf_Vno", 631 | 1, 632 | "kt", 633 | "normal operation speed", 634 | 0, 635 | "_vno", 636 | ), 637 | ( 638 | "sim/aircraft/view/acf_Vne", 639 | 1, 640 | "kt", 641 | "never exceed speed", 642 | 0, 643 | "_vne", 644 | ), 645 | ] 646 | 647 | self.logger = logging.getLogger(self.__class__.__name__) 648 | 649 | # Idle timer trigger reconnection 650 | self.idleTimerDuration = 10000 651 | self.idleTimer = QTimer() 652 | self.idleTimer.timeout.connect(self.reconnect) 653 | 654 | # Create local UDP socket 655 | self.udpSock = QUdpSocket(self) 656 | 657 | # manage received data 658 | self.udpSock.readyRead.connect(self.dataHandler) 659 | 660 | # manage stage change to send data request 661 | self.udpSock.stateChanged.connect(self.socketStateHandler) 662 | 663 | # bind the socket 664 | self.udpSock.bind( 665 | QHostAddress.SpecialAddress.AnyIPv4, 0, QUdpSocket.BindFlag.ShareAddress 666 | ) 667 | 668 | @Slot() 669 | def write_data_ref(self, path, data): 670 | """Idle timer expired. Trigger reconnection process.""" 671 | cmd = b"DREF\x00" # DREF command 672 | message = struct.pack("<5sf", cmd, data) 673 | message += bytes(path, "utf-8") + b"\x00" 674 | message += " ".encode("utf-8") * (509 - len(message)) 675 | if self.xpHost: 676 | self.udpSock.writeDatagram(message, self.xpHost, self.xpPort) 677 | 678 | @Slot() 679 | def reconnect(self): 680 | """Idle timer expired. Trigger reconnection process.""" 681 | self.logger.info("Connection Timeout expired") 682 | 683 | self.udpSock.close() 684 | self.idleTimer.stop() 685 | 686 | # let the screensaver activate 687 | if platform.machine() in "aarch64": 688 | os.system("xset s on") 689 | os.system("xset s 1") 690 | 691 | @Slot(QHostAddress, int) 692 | def xplaneConnect(self, addr, port): 693 | """Slot connecting triggering the connection to the XPlane.""" 694 | self.listener.xpInstance.disconnect(self.xplaneConnect) 695 | self.listener.deleteLater() 696 | 697 | self.xpHost = addr 698 | self.xpPort = port 699 | 700 | self.logger.info("Request datatefs") 701 | # initiate connection 702 | for idx, dataref in enumerate(self.datarefs): 703 | cmd = b"RREF\x00" # RREF command 704 | freq = dataref[1] 705 | ref = dataref[0].encode() 706 | message = struct.pack("<5sii400s", cmd, freq, idx, ref) 707 | self.logger.info("Request datatefs: {}".format(ref)) 708 | assert len(message) == 413 709 | self.udpSock.writeDatagram(message, addr, port) 710 | end = datetime_.now() + timedelta(milliseconds=20) 711 | while datetime_.now() < end: 712 | QtGui.QGuiApplication.processEvents() 713 | 714 | # start the idle timer 715 | self.idleTimer.start(self.idleTimerDuration) 716 | 717 | # now we can inhibit the screensaver 718 | if platform.machine() in "aarch64": 719 | os.system("xset s reset") 720 | os.system("xset s off") 721 | 722 | @Slot() 723 | def socketStateHandler(self): 724 | """Socket State handler.""" 725 | self.logger.info("socketStateHandler: {}".format(self.udpSock.state())) 726 | 727 | if self.udpSock.state() == QAbstractSocket.SocketState.BoundState: 728 | self.logger.info("Started Multicast listenner") 729 | # instantiate the multicast listener 730 | self.listener = pyG5MulticastListener(self) 731 | 732 | # connect the multicast listenner to the connect function 733 | self.listener.xpInstance.connect(self.xplaneConnect) 734 | 735 | elif self.udpSock.state() == QAbstractSocket.SocketState.UnconnectedState: 736 | # socket got disconnected issue reconnection 737 | self.udpSock.bind( 738 | QHostAddress.SpecialAddress.AnyIPv4, 0, QUdpSocket.BindFlag.ShareAddress 739 | ) 740 | 741 | @Slot() 742 | def dataHandler(self): 743 | """dataHandler.""" 744 | # data received restart the idle timer 745 | self.idleTimer.start(self.idleTimerDuration) 746 | 747 | while self.udpSock.hasPendingDatagrams(): 748 | datagram = self.udpSock.receiveDatagram() 749 | data = datagram.data() 750 | retvalues = {} 751 | # Read the Header "RREFO". 752 | header = data[0:5] 753 | if header != b"RREF,": 754 | self.logger.error("Unknown packet: ", binascii.hexlify(data)) 755 | else: 756 | # We get 8 bytes for every dataref sent: 757 | # An integer for idx and the float value. 758 | values = data[5:] 759 | lenvalue = 8 760 | numvalues = int(len(values) / lenvalue) 761 | idx = 0 762 | value = 0 763 | for i in range(0, numvalues): 764 | singledata = data[(5 + lenvalue * i) : (5 + lenvalue * (i + 1))] 765 | (idx, value) = struct.unpack("= 4: 248 | return "LOC" + navIndex 249 | 250 | logging.error("Failed to decode navtype") 251 | 252 | 253 | secWidth = 800 254 | secHeight = 480 255 | 256 | 257 | class pyG5SecondaryWidget(pyG5Widget): 258 | """Generate G5 wdiget view.""" 259 | 260 | xpdrCodeSignal = Signal(int) 261 | xpdrModeSignal = Signal(int) 262 | 263 | def __init__(self, parent=None): 264 | """g5Widget Constructor. 265 | 266 | Args: 267 | parent: Parent Widget 268 | 269 | Returns: 270 | self 271 | """ 272 | pyG5Widget.__init__(self, parent) 273 | 274 | self.xpdrKeyboard = False 275 | 276 | self.setFixedSize(secWidth, secHeight) 277 | 278 | self.xpdrXbase = 20 279 | self.xpdrYbase = 20 280 | 281 | self.xpdrwidth = 160 282 | self.xpdrheight = 40 283 | 284 | self.xpdrRect = QRectF( 285 | self.xpdrXbase, self.xpdrYbase, self.xpdrwidth, self.xpdrheight 286 | ) 287 | 288 | self.xpdrKeyXbase = 20 289 | self.xpdrKeyYbase = self.xpdrYbase + self.xpdrheight 290 | 291 | self.xpdrKeyWidth = 420 292 | self.xpdrKeyHeight = secHeight - (self.xpdrYbase + self.xpdrheight) - 20 293 | 294 | self.xpdrkeyRect = QRectF( 295 | self.xpdrKeyXbase, self.xpdrKeyYbase, self.xpdrKeyWidth, self.xpdrKeyHeight 296 | ) 297 | 298 | self.xpdrPos = 3 299 | 300 | self.keyArea = [] 301 | 302 | index = 0 303 | for i in [1, 2, 3, 4]: 304 | rect = QRectF( 305 | self.xpdrKeyXbase + 26.125 + index * 95, 306 | self.xpdrKeyYbase + 20, 307 | 82.5, 308 | 82.5, 309 | ) 310 | self.keyArea.append([rect, i]) 311 | index += 1 312 | 313 | index = 0 314 | for i in [5, 6, 7, 0]: 315 | rect = QRectF( 316 | self.xpdrKeyXbase + 26.125 + index * 95, 317 | self.xpdrKeyYbase + 20 + 95 + 20, 318 | 82.5, 319 | 82.5, 320 | ) 321 | self.keyArea.append([rect, i]) 322 | index += 1 323 | 324 | self.keyCtrlArea = [] 325 | self.keyCtrlArea.append( 326 | [ 327 | QRectF( 328 | self.xpdrKeyXbase, 329 | self.xpdrKeyYbase + self.xpdrKeyHeight / 2 + 40, 330 | self.xpdrKeyWidth / 2, 331 | self.xpdrKeyHeight / 4 - 20, 332 | ), 333 | "OFF", 334 | 0, 335 | ] 336 | ) 337 | self.keyCtrlArea.append( 338 | [ 339 | QRectF( 340 | self.xpdrKeyXbase, 341 | self.xpdrKeyYbase + self.xpdrKeyHeight * 3 / 4 + 20, 342 | self.xpdrKeyWidth / 2, 343 | self.xpdrKeyHeight / 4 - 20, 344 | ), 345 | "ON", 346 | 2, 347 | ] 348 | ) 349 | 350 | self.keyCtrlArea.append( 351 | [ 352 | QRectF( 353 | self.xpdrKeyXbase + self.xpdrKeyWidth / 2, 354 | self.xpdrKeyYbase + self.xpdrKeyHeight / 2 + 40, 355 | self.xpdrKeyWidth / 2, 356 | self.xpdrKeyHeight / 4 - 20, 357 | ), 358 | "STBY", 359 | 1, 360 | ] 361 | ) 362 | 363 | self.keyCtrlArea.append( 364 | [ 365 | QRectF( 366 | self.xpdrKeyXbase + self.xpdrKeyWidth / 2, 367 | self.xpdrKeyYbase + self.xpdrKeyHeight * 3 / 4 + 20, 368 | self.xpdrKeyWidth / 2, 369 | self.xpdrKeyHeight / 4 - 20, 370 | ), 371 | "ALT", 372 | 3, 373 | ] 374 | ) 375 | 376 | def mousePressEvent(self, event): 377 | """Mouse Pressed event overload.""" 378 | if self._avionicson: 379 | if self.xpdrRect.contains(event.position()): 380 | self.xpdrKeyboard = not self.xpdrKeyboard 381 | 382 | else: 383 | if self.xpdrkeyRect.contains(event.position()): 384 | for key in self.keyArea: 385 | if key[0].contains(event.position()): 386 | # the input is a BCD value received as integer. 387 | # First step is to turn it into a real integer 388 | codestr = "{:04d}".format(int(self._xpdrCode)) 389 | code = 0 390 | for idx, c in enumerate(codestr): 391 | code |= int(c) << (4 * (3 - idx)) 392 | 393 | # code is the integer value of the string 394 | # now apply on code the new number 395 | code = code & ((0xF << (4 * self.xpdrPos)) ^ 0xFFFF) 396 | code = code | (key[1] << (4 * self.xpdrPos)) 397 | 398 | # shift the position we update 399 | self.xpdrPos = (self.xpdrPos - 1) % 4 400 | 401 | # emit the new code value 402 | self._xpdrCode = int("{:04x}".format(code)) 403 | self.xpdrCodeSignal.emit(self._xpdrCode) 404 | 405 | for key in self.keyCtrlArea: 406 | if key[0].contains(event.position()): 407 | self.xpdrMode(key[2]) 408 | self.xpdrModeSignal.emit(key[2]) 409 | self.xpdrKeyboard = False 410 | else: 411 | self.xpdrKeyboard = False 412 | 413 | if not self.xpdrKeyboard: 414 | self.xpdrPos = 3 415 | self.update() 416 | 417 | def paintEvent(self, event): 418 | """Paint the widget.""" 419 | self.qp = QPainter(self) 420 | 421 | # Draw the background 422 | self.setPen(1, Qt.GlobalColor.black) 423 | self.qp.setBrush(QBrush(Qt.GlobalColor.black)) 424 | self.qp.drawRect(0, 0, secWidth, secHeight) 425 | 426 | self.setPen(1, Qt.GlobalColor.white) 427 | self.qp.setBrush(QBrush(Qt.GlobalColor.black)) 428 | 429 | # flaps settings 430 | flapXBase = 620 431 | flapYBase = 20 432 | flapHeight = secHeight - 40 433 | flapWidth = 130 434 | 435 | self.setPen(1, Qt.GlobalColor.white) 436 | self.qp.setBrush(QBrush(Qt.GlobalColor.white)) 437 | font = self.qp.font() 438 | font.setPixelSize(30) 439 | font.setBold(True) 440 | self.qp.setFont(font) 441 | 442 | # draw the title 443 | self.qp.drawText( 444 | QRectF( 445 | flapXBase, 446 | flapYBase, 447 | flapWidth, 448 | 40, 449 | ), 450 | Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTop, 451 | "FLAPS", 452 | ) 453 | 454 | font.setPixelSize(20) 455 | font.setBold(False) 456 | self.qp.setFont(font) 457 | 458 | # draw the flaps angle legend 459 | for i in range(0, 4): 460 | self.qp.drawText( 461 | QRectF( 462 | flapXBase, 463 | flapYBase + 40 + int((flapHeight) * i / 4), 464 | 40, 465 | 40, 466 | ), 467 | Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter, 468 | "{:02d}°".format(10 * i), 469 | ) 470 | 471 | # draw the indicator rectangle 472 | self.qp.setBrush(QBrush(Qt.GlobalColor.black)) 473 | self.qp.drawRect(flapXBase + 90, flapYBase + 40, 40, flapHeight - 40) 474 | 475 | # draw the indicator legend white 476 | self.setPen(1, Qt.GlobalColor.white) 477 | self.qp.setBrush(QBrush(Qt.GlobalColor.white)) 478 | rect = QRectF( 479 | flapXBase + 50, 480 | flapYBase + 40 + int((flapHeight - 40) / 3), 481 | 40, 482 | flapHeight - 40 - +int((flapHeight - 40) / 3), 483 | ) 484 | self.qp.drawRect(rect) 485 | self.setPen(1, Qt.GlobalColor.black) 486 | self.qp.setBrush(QBrush(Qt.GlobalColor.black)) 487 | self.qp.drawText( 488 | rect, Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter, "8\n5" 489 | ) 490 | 491 | # draw the indicator legend cyan 492 | self.setPen(1, Qt.GlobalColor.cyan) 493 | self.qp.setBrush(QBrush(Qt.GlobalColor.cyan)) 494 | rect = QRectF( 495 | flapXBase + 50, flapYBase + 40, 40, int((flapHeight - 40) / 3 + 20) 496 | ) 497 | self.qp.drawRect(rect) 498 | self.setPen(1, Qt.GlobalColor.black) 499 | self.qp.setBrush(QBrush(Qt.GlobalColor.black)) 500 | self.qp.drawText( 501 | rect, 502 | Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter, 503 | "1\n1\n0", 504 | ) 505 | 506 | self.setPen(1, Qt.GlobalColor.white) 507 | self.qp.setBrush(QBrush(Qt.GlobalColor.white)) 508 | 509 | self.qp.drawPolygon( 510 | QPolygonF( 511 | [ 512 | QPointF( 513 | flapXBase + flapWidth, 514 | flapYBase + 50 + self._flaps * int((flapHeight - 100)), 515 | ), 516 | QPointF( 517 | flapXBase + flapWidth, 518 | flapYBase + 70 + self._flaps * int((flapHeight - 100)), 519 | ), 520 | QPointF( 521 | flapXBase + flapWidth - 30, 522 | flapYBase + 70 + self._flaps * int((flapHeight - 100)), 523 | ), 524 | QPointF( 525 | flapXBase + flapWidth - 40, 526 | flapYBase + 60 + self._flaps * int((flapHeight - 100)), 527 | ), 528 | QPointF( 529 | flapXBase + flapWidth - 30, 530 | flapYBase + 50 + self._flaps * int((flapHeight - 100)), 531 | ), 532 | QPointF( 533 | flapXBase + flapWidth, 534 | flapYBase + 50 + self._flaps * int((flapHeight - 100)), 535 | ), 536 | ] 537 | ) 538 | ) 539 | 540 | # trim settings 541 | trimXBase = 460 542 | trimYBase = 20 543 | trimHeight = secHeight - 40 544 | trimWidth = 130 545 | 546 | self.setPen(1, Qt.GlobalColor.white) 547 | self.qp.setBrush(QBrush(Qt.GlobalColor.white)) 548 | font = self.qp.font() 549 | font.setPixelSize(30) 550 | font.setBold(True) 551 | self.qp.setFont(font) 552 | 553 | # draw the title 554 | self.qp.drawText( 555 | QRectF( 556 | trimXBase + 40, 557 | trimYBase, 558 | 90, 559 | 40, 560 | ), 561 | Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTop, 562 | "TRIM", 563 | ) 564 | 565 | font.setPixelSize(20) 566 | font.setBold(False) 567 | self.qp.setFont(font) 568 | 569 | # draw the flaps angle legend 570 | self.qp.drawText( 571 | QRectF( 572 | trimXBase, 573 | trimYBase + 40 + int((trimHeight) / 2 - 40), 574 | 80, 575 | 40, 576 | ), 577 | Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter, 578 | "Take-off", 579 | ) 580 | 581 | # draw the indicator rectangle 582 | self.qp.setBrush(QBrush(Qt.GlobalColor.black)) 583 | self.qp.drawRect(trimXBase + 90, trimYBase + 40, 40, trimHeight - 40) 584 | 585 | self.setPen(1, Qt.GlobalColor.white) 586 | self.qp.setBrush(QBrush(Qt.GlobalColor.white)) 587 | 588 | trimShift = (trimHeight - 60) * (self._trims / 2 + 0.5) 589 | 590 | self.qp.drawPolygon( 591 | QPolygonF( 592 | [ 593 | QPointF(trimXBase + trimWidth, trimYBase + 40 + trimShift), 594 | QPointF(trimXBase + trimWidth, trimYBase + 60 + trimShift), 595 | QPointF(trimXBase + trimWidth - 30, trimYBase + 60 + trimShift), 596 | QPointF(trimXBase + trimWidth - 40, trimYBase + 50 + trimShift), 597 | QPointF(trimXBase + trimWidth - 30, trimYBase + 40 + trimShift), 598 | QPointF(trimXBase + trimWidth, trimYBase + 40 + trimShift), 599 | ] 600 | ) 601 | ) 602 | 603 | # sqawk code and status 604 | if self._avionicson: 605 | font = self.qp.font() 606 | font.setPixelSize(self.xpdrheight - 6) 607 | font.setBold(True) 608 | self.qp.setFont(font) 609 | 610 | # draw the indicator rectangle 611 | self.setPen(2, Qt.GlobalColor.white) 612 | self.qp.setBrush(QBrush(Qt.GlobalColor.white)) 613 | self.qp.drawRect(self.xpdrRect) 614 | 615 | self.setPen(1, Qt.GlobalColor.black) 616 | self.qp.setBrush(QBrush(Qt.GlobalColor.black)) 617 | self.qp.drawText( 618 | self.xpdrRect, 619 | Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter, 620 | "XPDR", 621 | ) 622 | 623 | self.setPen(2, Qt.GlobalColor.white) 624 | self.qp.setBrush(QBrush(Qt.GlobalColor.black)) 625 | rect = QRectF( 626 | self.xpdrXbase + self.xpdrwidth, 627 | self.xpdrYbase, 628 | 420 - self.xpdrwidth, 629 | self.xpdrheight, 630 | ) 631 | self.qp.drawRect(rect) 632 | 633 | self.setPen(1, Qt.GlobalColor.white) 634 | self.qp.setBrush(QBrush(Qt.GlobalColor.white)) 635 | 636 | if int(self._xpdrMode) == 0: 637 | xpdrMode = "OFF" 638 | elif int(self._xpdrMode) == 1: 639 | xpdrMode = "STDBY" 640 | elif int(self._xpdrMode) == 2: 641 | xpdrMode = "ON" 642 | elif int(self._xpdrMode) == 3: 643 | xpdrMode = "ALT" 644 | elif int(self._xpdrMode) == 4: 645 | xpdrMode = "TEST" 646 | 647 | self.qp.drawText( 648 | rect, 649 | Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter, 650 | "{:04d} {}".format(int(self._xpdrCode), xpdrMode), 651 | ) 652 | 653 | if self.xpdrKeyboard: 654 | self.setPen(2, Qt.GlobalColor.white) 655 | self.qp.setBrush(QBrush(Qt.GlobalColor.black)) 656 | self.qp.drawRect(self.xpdrkeyRect) 657 | 658 | for key in self.keyArea: 659 | self.qp.drawEllipse(key[0]) 660 | self.qp.drawText( 661 | key[0], 662 | Qt.AlignmentFlag.AlignCenter, 663 | "{:01d}".format(key[1]), 664 | ) 665 | 666 | for key in self.keyCtrlArea: 667 | self.qp.drawRect(key[0]) 668 | self.qp.drawText( 669 | key[0], 670 | Qt.AlignmentFlag.AlignCenter, 671 | key[1], 672 | ) 673 | 674 | else: 675 | # carb heat status 676 | carbXbase = 20 677 | carbYbase = self.xpdrYbase + self.xpdrheight + 20 678 | 679 | carbwidth = 80 680 | carbheight = 80 681 | 682 | font.setPixelSize(20) 683 | font.setBold(False) 684 | self.qp.setFont(font) 685 | 686 | rect = QRectF(carbXbase, carbYbase, carbwidth, 40) 687 | 688 | self.qp.drawText( 689 | rect, 690 | Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter, 691 | "CARB", 692 | ) 693 | 694 | self.setPen(2, Qt.GlobalColor.white) 695 | if self._carbheat > 0.1: 696 | self.qp.setBrush(QBrush(Qt.GlobalColor.green)) 697 | else: 698 | self.qp.setBrush(QBrush(Qt.GlobalColor.black)) 699 | 700 | rect = QRectF(carbXbase, carbYbase + 40, carbwidth, carbheight) 701 | 702 | self.qp.drawEllipse(rect) 703 | 704 | # fuel pump status 705 | fuelXbase = 20 706 | fuelYbase = carbYbase + carbheight + 20 707 | 708 | fuelwidth = 80 709 | fuelheight = 80 710 | 711 | font.setPixelSize(20) 712 | font.setBold(False) 713 | self.qp.setFont(font) 714 | 715 | rect = QRectF(fuelXbase, fuelYbase, fuelwidth, 100) 716 | 717 | self.qp.drawText( 718 | rect, 719 | Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter, 720 | "FUEL\nPUMP", 721 | ) 722 | 723 | self.setPen(2, Qt.GlobalColor.white) 724 | self.qp.setBrush(QBrush(Qt.GlobalColor.green)) 725 | 726 | rect = QRectF(fuelXbase, fuelYbase + 80, fuelwidth, fuelheight) 727 | 728 | if self._fuelpump > 0 and self._avionicson: 729 | self.qp.setBrush(QBrush(Qt.GlobalColor.green)) 730 | else: 731 | self.qp.setBrush(QBrush(Qt.GlobalColor.black)) 732 | 733 | self.qp.drawEllipse(rect) 734 | 735 | # fuel feed settings 736 | 737 | ffXBase = 120 738 | ffYBase = carbYbase + 40 739 | 740 | ffWdidth = 440 - ffXBase 741 | ffHeight = 220 742 | 743 | self.setPen(1, Qt.GlobalColor.white) 744 | self.qp.setBrush(QBrush(Qt.GlobalColor.white)) 745 | rect = QRectF(ffXBase, carbYbase, ffWdidth, 20) 746 | self.qp.drawText( 747 | rect, 748 | Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter, 749 | "FUEL FEED", 750 | ) 751 | 752 | self.setPen(1, Qt.GlobalColor.white) 753 | self.qp.setBrush(QBrush(Qt.GlobalColor.black)) 754 | 755 | rect = QRectF(ffXBase, ffYBase, ffWdidth, ffHeight) 756 | self.qp.drawRect(rect) 757 | 758 | self.qp.drawText( 759 | rect, 760 | Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop, 761 | "BOTH", 762 | ) 763 | self.qp.drawText( 764 | rect, 765 | Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignBottom, 766 | "OFF", 767 | ) 768 | 769 | self.qp.drawText( 770 | rect, 771 | Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, 772 | "LEFT", 773 | ) 774 | 775 | self.qp.drawText( 776 | rect, 777 | Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter, 778 | "RIGHT", 779 | ) 780 | 781 | self.qp.translate(rect.center()) 782 | 783 | if self._fuelSel == 0: 784 | self.qp.rotate(180) 785 | elif self._fuelSel == 1: 786 | self.qp.rotate(-90) 787 | elif self._fuelSel == 2: 788 | self.qp.rotate(45) 789 | elif self._fuelSel == 3: 790 | self.qp.rotate(90) 791 | elif self._fuelSel == 4: 792 | self.qp.rotate(0) 793 | else: 794 | self.qp.rotate(0) 795 | 796 | brect = QRectF(-50, -50, 100, 100) 797 | self.qp.drawEllipse(brect) 798 | 799 | self.setPen(1, Qt.GlobalColor.white) 800 | self.qp.setBrush(QBrush(Qt.GlobalColor.white)) 801 | self.qp.drawPolygon( 802 | QPolygonF( 803 | [ 804 | QPointF(-10, +50), 805 | QPointF(+10, +50), 806 | QPointF(+10, -50), 807 | QPointF(0, -70), 808 | QPointF(-10, -50), 809 | ] 810 | ) 811 | ) 812 | 813 | self.setPen(1, Qt.GlobalColor.black) 814 | self.qp.setBrush(QBrush(Qt.GlobalColor.black)) 815 | brect = QRectF(-5, -5, 10, 10) 816 | self.qp.drawEllipse(brect) 817 | 818 | self.qp.resetTransform() 819 | 820 | # advisory panel (low voltage) 821 | advXBase = 20 822 | advYBase = fuelYbase + fuelheight + 100 823 | 824 | advWdidth = 420 825 | advHeight = 100 826 | 827 | advTable = [ 828 | { 829 | "text": "LOW\nVOLTS", 830 | "color": Qt.GlobalColor.red, 831 | "name": "_lowVolts", 832 | }, 833 | {"text": "LOW\nFUEL", "color": Qt.GlobalColor.red, "name": "_lowFuel"}, 834 | {"text": "OIL\nPRESS", "color": Qt.GlobalColor.red, "name": "_oilPres"}, 835 | {"text": "BRAKE", "color": Qt.GlobalColor.red, "name": "_parkBrake"}, 836 | { 837 | "text": "LOW\nVACUUM", 838 | "color": Qt.GlobalColor.yellow, 839 | "name": "_lowVacuum", 840 | }, 841 | { 842 | "text": "FUEL\nPRESS", 843 | "color": Qt.GlobalColor.yellow, 844 | "name": "_fuelPress", 845 | }, 846 | ] 847 | 848 | grayColor = QColor("#5d5b59") 849 | self.setPen(1, grayColor) 850 | self.qp.setBrush(QBrush(Qt.GlobalColor.black)) 851 | 852 | rect = QRectF(advXBase, advYBase, advWdidth, advHeight) 853 | self.qp.drawRect(rect) 854 | 855 | for i in range(0, 2): 856 | for j in range(0, 4): 857 | advrect = QRectF( 858 | advXBase + j * advWdidth / 4, 859 | advYBase + i * advHeight / 2, 860 | advWdidth / 4, 861 | advHeight / 2, 862 | ) 863 | self.qp.drawRect(advrect) 864 | 865 | if j + 4 * i < len(advTable): 866 | if getattr(self, advTable[4 * i + j]["name"]) == 1: 867 | self.setPen(1, advTable[4 * i + j]["color"]) 868 | 869 | self.qp.drawText( 870 | advrect, 871 | Qt.AlignmentFlag.AlignHCenter 872 | | Qt.AlignmentFlag.AlignVCenter, 873 | advTable[4 * i + j]["text"], 874 | ) 875 | 876 | self.setPen(1, grayColor) 877 | 878 | self.qp.end() 879 | 880 | 881 | class pyG5HSIWidget(pyG5Widget): 882 | """Generate G5 wdiget view.""" 883 | 884 | def __init__(self, parent=None): 885 | """g5Widget Constructor. 886 | 887 | Args: 888 | parent: Parent Widget 889 | 890 | Returns: 891 | self 892 | """ 893 | pyG5Widget.__init__(self, parent) 894 | 895 | def paintEvent(self, event): 896 | """Paint the widget.""" 897 | self.qp = QPainter(self) 898 | 899 | greyColor = QColor(128, 128, 128, 255) 900 | rotatinghsiCircleRadius = 160 901 | hsiCircleRadius = 90 902 | hsiTextRadius = 120 903 | hsiCenter = 190 904 | groundTrackDiamondSize = 7 905 | 906 | headingBoxWidth = 50 907 | headingBoxHeight = 22 908 | 909 | font = self.qp.font() 910 | font.setPixelSize(headingBoxHeight - 2) 911 | font.setBold(True) 912 | self.qp.setFont(font) 913 | 914 | # Draw the background 915 | self.setPen(1, Qt.GlobalColor.black) 916 | self.qp.setBrush(QBrush(Qt.GlobalColor.black)) 917 | self.qp.drawRect(0, 0, g5Width, g5Height) 918 | 919 | if self._avionicson == 0: 920 | self.setPen(1, Qt.GlobalColor.white) 921 | self.qp.drawLine(0, 0, g5Width, g5Height) 922 | self.qp.drawLine(0, g5Height, g5Width, 0) 923 | self.qp.end() 924 | return 925 | 926 | # Draw the Horizontal Situation Indicator circle 927 | self.setPen(2, greyColor) 928 | 929 | # offset the center to the Horizontal Situation Indicator center 930 | self.qp.translate(g5CenterX, hsiCenter) 931 | 932 | self.qp.drawArc( 933 | -hsiCircleRadius, 934 | -hsiCircleRadius, 935 | 2 * hsiCircleRadius, 936 | 2 * hsiCircleRadius, 937 | 0, 938 | 360 * 16, 939 | ) 940 | 941 | # Draw the fixed Horizontal Situation Indicator marker 942 | hsiPeripheralMarkers = [ 943 | 45, 944 | 90, 945 | 135, 946 | 225, 947 | 270, 948 | 315, 949 | ] 950 | self.setPen(2, Qt.GlobalColor.white) 951 | 952 | for marker in hsiPeripheralMarkers: 953 | self.qp.rotate(-marker) 954 | self.qp.drawLine(0, 170, 0, 185) 955 | self.qp.rotate(marker) 956 | 957 | # Draw the RotatingHSI lines and Text 958 | 959 | # rotate by the current magnetic heading 960 | self.qp.rotate(-self._magHeading) 961 | 962 | currentHead = 0 963 | while currentHead < 360: 964 | if (currentHead % 90) == 0: 965 | length = 20 966 | elif (currentHead % 10) == 0: 967 | length = 15 968 | else: 969 | length = 10 970 | self.qp.drawLine( 971 | 0, rotatinghsiCircleRadius - length, 0, rotatinghsiCircleRadius 972 | ) 973 | 974 | if currentHead == 0: 975 | text = "N" 976 | elif currentHead == 90: 977 | text = "E" 978 | elif currentHead == 180: 979 | text = "S" 980 | elif currentHead == 270: 981 | text = "W" 982 | elif (currentHead % 30) == 0: 983 | text = "{:2d}".format(int(currentHead / 10)) 984 | else: 985 | text = "" 986 | 987 | if len(text): 988 | self.qp.translate(0, -hsiTextRadius) 989 | self.qp.rotate(+self._magHeading - currentHead) 990 | self.qp.drawText( 991 | QRectF( 992 | -self.qp.font().pixelSize() / 2 - 3, 993 | -self.qp.font().pixelSize() / 2, 994 | self.qp.font().pixelSize() + 6, 995 | self.qp.font().pixelSize(), 996 | ), 997 | Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter, 998 | text, 999 | ) 1000 | self.qp.rotate(-self._magHeading + currentHead) 1001 | self.qp.translate(0, hsiTextRadius) 1002 | 1003 | self.qp.rotate(+5) 1004 | currentHead += 5 1005 | 1006 | # draw the Heading bug 1007 | self.setPen(1, Qt.GlobalColor.cyan) 1008 | self.qp.setBrush(QBrush(Qt.GlobalColor.cyan)) 1009 | 1010 | self.qp.rotate(180 + self._headingBug) 1011 | 1012 | self.qp.drawPolygon( 1013 | QPolygonF( 1014 | [ 1015 | QPointF(-15, rotatinghsiCircleRadius - 3), 1016 | QPointF(+15, rotatinghsiCircleRadius - 3), 1017 | QPointF(+15, rotatinghsiCircleRadius + 6), 1018 | QPointF(+6, rotatinghsiCircleRadius + 6), 1019 | QPointF(0, rotatinghsiCircleRadius + 1), 1020 | QPointF(-6, rotatinghsiCircleRadius + 6), 1021 | QPointF(-15, rotatinghsiCircleRadius + 6), 1022 | ] 1023 | ) 1024 | ) 1025 | 1026 | self.setPen(1, Qt.GlobalColor.black) 1027 | gpscdianonciator = "" 1028 | if int(self._hsiSource) == 2: 1029 | cdiSource = "GPS" 1030 | # 0=OCN, 1=ENR, 2=TERM, 3=DPRT, 4=MAPR, 5=APR, 6=RNPAR, 7=LNAV, 8=LNAV+V, 9=L/VNAV, 10=LP, 11=LPV, 12=LP+V, 13=GLS 1031 | tableMap = [ 1032 | "OCN", 1033 | "ENR", 1034 | "TERM", 1035 | "DPRT", 1036 | "MAPR", 1037 | "APR", 1038 | "RNPAR", 1039 | "LNAV", 1040 | "LNAV+V", 1041 | "L/VNAV", 1042 | "LP", 1043 | "LPV", 1044 | "LP+V", 1045 | "GLS", 1046 | "", 1047 | ] 1048 | try: 1049 | gpscdianonciator = tableMap[int(self._gpshsisens)] 1050 | except IndexError: 1051 | gpscdianonciator = tableMap[-1] 1052 | 1053 | navColor = Qt.GlobalColor.magenta 1054 | navdft = self._gpsdft 1055 | navfromto = self._gpsfromto 1056 | navcrs = self._gpscrs 1057 | if (self._gpsvnavavailable != -1000) or self._gpsgsavailable: 1058 | vertAvailable = 1 1059 | else: 1060 | vertAvailable = 0 1061 | gsDev = self._gpsgs 1062 | elif int(self._hsiSource) == 1: 1063 | cdiSource = "{}".format(self.getNavTypeString(self._nav2type, "2")) 1064 | navColor = Qt.GlobalColor.green 1065 | navdft = self._nav2dft 1066 | navfromto = self._nav2fromto 1067 | navcrs = self._nav2crs 1068 | vertAvailable = self._nav2gsavailable 1069 | gsDev = self._nav2gs 1070 | else: 1071 | cdiSource = "{}".format(self.getNavTypeString(self._nav1type, "1")) 1072 | navColor = Qt.GlobalColor.green 1073 | navdft = self._nav1dft 1074 | navfromto = self._nav1fromto 1075 | navcrs = self._nav1crs 1076 | vertAvailable = self._nav1gsavailable 1077 | gsDev = self._nav1gs 1078 | 1079 | # bearing 1 1080 | if int(self._nav1fromto) != 0: 1081 | self.qp.rotate(90 - self._headingBug + self._nav1bearing) 1082 | 1083 | self.setPen(2, Qt.GlobalColor.cyan) 1084 | 1085 | # upside 1086 | self.qp.drawPolyline( 1087 | QPolygonF( 1088 | [ 1089 | QPointF(rotatinghsiCircleRadius - 25, 0), 1090 | QPointF(hsiCircleRadius, 0), 1091 | ] 1092 | ) 1093 | ) 1094 | # backside 1095 | self.qp.drawPolyline( 1096 | QPolygonF( 1097 | [ 1098 | QPointF(-rotatinghsiCircleRadius + 25, 0), 1099 | QPointF(-hsiCircleRadius, 0), 1100 | ] 1101 | ) 1102 | ) 1103 | # arrow 1104 | self.qp.drawPolyline( 1105 | QPolygonF( 1106 | [ 1107 | QPointF(hsiCircleRadius + 20, -10), 1108 | QPointF(hsiCircleRadius + 30, 0), 1109 | QPointF(hsiCircleRadius + 20, 10), 1110 | ] 1111 | ) 1112 | ) 1113 | 1114 | self.qp.rotate(-90 + self._headingBug - self._nav1bearing) 1115 | 1116 | # bearing 2 1117 | if int(self._nav2fromto) != 0: 1118 | self.qp.rotate(90 - self._headingBug + self._nav2bearing) 1119 | 1120 | self.setPen(2, Qt.GlobalColor.cyan) 1121 | 1122 | # backside 1123 | self.qp.drawPolyline( 1124 | QPolygonF( 1125 | [ 1126 | QPointF(-hsiCircleRadius, -5), 1127 | QPointF(-hsiCircleRadius - 25, -5), 1128 | QPointF(-hsiCircleRadius - 30, 0), 1129 | QPointF(-rotatinghsiCircleRadius + 25, 0), 1130 | QPointF(-hsiCircleRadius - 30, 0), 1131 | QPointF(-hsiCircleRadius - 25, +5), 1132 | QPointF(-hsiCircleRadius, +5), 1133 | ] 1134 | ) 1135 | ) 1136 | 1137 | # upside 1138 | self.qp.drawPolyline( 1139 | QPolygonF( 1140 | [ 1141 | QPointF(rotatinghsiCircleRadius - 42, -5), 1142 | QPointF(hsiCircleRadius, -5), 1143 | ] 1144 | ) 1145 | ) 1146 | self.qp.drawPolyline( 1147 | QPolygonF( 1148 | [ 1149 | QPointF(rotatinghsiCircleRadius - 42, +5), 1150 | QPointF(hsiCircleRadius, +5), 1151 | ] 1152 | ) 1153 | ) 1154 | # arrow 1155 | self.qp.drawPolyline( 1156 | QPolygonF( 1157 | [ 1158 | QPointF(hsiCircleRadius + 25, -10), 1159 | QPointF(hsiCircleRadius + 35, 0), 1160 | QPointF(hsiCircleRadius + 45, 0), 1161 | QPointF(hsiCircleRadius + 35, 0), 1162 | QPointF(hsiCircleRadius + 25, 10), 1163 | ] 1164 | ) 1165 | ) 1166 | 1167 | self.qp.rotate(-90 + self._headingBug - self._nav2bearing) 1168 | 1169 | self.setPen(1, Qt.GlobalColor.black) 1170 | self.qp.setBrush(QBrush(navColor)) 1171 | # Draw the CDI 1172 | self.qp.rotate(90 - self._headingBug + navcrs) 1173 | 1174 | # CDI arrow 1175 | self.qp.drawPolygon( 1176 | QPolygonF( 1177 | [ 1178 | QPointF(rotatinghsiCircleRadius - 10, 0), 1179 | QPointF(rotatinghsiCircleRadius - 40, -20), 1180 | QPointF(rotatinghsiCircleRadius - 33, -3), 1181 | QPointF(hsiCircleRadius - 10, -3), 1182 | QPointF(hsiCircleRadius - 10, 3), 1183 | QPointF(rotatinghsiCircleRadius - 33, 3), 1184 | QPointF(rotatinghsiCircleRadius - 40, 20), 1185 | ] 1186 | ) 1187 | ) 1188 | # CDI bottom bar 1189 | self.qp.drawPolygon( 1190 | QPolygonF( 1191 | [ 1192 | QPointF(-rotatinghsiCircleRadius + 10, -3), 1193 | QPointF(-hsiCircleRadius + 10, -3), 1194 | QPointF(-hsiCircleRadius + 10, +3), 1195 | QPointF(-rotatinghsiCircleRadius + 10, +3), 1196 | ] 1197 | ) 1198 | ) 1199 | # CDI deflection bar 1200 | if int(navfromto) != 0: 1201 | hsiDeflectionBound = hsiCircleRadius / 75 * 2 1202 | deflection = ( 1203 | max(min(navdft, hsiDeflectionBound), -hsiDeflectionBound) / 2 * 75 1204 | ) 1205 | self.qp.drawPolygon( 1206 | QPolygonF( 1207 | [ 1208 | QPointF(hsiCircleRadius - 10, deflection - 3), 1209 | QPointF(-hsiCircleRadius + 10, deflection - 3), 1210 | QPointF(-hsiCircleRadius + 10, deflection + 3), 1211 | QPointF(hsiCircleRadius - 10, deflection + 3), 1212 | ] 1213 | ) 1214 | ) 1215 | 1216 | # NAV1 FromTo 1217 | fromToTipX = 65 1218 | if int(navfromto) == 2: 1219 | self.qp.rotate(180) 1220 | 1221 | self.qp.drawPolygon( 1222 | QPolygonF( 1223 | [ 1224 | QPointF(fromToTipX - 10, 0), 1225 | QPointF(fromToTipX - 40, -20), 1226 | QPointF(fromToTipX - 30, 0), 1227 | QPointF(fromToTipX - 40, 20), 1228 | ] 1229 | ) 1230 | ) 1231 | if int(navfromto) == 2: 1232 | self.qp.rotate(180) 1233 | 1234 | self.qp.rotate(90) 1235 | # CDI deflection circle 1236 | self.setPen(2, Qt.GlobalColor.white) 1237 | self.qp.setBrush(QBrush(Qt.GlobalColor.black)) 1238 | 1239 | for i in [-81, -41, 31, 69]: 1240 | self.qp.drawArc( 1241 | QRectF( 1242 | i, 1243 | -6, 1244 | 12, 1245 | 12, 1246 | ), 1247 | 0, 1248 | 360 * 16, 1249 | ) 1250 | 1251 | self.qp.resetTransform() 1252 | 1253 | font = self.qp.font() 1254 | font.setPixelSize(15) 1255 | font.setBold(False) 1256 | self.qp.setFont(font) 1257 | if int(self._hsiSource) == 2: 1258 | self.setPen(2, Qt.GlobalColor.magenta) 1259 | else: 1260 | self.setPen(2, Qt.GlobalColor.green) 1261 | 1262 | self.qp.drawText( 1263 | QRectF(g5CenterX - 70, hsiCenter - 50, 65, 18), 1264 | Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, 1265 | cdiSource, 1266 | ) 1267 | 1268 | if len(gpscdianonciator): 1269 | self.qp.drawText( 1270 | QRectF(g5CenterX + 25, hsiCenter - 50, 65, 18), 1271 | Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, 1272 | gpscdianonciator, 1273 | ) 1274 | 1275 | # Draw the heading Bug indicator bottom corner 1276 | self.setPen(2, Qt.GlobalColor.gray) 1277 | self.qp.setBrush(QBrush(Qt.GlobalColor.black)) 1278 | 1279 | headingWidth = 105 1280 | headingHeigth = 30 1281 | self.qp.drawRect(QRectF(g5Width, g5Height, -headingWidth, -headingHeigth)) 1282 | 1283 | self.setPen(2, Qt.GlobalColor.cyan) 1284 | # draw the bug symbol 1285 | self.setPen(1, Qt.GlobalColor.cyan) 1286 | self.qp.setBrush(QBrush(Qt.GlobalColor.cyan)) 1287 | 1288 | self.qp.drawPolygon( 1289 | QPolygonF( 1290 | [ 1291 | QPointF(381, 336), 1292 | QPointF(381, 354), 1293 | QPointF(387, 354), 1294 | QPointF(387, 349), 1295 | QPointF(382, 346), 1296 | QPointF(382, 344), 1297 | QPointF(387, 341), 1298 | QPointF(387, 336), 1299 | ] 1300 | ) 1301 | ) 1302 | 1303 | self.qp.drawText( 1304 | QRectF(412, 336, 65, 18), 1305 | Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, 1306 | "{:03d}˚".format(int(self._headingBug)), 1307 | ) 1308 | 1309 | # draw the dist box 1310 | if ( 1311 | int(self._hsiSource) == 2 1312 | or (int(self._hsiSource) == 1 and int(self._nav2fromto) != 0) 1313 | or (int(self._hsiSource) == 0 and int(self._nav1fromto) != 0) 1314 | ): 1315 | font.setPixelSize(12) 1316 | font.setBold(False) 1317 | self.qp.setFont(font) 1318 | distRect = QRectF(g5Width - 105, 0, 105, 45) 1319 | 1320 | self.setPen(2, greyColor) 1321 | self.qp.setBrush(QBrush(Qt.GlobalColor.black)) 1322 | self.qp.drawRect(distRect) 1323 | 1324 | self.qp.drawText( 1325 | distRect, 1326 | Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop, 1327 | "Dist NM", 1328 | ) 1329 | 1330 | font.setPixelSize(18) 1331 | font.setBold(True) 1332 | self.qp.setFont(font) 1333 | self.setPen(1, navColor) 1334 | if int(self._hsiSource) == 2: 1335 | dist = self._gpsdmedist 1336 | elif int(self._hsiSource) == 1: 1337 | dist = self._nav2dme 1338 | else: 1339 | dist = self._nav1dme 1340 | 1341 | distRect = QRectF(g5Width - 105, 12, 105, 45 - 12) 1342 | self.qp.drawText( 1343 | distRect, 1344 | Qt.AlignmentFlag.AlignCenter, 1345 | "{}".format(round(dist, 1)), 1346 | ) 1347 | 1348 | # set default font size 1349 | font = self.qp.font() 1350 | font.setPixelSize(18) 1351 | font.setBold(True) 1352 | self.qp.setFont(font) 1353 | 1354 | # draw the wind box 1355 | self.setPen(2, greyColor) 1356 | self.qp.setBrush(QBrush(Qt.GlobalColor.black)) 1357 | 1358 | self.qp.drawRect(0, 0, 105, 45) 1359 | 1360 | self.setPen(1, Qt.GlobalColor.white) 1361 | self.qp.setBrush(QBrush(Qt.GlobalColor.white)) 1362 | 1363 | self.qp.translate(25, 25) 1364 | 1365 | self.qp.rotate(180 - self._magHeading + self._windDirection) 1366 | 1367 | self.qp.drawPolygon( 1368 | QPolygonF( 1369 | [ 1370 | QPointF(-5, 0), 1371 | QPointF(0, -10), 1372 | QPointF(5, 0), 1373 | QPointF(2, 0), 1374 | QPointF(2, 10), 1375 | QPointF(-2, 10), 1376 | QPointF(-2, 0), 1377 | ] 1378 | ) 1379 | ) 1380 | 1381 | self.qp.resetTransform() 1382 | 1383 | self.qp.drawText( 1384 | QRectF(50, 2, 50, 20), 1385 | Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, 1386 | "{:03d}˚".format(int(self._windDirection)), 1387 | ) 1388 | 1389 | self.qp.drawText( 1390 | QRectF(50, 22, 50, 20), 1391 | Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, 1392 | "{:02d}kt".format(int(self._windSpeed * mstokt)), 1393 | ) 1394 | 1395 | # Draw the magnetic heading box 1396 | self.setPen(2, greyColor) 1397 | self.qp.setBrush(QBrush(Qt.GlobalColor.black)) 1398 | self.qp.drawPolygon( 1399 | QPolygonF( 1400 | [ 1401 | QPointF(g5CenterX - headingBoxWidth / 2, 1), 1402 | QPointF(g5CenterX - headingBoxWidth / 2, headingBoxHeight), 1403 | QPointF(g5CenterX - 6, headingBoxHeight), 1404 | QPointF(g5CenterX, headingBoxHeight + 8), 1405 | QPointF(g5CenterX + 6, headingBoxHeight), 1406 | QPointF(g5CenterX + headingBoxWidth / 2, headingBoxHeight), 1407 | QPointF(g5CenterX + headingBoxWidth / 2, 1), 1408 | ] 1409 | ) 1410 | ) 1411 | 1412 | self.qp.drawText( 1413 | QRectF( 1414 | g5CenterX - headingBoxWidth / 2, 1, headingBoxWidth, headingBoxHeight 1415 | ), 1416 | Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter, 1417 | "{:03d}˚".format(int(self._magHeading)), 1418 | ) 1419 | 1420 | # Draw the ground track 1421 | self.setPen(0, Qt.GlobalColor.transparent) 1422 | self.qp.setBrush(QBrush(Qt.GlobalColor.magenta)) 1423 | self.qp.translate(g5CenterX, hsiCenter) 1424 | self.qp.rotate(-self._magHeading + self._groundTrack) 1425 | self.qp.drawPolygon( 1426 | QPolygonF( 1427 | [ 1428 | QPointF( 1429 | -groundTrackDiamondSize, 1430 | -rotatinghsiCircleRadius - groundTrackDiamondSize, 1431 | ), 1432 | QPointF( 1433 | +groundTrackDiamondSize, 1434 | -rotatinghsiCircleRadius - groundTrackDiamondSize, 1435 | ), 1436 | QPointF(+0, -rotatinghsiCircleRadius), 1437 | ] 1438 | ) 1439 | ) 1440 | self.setPen(3, greyColor, Qt.PenStyle.DashLine) 1441 | self.qp.drawLine(0, 0, 0, -rotatinghsiCircleRadius) 1442 | self.qp.resetTransform() 1443 | 1444 | # draw the aircraft 1445 | self.setPen(1, Qt.GlobalColor.white) 1446 | self.qp.setBrush(QBrush(Qt.GlobalColor.white)) 1447 | 1448 | self.qp.drawPolygon( 1449 | QPolygonF( 1450 | [ 1451 | QPointF(240, 163), 1452 | QPointF(235, 169), 1453 | QPointF(235, 180), 1454 | QPointF(215, 195), 1455 | QPointF(215, 200), 1456 | QPointF(235, 195), 1457 | QPointF(235, 205), 1458 | QPointF(227, 213), 1459 | QPointF(227, 217), 1460 | QPointF(240, 213), 1461 | QPointF(253, 217), 1462 | QPointF(253, 213), 1463 | QPointF(245, 205), 1464 | QPointF(245, 195), 1465 | QPointF(265, 200), 1466 | QPointF(265, 195), 1467 | QPointF(245, 180), 1468 | QPointF(245, 169), 1469 | ] 1470 | ) 1471 | ) 1472 | 1473 | # draw the GlideScope 1474 | gsWidth = 16 1475 | gsHeigth = 192 1476 | gsCircleRad = 10 1477 | gsFromLeft = 20 1478 | gsDiamond = 16 1479 | 1480 | if vertAvailable: 1481 | # Vertical guidance source 1482 | rect = QRectF( 1483 | g5Width - gsFromLeft - gsWidth, 1484 | hsiCenter - gsHeigth / 2 - 15, 1485 | gsWidth, 1486 | 15, 1487 | ) 1488 | 1489 | font.setPixelSize(12) 1490 | self.qp.setFont(font) 1491 | self.setPen(1, navColor) 1492 | 1493 | vertSourceTxt = "G" 1494 | if int(self._hsiSource) == 2 and self._gpsgsavailable == 0: 1495 | vertSourceTxt = "V" 1496 | 1497 | self.qp.drawText( 1498 | rect, 1499 | Qt.AlignmentFlag.AlignCenter | Qt.AlignmentFlag.AlignVCenter, 1500 | vertSourceTxt, 1501 | ) 1502 | 1503 | self.setPen(2, greyColor) 1504 | self.qp.setBrush(QBrush(Qt.GlobalColor.transparent)) 1505 | 1506 | self.qp.drawRect(rect) 1507 | 1508 | # main rectangle 1509 | self.qp.drawRect( 1510 | QRectF( 1511 | g5Width - gsFromLeft - gsWidth, 1512 | hsiCenter - gsHeigth / 2, 1513 | gsWidth, 1514 | gsHeigth, 1515 | ) 1516 | ) 1517 | 1518 | self.qp.drawLine( 1519 | g5Width - gsFromLeft - gsWidth, 1520 | hsiCenter, 1521 | g5Width - gsFromLeft, 1522 | hsiCenter, 1523 | ) 1524 | 1525 | for offset in [-70, -35, 35, 70]: 1526 | self.qp.drawEllipse( 1527 | QPointF( 1528 | int(g5Width - gsFromLeft - gsWidth / 2), 1529 | int(hsiCenter + offset), 1530 | ), 1531 | gsCircleRad / 2, 1532 | gsCircleRad / 2, 1533 | ) 1534 | 1535 | self.setPen(1, Qt.GlobalColor.black) 1536 | self.qp.setBrush(QBrush(navColor)) 1537 | 1538 | self.qp.translate( 1539 | g5Width - gsFromLeft - gsWidth, hsiCenter + gsDev / 2.5 * gsHeigth / 2 1540 | ) 1541 | self.qp.drawPolygon( 1542 | QPolygonF( 1543 | [ 1544 | QPointF(0, 0), 1545 | QPointF(gsDiamond / 2, gsDiamond / 2), 1546 | QPointF(gsDiamond, 0), 1547 | QPointF(gsDiamond / 2, -gsDiamond / 2), 1548 | ] 1549 | ) 1550 | ) 1551 | 1552 | self.qp.resetTransform() 1553 | 1554 | crsBoxHeight = 30 1555 | crsBoxWidth = 105 1556 | 1557 | # draw the Selected Nav Bearing type 1558 | self.setPen(2, greyColor) 1559 | self.qp.setBrush(QBrush(Qt.GlobalColor.black)) 1560 | 1561 | if int(self._nav1fromto) != 0: 1562 | # draw the contour 1563 | self.qp.drawPolyline( 1564 | QPolygonF( 1565 | [ 1566 | QPointF(0, g5Height - crsBoxHeight), 1567 | QPointF(0, g5Height - 3 * crsBoxHeight), 1568 | QPointF(60, g5Height - 3 * crsBoxHeight), 1569 | ] 1570 | ) 1571 | ) 1572 | 1573 | # draw the contour arc 1574 | self.qp.drawArc( 1575 | QRectF(g5Width / 2 - 195, g5Height / 2 - 185, 390, 390), 1576 | 3268, 1577 | 350, 1578 | ) 1579 | 1580 | # set color to cyan and draw the bearing symbol 1581 | self.setPen(2, Qt.GlobalColor.cyan) 1582 | 1583 | self.qp.drawPolyline( 1584 | QPolygonF( 1585 | [ 1586 | QPointF(10, g5Height - 2.5 * crsBoxHeight), 1587 | QPointF(50, g5Height - 2.5 * crsBoxHeight), 1588 | ] 1589 | ) 1590 | ) 1591 | self.qp.drawPolyline( 1592 | QPolygonF( 1593 | [ 1594 | QPointF(30, g5Height - 2.8 * crsBoxHeight), 1595 | QPointF(40, g5Height - 2.5 * crsBoxHeight), 1596 | QPointF(30, g5Height - 2.2 * crsBoxHeight), 1597 | ] 1598 | ) 1599 | ) 1600 | 1601 | # draw the nav type 1602 | self.qp.drawText( 1603 | QRectF( 1604 | QPointF(0, g5Height - crsBoxHeight), 1605 | QPointF(crsBoxWidth, g5Height - 2 * crsBoxHeight), 1606 | ), 1607 | Qt.AlignmentFlag.AlignCenter | Qt.AlignmentFlag.AlignVCenter, 1608 | "{}".format(self.getNavTypeString(self._nav1type, "")), 1609 | ) 1610 | 1611 | if int(self._nav2fromto) != 0 and vertAvailable == 0: 1612 | # set color to grey 1613 | self.setPen(2, Qt.GlobalColor.gray) 1614 | 1615 | # draw the contour 1616 | self.qp.drawPolyline( 1617 | QPolygonF( 1618 | [ 1619 | QPointF(g5Width, g5Height - crsBoxHeight), 1620 | QPointF(g5Width, g5Height - 3 * crsBoxHeight), 1621 | QPointF(g5Width - 60, g5Height - 3 * crsBoxHeight), 1622 | ] 1623 | ) 1624 | ) 1625 | 1626 | # draw the contour arc 1627 | self.qp.drawArc( 1628 | QRectF(g5Width / 2 - 195, g5Height / 2 - 185, 390, 390), 1629 | 5036, 1630 | 335, 1631 | ) 1632 | 1633 | # set color to cyan and draw the bearing symbol 1634 | self.setPen(2, Qt.GlobalColor.cyan) 1635 | self.qp.drawPolyline( 1636 | QPolygonF( 1637 | [ 1638 | QPointF(g5Width - 40, g5Height - 2.5 * crsBoxHeight), 1639 | QPointF(g5Width - 50, g5Height - 2.5 * crsBoxHeight), 1640 | ] 1641 | ) 1642 | ) 1643 | self.qp.drawPolyline( 1644 | QPolygonF( 1645 | [ 1646 | QPointF(g5Width - 10, g5Height - 2.5 * crsBoxHeight + 5), 1647 | QPointF(g5Width - 34, g5Height - 2.5 * crsBoxHeight + 5), 1648 | ] 1649 | ) 1650 | ) 1651 | self.qp.drawPolyline( 1652 | QPolygonF( 1653 | [ 1654 | QPointF(g5Width - 10, g5Height - 2.5 * crsBoxHeight - 5), 1655 | QPointF(g5Width - 34, g5Height - 2.5 * crsBoxHeight - 5), 1656 | ] 1657 | ) 1658 | ) 1659 | self.qp.drawPolyline( 1660 | QPolygonF( 1661 | [ 1662 | QPointF(g5Width - 30, g5Height - 2.8 * crsBoxHeight), 1663 | QPointF(g5Width - 40, g5Height - 2.5 * crsBoxHeight), 1664 | QPointF(g5Width - 30, g5Height - 2.2 * crsBoxHeight), 1665 | ] 1666 | ) 1667 | ) 1668 | 1669 | # draw the nav type 1670 | self.qp.drawText( 1671 | QRectF( 1672 | QPointF(g5Width, g5Height - crsBoxHeight), 1673 | QPointF(g5Width - crsBoxWidth, g5Height - 2 * crsBoxHeight), 1674 | ), 1675 | Qt.AlignmentFlag.AlignCenter | Qt.AlignmentFlag.AlignVCenter, 1676 | "{}".format(self.getNavTypeString(self._nav2type, "")), 1677 | ) 1678 | 1679 | # draw the CRS selection 1680 | 1681 | self.setPen(2, greyColor) 1682 | self.qp.setBrush(QBrush(Qt.GlobalColor.black)) 1683 | 1684 | rect = QRectF(0, g5Height - crsBoxHeight, crsBoxWidth, crsBoxHeight) 1685 | self.qp.drawRect(rect) 1686 | 1687 | self.setPen(1, Qt.GlobalColor.white) 1688 | 1689 | font = self.qp.font() 1690 | font.setPixelSize(15) 1691 | self.qp.setFont(font) 1692 | 1693 | rect = QRectF(1, g5Height - crsBoxHeight + 1, crsBoxWidth - 2, crsBoxHeight - 2) 1694 | self.qp.drawText( 1695 | rect, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignBottom, "CRS" 1696 | ) 1697 | 1698 | font = self.qp.font() 1699 | font.setPixelSize(25) 1700 | self.qp.setFont(font) 1701 | if int(self._hsiSource) == 2: 1702 | self.setPen(1, Qt.GlobalColor.magenta) 1703 | else: 1704 | self.setPen(1, Qt.GlobalColor.green) 1705 | rect = QRectF(40, g5Height - crsBoxHeight + 1, 65, crsBoxHeight - 2) 1706 | self.qp.drawText( 1707 | rect, 1708 | Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, 1709 | "{:03d}˚".format(int(navcrs)), 1710 | ) 1711 | 1712 | self.qp.end() 1713 | 1714 | 1715 | class pyG5AIWidget(pyG5Widget): 1716 | """Generate G5 wdiget view.""" 1717 | 1718 | def __init__(self, parent=None): 1719 | """g5Widget Constructor. 1720 | 1721 | Args: 1722 | parent: Parent Widget 1723 | 1724 | Returns: 1725 | self 1726 | """ 1727 | pyG5Widget.__init__(self, parent) 1728 | 1729 | # parameters 1730 | self.rollArcRadius = g5CenterY * 0.8 1731 | self._pitchScale = 25 1732 | 1733 | def paintEvent(self, event): 1734 | """Paint the widget.""" 1735 | diamondHeight = 14 1736 | diamondWidth = 14 1737 | 1738 | self.qp = QPainter(self) 1739 | 1740 | if self._avionicson == 0: 1741 | self.setPen(1, Qt.GlobalColor.black) 1742 | self.qp.setBrush(QBrush(Qt.GlobalColor.black)) 1743 | self.qp.drawRect(0, 0, g5Width, g5Height) 1744 | self.setPen(1, Qt.GlobalColor.white) 1745 | self.qp.drawLine(0, 0, g5Width, g5Height) 1746 | self.qp.drawLine(0, g5Height, g5Width, 0) 1747 | self.qp.end() 1748 | return 1749 | 1750 | # set default font size 1751 | font = self.qp.font() 1752 | font.setPixelSize(6) 1753 | font.setBold(True) 1754 | self.qp.setFont(font) 1755 | 1756 | self.setPen(1, Qt.GlobalColor.white) 1757 | grad = QLinearGradient(g5CenterX, g5Height, g5CenterX, 0) 1758 | grad.setColorAt(1, QColor(0, 50, 200, 255)) 1759 | grad.setColorAt(0, QColor(0, 255, 255, 255)) 1760 | self.qp.setBrush(grad) 1761 | 1762 | # draw contour + backgorun sky 1763 | self.qp.drawRect(QRectF(0, 0, g5Width, g5Height)) 1764 | 1765 | # draw the rotating part depending on the roll angle 1766 | self.qp.translate(g5CenterX, g5CenterY) 1767 | self.qp.rotate(-self._rollAngle) 1768 | 1769 | # draw the ground 1770 | grad = QLinearGradient( 1771 | g5CenterX, 1772 | +self._pitchAngle / self._pitchScale * g5CenterY, 1773 | g5CenterX, 1774 | +g5Diag, 1775 | ) 1776 | grad.setColorAt(0, QColor(152, 103, 45)) 1777 | grad.setColorAt(1, QColor(255, 222, 173)) 1778 | self.qp.setBrush(grad) 1779 | 1780 | self.qp.drawRect( 1781 | QRectF( 1782 | QPointF( 1783 | -g5Diag, 1784 | +self._pitchAngle / self._pitchScale * g5CenterY, 1785 | ), 1786 | QPointF( 1787 | +g5Diag, 1788 | +g5Diag, 1789 | ), 1790 | ) 1791 | ) 1792 | 1793 | # draw the pitch lines 1794 | height = 0 1795 | pitch = 0 1796 | width = [10, 20, 10, 30] 1797 | mode = 0 1798 | while height < self.rollArcRadius - 40: 1799 | pitch += 2.5 1800 | height = ( 1801 | pitch / self._pitchScale * g5CenterY 1802 | + self._pitchAngle / self._pitchScale * g5CenterY 1803 | ) 1804 | self.qp.drawLine( 1805 | QPointF( 1806 | -width[mode], 1807 | height, 1808 | ), 1809 | QPointF( 1810 | width[mode], 1811 | height, 1812 | ), 1813 | ) 1814 | if width[mode] == 30: 1815 | self.qp.drawText(QPoint(30 + 3, int(height + 2)), str(int(pitch))) 1816 | self.qp.drawText(QPoint(-40, int(height + 2)), str(int(pitch))) 1817 | mode = (mode + 1) % 4 1818 | 1819 | height = 0 1820 | pitch = 0 1821 | width = [10, 20, 10, 30] 1822 | mode = 0 1823 | while height > -self.rollArcRadius + 30: 1824 | pitch -= 2.5 1825 | height = ( 1826 | pitch / self._pitchScale * g5CenterY 1827 | + self._pitchAngle / self._pitchScale * g5CenterY 1828 | ) 1829 | self.qp.drawLine( 1830 | QPointF( 1831 | -width[mode], 1832 | height, 1833 | ), 1834 | QPointF( 1835 | width[mode], 1836 | height, 1837 | ), 1838 | ) 1839 | if width[mode] == 30: 1840 | self.qp.drawText(QPoint(30 + 3, int(height + 2)), str(abs(int(pitch)))) 1841 | self.qp.drawText(QPoint(-40, int(height + 2)), str(abs(int(pitch)))) 1842 | 1843 | mode = (mode + 1) % 4 1844 | 1845 | # draw the static roll arc 1846 | self.setPen(3, Qt.GlobalColor.white) 1847 | 1848 | bondingRect = QRectF( 1849 | -self.rollArcRadius, 1850 | -self.rollArcRadius, 1851 | 2 * self.rollArcRadius, 1852 | 2 * self.rollArcRadius, 1853 | ) 1854 | self.qp.drawArc(bondingRect, 30 * 16, 120 * 16) 1855 | 1856 | # draw the Roll angle arc markers 1857 | rollangleindicator = [ 1858 | [-30, 10], 1859 | [-45, 5], 1860 | [-135, 5], 1861 | [-150, 10], 1862 | [-60, 10], 1863 | [-70, 5], 1864 | [-80, 5], 1865 | [-100, 5], 1866 | [-110, 5], 1867 | [-120, 10], 1868 | ] 1869 | 1870 | self.qp.setBrush(QBrush(Qt.GlobalColor.white)) 1871 | self.setPen(2, Qt.GlobalColor.white) 1872 | for lineParam in rollangleindicator: 1873 | self.qp.drawLine(self.alongRadiusCoord(lineParam[0], lineParam[1])) 1874 | 1875 | self.setPen(1, Qt.GlobalColor.white) 1876 | # draw the diamond on top of the roll arc 1877 | self.qp.drawPolygon( 1878 | QPolygonF( 1879 | [ 1880 | QPointF( 1881 | 0, 1882 | -self.rollArcRadius - 2, 1883 | ), 1884 | QPointF(-diamondWidth / 2, -self.rollArcRadius - diamondHeight), 1885 | QPointF(+diamondWidth / 2, -self.rollArcRadius - diamondHeight), 1886 | ] 1887 | ) 1888 | ) 1889 | 1890 | self.qp.resetTransform() 1891 | 1892 | # create the fixed diamond 1893 | 1894 | fixedDiamond = QPolygonF( 1895 | [ 1896 | QPointF(g5CenterX, g5CenterY - self.rollArcRadius + 2), 1897 | QPointF( 1898 | g5CenterX + diamondWidth / 2, 1899 | g5CenterY - self.rollArcRadius + diamondHeight, 1900 | ), 1901 | QPointF( 1902 | g5CenterX - diamondWidth / 2, 1903 | g5CenterY - self.rollArcRadius + diamondHeight, 1904 | ), 1905 | ] 1906 | ) 1907 | 1908 | self.qp.drawPolygon(fixedDiamond) 1909 | 1910 | # create the nose 1911 | self.qp.setBrush(QBrush(Qt.GlobalColor.yellow)) 1912 | self.qp.setBackgroundMode(Qt.BGMode.OpaqueMode) 1913 | 1914 | self.setPen(1, Qt.GlobalColor.black) 1915 | 1916 | # solid polygon left 1917 | nose = QPolygonF( 1918 | [ 1919 | QPointF(g5CenterX - 1, g5CenterY + 1), 1920 | QPointF(g5CenterX - 75, g5CenterY + 38), 1921 | QPointF(g5CenterX - 54, g5CenterY + 38), 1922 | ] 1923 | ) 1924 | self.qp.drawPolygon(nose) 1925 | 1926 | # solid polygon right 1927 | nose = QPolygonF( 1928 | [ 1929 | QPointF(g5CenterX + 1, g5CenterY + 1), 1930 | QPointF(g5CenterX + 75, g5CenterY + 38), 1931 | QPointF(g5CenterX + 54, g5CenterY + 38), 1932 | ] 1933 | ) 1934 | self.qp.drawPolygon(nose) 1935 | 1936 | # solid marker left 1937 | marker = QPolygonF( 1938 | [ 1939 | QPointF(120, g5CenterY - 5), 1940 | QPointF(155, g5CenterY - 5), 1941 | QPointF(160, g5CenterY), 1942 | QPointF(155, g5CenterY + 5), 1943 | QPointF(120, g5CenterY + 5), 1944 | ] 1945 | ) 1946 | self.qp.drawPolygon(marker) 1947 | 1948 | # solid marker right 1949 | marker = QPolygonF( 1950 | [ 1951 | QPointF(360, g5CenterY - 5), 1952 | QPointF(325, g5CenterY - 5), 1953 | QPointF(320, g5CenterY), 1954 | QPointF(325, g5CenterY + 5), 1955 | QPointF(360, g5CenterY + 5), 1956 | ] 1957 | ) 1958 | self.qp.drawPolygon(marker) 1959 | 1960 | brush = QBrush(QColor(0x7E, 0x7E, 0x34, 255)) 1961 | self.qp.setBrush(brush) 1962 | 1963 | # cross pattern polygon left 1964 | nose = QPolygonF( 1965 | [ 1966 | QPointF(g5CenterX - 2, g5CenterY + 2), 1967 | QPointF(g5CenterX - 33, g5CenterY + 38), 1968 | QPointF(g5CenterX - 54, g5CenterY + 38), 1969 | ] 1970 | ) 1971 | self.qp.drawPolygon(nose) 1972 | 1973 | # cross pattern polygon right 1974 | nose = QPolygonF( 1975 | [ 1976 | QPointF(g5CenterX + 2, g5CenterY + 2), 1977 | QPointF(g5CenterX + 33, g5CenterY + 38), 1978 | QPointF(g5CenterX + 54, g5CenterY + 38), 1979 | ] 1980 | ) 1981 | self.qp.drawPolygon(nose) 1982 | 1983 | self.setPen(0, Qt.GlobalColor.transparent) 1984 | # solid polygon right 1985 | nose = QPolygonF( 1986 | [ 1987 | QPointF(120, g5CenterY), 1988 | QPointF(160, g5CenterY), 1989 | QPointF(155, g5CenterY + 5), 1990 | QPointF(120, g5CenterY + 5), 1991 | ] 1992 | ) 1993 | self.qp.drawPolygon(nose) 1994 | # solid polygon right 1995 | nose = QPolygonF( 1996 | [ 1997 | QPointF(360, g5CenterY), 1998 | QPointF(320, g5CenterY), 1999 | QPointF(325, g5CenterY + 5), 2000 | QPointF(360, g5CenterY + 5), 2001 | ] 2002 | ) 2003 | self.qp.drawPolygon(nose) 2004 | 2005 | ################################################# 2006 | # SPEED TAPE 2007 | ################################################# 2008 | 2009 | speedBoxLeftAlign = 7 2010 | speedBoxHeight = 50 2011 | speedBoxWdith = 75 2012 | speedBoxSpikedimension = 10 2013 | tasHeight = 30 2014 | speedDeltaWidth = 4 2015 | 2016 | tapeScale = 50 2017 | 2018 | self.setPen(0, Qt.GlobalColor.transparent) 2019 | 2020 | self.qp.setBrush(QBrush(QColor(0, 0, 0, 90))) 2021 | self.qp.drawRect(QRectF(0, 0, speedBoxLeftAlign + speedBoxWdith + 15, g5Height)) 2022 | 2023 | if (self._kias + tapeScale / 2) > self._vne: 2024 | brush = QBrush(QColor(Qt.GlobalColor.red)) 2025 | self.qp.setBrush(brush) 2026 | 2027 | self.qp.drawRect( 2028 | QRectF( 2029 | speedBoxLeftAlign + speedBoxWdith + 8, 2030 | 0, 2031 | 8, 2032 | (1 - 2 * (self._vne - self._kias) / tapeScale) * g5CenterY, 2033 | ) 2034 | ) 2035 | 2036 | if (self._kias + tapeScale / 2) > self._vno: 2037 | brush = QBrush(QColor(Qt.GlobalColor.yellow)) 2038 | self.qp.setBrush(brush) 2039 | 2040 | self.qp.drawRect( 2041 | QRectF( 2042 | speedBoxLeftAlign + speedBoxWdith + 8, 2043 | (1 - 2 * (self._vne - self._kias) / tapeScale) * g5CenterY, 2044 | 8, 2045 | (2 * (self._vne - self._vno) / tapeScale) * g5CenterY, 2046 | ) 2047 | ) 2048 | 2049 | if (self._kias + tapeScale / 2) > self._vs: 2050 | brush = QBrush(QColor(Qt.GlobalColor.green)) 2051 | self.qp.setBrush(brush) 2052 | self.qp.drawRect( 2053 | QRectF( 2054 | speedBoxLeftAlign + speedBoxWdith + 8, 2055 | max(0, (1 - 2 * (self._vno - self._kias) / tapeScale) * g5CenterY), 2056 | 8, 2057 | (1 - 2 * (self._vs - self._kias) / tapeScale) * g5CenterY, 2058 | ) 2059 | ) 2060 | 2061 | if (self._kias + tapeScale / 2) > self._vs: 2062 | brush = QBrush(QColor(Qt.GlobalColor.white)) 2063 | self.qp.setBrush(brush) 2064 | self.qp.drawRect( 2065 | QRectF( 2066 | speedBoxLeftAlign + speedBoxWdith + 13, 2067 | max(0, (1 - 2 * (self._vfe - self._kias) / tapeScale) * g5CenterY), 2068 | 3, 2069 | (1 - 2 * (self._vs0 - self._kias) / tapeScale) * g5CenterY, 2070 | ) 2071 | ) 2072 | 2073 | self.setPen(2, Qt.GlobalColor.white) 2074 | 2075 | self.qp.setBackgroundMode(Qt.BGMode.TransparentMode) 2076 | font = self.qp.font() 2077 | font.setPixelSize(speedBoxHeight - 15) 2078 | 2079 | # set default font size 2080 | self.qp.setFont(font) 2081 | 2082 | currentTape = int(self._kias + tapeScale / 2) 2083 | while currentTape > max(0, self._kias - tapeScale / 2): 2084 | if (currentTape % 10) == 0: 2085 | tapeHeight = ( 2086 | 1 - 2 * (currentTape - self._kias) / tapeScale 2087 | ) * g5CenterY 2088 | self.qp.drawLine( 2089 | QPointF(speedBoxLeftAlign + speedBoxWdith + 5, tapeHeight), 2090 | QPointF(speedBoxLeftAlign + speedBoxWdith + 15, tapeHeight), 2091 | ) 2092 | 2093 | self.qp.drawText( 2094 | QRectF( 2095 | speedBoxLeftAlign, 2096 | tapeHeight - speedBoxHeight / 2, 2097 | speedBoxWdith, 2098 | speedBoxHeight, 2099 | ), 2100 | Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter, 2101 | "{:d}".format(int(currentTape)), 2102 | ) 2103 | 2104 | elif (currentTape % 5) == 0: 2105 | self.qp.drawLine( 2106 | QPointF( 2107 | speedBoxLeftAlign + speedBoxWdith + 8, 2108 | (1 - 2 * (currentTape - self._kias) / tapeScale) * g5CenterY, 2109 | ), 2110 | QPointF( 2111 | speedBoxLeftAlign + speedBoxWdith + 15, 2112 | (1 - 2 * (currentTape - self._kias) / tapeScale) * g5CenterY, 2113 | ), 2114 | ) 2115 | 2116 | currentTape -= 1 2117 | 2118 | speedBox = QPolygonF( 2119 | [ 2120 | QPointF(speedBoxLeftAlign, g5CenterY + speedBoxHeight / 2), 2121 | QPointF( 2122 | speedBoxLeftAlign + speedBoxWdith, g5CenterY + speedBoxHeight / 2 2123 | ), 2124 | QPointF( 2125 | speedBoxLeftAlign + speedBoxWdith, 2126 | g5CenterY + speedBoxSpikedimension, 2127 | ), 2128 | QPointF( 2129 | speedBoxLeftAlign + speedBoxWdith + speedBoxSpikedimension, 2130 | g5CenterY, 2131 | ), 2132 | QPointF( 2133 | speedBoxLeftAlign + speedBoxWdith, 2134 | g5CenterY - speedBoxSpikedimension, 2135 | ), 2136 | QPointF( 2137 | speedBoxLeftAlign + speedBoxWdith, g5CenterY - speedBoxHeight / 2 2138 | ), 2139 | QPointF(speedBoxLeftAlign, g5CenterY - speedBoxHeight / 2), 2140 | ] 2141 | ) 2142 | 2143 | self.setPen(2, Qt.GlobalColor.white) 2144 | 2145 | brush = QBrush(QColor(0, 0, 0, 255)) 2146 | self.qp.setBrush(brush) 2147 | 2148 | self.qp.drawPolygon(speedBox) 2149 | 2150 | font = self.qp.font() 2151 | font.setPixelSize(speedBoxHeight - 10) 2152 | # set default font size 2153 | self.qp.setFont(font) 2154 | 2155 | self.qp.drawText( 2156 | QRectF( 2157 | speedBoxLeftAlign, 2158 | g5CenterY - speedBoxHeight / 2, 2159 | speedBoxWdith, 2160 | speedBoxHeight, 2161 | ), 2162 | Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter, 2163 | "{:03d}".format(int(self._kias)), 2164 | ) 2165 | 2166 | # draw the TAS box 2167 | rect = QRectF( 2168 | 0, 2169 | 0, 2170 | speedBoxLeftAlign + speedBoxWdith + 15, 2171 | tasHeight, 2172 | ) 2173 | self.qp.drawRect(rect) 2174 | 2175 | font = self.qp.font() 2176 | font.setPixelSize(20) 2177 | # set default font size 2178 | self.qp.setFont(font) 2179 | 2180 | self.qp.drawText( 2181 | rect, 2182 | Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter, 2183 | "TAS {:03d} kt".format(int(self._ktas)), 2184 | ) 2185 | 2186 | # draw the TAS box 2187 | rect = QRectF( 2188 | 0, 2189 | g5Height - tasHeight, 2190 | speedBoxLeftAlign + speedBoxWdith + 15, 2191 | tasHeight, 2192 | ) 2193 | self.qp.drawRect(rect) 2194 | self.qp.drawText( 2195 | rect, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, "GS" 2196 | ) 2197 | 2198 | self.setPen(2, Qt.GlobalColor.magenta) 2199 | 2200 | self.qp.drawText( 2201 | rect, 2202 | Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter, 2203 | "{:03d} kt".format(int(self._gs * mstokt)), 2204 | ) 2205 | 2206 | self.setPen(1, Qt.GlobalColor.magenta) 2207 | 2208 | brush = QBrush(Qt.GlobalColor.magenta) 2209 | self.qp.setBrush(brush) 2210 | 2211 | self.qp.drawRect( 2212 | QRectF( 2213 | speedBoxLeftAlign + speedBoxWdith + 15, 2214 | g5CenterY, 2215 | speedDeltaWidth, 2216 | -2 * (self._kiasDelta * 10) / tapeScale * g5CenterY, 2217 | ) 2218 | ) 2219 | 2220 | ################################################# 2221 | # ALTITUDE TAPE 2222 | ################################################# 2223 | 2224 | altBoxRightAlign = 7 2225 | altBoxHeight = 30 2226 | altBoxWdith = 75 2227 | altBoxSpikedimension = 10 2228 | altTapeScale = 300 2229 | altTapeLeftAlign = g5Width - altBoxRightAlign - altBoxWdith 2230 | altSettingHeight = 30 2231 | 2232 | vsScale = 30 2233 | vsIndicatorWidth = 7 2234 | 2235 | alttapteLeftBound = altTapeLeftAlign - 1.5 * altBoxSpikedimension 2236 | self.setPen(0, Qt.GlobalColor.transparent) 2237 | self.qp.setBrush(QBrush(QColor(0, 0, 0, 90))) 2238 | self.qp.drawRect( 2239 | QRectF(alttapteLeftBound, 0, g5Width - alttapteLeftBound, int(g5Height)) 2240 | ) 2241 | self.setPen(2, Qt.GlobalColor.white) 2242 | 2243 | self.qp.setBackgroundMode(Qt.BGMode.TransparentMode) 2244 | font = self.qp.font() 2245 | font.setPixelSize(10) 2246 | # set default font size 2247 | self.qp.setFont(font) 2248 | 2249 | # VS tape 2250 | currentTape = vsScale 2251 | 2252 | while currentTape >= 0: 2253 | tapeHeight = (vsScale - currentTape) / vsScale * g5Height 2254 | if (currentTape % 5) == 0: 2255 | self.qp.drawLine( 2256 | QPointF(g5Width - 10, tapeHeight), 2257 | QPointF(g5Width, tapeHeight), 2258 | ) 2259 | self.qp.drawText( 2260 | QRectF( 2261 | g5Width - 30, 2262 | tapeHeight - 5, 2263 | 15, 2264 | vsIndicatorWidth + 3, 2265 | ), 2266 | Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter, 2267 | "{:d}".format(abs(int(currentTape - vsScale / 2))), 2268 | ) 2269 | else: 2270 | self.qp.drawLine( 2271 | QPointF(g5Width - vsIndicatorWidth, tapeHeight), 2272 | QPointF(g5Width, tapeHeight), 2273 | ) 2274 | 2275 | currentTape -= 1 2276 | # tapeHeight = (vsScale - currentTape) / vsScale * g5Height 2277 | vsHeight = -self._vh_ind_fpm / 100 / vsScale * g5Height 2278 | vsRect = QRectF(g5Width, g5CenterY, -vsIndicatorWidth, vsHeight) 2279 | 2280 | self.setPen(0, Qt.GlobalColor.transparent) 2281 | 2282 | brush = QBrush(QColor(Qt.GlobalColor.magenta)) 2283 | self.qp.setBrush(brush) 2284 | 2285 | self.qp.drawRect(vsRect) 2286 | 2287 | self.setPen(2, Qt.GlobalColor.white) 2288 | 2289 | font = self.qp.font() 2290 | font.setPixelSize(20) 2291 | # set default font size 2292 | self.qp.setFont(font) 2293 | 2294 | # altitude tape 2295 | currentTape = int(self._altitude + altTapeScale / 2) 2296 | 2297 | while currentTape > self._altitude - altTapeScale / 2: 2298 | if (currentTape % 20) == 0: 2299 | tapeHeight = ( 2300 | 1 - 2 * (currentTape - self._altitude) / altTapeScale 2301 | ) * g5CenterY 2302 | self.qp.drawLine( 2303 | QPointF(altTapeLeftAlign - 1.5 * altBoxSpikedimension, tapeHeight), 2304 | QPointF(altTapeLeftAlign - altBoxSpikedimension / 2, tapeHeight), 2305 | ) 2306 | if (currentTape % 100) == 0: 2307 | self.qp.drawText( 2308 | QRectF( 2309 | altTapeLeftAlign, 2310 | tapeHeight - speedBoxHeight / 2, 2311 | speedBoxWdith, 2312 | speedBoxHeight, 2313 | ), 2314 | Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, 2315 | "{:d}".format(int(currentTape)), 2316 | ) 2317 | 2318 | currentTape -= 1 2319 | 2320 | # altitude selector 2321 | pen = self.qp.pen() 2322 | pen.setColor(Qt.GlobalColor.cyan) 2323 | pen.setWidth(2) 2324 | self.qp.setPen(pen) 2325 | brush = QBrush(QColor(Qt.GlobalColor.cyan)) 2326 | self.qp.setBrush(brush) 2327 | 2328 | altSelCenter = g5CenterY 2329 | if self._altitudeSel >= int(self._altitude + altTapeScale / 2 - 24): 2330 | altSelCenter = altSettingHeight 2331 | elif self._altitudeSel <= int(self._altitude - altTapeScale / 2 + 24): 2332 | altSelCenter = g5Height - altSettingHeight 2333 | else: 2334 | altSelCenter = ( 2335 | (floor(self._altitude + altTapeScale / 2) - self._altitudeSel) 2336 | / altTapeScale 2337 | * g5Height 2338 | ) 2339 | 2340 | altSel = QPolygonF( 2341 | [ 2342 | QPointF(alttapteLeftBound, altSelCenter - altBoxHeight / 2), 2343 | QPointF(alttapteLeftBound, altSelCenter + altBoxHeight / 2), 2344 | QPointF(altTapeLeftAlign, altSelCenter + altBoxHeight / 2), 2345 | QPointF(altTapeLeftAlign, altSelCenter + altBoxSpikedimension), 2346 | QPointF(altTapeLeftAlign - altBoxSpikedimension, altSelCenter), 2347 | QPointF(altTapeLeftAlign, altSelCenter - altBoxSpikedimension), 2348 | QPointF(altTapeLeftAlign, altSelCenter - altBoxHeight / 2), 2349 | ] 2350 | ) 2351 | self.qp.drawPolygon(altSel) 2352 | 2353 | # Altitude Box 2354 | self.setPen(2, Qt.GlobalColor.white) 2355 | altBoxTextSplitRatio = 2 / 5 2356 | altBox = QPolygonF( 2357 | [ 2358 | QPointF(g5Width - altBoxRightAlign, g5CenterY - altBoxHeight), 2359 | QPointF( 2360 | g5Width - altBoxRightAlign - altBoxWdith * altBoxTextSplitRatio, 2361 | g5CenterY - altBoxHeight, 2362 | ), 2363 | QPointF( 2364 | g5Width - altBoxRightAlign - altBoxWdith * altBoxTextSplitRatio, 2365 | g5CenterY - altBoxHeight / 2, 2366 | ), 2367 | QPointF( 2368 | altTapeLeftAlign, 2369 | g5CenterY - altBoxHeight / 2, 2370 | ), 2371 | QPointF( 2372 | altTapeLeftAlign, 2373 | g5CenterY - altBoxSpikedimension, 2374 | ), 2375 | QPointF( 2376 | altTapeLeftAlign - altBoxSpikedimension, 2377 | g5CenterY, 2378 | ), 2379 | QPointF( 2380 | altTapeLeftAlign, 2381 | g5CenterY + altBoxSpikedimension, 2382 | ), 2383 | QPointF( 2384 | altTapeLeftAlign, 2385 | g5CenterY + altBoxHeight / 2, 2386 | ), 2387 | QPointF( 2388 | g5Width - altBoxRightAlign - altBoxWdith * altBoxTextSplitRatio, 2389 | g5CenterY + altBoxHeight / 2, 2390 | ), 2391 | QPointF( 2392 | g5Width - altBoxRightAlign - altBoxWdith * altBoxTextSplitRatio, 2393 | g5CenterY + altBoxHeight, 2394 | ), 2395 | QPointF(g5Width - altBoxRightAlign, g5CenterY + altBoxHeight), 2396 | ] 2397 | ) 2398 | 2399 | brush = QBrush(QColor(0, 0, 0, 255)) 2400 | self.qp.setBrush(brush) 2401 | 2402 | self.qp.drawPolygon(altBox) 2403 | 2404 | # implement the last 2 digits in 20 ft steps 2405 | altStep = 20 2406 | charWidth = 15 2407 | 2408 | if self._altitude < 0 and self._altitude > -1000: 2409 | # Add the minus sign 2410 | dispRect = QRectF( 2411 | altTapeLeftAlign, 2412 | g5CenterY - altBoxHeight / 2, 2413 | charWidth, 2414 | altBoxHeight, 2415 | ) 2416 | 2417 | self.qp.drawText( 2418 | dispRect, 2419 | Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter, 2420 | "-", 2421 | ) 2422 | 2423 | # extract lower digits 2424 | altLowerDigit = int("{:05d}".format(int(self._altitude))[3:5]) 2425 | 2426 | # floor the last to digit to the closest multiple of 20 2427 | altLowerDigitrounded = 20 * floor(altLowerDigit / 20) 2428 | 2429 | altLowerDigitMod20 = altLowerDigit % 20 2430 | 2431 | if self._altitude >= -40: 2432 | pass 2433 | if altLowerDigitrounded == 20: 2434 | altArray = [20, 0, 20, 40, 60] 2435 | elif altLowerDigitrounded == 40: 2436 | altArray = [0, 20, 40, 60, 80] 2437 | else: 2438 | altArray = [40, 20, 0, 20, 40] 2439 | 2440 | else: 2441 | altArray = [] 2442 | for i in range(5): 2443 | tmp = altLowerDigitrounded + altStep * (i - 2) 2444 | if int(self._altitude / 100) * 100 + tmp >= 0: 2445 | altArray.append(tmp % 100) 2446 | else: 2447 | altArray.append((100 - tmp) % 100) 2448 | 2449 | # define a clip rect to avoid overflowing the alt box 2450 | self.qp.setClipRect( 2451 | QRectF( 2452 | g5Width - altBoxRightAlign - altBoxWdith * altBoxTextSplitRatio, 2453 | g5CenterY - altBoxHeight, 2454 | altBoxWdith * altBoxTextSplitRatio, 2455 | 2 * altBoxHeight, 2456 | ) 2457 | ) 2458 | 2459 | # draw the last 2 digits altitude 2460 | self.qp.drawText( 2461 | QRectF( 2462 | altTapeLeftAlign + altBoxWdith * (1 - altBoxTextSplitRatio), 2463 | g5CenterY 2464 | - 2 * altBoxHeight 2465 | - 0.8 * altBoxHeight * (altLowerDigitMod20 / 20), 2466 | altBoxWdith * altBoxTextSplitRatio, 2467 | 4 * altBoxHeight, 2468 | ), 2469 | Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter, 2470 | "\n".join("{:02d}".format(t) for t in altArray), 2471 | ) 2472 | 2473 | # clear clip rect 2474 | self.qp.setClipRect(0, 0, g5Width, g5Height) 2475 | 2476 | if self._altitude >= 0: 2477 | # extract the last 2 digit 2478 | altLowerDigit = int(self._altitude % 100) 2479 | 2480 | # floor the last to digit to the closest multiple of 20 2481 | altLowerDigitrounded = 20 * floor(altLowerDigit / 20) 2482 | 2483 | altLowerDigitMod20 = altLowerDigit % 20 2484 | 2485 | # fill the array centered on the floor value in multiple of 20ft 2486 | altArray = [] 2487 | for i in range(5): 2488 | tmp = altLowerDigitrounded + altStep * (2 - i) 2489 | if int(self._altitude / 100) * 100 + tmp >= 0: 2490 | altArray.append(tmp % 100) 2491 | else: 2492 | altArray.append((100 - tmp) % 100) 2493 | 2494 | altString = "{:05d}".format(int(self._altitude)) 2495 | 2496 | if self._altitude > 9900: 2497 | dispRect = QRectF( 2498 | altTapeLeftAlign, 2499 | g5CenterY - altBoxHeight / 2, 2500 | charWidth, 2501 | altBoxHeight, 2502 | ) 2503 | 2504 | if ( 2505 | altString[1] == "9" 2506 | and altString[2] == "9" 2507 | and altLowerDigitrounded == 80 2508 | ): 2509 | self.qp.setClipRect(dispRect) 2510 | 2511 | self.qp.drawText( 2512 | QRectF( 2513 | altTapeLeftAlign, 2514 | g5CenterY 2515 | - altBoxHeight / 2 2516 | - 20 2517 | + +0.8 * altBoxHeight * (altLowerDigitMod20 / 20), 2518 | charWidth, 2519 | 60, 2520 | ), 2521 | Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop, 2522 | "{:01d}\n{}".format((int(altString[0]) + 1) % 10, altString[0]) 2523 | if self._altitude >= 10000 2524 | else "{:01d}\n ".format((int(altString[0]) + 1) % 10), 2525 | ) 2526 | 2527 | self.qp.setClipRect(0, 0, g5Width, g5Height) 2528 | 2529 | else: 2530 | self.qp.drawText( 2531 | dispRect, 2532 | Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter, 2533 | altString[0] if self._altitude >= 10000 else "", 2534 | ) 2535 | 2536 | if self._altitude >= 980: 2537 | dispRect = QRectF( 2538 | altTapeLeftAlign + charWidth, 2539 | g5CenterY - altBoxHeight / 2, 2540 | charWidth, 2541 | altBoxHeight, 2542 | ) 2543 | 2544 | if altString[2] == "9" and altLowerDigitrounded == 80: 2545 | self.qp.setClipRect(dispRect) 2546 | 2547 | self.qp.drawText( 2548 | QRectF( 2549 | altTapeLeftAlign + charWidth, 2550 | g5CenterY 2551 | - altBoxHeight / 2 2552 | - 20 2553 | + +0.8 * altBoxHeight * (altLowerDigitMod20 / 20), 2554 | charWidth, 2555 | 60, 2556 | ), 2557 | Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop, 2558 | "{:01d}\n{}".format((int(altString[1]) + 1) % 10, altString[1]) 2559 | if self._altitude >= 1000 2560 | else "{:01d}\n ".format((int(altString[1]) + 1) % 10), 2561 | ) 2562 | 2563 | self.qp.setClipRect(0, 0, g5Width, g5Height) 2564 | 2565 | else: 2566 | self.qp.drawText( 2567 | dispRect, 2568 | Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter, 2569 | altString[1] if self._altitude >= 1000 else "", 2570 | ) 2571 | pass 2572 | 2573 | dispRect = QRectF( 2574 | altTapeLeftAlign + 2 * charWidth, 2575 | g5CenterY - altBoxHeight / 2, 2576 | charWidth, 2577 | altBoxHeight, 2578 | ) 2579 | 2580 | if altLowerDigitrounded == 80: 2581 | self.qp.setClipRect(dispRect) 2582 | 2583 | self.qp.drawText( 2584 | QRectF( 2585 | altTapeLeftAlign + 2 * charWidth, 2586 | g5CenterY 2587 | - altBoxHeight / 2 2588 | - 20 2589 | + +0.8 * altBoxHeight * (altLowerDigitMod20 / 20), 2590 | charWidth, 2591 | 60, 2592 | ), 2593 | Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop, 2594 | "{:01d}\n{}".format((int(altString[2]) + 1) % 10, altString[2]), 2595 | ) 2596 | self.qp.setClipRect(0, 0, g5Width, g5Height) 2597 | else: 2598 | self.qp.drawText( 2599 | dispRect, 2600 | Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter, 2601 | altString[2], 2602 | ) 2603 | 2604 | # define a clip rect to avoid overflowing the alt box 2605 | self.qp.setClipRect( 2606 | QRectF( 2607 | g5Width - altBoxRightAlign - altBoxWdith * altBoxTextSplitRatio, 2608 | g5CenterY - altBoxHeight, 2609 | altBoxWdith * altBoxTextSplitRatio, 2610 | 2 * altBoxHeight, 2611 | ) 2612 | ) 2613 | 2614 | # draw the last 2 digits altitude 2615 | self.qp.drawText( 2616 | QRectF( 2617 | altTapeLeftAlign + altBoxWdith * (1 - altBoxTextSplitRatio), 2618 | g5CenterY 2619 | - 2 * altBoxHeight 2620 | + 0.8 * altBoxHeight * (altLowerDigitMod20 / 20), 2621 | altBoxWdith * altBoxTextSplitRatio, 2622 | 4 * altBoxHeight, 2623 | ), 2624 | Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter, 2625 | "\n".join("{:02d}".format(t) for t in altArray), 2626 | ) 2627 | 2628 | # clear clip rect 2629 | self.qp.setClipRect(0, 0, g5Width, g5Height) 2630 | 2631 | # draw the altimeter setting 2632 | pen = self.qp.pen() 2633 | pen.setColor(Qt.GlobalColor.cyan) 2634 | pen.setWidth(2) 2635 | self.qp.setPen(pen) 2636 | leftAlign = altTapeLeftAlign - 1.5 * altBoxSpikedimension 2637 | rect = QRectF( 2638 | leftAlign, 2639 | g5Height - altSettingHeight, 2640 | g5Width - leftAlign, 2641 | altSettingHeight, 2642 | ) 2643 | self.qp.drawRect(rect) 2644 | 2645 | if 1: 2646 | self.qp.drawText( 2647 | rect, 2648 | Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter, 2649 | "{:04.00f}".format(33.863886 * self._alt_setting), 2650 | ) 2651 | else: 2652 | self.qp.drawText( 2653 | rect, 2654 | Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter, 2655 | "{:02.02f}".format(self._alt_setting), 2656 | ) 2657 | 2658 | # draw the altitude selector 2659 | pen = self.qp.pen() 2660 | pen.setColor(Qt.GlobalColor.cyan) 2661 | pen.setWidth(2) 2662 | self.qp.setPen(pen) 2663 | leftAlign = altTapeLeftAlign - 1.5 * altBoxSpikedimension 2664 | rect = QRectF( 2665 | leftAlign, 2666 | 0, 2667 | g5Width - leftAlign, 2668 | altSettingHeight, 2669 | ) 2670 | self.qp.drawRect(rect) 2671 | 2672 | self.qp.drawText( 2673 | rect, 2674 | Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter, 2675 | "{:d}ft".format(int(self._altitudeSel)), 2676 | ) 2677 | 2678 | ################################################# 2679 | # Turn coordinator 2680 | ################################################# 2681 | 2682 | turnrateHalfWidth = 62 2683 | turnrateHeight = 15 2684 | slipballHeigh = 320 2685 | slipballRadius = 15 2686 | slipballMarkeWidth = 6 2687 | slipballMovementMax = 1 2688 | slipballMovementWdith = 15 2689 | 2690 | self.setPen(1, QColor(0, 0, 0, 127)) 2691 | 2692 | self.qp.drawLine( 2693 | QPointF(g5CenterX, g5Height - turnrateHeight), 2694 | QPointF(g5CenterX, g5Height), 2695 | ) 2696 | self.qp.drawLine( 2697 | QPointF(g5CenterX - turnrateHalfWidth, g5Height - turnrateHeight), 2698 | QPointF(g5CenterX + turnrateHalfWidth, g5Height - turnrateHeight), 2699 | ) 2700 | 2701 | self.setPen(0, Qt.GlobalColor.transparent) 2702 | 2703 | brush = QBrush(QColor(Qt.GlobalColor.magenta)) 2704 | self.qp.setBrush(brush) 2705 | rect = QRectF( 2706 | g5CenterX, 2707 | g5Height - turnrateHeight + 1, 2708 | min(max(self._turnRate, -73), 73) / 32 * turnrateHalfWidth, 2709 | turnrateHeight - 2, 2710 | ) 2711 | self.qp.drawRect(rect) 2712 | 2713 | self.setPen(1, QColor(255, 255, 255, 128)) 2714 | 2715 | self.qp.drawLine( 2716 | QPointF(g5CenterX - turnrateHalfWidth, g5Height - turnrateHeight), 2717 | QPointF(g5CenterX - turnrateHalfWidth, g5Height), 2718 | ) 2719 | self.qp.drawLine( 2720 | QPointF(g5CenterX + turnrateHalfWidth, g5Height - turnrateHeight), 2721 | QPointF(g5CenterX + turnrateHalfWidth, g5Height), 2722 | ) 2723 | 2724 | # slip ball 2725 | # draw the static roll arc 2726 | self.setPen(2, QColor(0, 0, 0, 128)) 2727 | 2728 | self.qp.setBrush(QBrush(QColor(220, 220, 220))) 2729 | 2730 | self.qp.drawRect( 2731 | QRectF( 2732 | g5CenterX - slipballRadius, 2733 | slipballHeigh - slipballRadius, 2734 | -slipballMarkeWidth, 2735 | 2 * slipballRadius, 2736 | ) 2737 | ) 2738 | self.qp.drawRect( 2739 | QRectF( 2740 | g5CenterX + slipballRadius, 2741 | slipballHeigh - slipballRadius, 2742 | slipballMarkeWidth, 2743 | 2 * slipballRadius, 2744 | ) 2745 | ) 2746 | # set slip ball gradian 2747 | grad = QRadialGradient( 2748 | g5CenterX - self._slip * slipballMovementMax * slipballMovementWdith, 2749 | slipballHeigh, 2750 | slipballRadius, 2751 | g5CenterX - self._slip * slipballMovementMax * slipballMovementWdith, 2752 | slipballHeigh, 2753 | ) 2754 | grad.setColorAt(0, QColor(255, 255, 255, 200)) 2755 | grad.setColorAt(1, QColor(160, 160, 160, 200)) 2756 | self.qp.setBrush(grad) 2757 | 2758 | self.qp.drawEllipse( 2759 | QPoint( 2760 | int( 2761 | g5CenterX - self._slip * slipballMovementMax * slipballMovementWdith 2762 | ), 2763 | int(slipballHeigh), 2764 | ), 2765 | slipballRadius, 2766 | slipballRadius, 2767 | ) 2768 | 2769 | self.qp.end() 2770 | 2771 | def pitchLine(self, offset, length): 2772 | """Return a pitch line. 2773 | 2774 | As the pitch line is drawn using translate and rotate 2775 | align the pitch line around the center 2776 | 2777 | Args: 2778 | angle: in degrees 2779 | length: in pixel 2780 | 2781 | Returns: 2782 | Qline 2783 | """ 2784 | pass 2785 | 2786 | def alongRadiusCoord(self, angle, length): 2787 | """Return a line along the radius of the circle. 2788 | 2789 | Args: 2790 | angle: in degrees 2791 | length: in pixel 2792 | 2793 | Returns: 2794 | Qline 2795 | """ 2796 | startPoint = QPoint( 2797 | int(self.rollArcRadius * cos(radians(angle))), 2798 | int(self.rollArcRadius * sin(radians(angle))), 2799 | ) 2800 | endPoint = QPoint( 2801 | int((self.rollArcRadius + length) * cos(radians(angle))), 2802 | int((self.rollArcRadius + length) * sin(radians(angle))), 2803 | ) 2804 | 2805 | return QLine(startPoint, endPoint) 2806 | 2807 | 2808 | class pyG5FMA(pyG5Widget): 2809 | """Generate G5 wdiget view.""" 2810 | 2811 | def __init__(self, parent=None): 2812 | """g5Widget Constructor. 2813 | 2814 | Args: 2815 | parent: Parent Widget 2816 | 2817 | Returns: 2818 | self 2819 | """ 2820 | pyG5Widget.__init__(self, parent) 2821 | 2822 | def paintEvent(self, event): 2823 | """Paint the widget.""" 2824 | self.qp = QPainter(self) 2825 | 2826 | self.setPen(1, Qt.GlobalColor.black) 2827 | self.qp.setBrush(QBrush(Qt.GlobalColor.black)) 2828 | self.qp.drawRect(0, 0, g5Width, fmaHeight) 2829 | 2830 | if self._avionicson == 0: 2831 | self.setPen(1, Qt.GlobalColor.white) 2832 | self.qp.drawLine(0, 0, g5Width, fmaHeight) 2833 | self.qp.drawLine(0, fmaHeight, g5Width, 0) 2834 | self.qp.end() 2835 | return 2836 | 2837 | # draw the FMA sections delimiters 2838 | delimMargin = 5 2839 | self.setPen(2, Qt.GlobalColor.white) 2840 | self.qp.drawLine( 2841 | QLineF(g5Width / 2, delimMargin, g5Width / 2, fmaHeight - delimMargin) 2842 | ) 2843 | self.qp.drawLine( 2844 | QLineF(g5Width / 3, delimMargin, g5Width / 3, fmaHeight - delimMargin) 2845 | ) 2846 | 2847 | self.setPen(2, Qt.GlobalColor.green) 2848 | 2849 | font = self.qp.font() 2850 | font.setPixelSize(20) 2851 | font.setBold(True) 2852 | self.qp.setFont(font) 2853 | 2854 | # draw the text when the AP is engaged 2855 | if self._apMode != 0: 2856 | # Draw the AP mode 2857 | mode = "AP" if self._apMode == 2 else "FD" 2858 | self.qp.drawText( 2859 | QRectF( 2860 | g5Width / 3 + delimMargin, 2861 | delimMargin, 2862 | g5Width / 6 - 2 * delimMargin, 2863 | fmaHeight - 2 * delimMargin, 2864 | ), 2865 | Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter, 2866 | mode, 2867 | ) 2868 | 2869 | # find the engaged horizontal navigation mode 2870 | if int(self._apState) & 0x2: 2871 | hmode = "HDG" 2872 | elif int(self._apState) & 0x4: 2873 | hmode = "ROL" 2874 | elif int(self._apState) & 0x200: 2875 | if int(self._hsiSource) == 2: 2876 | hmode = "GPS" 2877 | elif int(self._hsiSource) == 1: 2878 | hmode = "{}".format(self.getNavTypeString(self._nav2type, "")) 2879 | elif int(self._hsiSource) == 0: 2880 | hmode = "{}".format(self.getNavTypeString(self._nav1type, "")) 2881 | else: 2882 | hmode = "ERR" 2883 | else: 2884 | hmode = "" 2885 | self.qp.drawText( 2886 | QRectF( 2887 | g5Width / 6 + delimMargin, 2888 | delimMargin, 2889 | g5Width / 6 - 2 * delimMargin, 2890 | fmaHeight - 2 * delimMargin, 2891 | ), 2892 | Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter, 2893 | hmode, 2894 | ) 2895 | 2896 | # find the engaged vertical navigation mode 2897 | if int(self._apState) & 0x8: 2898 | vmode = "FLC {} kts".format(int(self._apAirSpeed)) 2899 | elif int(self._apState) & 0x10: 2900 | vmode = "VS {} fpm".format(int(self._apVS)) 2901 | elif int(self._apState) & 0x800: 2902 | vmode = "GS" 2903 | elif int(self._apState) & 0x4000: 2904 | vmode = "ALT {} ft".format(int(self._altitudeHold)) 2905 | elif int(self._apState) & 0x40000: 2906 | vmode = "VPATH" 2907 | else: 2908 | vmode = "PIT" 2909 | 2910 | self.qp.drawText( 2911 | QRectF( 2912 | g5Width / 2 + delimMargin, 2913 | delimMargin, 2914 | g5Width * 2 / 6 - 2 * delimMargin, 2915 | fmaHeight - 2 * delimMargin, 2916 | ), 2917 | Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, 2918 | vmode, 2919 | ) 2920 | 2921 | # draw the armed horizontal navigation mode 2922 | self.setPen(2, Qt.GlobalColor.white) 2923 | 2924 | if int(self._apState) & 0x100: 2925 | if int(self._hsiSource) == 2: 2926 | hmode = "GPS" 2927 | elif int(self._hsiSource) == 1: 2928 | hmode = "{}".format(self.getNavTypeString(self._nav2type, "")) 2929 | elif int(self._hsiSource) == 0: 2930 | hmode = "{}".format(self.getNavTypeString(self._nav1type, "")) 2931 | else: 2932 | hmode = "ERR" 2933 | 2934 | self.qp.drawText( 2935 | QRectF( 2936 | delimMargin, 2937 | delimMargin, 2938 | g5Width / 6 - 2 * delimMargin, 2939 | fmaHeight - 2 * delimMargin, 2940 | ), 2941 | Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter, 2942 | hmode, 2943 | ) 2944 | vmode = "" 2945 | if int(self._apState) & 0x20000: 2946 | vmode += " VPATH" if len(vmode) else "VPTH" 2947 | else: 2948 | if int(self._apState) & 0x20: 2949 | if self._altitudeVNAV > self._apAltitude: 2950 | vmode += " ALTV" if len(vmode) else "ALTV" 2951 | else: 2952 | vmode += " ALTS" if len(vmode) else "ALTS" 2953 | if int(self._apState) & 0x400: 2954 | vmode += " GS" if len(vmode) else "GS" 2955 | 2956 | self.qp.drawText( 2957 | QRectF( 2958 | g5Width * 4 / 6 + delimMargin, 2959 | delimMargin, 2960 | g5Width * 2 / 6 - 2 * delimMargin, 2961 | fmaHeight - 2 * delimMargin, 2962 | ), 2963 | Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter, 2964 | vmode, 2965 | ) 2966 | 2967 | self.qp.end() 2968 | -------------------------------------------------------------------------------- /pyG5/pyG5ViewTester.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on 8 Aug 2021. 3 | 4 | @author: Ben Lauret 5 | """ 6 | 7 | import sys 8 | 9 | from PySide6.QtCore import Qt 10 | from PySide6.QtWidgets import ( 11 | QApplication, 12 | QHBoxLayout, 13 | QGridLayout, 14 | QLabel, 15 | QMainWindow, 16 | QMenu, 17 | QSlider, 18 | QSpinBox, 19 | QVBoxLayout, 20 | QWidget, 21 | QScrollArea, 22 | ) 23 | 24 | from PySide6.QtGui import QKeySequence, QAction 25 | 26 | from pyG5.pyG5View import pyG5DualStackFMA, g5Width, g5Height, pyG5SecondaryWidget 27 | 28 | sliderWdith = 300 29 | 30 | 31 | def controlWidgetGen(control): 32 | """Generate control widget. 33 | 34 | Args: 35 | control: dictionary containing name, min, max 36 | 37 | Returns: 38 | QWdiget 39 | """ 40 | layout = QGridLayout() 41 | 42 | w = QWidget() 43 | w.setLayout(layout) 44 | 45 | layout.addWidget(QLabel(control["name"], parent=w), 0, 0) 46 | 47 | slider = QSlider(Qt.Orientation.Horizontal, parent=w) 48 | slider.setRange(control["min"], control["max"]) 49 | 50 | spinbox = QSpinBox(parent=w) 51 | spinbox.setRange(control["min"], control["max"]) 52 | 53 | slider.valueChanged.connect(spinbox.setValue) 54 | spinbox.valueChanged.connect(slider.setValue) 55 | 56 | layout.addWidget(slider, 0, 1) 57 | layout.addWidget(spinbox, 0, 2) 58 | 59 | return (w, slider) 60 | 61 | 62 | def makeControlDict(name, min, max): 63 | """Generate control dictionary. 64 | 65 | Args: 66 | name: string 67 | min: int 68 | max: int 69 | 70 | Returns: 71 | dictionary 72 | """ 73 | return {"name": name, "min": min, "max": max} 74 | 75 | 76 | if __name__ == "__main__": 77 | # Create an PyQT4 application object. 78 | a = QApplication(sys.argv) 79 | 80 | # The QWidget widget is the base class of all user interface objects in PyQt4. 81 | w = QMainWindow() 82 | 83 | # Set window size. 84 | w.resize(sliderWdith + g5Width, g5Height) 85 | w.move(0, 0) 86 | # Set window title 87 | w.setWindowTitle("Garmin G5") 88 | file_menu = QMenu("&File", w) 89 | 90 | quitAction = QAction("&Quit", w) 91 | quitAction.setShortcut(QKeySequence("Ctrl+w")) 92 | quitAction.triggered.connect(w.close) 93 | file_menu.addAction(quitAction) 94 | 95 | menuBar = w.menuBar() 96 | menuBar.addMenu(file_menu) 97 | 98 | hlayout = QHBoxLayout() 99 | mainWidget = QWidget() 100 | mainWidget.setLayout(hlayout) 101 | 102 | scrollArea = QScrollArea(w) 103 | controlWidget = QWidget(w) 104 | scrollArea.setWidget(controlWidget) 105 | scrollArea.setFixedWidth(380) 106 | scrollArea.setMinimumHeight(160) 107 | scrollArea.setWidgetResizable(True) 108 | scrollArea.setObjectName("scrollArea") 109 | controlVLayout = QVBoxLayout() 110 | controlWidget.setLayout(controlVLayout) 111 | 112 | secView = pyG5SecondaryWidget() 113 | vlayout = QVBoxLayout() 114 | vlayout.addWidget(scrollArea) 115 | vlayout.addWidget(secView) 116 | 117 | hlayout.addLayout(vlayout) 118 | g5View = pyG5DualStackFMA() 119 | hlayout.addWidget(g5View) 120 | 121 | controls = [ 122 | makeControlDict("altitude", -1000, 45000), 123 | makeControlDict("altitudeSel", -1000, 45000), 124 | makeControlDict("flaps", 0, 4), 125 | makeControlDict("trims", -1, 1), 126 | makeControlDict("carbheat", 0, 1), 127 | makeControlDict("fuelsel", -1, 1), 128 | makeControlDict("avionicson", 0, 1), 129 | makeControlDict("magHeading", 0, 360), 130 | makeControlDict("groundTrack", 0, 360), 131 | makeControlDict("pitchAngle", -25, +25), 132 | makeControlDict("rollAngle", -70, +70), 133 | makeControlDict("kias", 0, 230), 134 | makeControlDict("kiasDelta", -30, 30), 135 | makeControlDict("gs", 0, 230), 136 | makeControlDict("vh_ind_fpm", -1500, 1500), 137 | makeControlDict("turnRate", -130, 130), 138 | makeControlDict("slip", -10, 10), 139 | makeControlDict("headingBug", 0, 360), 140 | makeControlDict("windDirection", 0, 360), 141 | makeControlDict("windSpeed", 0, 200), 142 | makeControlDict("hsiSource", 0, 2), 143 | makeControlDict("nav1crs", 0, 360), 144 | makeControlDict("nav1dft", -3, 3), 145 | makeControlDict("nav1fromto", 0, 1), 146 | makeControlDict("nav1bearing", 0, 360), 147 | makeControlDict("nav1gsavailable", 0, 1), 148 | makeControlDict("nav1gs", -30, 30), 149 | makeControlDict("nav2crs", 0, 360), 150 | makeControlDict("nav2dft", -3, 3), 151 | makeControlDict("nav2fromto", 0, 1), 152 | makeControlDict("nav2bearing", 0, 360), 153 | makeControlDict("nav2gsavailable", 0, 1), 154 | makeControlDict("nav2gs", -30, 30), 155 | makeControlDict("gpscrs", 0, 360), 156 | makeControlDict("gpsdft", -3, 3), 157 | makeControlDict("gpsgsavailable", 0, 1), 158 | makeControlDict("gpsgs", -30, 30), 159 | makeControlDict("gpshsisens", 0, 15), 160 | makeControlDict("parkBrake", 0, 1), 161 | ] 162 | 163 | for control in controls: 164 | widget, slider = controlWidgetGen(control) 165 | try: 166 | slider.valueChanged.connect(getattr(g5View.pyG5AI, control["name"])) 167 | slider.valueChanged.connect(getattr(g5View.pyG5HSI, control["name"])) 168 | slider.valueChanged.connect(getattr(g5View.pyG5FMA, control["name"])) 169 | slider.valueChanged.connect(getattr(secView, control["name"])) 170 | print("Slider connected: {}".format(control["name"])) 171 | except Exception as inst: 172 | print("{} control not connected to view: {}".format(control["name"], inst)) 173 | 174 | controlVLayout.addWidget(widget) 175 | controlVLayout.addStretch() 176 | 177 | w.setCentralWidget(mainWidget) 178 | # Show window 179 | w.show() 180 | 181 | sys.exit(a.exec()) 182 | 183 | pass 184 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PySide6==6.7.1 2 | PySide6_Addons==6.7.1 3 | PySide6_Essentials==6.7.1 4 | shiboken6==6.7.1 5 | wheel==0.43.0 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Setup for the pyG5 packaging.""" 2 | 3 | from setuptools import find_packages, setup 4 | 5 | from pyG5.pyG5Main import __version__ 6 | 7 | 8 | with open("README.md") as readme_file: 9 | readme = readme_file.read() 10 | 11 | 12 | # commented out due to impossibility 13 | # to install PyQt5 automatically from pip on Raspbian 14 | # requirements = ["PyQt5"] 15 | requirements = ["PySide6"] 16 | 17 | test_requirements = [ 18 | # TODO: put package test requirements here 19 | ] 20 | 21 | PackageDescription = """ 22 | PyQt5 application connecting to X-Plane flight simulator and displaying a garmin G5 23 | attitude indicator as well as Horizontal Situation indicator 24 | 25 | """ 26 | 27 | 28 | setup( 29 | name="pyG5", 30 | version=__version__, 31 | description=PackageDescription, 32 | long_description_content_type="text/markdown", 33 | long_description=readme, 34 | author="Ben Lauret", 35 | author_email="ben@lauretland.com", 36 | url="https://github.com/blauret/pyG5", 37 | packages=find_packages(where="."), 38 | package_dir={"pyG5": "pyG5"}, 39 | include_package_data=True, 40 | install_requires=requirements, 41 | dependency_links=[], 42 | license="MIT license", 43 | zip_safe=False, 44 | keywords=["X-Plane", "python", "PyQt5", "Garmin", "G5"], 45 | classifiers=[ 46 | "Development Status :: 2 - Pre-Alpha", 47 | "Intended Audience :: Developers", 48 | "License :: OSI Approved :: MIT License", 49 | "Natural Language :: English", 50 | "Programming Language :: Python :: 3.9", 51 | ], 52 | test_suite="tests", 53 | scripts=[ 54 | "Scripts/pyG5DualStacked", 55 | ], 56 | tests_require=test_requirements, 57 | ) 58 | --------------------------------------------------------------------------------