Compare commits
1 Commits
v0.58.0
...
config_plo
| Author | SHA1 | Date | |
|---|---|---|---|
| a523e66536 |
@@ -1,3 +1,2 @@
|
||||
black --line-length=100 $(git diff --cached --name-only --diff-filter=ACM -- '*.py')
|
||||
isort --line-length=100 --profile=black --multi-line=3 --trailing-comma $(git diff --cached --name-only --diff-filter=ACM -- '*.py')
|
||||
git add $(git diff --cached --name-only --diff-filter=ACM -- '*.py')
|
||||
black --line-length=100 $(git diff --cached --name-only --diff-filter=ACM -- '***.py')
|
||||
git add $(git diff --cached --name-only --diff-filter=ACM -- '***.py')
|
||||
|
||||
304
.gitlab-ci.yml
@@ -1,59 +1,29 @@
|
||||
# This file is a template, and might need editing before it works on your project.
|
||||
# Official language image. Look for the different tagged releases at:
|
||||
# https://hub.docker.com/r/library/python/tags/
|
||||
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.11
|
||||
image: $CI_DOCKER_REGISTRY/python:3.9
|
||||
#commands to run in the Docker container before starting each job.
|
||||
variables:
|
||||
DOCKER_TLS_CERTDIR: ""
|
||||
BEC_CORE_BRANCH: "main"
|
||||
OPHYD_DEVICES_BRANCH: "main"
|
||||
CHILD_PIPELINE_BRANCH: $CI_DEFAULT_BRANCH
|
||||
|
||||
workflow:
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "schedule"
|
||||
- if: $CI_PIPELINE_SOURCE == "web"
|
||||
- if: $CI_PIPELINE_SOURCE == "pipeline"
|
||||
- if: $CI_PIPELINE_SOURCE == "parent_pipeline"
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
|
||||
when: never
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
|
||||
include:
|
||||
- template: Security/Secret-Detection.gitlab-ci.yml
|
||||
|
||||
# different stages in the pipeline
|
||||
stages:
|
||||
- Formatter
|
||||
- test
|
||||
- AdditionalTests
|
||||
- End2End
|
||||
- Unittests
|
||||
- Deploy
|
||||
|
||||
before_script:
|
||||
- if [[ "$CI_PROJECT_PATH" != "bec/bec_widgets" ]]; then
|
||||
echo -e "\033[35;1m Using branch $CHILD_PIPELINE_BRANCH of BEC Widgets \033[0;m";
|
||||
test -d bec_widgets || git clone --branch $CHILD_PIPELINE_BRANCH https://gitlab.psi.ch/bec/bec_widgets.git; cd bec_widgets;
|
||||
fi
|
||||
|
||||
formatter:
|
||||
stage: Formatter
|
||||
needs: []
|
||||
script:
|
||||
- pip install black isort
|
||||
- isort --check --diff ./
|
||||
- black --check --diff --color ./
|
||||
rules:
|
||||
- if: $CI_PROJECT_PATH == "bec/bec_widgets"
|
||||
|
||||
- pip install black
|
||||
- black --check --diff --color --line-length=100 ./
|
||||
pylint:
|
||||
stage: Formatter
|
||||
needs: []
|
||||
before_script:
|
||||
- pip install pylint pylint-exit anybadge
|
||||
- pip install -e .[dev,pyqt6]
|
||||
script:
|
||||
- pip install pylint pylint-exit anybadge
|
||||
- pip install -e .[dev]
|
||||
- mkdir ./pylint
|
||||
- pylint ./bec_widgets --output-format=text --output=./pylint/pylint.log | tee ./pylint/pylint.log || pylint-exit $?
|
||||
- PYLINT_SCORE=$(sed -n 's/^Your code has been rated at \([-0-9.]*\)\/.*/\1/p' ./pylint/pylint.log)
|
||||
@@ -63,231 +33,25 @@ pylint:
|
||||
paths:
|
||||
- ./pylint/
|
||||
expire_in: 1 week
|
||||
rules:
|
||||
- if: $CI_PROJECT_PATH == "bec/bec_widgets"
|
||||
|
||||
pylint-check:
|
||||
stage: Formatter
|
||||
needs: []
|
||||
allow_failure: true
|
||||
before_script:
|
||||
- pip install pylint pylint-exit anybadge
|
||||
- apt-get update
|
||||
- apt-get install -y bc
|
||||
script:
|
||||
# Identify changed Python files
|
||||
- if [ "$CI_PIPELINE_SOURCE" == "merge_request_event" ]; then
|
||||
TARGET_BRANCH_COMMIT_SHA=$(git rev-parse $CI_MERGE_REQUEST_TARGET_BRANCH_NAME);
|
||||
CHANGED_FILES=$(git diff --name-only $SOURCE_BRANCH_COMMIT_SHA $TARGET_BRANCH_COMMIT_SHA | grep '\.py$' || true);
|
||||
else
|
||||
CHANGED_FILES=$(git diff --name-only $CI_COMMIT_BEFORE_SHA $CI_COMMIT_SHA | grep '\.py$' || true);
|
||||
fi
|
||||
- if [ -z "$CHANGED_FILES" ]; then echo "No Python files changed."; exit 0; fi
|
||||
|
||||
# Run pylint only on changed files
|
||||
- mkdir ./pylint
|
||||
- pylint $CHANGED_FILES --output-format=text . | tee ./pylint/pylint_changed_files.log || pylint-exit $?
|
||||
- PYLINT_SCORE=$(sed -n 's/^Your code has been rated at \([-0-9.]*\)\/.*/\1/p' ./pylint/pylint_changed_files.log)
|
||||
- echo "Pylint score is $PYLINT_SCORE"
|
||||
|
||||
# Fail the job if the pylint score is below 9
|
||||
- if [ "$(echo "$PYLINT_SCORE < 9" | bc)" -eq 1 ]; then echo "Your pylint score is below the acceptable threshold (9)."; exit 1; fi
|
||||
artifacts:
|
||||
paths:
|
||||
- ./pylint/
|
||||
expire_in: 1 week
|
||||
rules:
|
||||
- if: $CI_PROJECT_PATH == "bec/bec_widgets"
|
||||
|
||||
tests:
|
||||
stage: test
|
||||
stage: Unittests
|
||||
needs: []
|
||||
variables:
|
||||
QT_QPA_PLATFORM: "offscreen"
|
||||
script:
|
||||
- git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
|
||||
- git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
|
||||
- export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
|
||||
- apt-get update
|
||||
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
|
||||
- pip install -e ./bec/bec_lib[dev]
|
||||
- pip install -e .[dev,pyqt6]
|
||||
- coverage run --source=./bec_widgets -m pytest -v --junitxml=report.xml --random-order --full-trace ./tests/unit_tests
|
||||
- apt-get install -y libgl1-mesa-glx x11-utils libxkbcommon-x11-0
|
||||
- pip install .[dev]
|
||||
- coverage run --source=./bec_widgets -m pytest -v --junitxml=report.xml --random-order --full-trace ./tests
|
||||
- coverage report
|
||||
- coverage xml
|
||||
coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
|
||||
artifacts:
|
||||
reports:
|
||||
junit: report.xml
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: coverage.xml
|
||||
cobertura: coverage.xml
|
||||
|
||||
tests-3.10-pyside6:
|
||||
extends: "tests"
|
||||
stage: AdditionalTests
|
||||
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.10
|
||||
script:
|
||||
- git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
|
||||
- git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
|
||||
- export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
|
||||
- apt-get update
|
||||
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
|
||||
- pip install -e ./bec/bec_lib[dev]
|
||||
- pip install -e .[dev,pyside6]
|
||||
- pytest -v --junitxml=report.xml --random-order ./tests/unit_tests
|
||||
allow_failure: true
|
||||
|
||||
tests-3.12-pyside6:
|
||||
extends: "tests"
|
||||
stage: AdditionalTests
|
||||
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.12
|
||||
script:
|
||||
- git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
|
||||
- git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
|
||||
- export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
|
||||
- apt-get update
|
||||
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
|
||||
- pip install -e ./bec/bec_lib[dev]
|
||||
- pip install -e .[dev,pyside6]
|
||||
- pytest -v --junitxml=report.xml --random-order ./tests/unit_tests
|
||||
allow_failure: true
|
||||
|
||||
tests-3.10-pyqt5:
|
||||
extends: "tests"
|
||||
stage: AdditionalTests
|
||||
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.10
|
||||
script:
|
||||
- git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
|
||||
- git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
|
||||
- export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
|
||||
- apt-get update
|
||||
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
|
||||
- pip install -e ./bec/bec_lib[dev]
|
||||
- pip install -e .[dev,pyqt5]
|
||||
- pytest -v --junitxml=report.xml --random-order ./tests/unit_tests
|
||||
allow_failure: true
|
||||
|
||||
tests-3.11-pyqt5:
|
||||
extends: "tests"
|
||||
stage: AdditionalTests
|
||||
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.11
|
||||
script:
|
||||
- git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
|
||||
- git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
|
||||
- export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
|
||||
- apt-get update
|
||||
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
|
||||
- pip install -e ./bec/bec_lib[dev]
|
||||
- pip install -e .[dev,pyqt5]
|
||||
- pytest -v --junitxml=report.xml --random-order ./tests/unit_tests
|
||||
allow_failure: true
|
||||
|
||||
tests-3.12-pyqt5:
|
||||
extends: "tests"
|
||||
stage: AdditionalTests
|
||||
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.12
|
||||
script:
|
||||
- git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
|
||||
- git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
|
||||
- export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
|
||||
- apt-get update
|
||||
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
|
||||
- pip install -e ./bec/bec_lib[dev]
|
||||
- pip install -e .[dev,pyqt5]
|
||||
- pytest -v --junitxml=report.xml --random-order ./tests/unit_tests
|
||||
allow_failure: true
|
||||
|
||||
tests-3.10-pyqt6:
|
||||
extends: "tests"
|
||||
stage: AdditionalTests
|
||||
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.10
|
||||
script:
|
||||
- git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
|
||||
- git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
|
||||
- export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
|
||||
- apt-get update
|
||||
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
|
||||
- pip install -e ./bec/bec_lib[dev]
|
||||
- pip install -e .[dev,pyqt6]
|
||||
- pytest -v --junitxml=report.xml --random-order ./tests/unit_tests
|
||||
allow_failure: true
|
||||
|
||||
tests-3.11-pyqt6:
|
||||
extends: "tests"
|
||||
stage: AdditionalTests
|
||||
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.11
|
||||
script:
|
||||
- git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
|
||||
- git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
|
||||
- export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
|
||||
- apt-get update
|
||||
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
|
||||
- pip install -e ./bec/bec_lib[dev]
|
||||
- pip install -e .[dev,pyqt6]
|
||||
- pytest -v --junitxml=report.xml --random-order ./tests/unit_tests
|
||||
allow_failure: true
|
||||
|
||||
tests-3.12-pyqt6:
|
||||
extends: "tests"
|
||||
stage: AdditionalTests
|
||||
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.12
|
||||
script:
|
||||
- git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
|
||||
- git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
|
||||
- export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
|
||||
- apt-get update
|
||||
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
|
||||
- pip install -e ./bec/bec_lib[dev]
|
||||
- pip install -e .[dev,pyqt6]
|
||||
- pytest -v --junitxml=report.xml --random-order ./tests/unit_tests
|
||||
allow_failure: true
|
||||
|
||||
end-2-end-conda:
|
||||
stage: End2End
|
||||
needs: []
|
||||
image: continuumio/miniconda3
|
||||
allow_failure: false
|
||||
variables:
|
||||
QT_QPA_PLATFORM: "offscreen"
|
||||
script:
|
||||
- apt-get update
|
||||
- apt-get install -y libgl1-mesa-glx libegl1-mesa x11-utils libxkbcommon-x11-0 libdbus-1-3
|
||||
- conda config --prepend channels conda-forge
|
||||
- conda config --set channel_priority strict
|
||||
- conda config --set always_yes yes --set changeps1 no
|
||||
- conda create -q -n test-environment python=3.11
|
||||
- conda init bash
|
||||
- source ~/.bashrc
|
||||
- conda activate test-environment
|
||||
|
||||
- git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
|
||||
- git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
|
||||
- export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
|
||||
|
||||
- cd ./bec
|
||||
- source ./bin/install_bec_dev.sh -t
|
||||
|
||||
- pip install -e ./bec_lib[dev]
|
||||
- pip install -e ./bec_ipython_client[dev]
|
||||
- cd ../
|
||||
- pip install -e .[dev,pyqt6]
|
||||
- cd ./tests/end-2-end
|
||||
- pytest -v --start-servers --flush-redis --random-order
|
||||
|
||||
artifacts:
|
||||
when: on_failure
|
||||
paths:
|
||||
- ./logs/*.log
|
||||
expire_in: 1 week
|
||||
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "schedule"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "web"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "pipeline"'
|
||||
- if: '$CI_PIPELINE_SOURCE == "parent_pipeline"'
|
||||
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main"'
|
||||
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "production"'
|
||||
|
||||
semver:
|
||||
stage: Deploy
|
||||
@@ -303,29 +67,31 @@ semver:
|
||||
- git fetch --tags
|
||||
- git tag
|
||||
|
||||
# build and publish package
|
||||
- pip install python-semantic-release==9.* wheel build twine
|
||||
# build
|
||||
- pip install python-semantic-release wheel
|
||||
- export GL_TOKEN=$CI_UPDATES
|
||||
- semantic-release -vv version
|
||||
|
||||
# check if any artifacts were created
|
||||
- if [ ! -d dist ]; then echo No release will be made; exit 0; fi
|
||||
- twine upload dist/* -u __token__ -p $CI_PYPI_TOKEN --skip-existing
|
||||
- semantic-release publish
|
||||
|
||||
- export REPOSITORY_USERNAME=__token__
|
||||
- export REPOSITORY_PASSWORD=$CI_PYPI_TOKEN
|
||||
- >
|
||||
semantic-release publish -v DEBUG
|
||||
-D version_variable=./setup.py:__version__
|
||||
-D hvcs=gitlab
|
||||
|
||||
allow_failure: false
|
||||
rules:
|
||||
- if: '$CI_COMMIT_REF_NAME == "main" && $CI_PROJECT_PATH == "bec/bec_widgets"'
|
||||
- if: '$CI_COMMIT_REF_NAME == "master"'
|
||||
|
||||
pages:
|
||||
stage: Deploy
|
||||
needs: ["semver"]
|
||||
variables:
|
||||
TARGET_BRANCH: $CI_COMMIT_REF_NAME
|
||||
rules:
|
||||
- if: '$CI_COMMIT_TAG != null'
|
||||
variables:
|
||||
TARGET_BRANCH: $CI_COMMIT_TAG
|
||||
- if: '$CI_COMMIT_REF_NAME == "main" && $CI_PROJECT_PATH == "bec/bec_widgets"'
|
||||
script:
|
||||
- curl -X POST -d "branches=$CI_COMMIT_REF_NAME" -d "token=$RTD_TOKEN" https://readthedocs.org/api/v2/webhook/bec-widgets/253243/
|
||||
# pages:
|
||||
# stage: Deploy
|
||||
# needs: ["tests"]
|
||||
# script:
|
||||
# - git clone --branch $OPHYD_DEVICES_BRANCH https://oauth2:$CI_OPHYD_DEVICES_KEY@gitlab.psi.ch/bec/ophyd_devices.git
|
||||
# - export OPHYD_DEVICES_PATH=$PWD/ophyd_devices
|
||||
# - pip install -r ./docs/source/requirements.txt
|
||||
# - apt-get install -y gcc
|
||||
# - *install-bec-services
|
||||
# - cd ./docs/source; make html
|
||||
# - curl -X POST -d "branches=$CI_COMMIT_REF_NAME" -d "token=$RTD_TOKEN" https://readthedocs.org/api/v2/webhook/beamline-experiment-control/221870/
|
||||
# rules:
|
||||
# - if: '$CI_COMMIT_REF_NAME == "master"'
|
||||
# - if: '$CI_COMMIT_REF_NAME == "production"'
|
||||
@@ -1,17 +0,0 @@
|
||||
## Bug report
|
||||
|
||||
## Summary
|
||||
|
||||
[Provide a brief description of the bug.]
|
||||
|
||||
## Expected Behavior vs Actual Behavior
|
||||
|
||||
[Describe what you expected to happen and what actually happened.]
|
||||
|
||||
## Steps to Reproduce
|
||||
|
||||
[Outline the steps that lead to the bug's occurrence. Be specific and provide a clear sequence of actions.]
|
||||
|
||||
## Related Issues
|
||||
|
||||
[Paste links to any related issues or feature requests.]
|
||||
@@ -1,27 +0,0 @@
|
||||
## Documentation Section
|
||||
|
||||
[Specify the section or page of the documentation that needs updating]
|
||||
|
||||
## Current Information
|
||||
|
||||
[Provide the current information in the documentation that needs to be updated]
|
||||
|
||||
## Proposed Update
|
||||
|
||||
[Describe the proposed update or correction. Be specific about the changes that need to be made]
|
||||
|
||||
## Reason for Update
|
||||
|
||||
[Explain the reason for the documentation update. Include any recent changes, new features, or corrections that necessitate the update]
|
||||
|
||||
## Additional Context
|
||||
|
||||
[Include any additional context or information that can help the documentation team understand the update better]
|
||||
|
||||
## Attachments
|
||||
|
||||
[Attach any files, screenshots, or references that can assist in making the documentation update]
|
||||
|
||||
## Priority
|
||||
|
||||
[Assign a priority level to the documentation update based on its urgency. Use a scale such as Low, Medium, High]
|
||||
@@ -1,40 +0,0 @@
|
||||
## Feature Summary
|
||||
|
||||
[Provide a brief and clear summary of the new feature you are requesting]
|
||||
|
||||
## Problem Description
|
||||
|
||||
[Explain the problem or need that this feature aims to address. Be specific about the issues or gaps in the current functionality]
|
||||
|
||||
## Use Case
|
||||
|
||||
[Describe a real-world scenario or use case where this feature would be beneficial. Explain how it would improve the user experience or workflow]
|
||||
|
||||
## Proposed Solution
|
||||
|
||||
[If you have a specific solution in mind, describe it here. Explain how it would work and how it would address the problem described above]
|
||||
|
||||
## Benefits
|
||||
|
||||
[Explain the benefits and advantages of implementing this feature. Highlight how it adds value to the product or improves user satisfaction]
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
[If you've considered alternative solutions or workarounds, mention them here. Explain why the proposed feature is the preferred option]
|
||||
|
||||
## Impact on Existing Functionality
|
||||
|
||||
[Discuss how the new feature might impact or interact with existing features. Address any potential conflicts or dependencies]
|
||||
|
||||
## Priority
|
||||
|
||||
[Assign a priority level to the feature request based on its importance. Use a scale such as Low, Medium, High]
|
||||
|
||||
## Attachments
|
||||
|
||||
[Include any relevant attachments, such as sketches, diagrams, or references that can help the development team understand your feature request better]
|
||||
|
||||
## Additional Information
|
||||
|
||||
[Provide any additional information that might be relevant to the feature request, such as user feedback, market trends, or similar features in other products]
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
## Description
|
||||
|
||||
[Provide a brief description of the changes introduced by this merge request.]
|
||||
|
||||
## Related Issues
|
||||
|
||||
[Cite any related issues or feature requests that are addressed or resolved by this merge request. Use the gitlab syntax for linking issues, for example, `fixes #123` or `closes #123`.]
|
||||
|
||||
## Type of Change
|
||||
|
||||
- Change 1
|
||||
- Change 2
|
||||
|
||||
## Potential side effects
|
||||
|
||||
[Describe any potential side effects or risks of merging this MR.]
|
||||
|
||||
## Screenshots / GIFs (if applicable)
|
||||
|
||||
[Include any relevant screenshots or GIFs to showcase the changes made.]
|
||||
|
||||
## Additional Comments
|
||||
|
||||
[Add any additional comments or information that may be helpful for reviewers.]
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Documentation is up-to-date.
|
||||
|
||||
581
.pylintrc
@@ -1,581 +0,0 @@
|
||||
[MASTER]
|
||||
|
||||
# A comma-separated list of package or module names from where C extensions may
|
||||
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||
# run arbitrary code.
|
||||
extension-pkg-allow-list=PyQt5, pyqtgraph
|
||||
|
||||
# A comma-separated list of package or module names from where C extensions may
|
||||
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||
# run arbitrary code. (This is an alternative name to extension-pkg-allow-list
|
||||
# for backward compatibility.)
|
||||
extension-pkg-whitelist=
|
||||
|
||||
# Return non-zero exit code if any of these messages/categories are detected,
|
||||
# even if score is above --fail-under value. Syntax same as enable. Messages
|
||||
# specified are enabled, while categories only check already-enabled messages.
|
||||
fail-on=
|
||||
|
||||
# Specify a score threshold to be exceeded before program exits with error.
|
||||
fail-under=8.0
|
||||
|
||||
# Files or directories to be skipped. They should be base names, not paths.
|
||||
ignore=CVS
|
||||
|
||||
# Add files or directories matching the regex patterns to the ignore-list. The
|
||||
# regex matches against paths and can be in Posix or Windows format.
|
||||
ignore-paths=
|
||||
|
||||
# Files or directories matching the regex patterns are skipped. The regex
|
||||
# matches against base names, not paths.
|
||||
ignore-patterns=
|
||||
|
||||
# Python code to execute, usually for sys.path manipulation such as
|
||||
# pygtk.require().
|
||||
#init-hook=
|
||||
|
||||
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
|
||||
# number of processors available to use.
|
||||
jobs=1
|
||||
|
||||
# Control the amount of potential inferred values when inferring a single
|
||||
# object. This can help the performance when dealing with large functions or
|
||||
# complex, nested conditions.
|
||||
limit-inference-results=100
|
||||
|
||||
# List of plugins (as comma separated values of python module names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=
|
||||
|
||||
# Pickle collected data for later comparisons.
|
||||
persistent=yes
|
||||
|
||||
# Minimum Python version to use for version dependent checks. Will default to
|
||||
# the version used to run pylint.
|
||||
py-version=3.10
|
||||
|
||||
# When enabled, pylint would attempt to guess common misconfiguration and emit
|
||||
# user-friendly hints instead of false-positive error messages.
|
||||
suggestion-mode=yes
|
||||
|
||||
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
||||
# active Python interpreter and may run arbitrary code.
|
||||
unsafe-load-any-extension=no
|
||||
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
|
||||
# Only show warnings with the listed confidence levels. Leave empty to show
|
||||
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
|
||||
confidence=
|
||||
|
||||
# Disable the message, report, category or checker with the given id(s). You
|
||||
# can either give multiple identifiers separated by comma (,) or put this
|
||||
# option multiple times (only on the command line, not in the configuration
|
||||
# file where it should appear only once). You can also use "--disable=all" to
|
||||
# disable everything first and then reenable specific checks. For example, if
|
||||
# you want to run only the similarities checker, you can use "--disable=all
|
||||
# --enable=similarities". If you want to run only the classes checker, but have
|
||||
# no Warning level messages displayed, use "--disable=all --enable=classes
|
||||
# --disable=W".
|
||||
disable=missing-module-docstring,
|
||||
missing-class-docstring,
|
||||
import-error,
|
||||
no-name-in-module,
|
||||
raw-checker-failed,
|
||||
bad-inline-option,
|
||||
locally-disabled,
|
||||
file-ignored,
|
||||
suppressed-message,
|
||||
deprecated-pragma,
|
||||
use-symbolic-message-instead,
|
||||
unused-wildcard-import,
|
||||
logging-fstring-interpolation,
|
||||
line-too-long,
|
||||
too-many-instance-attributes,
|
||||
wrong-import-order
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
# multiple time (only on the command line, not in the configuration file where
|
||||
# it should appear only once). See also the "--disable" option for examples.
|
||||
enable=c-extension-no-member
|
||||
|
||||
|
||||
[REPORTS]
|
||||
|
||||
# Python expression which should return a score less than or equal to 10. You
|
||||
# have access to the variables 'error', 'warning', 'refactor', and 'convention'
|
||||
# which contain the number of messages in each category, as well as 'statement'
|
||||
# which is the total number of statements analyzed. This score is used by the
|
||||
# global evaluation report (RP0004).
|
||||
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
|
||||
|
||||
# Template used to display messages. This is a python new-style format string
|
||||
# used to format the message information. See doc for all details.
|
||||
#msg-template=
|
||||
|
||||
# Set the output format. Available formats are text, parseable, colorized, json
|
||||
# and msvs (visual studio). You can also give a reporter class, e.g.
|
||||
# mypackage.mymodule.MyReporterClass.
|
||||
output-format=text
|
||||
|
||||
# Tells whether to display a full report or only the messages.
|
||||
reports=no
|
||||
|
||||
# Activate the evaluation score.
|
||||
score=yes
|
||||
|
||||
|
||||
[REFACTORING]
|
||||
|
||||
# Maximum number of nested blocks for function / method body
|
||||
max-nested-blocks=5
|
||||
|
||||
# Complete name of functions that never returns. When checking for
|
||||
# inconsistent-return-statements if a never returning function is called then
|
||||
# it will be considered as an explicit return statement and no message will be
|
||||
# printed.
|
||||
never-returning-functions=sys.exit,argparse.parse_error
|
||||
|
||||
|
||||
[LOGGING]
|
||||
|
||||
# The type of string formatting that logging methods do. `old` means using %
|
||||
# formatting, `new` is for `{}` formatting.
|
||||
logging-format-style=old
|
||||
|
||||
# Logging modules to check that the string format arguments are in logging
|
||||
# function parameter format.
|
||||
logging-modules=logging
|
||||
|
||||
|
||||
[SPELLING]
|
||||
|
||||
# Limits count of emitted suggestions for spelling mistakes.
|
||||
max-spelling-suggestions=4
|
||||
|
||||
# Spelling dictionary name. Available dictionaries: none. To make it work,
|
||||
# install the 'python-enchant' package.
|
||||
spelling-dict=
|
||||
|
||||
# List of comma separated words that should be considered directives if they
|
||||
# appear and the beginning of a comment and should not be checked.
|
||||
spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:
|
||||
|
||||
# List of comma separated words that should not be checked.
|
||||
spelling-ignore-words=
|
||||
|
||||
# A path to a file that contains the private dictionary; one word per line.
|
||||
spelling-private-dict-file=
|
||||
|
||||
# Tells whether to store unknown words to the private dictionary (see the
|
||||
# --spelling-private-dict-file option) instead of raising a message.
|
||||
spelling-store-unknown-words=no
|
||||
|
||||
|
||||
[MISCELLANEOUS]
|
||||
|
||||
# List of note tags to take in consideration, separated by a comma.
|
||||
notes=FIXME,
|
||||
XXX,
|
||||
TODO
|
||||
|
||||
# Regular expression of note tags to take in consideration.
|
||||
#notes-rgx=
|
||||
|
||||
|
||||
[TYPECHECK]
|
||||
|
||||
# List of decorators that produce context managers, such as
|
||||
# contextlib.contextmanager. Add to this list to register other decorators that
|
||||
# produce valid context managers.
|
||||
contextmanager-decorators=contextlib.contextmanager
|
||||
|
||||
# List of members which are set dynamically and missed by pylint inference
|
||||
# system, and so shouldn't trigger E1101 when accessed. Python regular
|
||||
# expressions are accepted.
|
||||
generated-members=
|
||||
|
||||
# Tells whether missing members accessed in mixin class should be ignored. A
|
||||
# class is considered mixin if its name matches the mixin-class-rgx option.
|
||||
ignore-mixin-members=yes
|
||||
|
||||
# Tells whether to warn about missing members when the owner of the attribute
|
||||
# is inferred to be None.
|
||||
ignore-none=yes
|
||||
|
||||
# This flag controls whether pylint should warn about no-member and similar
|
||||
# checks whenever an opaque object is returned when inferring. The inference
|
||||
# can return multiple potential results while evaluating a Python object, but
|
||||
# some branches might not be evaluated, which results in partial inference. In
|
||||
# that case, it might be useful to still emit no-member and other checks for
|
||||
# the rest of the inferred objects.
|
||||
ignore-on-opaque-inference=yes
|
||||
|
||||
# List of class names for which member attributes should not be checked (useful
|
||||
# for classes with dynamically set attributes). This supports the use of
|
||||
# qualified names.
|
||||
ignored-classes=optparse.Values,thread._local,_thread._local
|
||||
|
||||
# List of module names for which member attributes should not be checked
|
||||
# (useful for modules/projects where namespaces are manipulated during runtime
|
||||
# and thus existing member attributes cannot be deduced by static analysis). It
|
||||
# supports qualified module names, as well as Unix pattern matching.
|
||||
ignored-modules=
|
||||
|
||||
# Show a hint with possible names when a member name was not found. The aspect
|
||||
# of finding the hint is based on edit distance.
|
||||
missing-member-hint=yes
|
||||
|
||||
# The minimum edit distance a name should have in order to be considered a
|
||||
# similar match for a missing member name.
|
||||
missing-member-hint-distance=1
|
||||
|
||||
# The total number of similar names that should be taken in consideration when
|
||||
# showing a hint for a missing member.
|
||||
missing-member-max-choices=1
|
||||
|
||||
# Regex pattern to define which classes are considered mixins ignore-mixin-
|
||||
# members is set to 'yes'
|
||||
mixin-class-rgx=.*[Mm]ixin
|
||||
|
||||
# List of decorators that change the signature of a decorated function.
|
||||
signature-mutators=
|
||||
|
||||
|
||||
[VARIABLES]
|
||||
|
||||
# List of additional names supposed to be defined in builtins. Remember that
|
||||
# you should avoid defining new builtins when possible.
|
||||
additional-builtins=
|
||||
|
||||
# Tells whether unused global variables should be treated as a violation.
|
||||
allow-global-unused-variables=yes
|
||||
|
||||
# List of names allowed to shadow builtins
|
||||
allowed-redefined-builtins=
|
||||
|
||||
# List of strings which can identify a callback function by name. A callback
|
||||
# name must start or end with one of those strings.
|
||||
callbacks=cb_,
|
||||
_cb
|
||||
|
||||
# A regular expression matching the name of dummy variables (i.e. expected to
|
||||
# not be used).
|
||||
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
|
||||
|
||||
# Argument names that match this expression will be ignored. Default to name
|
||||
# with leading underscore.
|
||||
ignored-argument-names=_.*|^ignored_|^unused_
|
||||
|
||||
# Tells whether we should check for unused import in __init__ files.
|
||||
init-import=no
|
||||
|
||||
# List of qualified module names which can have objects that can redefine
|
||||
# builtins.
|
||||
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
|
||||
|
||||
|
||||
[FORMAT]
|
||||
|
||||
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
|
||||
expected-line-ending-format=
|
||||
|
||||
# Regexp for a line that is allowed to be longer than the limit.
|
||||
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
|
||||
|
||||
# Number of spaces of indent required inside a hanging or continued line.
|
||||
indent-after-paren=4
|
||||
|
||||
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
||||
# tab).
|
||||
indent-string=' '
|
||||
|
||||
# Maximum number of characters on a single line.
|
||||
max-line-length=100
|
||||
|
||||
# Maximum number of lines in a module.
|
||||
max-module-lines=1000
|
||||
|
||||
# Allow the body of a class to be on the same line as the declaration if body
|
||||
# contains single statement.
|
||||
single-line-class-stmt=no
|
||||
|
||||
# Allow the body of an if to be on the same line as the test if there is no
|
||||
# else.
|
||||
single-line-if-stmt=no
|
||||
|
||||
|
||||
[SIMILARITIES]
|
||||
|
||||
# Comments are removed from the similarity computation
|
||||
ignore-comments=yes
|
||||
|
||||
# Docstrings are removed from the similarity computation
|
||||
ignore-docstrings=yes
|
||||
|
||||
# Imports are removed from the similarity computation
|
||||
ignore-imports=no
|
||||
|
||||
# Signatures are removed from the similarity computation
|
||||
ignore-signatures=no
|
||||
|
||||
# Minimum lines number of a similarity.
|
||||
min-similarity-lines=4
|
||||
|
||||
|
||||
[BASIC]
|
||||
|
||||
# Naming style matching correct argument names.
|
||||
argument-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct argument names. Overrides argument-
|
||||
# naming-style.
|
||||
#argument-rgx=
|
||||
|
||||
# Naming style matching correct attribute names.
|
||||
attr-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct attribute names. Overrides attr-naming-
|
||||
# style.
|
||||
#attr-rgx=
|
||||
|
||||
# Bad variable names which should always be refused, separated by a comma.
|
||||
bad-names=foo,
|
||||
bar,
|
||||
baz,
|
||||
toto,
|
||||
tutu,
|
||||
tata
|
||||
|
||||
# Bad variable names regexes, separated by a comma. If names match any regex,
|
||||
# they will always be refused
|
||||
bad-names-rgxs=
|
||||
|
||||
# Naming style matching correct class attribute names.
|
||||
class-attribute-naming-style=any
|
||||
|
||||
# Regular expression matching correct class attribute names. Overrides class-
|
||||
# attribute-naming-style.
|
||||
#class-attribute-rgx=
|
||||
|
||||
# Naming style matching correct class constant names.
|
||||
class-const-naming-style=UPPER_CASE
|
||||
|
||||
# Regular expression matching correct class constant names. Overrides class-
|
||||
# const-naming-style.
|
||||
#class-const-rgx=
|
||||
|
||||
# Naming style matching correct class names.
|
||||
class-naming-style=PascalCase
|
||||
|
||||
# Regular expression matching correct class names. Overrides class-naming-
|
||||
# style.
|
||||
#class-rgx=
|
||||
|
||||
# Naming style matching correct constant names.
|
||||
const-naming-style=UPPER_CASE
|
||||
|
||||
# Regular expression matching correct constant names. Overrides const-naming-
|
||||
# style.
|
||||
#const-rgx=
|
||||
|
||||
# Minimum line length for functions/classes that require docstrings, shorter
|
||||
# ones are exempt.
|
||||
docstring-min-length=-1
|
||||
|
||||
# Naming style matching correct function names.
|
||||
function-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct function names. Overrides function-
|
||||
# naming-style.
|
||||
#function-rgx=
|
||||
|
||||
# Good variable names which should always be accepted, separated by a comma.
|
||||
good-names=i,
|
||||
ii,
|
||||
jj,
|
||||
kk,
|
||||
dr,
|
||||
j,
|
||||
k,
|
||||
ex,
|
||||
Run,
|
||||
cb,
|
||||
_
|
||||
|
||||
# Good variable names regexes, separated by a comma. If names match any regex,
|
||||
# they will always be accepted
|
||||
good-names-rgxs=.*scanID.*,.*RID.*,.*pointID.*,.*ID.*,.*_2D.*,.*_1D.*
|
||||
|
||||
# Include a hint for the correct naming format with invalid-name.
|
||||
include-naming-hint=no
|
||||
|
||||
# Naming style matching correct inline iteration names.
|
||||
inlinevar-naming-style=any
|
||||
|
||||
# Regular expression matching correct inline iteration names. Overrides
|
||||
# inlinevar-naming-style.
|
||||
#inlinevar-rgx=
|
||||
|
||||
# Naming style matching correct method names.
|
||||
method-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct method names. Overrides method-naming-
|
||||
# style.
|
||||
#method-rgx=
|
||||
|
||||
# Naming style matching correct module names.
|
||||
module-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct module names. Overrides module-naming-
|
||||
# style.
|
||||
#module-rgx=
|
||||
|
||||
# Colon-delimited sets of names that determine each other's naming style when
|
||||
# the name regexes allow several styles.
|
||||
name-group=
|
||||
|
||||
# Regular expression which should only match function or class names that do
|
||||
# not require a docstring.
|
||||
no-docstring-rgx=^_
|
||||
|
||||
# List of decorators that produce properties, such as abc.abstractproperty. Add
|
||||
# to this list to register other decorators that produce valid properties.
|
||||
# These decorators are taken in consideration only for invalid-name.
|
||||
property-classes=abc.abstractproperty
|
||||
|
||||
# Naming style matching correct variable names.
|
||||
variable-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct variable names. Overrides variable-
|
||||
# naming-style.
|
||||
#variable-rgx=
|
||||
|
||||
|
||||
[STRING]
|
||||
|
||||
# This flag controls whether inconsistent-quotes generates a warning when the
|
||||
# character used as a quote delimiter is used inconsistently within a module.
|
||||
check-quote-consistency=no
|
||||
|
||||
# This flag controls whether the implicit-str-concat should generate a warning
|
||||
# on implicit string concatenation in sequences defined over several lines.
|
||||
check-str-concat-over-line-jumps=no
|
||||
|
||||
|
||||
[IMPORTS]
|
||||
|
||||
# List of modules that can be imported at any level, not just the top level
|
||||
# one.
|
||||
allow-any-import-level=
|
||||
|
||||
# Allow wildcard imports from modules that define __all__.
|
||||
allow-wildcard-with-all=no
|
||||
|
||||
# Analyse import fallback blocks. This can be used to support both Python 2 and
|
||||
# 3 compatible code, which means that the block might have code that exists
|
||||
# only in one or another interpreter, leading to false positives when analysed.
|
||||
analyse-fallback-blocks=no
|
||||
|
||||
# Deprecated modules which should not be used, separated by a comma.
|
||||
deprecated-modules=
|
||||
|
||||
# Output a graph (.gv or any supported image format) of external dependencies
|
||||
# to the given file (report RP0402 must not be disabled).
|
||||
ext-import-graph=
|
||||
|
||||
# Output a graph (.gv or any supported image format) of all (i.e. internal and
|
||||
# external) dependencies to the given file (report RP0402 must not be
|
||||
# disabled).
|
||||
import-graph=
|
||||
|
||||
# Output a graph (.gv or any supported image format) of internal dependencies
|
||||
# to the given file (report RP0402 must not be disabled).
|
||||
int-import-graph=
|
||||
|
||||
# Force import order to recognize a module as part of the standard
|
||||
# compatibility libraries.
|
||||
known-standard-library=
|
||||
|
||||
# Force import order to recognize a module as part of a third party library.
|
||||
known-third-party=enchant
|
||||
|
||||
# Couples of modules and preferred modules, separated by a comma.
|
||||
preferred-modules=
|
||||
|
||||
|
||||
[CLASSES]
|
||||
|
||||
# Warn about protected attribute access inside special methods
|
||||
check-protected-access-in-special-methods=no
|
||||
|
||||
# List of method names used to declare (i.e. assign) instance attributes.
|
||||
defining-attr-methods=__init__,
|
||||
__new__,
|
||||
setUp,
|
||||
__post_init__
|
||||
|
||||
# List of member names, which should be excluded from the protected access
|
||||
# warning.
|
||||
exclude-protected=_asdict,
|
||||
_fields,
|
||||
_replace,
|
||||
_source,
|
||||
_make
|
||||
|
||||
# List of valid names for the first argument in a class method.
|
||||
valid-classmethod-first-arg=cls
|
||||
|
||||
# List of valid names for the first argument in a metaclass class method.
|
||||
valid-metaclass-classmethod-first-arg=cls
|
||||
|
||||
|
||||
[DESIGN]
|
||||
|
||||
# List of regular expressions of class ancestor names to ignore when counting
|
||||
# public methods (see R0903)
|
||||
exclude-too-few-public-methods=
|
||||
|
||||
# List of qualified class names to ignore when counting class parents (see
|
||||
# R0901)
|
||||
ignored-parents=
|
||||
|
||||
# Maximum number of arguments for function / method.
|
||||
max-args=5
|
||||
|
||||
# Maximum number of attributes for a class (see R0902).
|
||||
max-attributes=7
|
||||
|
||||
# Maximum number of boolean expressions in an if statement (see R0916).
|
||||
max-bool-expr=5
|
||||
|
||||
# Maximum number of branch for function / method body.
|
||||
max-branches=12
|
||||
|
||||
# Maximum number of locals for function / method body.
|
||||
max-locals=15
|
||||
|
||||
# Maximum number of parents for a class (see R0901).
|
||||
max-parents=7
|
||||
|
||||
# Maximum number of public methods for a class (see R0904).
|
||||
max-public-methods=20
|
||||
|
||||
# Maximum number of return / yield for function / method body.
|
||||
max-returns=6
|
||||
|
||||
# Maximum number of statements in function / method body.
|
||||
max-statements=50
|
||||
|
||||
# Minimum number of public methods for a class (see R0903).
|
||||
min-public-methods=2
|
||||
|
||||
|
||||
[EXCEPTIONS]
|
||||
|
||||
# Exceptions that will emit a warning when being caught. Defaults to
|
||||
# "BaseException, Exception".
|
||||
overgeneral-exceptions=BaseException,
|
||||
Exception
|
||||
@@ -1,25 +0,0 @@
|
||||
# .readthedocs.yaml
|
||||
# Read the Docs configuration file
|
||||
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||
|
||||
# Required
|
||||
version: 2
|
||||
|
||||
# Set the version of Python and other tools you might need
|
||||
build:
|
||||
os: ubuntu-20.04
|
||||
tools:
|
||||
python: "3.10"
|
||||
|
||||
# Build documentation in the docs/ directory with Sphinx
|
||||
sphinx:
|
||||
configuration: docs/conf.py
|
||||
|
||||
# If using Sphinx, optionally build your docs in additional formats such as PDF
|
||||
# formats:
|
||||
# - pdf
|
||||
|
||||
# Optionally declare the Python requirements required to build your docs
|
||||
python:
|
||||
install:
|
||||
- requirements: docs/requirements.txt
|
||||
154
CHANGELOG.md
@@ -1,157 +1,15 @@
|
||||
# CHANGELOG
|
||||
# Changelog
|
||||
|
||||
<!--next-version-placeholder-->
|
||||
|
||||
|
||||
## v0.58.0 (2024-06-07)
|
||||
## v0.2.0 (2023-07-13)
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(utils.colors): general color validators ([`3094632`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/30946321348abc349fb4003dc39d0232dc19606c))
|
||||
* Move ivan's qtwidgets to bec-widgets ([`34e5ed2`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/34e5ed2cf7e6128a51c110db8870d9560f2b2831))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix: bar colormap dynamic setting ([`67fd5e8`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/67fd5e8581f60fe64027ac57f1f12cefa4d28343))
|
||||
|
||||
* fix: formatting isort ([`bf699ec`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/bf699ec1fbe2aacd31854e84fb0438c336840fcf))
|
||||
|
||||
* fix(curve): 2D scatter updated if color_map_z is changed ([`6985ff0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6985ff0fcef9791b53198206ec8cbccd1d65ef99))
|
||||
|
||||
* fix(curve): color_map_z setting works ([`33f7be4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/33f7be42c512402dab3fdd9781a8234e3ec5f4ba))
|
||||
|
||||
### Test
|
||||
|
||||
* test(color): validation tests added ([`c0ddece`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c0ddeceeeabacbf33019a8f24b18821926dc17ac))
|
||||
|
||||
|
||||
## v0.57.7 (2024-06-07)
|
||||
|
||||
### Documentation
|
||||
|
||||
* docs: added schema of BECDockArea and BECFigure ([`828067f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/828067f486a905eb4678538df58e2bdd6c770de1))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix: add model_config to pydantic models to allow runtime checks after creation ([`ca5e8d2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ca5e8d2fbbffbf221cc5472710fef81a33ee29d6))
|
||||
|
||||
|
||||
## v0.57.6 (2024-06-06)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(bar): docstrings extended ([`edb1775`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/edb1775967c3ff0723d0edad2b764f1ffc832b7c))
|
||||
|
||||
|
||||
## v0.57.5 (2024-06-06)
|
||||
|
||||
### Documentation
|
||||
|
||||
* docs(figure): docs adjusted to be compatible with new signature ([`c037b87`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c037b87675af91b26e8c7c60e76622d4ed4cf5d5))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(waveform): added .plot method with the same signature as BECFigure.plot ([`8479caf`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8479caf53a7325788ca264e5bd9aee01f1d4c5a0))
|
||||
|
||||
* fix(plot_base): .plot removed from plot_base.py, because there is no use case for it ([`82e2c89`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/82e2c898d2e26f786b2d481f85c647472675e75b))
|
||||
|
||||
### Refactor
|
||||
|
||||
* refactor(figure): logic for .add_image and .image consolidated; logic for .add_plot and .plot consolidated ([`52bc322`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/52bc322b2b8d3ef92ff3480e61bddaf32464f976))
|
||||
|
||||
|
||||
## v0.57.4 (2024-06-06)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(docks): set_title do update dock internal _name now ([`15cbc21`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/15cbc21e5bb3cf85f5822d44a2b3665b5aa2f346))
|
||||
|
||||
* fix(docks): docks widget_list adn dockarea panels return values fixed ([`ffae5ee`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/ffae5ee54e6b43da660131092452adff195ba4fb))
|
||||
|
||||
|
||||
## v0.57.3 (2024-06-06)
|
||||
|
||||
### Documentation
|
||||
|
||||
* docs(bar): docs updated ([`4be0d14`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4be0d14b7445c2322c2aef86257db168a841265c))
|
||||
|
||||
* docs: fixed syntax of add_widget ([`a951ebf`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a951ebf1be6c086d094aa8abef5e0dfd1b3b8558))
|
||||
|
||||
* docs: added auto update; closes #206 ([`32da803`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/32da803df9f7259842c43e85ba9a0ce29a266d06))
|
||||
|
||||
* docs: cleanup ([`07d60cf`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/07d60cf7355d2edadb3c5ef8b86607d74b360455))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(ring): automatic updates are disabled uf user specify updates manually with .set_update; 'scan_progres' do not reset number of rings ([`e883dba`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e883dbad814dbcc0a19c341041c6d836e58a5918))
|
||||
|
||||
* fix(ring): enable_auto_updates(True) do not reset properties of already setup bars ([`a2abad3`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a2abad344f4c0039516eb60a825afb6822c5b19a))
|
||||
|
||||
* fix(ring): set_min_max accepts floats ([`d44b1cf`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d44b1cf8b107cf02deedd9154b77d01c7f9ed05d))
|
||||
|
||||
* fix(ring): set_update changed to Literals, no need to specify endpoint manually ([`c5b6499`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c5b6499e41eb1495bf260436ca3e1b036182c360))
|
||||
|
||||
|
||||
## v0.57.2 (2024-06-06)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(test/e2e): autoupdate e2e rewritten ([`e1af5ca`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/e1af5ca60f0616835f9f41d84412f29dc298c644))
|
||||
|
||||
* fix(test/e2e): spiral_progress_bar e2e tests rewritten to use config_dict ([`7fb31fc`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/7fb31fc4d762ff4ca839971b3092a084186f81b8))
|
||||
|
||||
* fix(test/e2e): dockarea and dock e2e tests changed to check asserts against config_dict ([`5c6ba65`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/5c6ba65469863ea1e6fc5abdc742650e20eba9b9))
|
||||
|
||||
* fix: rpc_server_dock fixture now spawns the server process ([`cd9fc46`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/cd9fc46ff8a947242c8c28adcd73d7de60b11c44))
|
||||
|
||||
* fix: accept scalars or numpy arrays of 1 element ([`2a88e17`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/2a88e17b23436c55d25b7d3449e4af3a7689661c))
|
||||
|
||||
### Refactor
|
||||
|
||||
* refactor: move _get_output and _start_plot_process at the module level ([`69f4371`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/69f4371007c66aee6b7521a6803054025adf8c92))
|
||||
|
||||
|
||||
## v0.57.1 (2024-06-06)
|
||||
|
||||
### Documentation
|
||||
|
||||
* docs: docs refactored from add_widget_bec to add_widget ([`c3f4845`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c3f4845b4f95005ff737fed5542600b0b9a9cc2b))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix: tests references to add_widget_bec refactored ([`f51b25f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/f51b25f0af4ab8b0a75ee48a40bfbb079c16e9d1))
|
||||
|
||||
* fix(dock): add_widget and add_widget_bec consolidated ([`8ae323f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8ae323f5c3c0d9d0f202d31d5e8374a272a26be2))
|
||||
|
||||
|
||||
## v0.57.0 (2024-06-05)
|
||||
|
||||
### Documentation
|
||||
|
||||
* docs: extend user documentation for BEC Widgets ([`4160f3d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4160f3d6d7ec1122785b5e3fdfc4afe67a95e9a1))
|
||||
## v0.1.0 (2023-07-11)
|
||||
|
||||
### Feature
|
||||
|
||||
* feat(widgets/console): BECJupyterConsole added ([`8c03034`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/8c03034acf6b3ed1e346ebf1b785d41068513cc5))
|
||||
|
||||
|
||||
## v0.56.3 (2024-06-05)
|
||||
|
||||
### Ci
|
||||
|
||||
* ci: increased verbosity for e2e tests ([`4af1abe`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4af1abe4e15b62d2f7e70bf987a1a7d8694ef4d5))
|
||||
|
||||
### Fix
|
||||
|
||||
* fix: fixed support for auto updates ([`131f49d`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/131f49da8ea65af4d44b50e81c1acfc29cd92093))
|
||||
|
||||
|
||||
## v0.56.2 (2024-06-05)
|
||||
|
||||
### Fix
|
||||
|
||||
* fix(bar): ring saves current value in config ([`9648e3e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/9648e3ea96a4109be6be694d855151ed6d3ad661))
|
||||
|
||||
* fix(dock): dock saves configs of all children widgets ([`4be756a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/4be756a8676421c3a3451458995232407295df84))
|
||||
|
||||
* fix(dock_area): save/restore state is saved in config ([`46face0`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/46face0ee59122f04cb383da685a6658beeeb389))
|
||||
|
||||
* fix(figure): added correct types of configs to subplot widgets ([`6f3b1ea`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/6f3b1ea985c18929b9bab54239eeb600f03b274a))
|
||||
* Added config plotter ([`db274c6`](https://gitlab.psi.ch/bec/bec-widgets/-/commit/db274c644f643f830c35b6a92edd328bf7e24f59))
|
||||
|
||||
29
LICENSE
@@ -1,29 +0,0 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2023, bec
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
72
README.md
@@ -1,74 +1,2 @@
|
||||
# BEC Widgets
|
||||
|
||||
BEC Widgets is a GUI framework designed for interaction with [BEC (Beamline Experiment Control)](https://gitlab.psi.ch/bec/bec).
|
||||
## Installation
|
||||
|
||||
Use the package manager [pip](https://pip.pypa.io/en/stable/) to install BEC Widgets:
|
||||
|
||||
```bash
|
||||
pip install bec_widgets PyQt6
|
||||
```
|
||||
|
||||
For development purposes, you can clone the repository and install the package locally in editable mode:
|
||||
|
||||
```bash
|
||||
git clone https://gitlab.psi.ch/bec/bec-widgets
|
||||
cd bec_widgets
|
||||
pip install -e .[dev,pyqt6]
|
||||
```
|
||||
|
||||
BEC Widgets currently supports both PyQt5 and PyQt6, however, no default distribution is specified. As a result, users must install one of the supported
|
||||
Python Qt distributions manually.
|
||||
|
||||
To select a specific Python Qt distribution, install the package with an additional tag:
|
||||
|
||||
```bash
|
||||
pip install bec_widgets[pyqt6]
|
||||
```
|
||||
or
|
||||
|
||||
```bash
|
||||
pip install bec_widgets[pyqt5]
|
||||
```
|
||||
## Documentation
|
||||
|
||||
Documentation of BEC Widgets can be found [here](https://bec-widgets.readthedocs.io/en/latest/). The documentation of the BEC can be found [here](https://bec.readthedocs.io/en/latest/).
|
||||
|
||||
## Contributing
|
||||
|
||||
All commits should use the Angular commit scheme:
|
||||
|
||||
> #### <a name="commit-header"></a>Angular Commit Message Header
|
||||
>
|
||||
> ```
|
||||
> <type>(<scope>): <short summary>
|
||||
> │ │ │
|
||||
> │ │ └─⫸ Summary in present tense. Not capitalized. No period at the end.
|
||||
> │ │
|
||||
> │ └─⫸ Commit Scope: animations|bazel|benchpress|common|compiler|compiler-cli|core|
|
||||
> │ elements|forms|http|language-service|localize|platform-browser|
|
||||
> │ platform-browser-dynamic|platform-server|router|service-worker|
|
||||
> │ upgrade|zone.js|packaging|changelog|docs-infra|migrations|ngcc|ve|
|
||||
> │ devtools
|
||||
> │
|
||||
> └─⫸ Commit Type: build|ci|docs|feat|fix|perf|refactor|test
|
||||
> ```
|
||||
>
|
||||
> The `<type>` and `<summary>` fields are mandatory, the `(<scope>)` field is optional.
|
||||
|
||||
> ##### Type
|
||||
>
|
||||
> Must be one of the following:
|
||||
>
|
||||
> * **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
|
||||
> * **ci**: Changes to our CI configuration files and scripts (examples: CircleCi, SauceLabs)
|
||||
> * **docs**: Documentation only changes
|
||||
> * **feat**: A new feature
|
||||
> * **fix**: A bug fix
|
||||
> * **perf**: A code change that improves performance
|
||||
> * **refactor**: A code change that neither fixes a bug nor adds a feature
|
||||
> * **test**: Adding missing tests or correcting existing tests
|
||||
|
||||
## License
|
||||
|
||||
[BSD-3-Clause](https://choosealicense.com/licenses/bsd-3-clause/)
|
||||
|
Before Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 3.8 KiB |
98
bec_widgets/cli.py
Normal file
@@ -0,0 +1,98 @@
|
||||
import argparse
|
||||
import os
|
||||
from threading import RLock
|
||||
|
||||
from PyQt5 import uic
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
from PyQt5.QtWidgets import QApplication, QMainWindow
|
||||
from scan_plot import BECScanPlot
|
||||
|
||||
from bec_lib.core import BECMessage, MessageEndpoints, RedisConnector
|
||||
|
||||
|
||||
class BEC_UI(QMainWindow):
|
||||
new_scan_data = pyqtSignal(dict)
|
||||
new_dap_data = pyqtSignal(dict) # signal per proc instance?
|
||||
new_scan = pyqtSignal()
|
||||
|
||||
def __init__(self, uipath):
|
||||
super().__init__()
|
||||
self._scan_channels = set()
|
||||
self._dap_channels = set()
|
||||
|
||||
self._scan_thread = None
|
||||
self._dap_threads = []
|
||||
|
||||
ui = uic.loadUi(uipath, self)
|
||||
|
||||
_, fname = os.path.split(uipath)
|
||||
self.setWindowTitle(fname)
|
||||
|
||||
for sp in ui.findChildren(BECScanPlot):
|
||||
for chan in (sp.x_channel, *sp.y_channel_list):
|
||||
if chan.startswith("dap."):
|
||||
chan = chan.partition("dap.")[-1]
|
||||
self._dap_channels.add(chan)
|
||||
else:
|
||||
self._scan_channels.add(chan)
|
||||
|
||||
sp.initialize() # TODO: move this elsewhere?
|
||||
|
||||
self.new_scan_data.connect(sp.redraw_scan) # TODO: merge
|
||||
self.new_dap_data.connect(sp.redraw_dap)
|
||||
self.new_scan.connect(sp.clearData)
|
||||
|
||||
# Scan setup
|
||||
self._scan_id = None
|
||||
scan_lock = RLock()
|
||||
|
||||
def _scan_cb(msg):
|
||||
msg = BECMessage.ScanMessage.loads(msg.value)
|
||||
with scan_lock:
|
||||
scan_id = msg[0].content["scanID"]
|
||||
if self._scan_id != scan_id:
|
||||
self._scan_id = scan_id
|
||||
self.new_scan.emit()
|
||||
self.new_scan_data.emit(msg[0].content["data"])
|
||||
|
||||
bec_connector = RedisConnector("localhost:6379")
|
||||
|
||||
if self._scan_channels:
|
||||
scan_readback = MessageEndpoints.scan_segment()
|
||||
self._scan_thread = bec_connector.consumer(
|
||||
topics=scan_readback,
|
||||
cb=_scan_cb,
|
||||
)
|
||||
self._scan_thread.start()
|
||||
|
||||
# DAP setup
|
||||
def _proc_cb(msg):
|
||||
msg = BECMessage.ProcessedDataMessage.loads(msg.value)
|
||||
self.new_dap_data.emit(msg.content["data"])
|
||||
|
||||
if self._dap_channels:
|
||||
for chan in self._dap_channels:
|
||||
proc_ep = MessageEndpoints.processed_data(chan)
|
||||
dap_thread = bec_connector.consumer(topics=proc_ep, cb=_proc_cb)
|
||||
dap_thread.start()
|
||||
self._dap_threads.append(dap_thread)
|
||||
|
||||
self.show()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="bec-pyqt", formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
||||
)
|
||||
|
||||
parser.add_argument("uipath", type=str, help="Path to a BEC ui file")
|
||||
|
||||
args, rem = parser.parse_known_args()
|
||||
|
||||
app = QApplication(rem)
|
||||
BEC_UI(args.uipath)
|
||||
app.exec_()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1 +0,0 @@
|
||||
from .client import BECDockArea, BECFigure
|
||||
@@ -1,151 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .client import BECDockArea, BECFigure
|
||||
|
||||
|
||||
class ScanInfo(BaseModel):
|
||||
scan_id: str
|
||||
scan_number: int
|
||||
scan_name: str
|
||||
scan_report_devices: list
|
||||
monitored_devices: list
|
||||
status: str
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
|
||||
|
||||
class AutoUpdates:
|
||||
create_default_dock: bool = False
|
||||
enabled: bool = False
|
||||
dock_name: str = None
|
||||
|
||||
def __init__(self, gui: BECDockArea):
|
||||
self.gui = gui
|
||||
|
||||
def start_default_dock(self):
|
||||
"""
|
||||
Create a default dock for the auto updates.
|
||||
"""
|
||||
dock = self.gui.add_dock("default_figure")
|
||||
dock.add_widget("BECFigure")
|
||||
self.dock_name = "default_figure"
|
||||
|
||||
@staticmethod
|
||||
def get_scan_info(msg) -> ScanInfo:
|
||||
"""
|
||||
Update the script with the given data.
|
||||
"""
|
||||
info = msg.info
|
||||
status = msg.status
|
||||
scan_id = msg.scan_id
|
||||
scan_number = info.get("scan_number", 0)
|
||||
scan_name = info.get("scan_name", "Unknown")
|
||||
scan_report_devices = info.get("scan_report_devices", [])
|
||||
monitored_devices = info.get("readout_priority", {}).get("monitored", [])
|
||||
monitored_devices = [dev for dev in monitored_devices if dev not in scan_report_devices]
|
||||
return ScanInfo(
|
||||
scan_id=scan_id,
|
||||
scan_number=scan_number,
|
||||
scan_name=scan_name,
|
||||
scan_report_devices=scan_report_devices,
|
||||
monitored_devices=monitored_devices,
|
||||
status=status,
|
||||
)
|
||||
|
||||
def get_default_figure(self) -> BECFigure | None:
|
||||
"""
|
||||
Get the default figure from the GUI.
|
||||
"""
|
||||
dock = self.gui.panels.get(self.dock_name, [])
|
||||
if not dock:
|
||||
return None
|
||||
widgets = dock.widget_list
|
||||
if not widgets:
|
||||
return None
|
||||
return widgets[0]
|
||||
|
||||
def run(self, msg):
|
||||
"""
|
||||
Run the update function if enabled.
|
||||
"""
|
||||
if not self.enabled:
|
||||
return
|
||||
if msg.status != "open":
|
||||
return
|
||||
info = self.get_scan_info(msg)
|
||||
self.handler(info)
|
||||
|
||||
@staticmethod
|
||||
def get_selected_device(monitored_devices, selected_device):
|
||||
"""
|
||||
Get the selected device for the plot. If no device is selected, the first
|
||||
device in the monitored devices list is selected.
|
||||
"""
|
||||
if selected_device:
|
||||
return selected_device
|
||||
if len(monitored_devices) > 0:
|
||||
sel_device = monitored_devices[0]
|
||||
return sel_device
|
||||
return None
|
||||
|
||||
def handler(self, info: ScanInfo) -> None:
|
||||
"""
|
||||
Default update function.
|
||||
"""
|
||||
if info.scan_name == "line_scan" and info.scan_report_devices:
|
||||
self.simple_line_scan(info)
|
||||
return
|
||||
if info.scan_name == "grid_scan" and info.scan_report_devices:
|
||||
self.simple_grid_scan(info)
|
||||
return
|
||||
if info.scan_report_devices:
|
||||
self.best_effort(info)
|
||||
return
|
||||
|
||||
def simple_line_scan(self, info: ScanInfo) -> None:
|
||||
"""
|
||||
Simple line scan.
|
||||
"""
|
||||
fig = self.get_default_figure()
|
||||
if not fig:
|
||||
return
|
||||
dev_x = info.scan_report_devices[0]
|
||||
dev_y = self.get_selected_device(info.monitored_devices, self.gui.selected_device)
|
||||
if not dev_y:
|
||||
return
|
||||
fig.clear_all()
|
||||
plt = fig.plot(x_name=dev_x, y_name=dev_y)
|
||||
plt.set(title=f"Scan {info.scan_number}", x_label=dev_x, y_label=dev_y)
|
||||
|
||||
def simple_grid_scan(self, info: ScanInfo) -> None:
|
||||
"""
|
||||
Simple grid scan.
|
||||
"""
|
||||
fig = self.get_default_figure()
|
||||
if not fig:
|
||||
return
|
||||
dev_x = info.scan_report_devices[0]
|
||||
dev_y = info.scan_report_devices[1]
|
||||
dev_z = self.get_selected_device(info.monitored_devices, self.gui.selected_device)
|
||||
fig.clear_all()
|
||||
plt = fig.plot(x_name=dev_x, y_name=dev_y, z_name=dev_z, label=f"Scan {info.scan_number}")
|
||||
plt.set(title=f"Scan {info.scan_number}", x_label=dev_x, y_label=dev_y)
|
||||
|
||||
def best_effort(self, info: ScanInfo) -> None:
|
||||
"""
|
||||
Best effort scan.
|
||||
"""
|
||||
fig = self.get_default_figure()
|
||||
if not fig:
|
||||
return
|
||||
dev_x = info.scan_report_devices[0]
|
||||
dev_y = self.get_selected_device(info.monitored_devices, self.gui.selected_device)
|
||||
if not dev_y:
|
||||
return
|
||||
fig.clear_all()
|
||||
plt = fig.plot(x_name=dev_x, y_name=dev_y, label=f"Scan {info.scan_number}")
|
||||
plt.set(title=f"Scan {info.scan_number}", x_label=dev_x, y_label=dev_y)
|
||||
@@ -1,317 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import importlib.metadata as imd
|
||||
import os
|
||||
import select
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from functools import wraps
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.utils.import_utils import isinstance_based_on_class_name, lazy_import, lazy_import_from
|
||||
from qtpy.QtCore import QCoreApplication
|
||||
|
||||
import bec_widgets.cli.client as client
|
||||
from bec_widgets.cli.auto_updates import AutoUpdates
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bec_lib.device import DeviceBase
|
||||
|
||||
from bec_widgets.cli.client import BECDockArea, BECFigure
|
||||
|
||||
messages = lazy_import("bec_lib.messages")
|
||||
# from bec_lib.connector import MessageObject
|
||||
MessageObject = lazy_import_from("bec_lib.connector", ("MessageObject",))
|
||||
BECDispatcher = lazy_import_from("bec_widgets.utils.bec_dispatcher", ("BECDispatcher",))
|
||||
|
||||
|
||||
def rpc_call(func):
|
||||
"""
|
||||
A decorator for calling a function on the server.
|
||||
|
||||
Args:
|
||||
func: The function to call.
|
||||
|
||||
Returns:
|
||||
The result of the function call.
|
||||
"""
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
# we could rely on a strict type check here, but this is more flexible
|
||||
# moreover, it would anyway crash for objects...
|
||||
out = []
|
||||
for arg in args:
|
||||
if hasattr(arg, "name"):
|
||||
arg = arg.name
|
||||
out.append(arg)
|
||||
args = tuple(out)
|
||||
for key, val in kwargs.items():
|
||||
if hasattr(val, "name"):
|
||||
kwargs[key] = val.name
|
||||
if not self.gui_is_alive():
|
||||
raise RuntimeError("GUI is not alive")
|
||||
return self._run_rpc(func.__name__, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def _get_output(process) -> None:
|
||||
try:
|
||||
os.set_blocking(process.stdout.fileno(), False)
|
||||
os.set_blocking(process.stderr.fileno(), False)
|
||||
while process.poll() is None:
|
||||
readylist, _, _ = select.select([process.stdout, process.stderr], [], [], 1)
|
||||
if process.stdout in readylist:
|
||||
output = process.stdout.read(1024)
|
||||
if output:
|
||||
print(output, end="")
|
||||
if process.stderr in readylist:
|
||||
error_output = process.stderr.read(1024)
|
||||
if error_output:
|
||||
print(error_output, end="", file=sys.stderr)
|
||||
except Exception as e:
|
||||
print(f"Error reading process output: {str(e)}")
|
||||
|
||||
|
||||
def _start_plot_process(gui_id, gui_class, config) -> None:
|
||||
"""
|
||||
Start the plot in a new process.
|
||||
"""
|
||||
# pylint: disable=subprocess-run-check
|
||||
monitor_module = importlib.import_module("bec_widgets.cli.server")
|
||||
monitor_path = monitor_module.__file__
|
||||
|
||||
command = [
|
||||
sys.executable,
|
||||
"-u",
|
||||
monitor_path,
|
||||
"--id",
|
||||
gui_id,
|
||||
"--config",
|
||||
config,
|
||||
"--gui_class",
|
||||
gui_class.__name__,
|
||||
]
|
||||
process = subprocess.Popen(command, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
process_output_processing_thread = threading.Thread(target=_get_output, args=(process,))
|
||||
process_output_processing_thread.start()
|
||||
return process, process_output_processing_thread
|
||||
|
||||
|
||||
class BECGuiClientMixin:
|
||||
def __init__(self, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self._process = None
|
||||
self._process_output_processing_thread = None
|
||||
self.auto_updates = self._get_update_script()
|
||||
self._target_endpoint = MessageEndpoints.scan_status()
|
||||
self._selected_device = None
|
||||
self.stderr_output = []
|
||||
|
||||
def _get_update_script(self) -> AutoUpdates | None:
|
||||
eps = imd.entry_points(group="bec.widgets.auto_updates")
|
||||
for ep in eps:
|
||||
if ep.name == "plugin_widgets_update":
|
||||
try:
|
||||
return ep.load()(gui=self)
|
||||
except Exception as e:
|
||||
print(f"Error loading auto update script from plugin: {str(e)}")
|
||||
return None
|
||||
|
||||
@property
|
||||
def selected_device(self):
|
||||
"""
|
||||
Selected device for the plot.
|
||||
"""
|
||||
return self._selected_device
|
||||
|
||||
@selected_device.setter
|
||||
def selected_device(self, device: str | DeviceBase):
|
||||
if isinstance_based_on_class_name(device, "bec_lib.device.DeviceBase"):
|
||||
self._selected_device = device.name
|
||||
elif isinstance(device, str):
|
||||
self._selected_device = device
|
||||
else:
|
||||
raise ValueError("Device must be a string or a device object")
|
||||
|
||||
def _start_update_script(self) -> None:
|
||||
self._client.connector.register(
|
||||
self._target_endpoint, cb=self._handle_msg_update, parent=self
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _handle_msg_update(msg: MessageObject, parent: BECGuiClientMixin) -> None:
|
||||
if parent.auto_updates is not None:
|
||||
# pylint: disable=protected-access
|
||||
parent._update_script_msg_parser(msg.value)
|
||||
|
||||
def _update_script_msg_parser(self, msg: messages.BECMessage) -> None:
|
||||
if isinstance(msg, messages.ScanStatusMessage):
|
||||
if not self.gui_is_alive():
|
||||
return
|
||||
self.auto_updates.run(msg)
|
||||
|
||||
def show(self) -> None:
|
||||
"""
|
||||
Show the figure.
|
||||
"""
|
||||
if self._process is None or self._process.poll() is not None:
|
||||
self._start_update_script()
|
||||
self._process, self._process_output_processing_thread = _start_plot_process(
|
||||
self._gui_id, self.__class__, self._client._service_config.redis
|
||||
)
|
||||
while not self.gui_is_alive():
|
||||
print("Waiting for GUI to start...")
|
||||
time.sleep(1)
|
||||
|
||||
def close(self) -> None:
|
||||
"""
|
||||
Close the figure.
|
||||
"""
|
||||
if self._process is None:
|
||||
return
|
||||
if self.gui_is_alive():
|
||||
self._run_rpc("close", (), wait_for_rpc_response=True)
|
||||
else:
|
||||
self._run_rpc("close", (), wait_for_rpc_response=False)
|
||||
self._process.terminate()
|
||||
self._process_output_processing_thread.join()
|
||||
self._process = None
|
||||
self._client.shutdown()
|
||||
|
||||
def print_log(self) -> None:
|
||||
"""
|
||||
Print the log of the plot process.
|
||||
"""
|
||||
if self._process is None:
|
||||
return
|
||||
print("".join(self.stderr_output))
|
||||
# Flush list
|
||||
self.stderr_output.clear()
|
||||
|
||||
|
||||
class RPCResponseTimeoutError(Exception):
|
||||
"""Exception raised when an RPC response is not received within the expected time."""
|
||||
|
||||
def __init__(self, request_id, timeout):
|
||||
super().__init__(
|
||||
f"RPC response not received within {timeout} seconds for request ID {request_id}"
|
||||
)
|
||||
|
||||
|
||||
class RPCBase:
|
||||
def __init__(self, gui_id: str = None, config: dict = None, parent=None) -> None:
|
||||
self._client = BECDispatcher().client
|
||||
self._config = config if config is not None else {}
|
||||
self._gui_id = gui_id if gui_id is not None else str(uuid.uuid4())
|
||||
self._parent = parent
|
||||
super().__init__()
|
||||
# print(f"RPCBase: {self._gui_id}")
|
||||
|
||||
def __repr__(self):
|
||||
type_ = type(self)
|
||||
qualname = type_.__qualname__
|
||||
return f"<{qualname} object at {hex(id(self))}>"
|
||||
|
||||
@property
|
||||
def _root(self):
|
||||
"""
|
||||
Get the root widget. This is the BECFigure widget that holds
|
||||
the anchor gui_id.
|
||||
"""
|
||||
parent = self
|
||||
# pylint: disable=protected-access
|
||||
while parent._parent is not None:
|
||||
parent = parent._parent
|
||||
return parent
|
||||
|
||||
def _run_rpc(self, method, *args, wait_for_rpc_response=True, **kwargs):
|
||||
"""
|
||||
Run the RPC call.
|
||||
|
||||
Args:
|
||||
method: The method to call.
|
||||
args: The arguments to pass to the method.
|
||||
wait_for_rpc_response: Whether to wait for the RPC response.
|
||||
kwargs: The keyword arguments to pass to the method.
|
||||
|
||||
Returns:
|
||||
The result of the RPC call.
|
||||
"""
|
||||
request_id = str(uuid.uuid4())
|
||||
rpc_msg = messages.GUIInstructionMessage(
|
||||
action=method,
|
||||
parameter={"args": args, "kwargs": kwargs, "gui_id": self._gui_id},
|
||||
metadata={"request_id": request_id},
|
||||
)
|
||||
|
||||
# pylint: disable=protected-access
|
||||
receiver = self._root._gui_id
|
||||
self._client.connector.set_and_publish(MessageEndpoints.gui_instructions(receiver), rpc_msg)
|
||||
|
||||
if not wait_for_rpc_response:
|
||||
return None
|
||||
response = self._wait_for_response(request_id)
|
||||
# get class name
|
||||
if not response.content["accepted"]:
|
||||
raise ValueError(response.content["message"]["error"])
|
||||
msg_result = response.content["message"].get("result")
|
||||
return self._create_widget_from_msg_result(msg_result)
|
||||
|
||||
def _create_widget_from_msg_result(self, msg_result):
|
||||
if msg_result is None:
|
||||
return None
|
||||
if isinstance(msg_result, list):
|
||||
return [self._create_widget_from_msg_result(res) for res in msg_result]
|
||||
if isinstance(msg_result, dict):
|
||||
if "__rpc__" not in msg_result:
|
||||
return {
|
||||
key: self._create_widget_from_msg_result(val) for key, val in msg_result.items()
|
||||
}
|
||||
cls = msg_result.pop("widget_class", None)
|
||||
msg_result.pop("__rpc__", None)
|
||||
|
||||
if not cls:
|
||||
return msg_result
|
||||
|
||||
cls = getattr(client, cls)
|
||||
# print(msg_result)
|
||||
return cls(parent=self, **msg_result)
|
||||
return msg_result
|
||||
|
||||
def _wait_for_response(self, request_id: str, timeout: int = 5):
|
||||
"""
|
||||
Wait for the response from the server.
|
||||
|
||||
Args:
|
||||
request_id(str): The request ID.
|
||||
timeout(int): The timeout in seconds.
|
||||
|
||||
Returns:
|
||||
The response from the server.
|
||||
"""
|
||||
start_time = time.time()
|
||||
response = None
|
||||
|
||||
while response is None and self.gui_is_alive() and (time.time() - start_time) < timeout:
|
||||
response = self._client.connector.get(
|
||||
MessageEndpoints.gui_instruction_response(request_id)
|
||||
)
|
||||
QCoreApplication.processEvents() # keep UI responsive (and execute signals/slots)
|
||||
if response is None and (time.time() - start_time) >= timeout:
|
||||
raise RPCResponseTimeoutError(request_id, timeout)
|
||||
|
||||
return response
|
||||
|
||||
def gui_is_alive(self):
|
||||
"""
|
||||
Check if the GUI is alive.
|
||||
"""
|
||||
heart = self._client.connector.get(MessageEndpoints.gui_heartbeat(self._root._gui_id))
|
||||
return heart is not None
|
||||
@@ -1,139 +0,0 @@
|
||||
# pylint: disable=missing-module-docstring
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import sys
|
||||
|
||||
import black
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import get_overloads
|
||||
else:
|
||||
print(
|
||||
"Python version is less than 3.11, using dummy function for get_overloads. "
|
||||
"If you want to use the real function 'typing.get_overloads()', please use Python 3.11 or later."
|
||||
)
|
||||
|
||||
def get_overloads(obj):
|
||||
# Dummy function for Python versions before 3.11
|
||||
return []
|
||||
|
||||
|
||||
class ClientGenerator:
|
||||
def __init__(self):
|
||||
self.header = """# This file was automatically generated by generate_cli.py\n
|
||||
from bec_widgets.cli.client_utils import rpc_call, RPCBase, BECGuiClientMixin
|
||||
from typing import Literal, Optional, overload"""
|
||||
|
||||
self.content = ""
|
||||
|
||||
def generate_client(self, published_classes: list):
|
||||
"""
|
||||
Generate the client for the published classes.
|
||||
|
||||
Args:
|
||||
published_classes(list): The list of published classes (e.g. [BECWaveform1D, BECFigure]).
|
||||
"""
|
||||
for cls in published_classes:
|
||||
self.content += "\n\n"
|
||||
self.generate_content_for_class(cls)
|
||||
|
||||
def generate_content_for_class(self, cls):
|
||||
"""
|
||||
Generate the content for the class.
|
||||
|
||||
Args:
|
||||
cls: The class for which to generate the content.
|
||||
"""
|
||||
|
||||
class_name = cls.__name__
|
||||
module = cls.__module__
|
||||
|
||||
# Generate the header
|
||||
# self.header += f"""
|
||||
# from {module} import {class_name}"""
|
||||
|
||||
# Generate the content
|
||||
if cls.__name__ == "BECDockArea":
|
||||
self.content += f"""
|
||||
class {class_name}(RPCBase, BECGuiClientMixin):"""
|
||||
else:
|
||||
self.content += f"""
|
||||
class {class_name}(RPCBase):"""
|
||||
for method in cls.USER_ACCESS:
|
||||
obj = getattr(cls, method)
|
||||
if isinstance(obj, property):
|
||||
self.content += """
|
||||
@property
|
||||
@rpc_call"""
|
||||
sig = str(inspect.signature(obj.fget))
|
||||
doc = inspect.getdoc(obj.fget)
|
||||
else:
|
||||
sig = str(inspect.signature(obj))
|
||||
doc = inspect.getdoc(obj)
|
||||
overloads = get_overloads(obj)
|
||||
for overload in overloads:
|
||||
sig_overload = str(inspect.signature(overload))
|
||||
self.content += f"""
|
||||
@overload
|
||||
def {method}{str(sig_overload)}: ...
|
||||
"""
|
||||
|
||||
self.content += """
|
||||
@rpc_call"""
|
||||
self.content += f"""
|
||||
def {method}{str(sig)}:
|
||||
\"\"\"
|
||||
{doc}
|
||||
\"\"\""""
|
||||
|
||||
def write(self, file_name: str):
|
||||
"""
|
||||
Write the content to a file, automatically formatted with black.
|
||||
|
||||
Args:
|
||||
file_name(str): The name of the file to write to.
|
||||
"""
|
||||
# Combine header and content, then format with black
|
||||
full_content = self.header + "\n" + self.content
|
||||
try:
|
||||
formatted_content = black.format_str(full_content, mode=black.FileMode(line_length=100))
|
||||
except black.NothingChanged:
|
||||
formatted_content = full_content
|
||||
|
||||
with open(file_name, "w", encoding="utf-8") as file:
|
||||
file.write(formatted_content)
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import os
|
||||
|
||||
from bec_widgets.utils import BECConnector
|
||||
from bec_widgets.widgets import BECDock, BECDockArea, BECFigure, SpiralProgressBar
|
||||
from bec_widgets.widgets.figure.plots.image.image import BECImageShow
|
||||
from bec_widgets.widgets.figure.plots.image.image_item import BECImageItem
|
||||
from bec_widgets.widgets.figure.plots.motor_map.motor_map import BECMotorMap
|
||||
from bec_widgets.widgets.figure.plots.plot_base import BECPlotBase
|
||||
from bec_widgets.widgets.figure.plots.waveform.waveform import BECWaveform
|
||||
from bec_widgets.widgets.figure.plots.waveform.waveform_curve import BECCurve
|
||||
from bec_widgets.widgets.spiral_progress_bar.ring import Ring
|
||||
|
||||
current_path = os.path.dirname(__file__)
|
||||
client_path = os.path.join(current_path, "client.py")
|
||||
clss = [
|
||||
BECPlotBase,
|
||||
BECWaveform,
|
||||
BECFigure,
|
||||
BECCurve,
|
||||
BECImageShow,
|
||||
BECConnector,
|
||||
BECImageItem,
|
||||
BECMotorMap,
|
||||
BECDock,
|
||||
BECDockArea,
|
||||
SpiralProgressBar,
|
||||
Ring,
|
||||
]
|
||||
generator = ClientGenerator()
|
||||
generator.generate_client(clss)
|
||||
generator.write(client_path)
|
||||
@@ -1,80 +0,0 @@
|
||||
from threading import Lock
|
||||
from weakref import WeakValueDictionary
|
||||
|
||||
from qtpy.QtCore import QObject
|
||||
|
||||
|
||||
class RPCRegister:
|
||||
"""
|
||||
A singleton class that keeps track of all the RPC objects registered in the system for CLI usage.
|
||||
"""
|
||||
|
||||
_instance = None
|
||||
_initialized = False
|
||||
_lock = Lock()
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if cls._instance is None:
|
||||
cls._instance = super(RPCRegister, cls).__new__(cls)
|
||||
cls._initialized = False
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
if self._initialized:
|
||||
return
|
||||
self._rpc_register = WeakValueDictionary()
|
||||
self._initialized = True
|
||||
|
||||
def add_rpc(self, rpc: QObject):
|
||||
"""
|
||||
Add an RPC object to the register.
|
||||
|
||||
Args:
|
||||
rpc(QObject): The RPC object to be added to the register.
|
||||
"""
|
||||
if not hasattr(rpc, "gui_id"):
|
||||
raise ValueError("RPC object must have a 'gui_id' attribute.")
|
||||
self._rpc_register[rpc.gui_id] = rpc
|
||||
|
||||
def remove_rpc(self, rpc: str):
|
||||
"""
|
||||
Remove an RPC object from the register.
|
||||
|
||||
Args:
|
||||
rpc(str): The RPC object to be removed from the register.
|
||||
"""
|
||||
if not hasattr(rpc, "gui_id"):
|
||||
raise ValueError(f"RPC object {rpc} must have a 'gui_id' attribute.")
|
||||
self._rpc_register.pop(rpc.gui_id, None)
|
||||
|
||||
def get_rpc_by_id(self, gui_id: str) -> QObject:
|
||||
"""
|
||||
Get an RPC object by its ID.
|
||||
|
||||
Args:
|
||||
gui_id(str): The ID of the RPC object to be retrieved.
|
||||
|
||||
Returns:
|
||||
QObject: The RPC object with the given ID.
|
||||
"""
|
||||
rpc_object = self._rpc_register.get(gui_id, None)
|
||||
return rpc_object
|
||||
|
||||
def list_all_connections(self) -> dict:
|
||||
"""
|
||||
List all the registered RPC objects.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing all the registered RPC objects.
|
||||
"""
|
||||
with self._lock:
|
||||
connections = dict(self._rpc_register)
|
||||
return connections
|
||||
|
||||
@classmethod
|
||||
def reset_singleton(cls):
|
||||
"""
|
||||
Reset the singleton instance.
|
||||
"""
|
||||
cls._instance = None
|
||||
cls._initialized = False
|
||||
@@ -1,26 +0,0 @@
|
||||
from bec_widgets.utils import BECConnector
|
||||
from bec_widgets.widgets.figure import BECFigure
|
||||
from bec_widgets.widgets.spiral_progress_bar.spiral_progress_bar import SpiralProgressBar
|
||||
|
||||
|
||||
class RPCWidgetHandler:
|
||||
"""Handler class for creating widgets from RPC messages."""
|
||||
|
||||
widget_classes = {"BECFigure": BECFigure, "SpiralProgressBar": SpiralProgressBar}
|
||||
|
||||
@staticmethod
|
||||
def create_widget(widget_type, **kwargs) -> BECConnector:
|
||||
"""
|
||||
Create a widget from an RPC message.
|
||||
|
||||
Args:
|
||||
widget_type(str): The type of the widget.
|
||||
**kwargs: The keyword arguments for the widget.
|
||||
|
||||
Returns:
|
||||
widget(BECConnector): The created widget.
|
||||
"""
|
||||
widget_class = RPCWidgetHandler.widget_classes.get(widget_type)
|
||||
if widget_class:
|
||||
return widget_class(**kwargs)
|
||||
raise ValueError(f"Unknown widget type: {widget_type}")
|
||||
@@ -1,168 +0,0 @@
|
||||
import inspect
|
||||
from typing import Union
|
||||
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.utils.import_utils import lazy_import
|
||||
from qtpy.QtCore import QTimer
|
||||
|
||||
from bec_widgets.cli.rpc_register import RPCRegister
|
||||
from bec_widgets.utils import BECDispatcher
|
||||
from bec_widgets.utils.bec_connector import BECConnector
|
||||
from bec_widgets.widgets.dock.dock_area import BECDockArea
|
||||
from bec_widgets.widgets.figure import BECFigure
|
||||
|
||||
messages = lazy_import("bec_lib.messages")
|
||||
|
||||
|
||||
class BECWidgetsCLIServer:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
gui_id: str = None,
|
||||
dispatcher: BECDispatcher = None,
|
||||
client=None,
|
||||
config=None,
|
||||
gui_class: Union["BECFigure", "BECDockArea"] = BECFigure,
|
||||
) -> None:
|
||||
self.dispatcher = BECDispatcher(config=config) if dispatcher is None else dispatcher
|
||||
self.client = self.dispatcher.client if client is None else client
|
||||
self.client.start()
|
||||
self.gui_id = gui_id
|
||||
self.gui = gui_class(gui_id=self.gui_id)
|
||||
self.rpc_register = RPCRegister()
|
||||
self.rpc_register.add_rpc(self.gui)
|
||||
|
||||
self.dispatcher.connect_slot(
|
||||
self.on_rpc_update, MessageEndpoints.gui_instructions(self.gui_id)
|
||||
)
|
||||
|
||||
# Setup QTimer for heartbeat
|
||||
self._shutdown_event = False
|
||||
self._heartbeat_timer = QTimer()
|
||||
self._heartbeat_timer.timeout.connect(self.emit_heartbeat)
|
||||
self._heartbeat_timer.start(200) # Emit heartbeat every 1 seconds
|
||||
|
||||
def on_rpc_update(self, msg: dict, metadata: dict):
|
||||
request_id = metadata.get("request_id")
|
||||
try:
|
||||
obj = self.get_object_from_config(msg["parameter"])
|
||||
method = msg["action"]
|
||||
args = msg["parameter"].get("args", [])
|
||||
kwargs = msg["parameter"].get("kwargs", {})
|
||||
res = self.run_rpc(obj, method, args, kwargs)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
self.send_response(request_id, False, {"error": str(e)})
|
||||
else:
|
||||
self.send_response(request_id, True, {"result": res})
|
||||
|
||||
def send_response(self, request_id: str, accepted: bool, msg: dict):
|
||||
self.client.connector.set_and_publish(
|
||||
MessageEndpoints.gui_instruction_response(request_id),
|
||||
messages.RequestResponseMessage(accepted=accepted, message=msg),
|
||||
expire=60,
|
||||
)
|
||||
|
||||
def get_object_from_config(self, config: dict):
|
||||
gui_id = config.get("gui_id")
|
||||
obj = self.rpc_register.get_rpc_by_id(gui_id)
|
||||
if obj is None:
|
||||
raise ValueError(f"Object with gui_id {gui_id} not found")
|
||||
return obj
|
||||
|
||||
def run_rpc(self, obj, method, args, kwargs):
|
||||
method_obj = getattr(obj, method)
|
||||
# check if the method accepts args and kwargs
|
||||
if not callable(method_obj):
|
||||
res = method_obj
|
||||
else:
|
||||
sig = inspect.signature(method_obj)
|
||||
if sig.parameters:
|
||||
res = method_obj(*args, **kwargs)
|
||||
else:
|
||||
res = method_obj()
|
||||
|
||||
if isinstance(res, list):
|
||||
res = [self.serialize_object(obj) for obj in res]
|
||||
elif isinstance(res, dict):
|
||||
res = {key: self.serialize_object(val) for key, val in res.items()}
|
||||
else:
|
||||
res = self.serialize_object(res)
|
||||
return res
|
||||
|
||||
def serialize_object(self, obj):
|
||||
if isinstance(obj, BECConnector):
|
||||
return {
|
||||
"gui_id": obj.gui_id,
|
||||
"widget_class": obj.__class__.__name__,
|
||||
"config": obj.config.model_dump(),
|
||||
"__rpc__": True,
|
||||
}
|
||||
return obj
|
||||
|
||||
def emit_heartbeat(self):
|
||||
if self._shutdown_event is False:
|
||||
self.client.connector.set(
|
||||
MessageEndpoints.gui_heartbeat(self.gui_id),
|
||||
messages.StatusMessage(name=self.gui_id, status=1, info={}),
|
||||
expire=10,
|
||||
)
|
||||
|
||||
def shutdown(self): # TODO not sure if needed when cleanup is done at level of BECConnector
|
||||
self._shutdown_event = True
|
||||
self._heartbeat_timer.stop()
|
||||
self.client.shutdown()
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
from qtpy.QtCore import QSize
|
||||
from qtpy.QtGui import QIcon
|
||||
from qtpy.QtWidgets import QApplication, QMainWindow
|
||||
|
||||
import bec_widgets
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
app.setApplicationName("BEC Figure")
|
||||
module_path = os.path.dirname(bec_widgets.__file__)
|
||||
icon = QIcon()
|
||||
icon.addFile(os.path.join(module_path, "assets", "bec_widgets_icon.png"), size=QSize(48, 48))
|
||||
app.setWindowIcon(icon)
|
||||
|
||||
win = QMainWindow()
|
||||
win.setWindowTitle("BEC Widgets")
|
||||
|
||||
parser = argparse.ArgumentParser(description="BEC Widgets CLI Server")
|
||||
parser.add_argument("--id", type=str, help="The id of the server")
|
||||
parser.add_argument(
|
||||
"--gui_class",
|
||||
type=str,
|
||||
help="Name of the gui class to be rendered. Possible values: \n- BECFigure\n- BECDockArea",
|
||||
)
|
||||
parser.add_argument("--config", type=str, help="Config to connect to redis.")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.gui_class == "BECFigure":
|
||||
gui_class = BECFigure
|
||||
elif args.gui_class == "BECDockArea":
|
||||
gui_class = BECDockArea
|
||||
else:
|
||||
print(
|
||||
"Please specify a valid gui_class to run. Use -h for help."
|
||||
"\n Starting with default gui_class BECFigure."
|
||||
)
|
||||
gui_class = BECFigure
|
||||
|
||||
server = BECWidgetsCLIServer(gui_id=args.id, config=args.config, gui_class=gui_class)
|
||||
|
||||
gui = server.gui
|
||||
win.setCentralWidget(gui)
|
||||
win.resize(800, 600)
|
||||
win.show()
|
||||
|
||||
app.aboutToQuit.connect(server.shutdown)
|
||||
sys.exit(app.exec())
|
||||
138
bec_widgets/config_plotter.py
Normal file
@@ -0,0 +1,138 @@
|
||||
from typing import List
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from PyQt5.QtWidgets import QApplication, QGridLayout, QSizePolicy, QWidget
|
||||
from pyqtgraph import mkPen
|
||||
from pyqtgraph.Qt import QtCore
|
||||
|
||||
|
||||
class ConfigPlotter(QWidget):
|
||||
"""
|
||||
ConfigPlotter is a widget that can be used to plot data from multiple channels
|
||||
in a grid layout. The layout is specified by a list of dicts, where each dict
|
||||
specifies the position of the plot in the grid, the channels to plot, and the
|
||||
type of plot to use. The plot type is specified by the name of the pyqtgraph
|
||||
item to use. For example, to plot a single channel in a PlotItem, the config
|
||||
would look like this:
|
||||
|
||||
config = [
|
||||
{
|
||||
"cols": 1,
|
||||
"rows": 1,
|
||||
"y": 0,
|
||||
"x": 0,
|
||||
"config": {"channels": ["a"], "label_xy": ["", "a"], "item": "PlotItem"},
|
||||
}
|
||||
]
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, configs: List[dict], parent=None):
|
||||
super(ConfigPlotter, self).__init__()
|
||||
self.configs = configs
|
||||
self.plots = {}
|
||||
self._init_ui()
|
||||
self._init_plots()
|
||||
|
||||
def _init_ui(self):
|
||||
pg.setConfigOption("background", "w")
|
||||
pg.setConfigOption("foreground", "k")
|
||||
self.layout = QGridLayout()
|
||||
self.setLayout(self.layout)
|
||||
self.pen = mkPen(color=(56, 76, 107), width=4, style=QtCore.Qt.SolidLine)
|
||||
self.show()
|
||||
|
||||
def _init_plots(self):
|
||||
for config in self.configs:
|
||||
channels = config["config"]["channels"]
|
||||
for channel in channels:
|
||||
# call the corresponding init function, e.g. init_plotitem
|
||||
init_func = getattr(self, f"init_{config['config']['item']}")
|
||||
init_func(channel, config)
|
||||
|
||||
# self.init_ImageItem(channel, config["config"], item)
|
||||
|
||||
def init_PlotItem(self, channel: str, config: dict):
|
||||
"""
|
||||
Initialize a PlotItem
|
||||
|
||||
Args:
|
||||
channel(str): channel to plot
|
||||
config(dict): config dict for the channel
|
||||
"""
|
||||
# pylint: disable=invalid-name
|
||||
plot_widget = pg.PlotWidget()
|
||||
plot_widget.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||
self.layout.addWidget(plot_widget, config["y"], config["x"], config["rows"], config["cols"])
|
||||
plot_data = plot_widget.plot(np.random.rand(100), pen=self.pen)
|
||||
# item.setLabel("left", channel)
|
||||
# self.plots[channel] = {"item": item, "plot_data": plot_data}
|
||||
|
||||
def init_ImageItem(self, channel: str, config: dict):
|
||||
"""
|
||||
Initialize an ImageItem
|
||||
|
||||
Args:
|
||||
channel(str): channel to plot
|
||||
config(dict): config dict for the channel
|
||||
"""
|
||||
# pylint: disable=invalid-name
|
||||
item = pg.PlotItem()
|
||||
self.layout.addItem(
|
||||
item,
|
||||
row=config["y"],
|
||||
col=config["x"],
|
||||
rowspan=config["rows"],
|
||||
colspan=config["cols"],
|
||||
)
|
||||
img = pg.ImageItem()
|
||||
item.addItem(img)
|
||||
img.setImage(np.random.rand(100, 100))
|
||||
self.plots[channel] = {"item": item, "plot_data": img}
|
||||
|
||||
def init_ImageView(self, channel: str, config: dict):
|
||||
"""
|
||||
Initialize an ImageView
|
||||
|
||||
Args:
|
||||
channel(str): channel to plot
|
||||
config(dict): config dict for the channel
|
||||
"""
|
||||
# pylint: disable=invalid-name
|
||||
img = pg.ImageView()
|
||||
img.setImage(np.random.rand(100, 100))
|
||||
self.layout.addWidget(img, config["y"], config["x"], config["rows"], config["cols"])
|
||||
self.plots[channel] = {"item": img, "plot_data": img}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
CONFIG = [
|
||||
{
|
||||
"cols": 1,
|
||||
"rows": 1,
|
||||
"y": 0,
|
||||
"x": 0,
|
||||
"config": {"channels": ["a"], "label_xy": ["", "a"], "item": "PlotItem"},
|
||||
},
|
||||
{
|
||||
"cols": 1,
|
||||
"rows": 1,
|
||||
"y": 1,
|
||||
"x": 0,
|
||||
"config": {"channels": ["b"], "label_xy": ["", "b"], "item": "PlotItem"},
|
||||
},
|
||||
{
|
||||
"cols": 1,
|
||||
"rows": 2,
|
||||
"y": 0,
|
||||
"x": 1,
|
||||
"config": {"channels": ["c"], "label_xy": ["", "c"], "item": "ImageView"},
|
||||
},
|
||||
]
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
win = ConfigPlotter(CONFIG)
|
||||
pg.exec()
|
||||
@@ -1,9 +0,0 @@
|
||||
from .motor_movement import (
|
||||
MotorControlApp,
|
||||
MotorControlMap,
|
||||
MotorControlPanel,
|
||||
MotorControlPanelAbsolute,
|
||||
MotorControlPanelRelative,
|
||||
MotorCoordinateTable,
|
||||
MotorThread,
|
||||
)
|
||||
@@ -1,154 +0,0 @@
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
import qdarktheme
|
||||
from qtconsole.inprocess import QtInProcessKernelManager
|
||||
from qtconsole.rich_jupyter_widget import RichJupyterWidget
|
||||
from qtpy.QtCore import QSize
|
||||
from qtpy.QtGui import QIcon
|
||||
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils import BECDispatcher, UILoader
|
||||
from bec_widgets.widgets import BECFigure
|
||||
from bec_widgets.widgets.dock.dock_area import BECDockArea
|
||||
from bec_widgets.widgets.jupyter_console.jupyter_console import BECJupyterConsole
|
||||
|
||||
# class JupyterConsoleWidget(RichJupyterWidget): # pragma: no cover:
|
||||
# def __init__(self):
|
||||
# super().__init__()
|
||||
#
|
||||
# self.kernel_manager = QtInProcessKernelManager()
|
||||
# self.kernel_manager.start_kernel(show_banner=False)
|
||||
# self.kernel_client = self.kernel_manager.client()
|
||||
# self.kernel_client.start_channels()
|
||||
#
|
||||
# self.kernel_manager.kernel.shell.push({"np": np, "pg": pg})
|
||||
#
|
||||
# def shutdown_kernel(self):
|
||||
# self.kernel_client.stop_channels()
|
||||
# self.kernel_manager.shutdown_kernel()
|
||||
|
||||
|
||||
class JupyterConsoleWindow(QWidget): # pragma: no cover:
|
||||
"""A widget that contains a Jupyter console linked to BEC Widgets with full API access (contains Qt and pyqtgraph API)."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
current_path = os.path.dirname(__file__)
|
||||
self.ui = UILoader().load_ui(os.path.join(current_path, "jupyter_console_window.ui"), self)
|
||||
|
||||
self._init_ui()
|
||||
|
||||
self.ui.splitter.setSizes([200, 100])
|
||||
self.safe_close = False
|
||||
|
||||
# console push
|
||||
if self.console.inprocess is True:
|
||||
self.console.kernel_manager.kernel.shell.push(
|
||||
{
|
||||
"np": np,
|
||||
"pg": pg,
|
||||
"fig": self.figure,
|
||||
"dock": self.dock,
|
||||
"w1": self.w1,
|
||||
"w2": self.w2,
|
||||
"w3": self.w3,
|
||||
"d0": self.d0,
|
||||
"d1": self.d1,
|
||||
"d2": self.d2,
|
||||
"fig0": self.fig0,
|
||||
"fig1": self.fig1,
|
||||
"fig2": self.fig2,
|
||||
"bar": self.bar,
|
||||
}
|
||||
)
|
||||
|
||||
def _init_ui(self):
|
||||
# Plotting window
|
||||
self.glw_1_layout = QVBoxLayout(self.ui.glw) # Create a new QVBoxLayout
|
||||
self.figure = BECFigure(parent=self, gui_id="remote") # Create a new BECDeviceMonitor
|
||||
self.glw_1_layout.addWidget(self.figure) # Add BECDeviceMonitor to the layout
|
||||
|
||||
self.dock_layout = QVBoxLayout(self.ui.dock_placeholder)
|
||||
self.dock = BECDockArea(gui_id="remote")
|
||||
self.dock_layout.addWidget(self.dock)
|
||||
|
||||
# add stuff to figure
|
||||
self._init_figure()
|
||||
|
||||
# init dock for testing
|
||||
self._init_dock()
|
||||
|
||||
self.console_layout = QVBoxLayout(self.ui.widget_console)
|
||||
self.console = BECJupyterConsole(inprocess=True)
|
||||
self.console_layout.addWidget(self.console)
|
||||
|
||||
def _init_figure(self):
|
||||
self.figure.plot(x_name="samx", y_name="samy", z_name="bpm4i", color_map_z="cividis")
|
||||
self.figure.motor_map("samx", "samy")
|
||||
self.figure.image("eiger", color_map="viridis", vrange=(0, 100))
|
||||
self.figure.add_plot(x_name="samx", y_name="samy", z_name="bpm4i", color_map_z="magma")
|
||||
|
||||
self.figure.change_layout(2, 2)
|
||||
|
||||
self.w1 = self.figure[0, 0]
|
||||
self.w2 = self.figure[0, 1]
|
||||
self.w3 = self.figure[1, 0]
|
||||
|
||||
# curves for w1
|
||||
self.c1 = self.w1.get_config()
|
||||
|
||||
def _init_dock(self):
|
||||
|
||||
self.d0 = self.dock.add_dock(name="dock_0")
|
||||
self.fig0 = self.d0.add_widget("BECFigure")
|
||||
data = np.random.rand(10, 2)
|
||||
self.fig0.plot(data, label="2d Data")
|
||||
self.fig0.image("eiger", vrange=(0, 100))
|
||||
|
||||
self.d1 = self.dock.add_dock(name="dock_1", position="right")
|
||||
self.fig1 = self.d1.add_widget("BECFigure")
|
||||
self.fig1.plot(x_name="samx", y_name="bpm4i")
|
||||
self.fig1.plot(x_name="samx", y_name="bpm3a")
|
||||
|
||||
self.d2 = self.dock.add_dock(name="dock_2", position="bottom")
|
||||
self.fig2 = self.d2.add_widget("BECFigure", row=0, col=0)
|
||||
self.fig2.plot(x_name="samx", y_name="bpm4i")
|
||||
self.bar = self.d2.add_widget("SpiralProgressBar", row=0, col=1)
|
||||
self.bar.set_diameter(200)
|
||||
|
||||
self.dock.save_state()
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Override to handle things when main window is closed."""
|
||||
self.dock.cleanup()
|
||||
self.figure.clear_all()
|
||||
self.figure.client.shutdown()
|
||||
super().closeEvent(event)
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
import bec_widgets
|
||||
|
||||
module_path = os.path.dirname(bec_widgets.__file__)
|
||||
|
||||
bec_dispatcher = BECDispatcher()
|
||||
client = bec_dispatcher.client
|
||||
client.start()
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
app.setApplicationName("Jupyter Console")
|
||||
app.setApplicationDisplayName("Jupyter Console")
|
||||
# qdarktheme.setup_theme("auto")
|
||||
icon = QIcon()
|
||||
icon.addFile(os.path.join(module_path, "assets", "terminal_icon.png"), size=QSize(48, 48))
|
||||
app.setWindowIcon(icon)
|
||||
win = JupyterConsoleWindow()
|
||||
win.show()
|
||||
|
||||
app.aboutToQuit.connect(win.close)
|
||||
sys.exit(app.exec_())
|
||||
@@ -1,54 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>2104</width>
|
||||
<height>966</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Plotting Console</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<widget class="QSplitter" name="splitter">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<widget class="QTabWidget" name="tabWidget">
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="tab_1">
|
||||
<attribute name="title">
|
||||
<string>BECDock</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QWidget" name="dock_placeholder" native="true"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_2">
|
||||
<attribute name="title">
|
||||
<string>BECFigure</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<widget class="QWidget" name="glw" native="true"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<widget class="QWidget" name="widget_console" native="true"/>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -1,9 +0,0 @@
|
||||
from .motor_control_compilations import (
|
||||
MotorControlApp,
|
||||
MotorControlMap,
|
||||
MotorControlPanel,
|
||||
MotorControlPanelAbsolute,
|
||||
MotorControlPanelRelative,
|
||||
MotorCoordinateTable,
|
||||
MotorThread,
|
||||
)
|
||||
@@ -1,250 +0,0 @@
|
||||
# pylint: disable = no-name-in-module,missing-class-docstring, missing-module-docstring
|
||||
|
||||
import qdarktheme
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtWidgets import QApplication, QSplitter, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
||||
from bec_widgets.widgets.motor_control.motor_control import MotorThread
|
||||
from bec_widgets.widgets.motor_control.motor_table.motor_table import MotorCoordinateTable
|
||||
from bec_widgets.widgets.motor_control.movement_absolute.movement_absolute import (
|
||||
MotorControlAbsolute,
|
||||
)
|
||||
from bec_widgets.widgets.motor_control.movement_relative.movement_relative import (
|
||||
MotorControlRelative,
|
||||
)
|
||||
from bec_widgets.widgets.motor_control.selection.selection import MotorControlSelection
|
||||
|
||||
CONFIG_DEFAULT = {
|
||||
"motor_control": {
|
||||
"motor_x": "samx",
|
||||
"motor_y": "samy",
|
||||
"step_size_x": 3,
|
||||
"step_size_y": 3,
|
||||
"precision": 4,
|
||||
"step_x_y_same": False,
|
||||
"move_with_arrows": False,
|
||||
},
|
||||
"plot_settings": {
|
||||
"colormap": "Greys",
|
||||
"scatter_size": 5,
|
||||
"max_points": 1000,
|
||||
"num_dim_points": 100,
|
||||
"precision": 2,
|
||||
"num_columns": 1,
|
||||
"background_value": 25,
|
||||
},
|
||||
"motors": [
|
||||
{
|
||||
"plot_name": "Motor Map",
|
||||
"x_label": "Motor X",
|
||||
"y_label": "Motor Y",
|
||||
"signals": {
|
||||
"x": [{"name": "samx", "entry": "samx"}],
|
||||
"y": [{"name": "samy", "entry": "samy"}],
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class MotorControlApp(QWidget):
|
||||
def __init__(self, parent=None, client=None, config=None):
|
||||
super().__init__(parent)
|
||||
|
||||
bec_dispatcher = BECDispatcher()
|
||||
self.client = bec_dispatcher.client if client is None else client
|
||||
self.config = config
|
||||
|
||||
# Widgets
|
||||
self.motor_control_panel = MotorControlPanel(client=self.client, config=self.config)
|
||||
# Create MotorMap
|
||||
# self.motion_map = MotorMap(client=self.client, config=self.config)
|
||||
# Create MotorCoordinateTable
|
||||
self.motor_table = MotorCoordinateTable(client=self.client, config=self.config)
|
||||
|
||||
# Create the splitter and add MotorMap and MotorControlPanel
|
||||
splitter = QSplitter(Qt.Horizontal)
|
||||
# splitter.addWidget(self.motion_map)
|
||||
splitter.addWidget(self.motor_control_panel)
|
||||
splitter.addWidget(self.motor_table)
|
||||
|
||||
# Set the main layout
|
||||
layout = QVBoxLayout(self)
|
||||
layout.addWidget(splitter)
|
||||
self.setLayout(layout)
|
||||
|
||||
# Connecting signals and slots
|
||||
# self.motor_control_panel.selection_widget.selected_motors_signal.connect(
|
||||
# lambda x, y: self.motion_map.change_motors(x, y, 0)
|
||||
# )
|
||||
self.motor_control_panel.absolute_widget.coordinates_signal.connect(
|
||||
self.motor_table.add_coordinate
|
||||
)
|
||||
self.motor_control_panel.relative_widget.precision_signal.connect(
|
||||
self.motor_table.set_precision
|
||||
)
|
||||
self.motor_control_panel.relative_widget.precision_signal.connect(
|
||||
self.motor_control_panel.absolute_widget.set_precision
|
||||
)
|
||||
|
||||
# self.motor_table.plot_coordinates_signal.connect(self.motion_map.plot_saved_coordinates)
|
||||
|
||||
|
||||
class MotorControlMap(QWidget):
|
||||
def __init__(self, parent=None, client=None, config=None):
|
||||
super().__init__(parent)
|
||||
|
||||
bec_dispatcher = BECDispatcher()
|
||||
self.client = bec_dispatcher.client if client is None else client
|
||||
self.config = config
|
||||
|
||||
# Widgets
|
||||
self.motor_control_panel = MotorControlPanel(client=self.client, config=self.config)
|
||||
# Create MotorMap
|
||||
# self.motion_map = MotorMap(client=self.client, config=self.config)
|
||||
|
||||
# Create the splitter and add MotorMap and MotorControlPanel
|
||||
splitter = QSplitter(Qt.Horizontal)
|
||||
# splitter.addWidget(self.motion_map)
|
||||
splitter.addWidget(self.motor_control_panel)
|
||||
|
||||
# Set the main layout
|
||||
layout = QVBoxLayout(self)
|
||||
layout.addWidget(splitter)
|
||||
self.setLayout(layout)
|
||||
|
||||
# Connecting signals and slots
|
||||
# self.motor_control_panel.selection_widget.selected_motors_signal.connect(
|
||||
# lambda x, y: self.motion_map.change_motors(x, y, 0)
|
||||
# )
|
||||
|
||||
|
||||
class MotorControlPanel(QWidget):
|
||||
def __init__(self, parent=None, client=None, config=None):
|
||||
super().__init__(parent)
|
||||
|
||||
bec_dispatcher = BECDispatcher()
|
||||
self.client = bec_dispatcher.client if client is None else client
|
||||
self.config = config
|
||||
|
||||
self.motor_thread = MotorThread(client=self.client)
|
||||
|
||||
self.selection_widget = MotorControlSelection(
|
||||
client=self.client, config=self.config, motor_thread=self.motor_thread
|
||||
)
|
||||
self.relative_widget = MotorControlRelative(
|
||||
client=self.client, config=self.config, motor_thread=self.motor_thread
|
||||
)
|
||||
self.absolute_widget = MotorControlAbsolute(
|
||||
client=self.client, config=self.config, motor_thread=self.motor_thread
|
||||
)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
layout.addWidget(self.selection_widget)
|
||||
layout.addWidget(self.relative_widget)
|
||||
layout.addWidget(self.absolute_widget)
|
||||
|
||||
# Connecting signals and slots
|
||||
self.selection_widget.selected_motors_signal.connect(self.relative_widget.change_motors)
|
||||
self.selection_widget.selected_motors_signal.connect(self.absolute_widget.change_motors)
|
||||
|
||||
# Set the window to a fixed size based on its contents
|
||||
# self.layout().setSizeConstraint(layout.SetFixedSize)
|
||||
|
||||
|
||||
class MotorControlPanelAbsolute(QWidget):
|
||||
def __init__(self, parent=None, client=None, config=None):
|
||||
super().__init__(parent)
|
||||
|
||||
bec_dispatcher = BECDispatcher()
|
||||
self.client = bec_dispatcher.client if client is None else client
|
||||
self.config = config
|
||||
|
||||
self.motor_thread = MotorThread(client=self.client)
|
||||
|
||||
self.selection_widget = MotorControlSelection(
|
||||
client=client, config=config, motor_thread=self.motor_thread
|
||||
)
|
||||
self.absolute_widget = MotorControlAbsolute(
|
||||
client=client, config=config, motor_thread=self.motor_thread
|
||||
)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.addWidget(self.selection_widget)
|
||||
layout.addWidget(self.absolute_widget)
|
||||
|
||||
# Connecting signals and slots
|
||||
self.selection_widget.selected_motors_signal.connect(self.absolute_widget.change_motors)
|
||||
|
||||
|
||||
class MotorControlPanelRelative(QWidget):
|
||||
def __init__(self, parent=None, client=None, config=None):
|
||||
super().__init__(parent)
|
||||
|
||||
bec_dispatcher = BECDispatcher()
|
||||
self.client = bec_dispatcher.client if client is None else client
|
||||
self.config = config
|
||||
|
||||
self.motor_thread = MotorThread(client=self.client)
|
||||
|
||||
self.selection_widget = MotorControlSelection(
|
||||
client=client, config=config, motor_thread=self.motor_thread
|
||||
)
|
||||
self.relative_widget = MotorControlRelative(
|
||||
client=client, config=config, motor_thread=self.motor_thread
|
||||
)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.addWidget(self.selection_widget)
|
||||
layout.addWidget(self.relative_widget)
|
||||
|
||||
# Connecting signals and slots
|
||||
self.selection_widget.selected_motors_signal.connect(self.relative_widget.change_motors)
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
parser = argparse.ArgumentParser(description="Run various Motor Control Widgets compositions.")
|
||||
parser.add_argument(
|
||||
"-v",
|
||||
"--variant",
|
||||
type=str,
|
||||
choices=["app", "map", "panel", "panel_abs", "panel_rel"],
|
||||
help="Select the variant of the motor control to run. "
|
||||
"'app' for the full application, "
|
||||
"'map' for MotorMap, "
|
||||
"'panel' for the MotorControlPanel, "
|
||||
"'panel_abs' for MotorControlPanel with absolute control, "
|
||||
"'panel_rel' for MotorControlPanel with relative control.",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
bec_dispatcher = BECDispatcher()
|
||||
client = bec_dispatcher.client
|
||||
client.start()
|
||||
|
||||
app = QApplication([])
|
||||
qdarktheme.setup_theme("auto")
|
||||
|
||||
if args.variant == "app":
|
||||
window = MotorControlApp(client=client) # , config=CONFIG_DEFAULT)
|
||||
elif args.variant == "map":
|
||||
window = MotorControlMap(client=client) # , config=CONFIG_DEFAULT)
|
||||
elif args.variant == "panel":
|
||||
window = MotorControlPanel(client=client) # , config=CONFIG_DEFAULT)
|
||||
elif args.variant == "panel_abs":
|
||||
window = MotorControlPanelAbsolute(client=client) # , config=CONFIG_DEFAULT)
|
||||
elif args.variant == "panel_rel":
|
||||
window = MotorControlPanelRelative(client=client) # , config=CONFIG_DEFAULT)
|
||||
else:
|
||||
print("Please specify a valid variant to run. Use -h for help.")
|
||||
print("Running the full application by default.")
|
||||
window = MotorControlApp(client=client) # , config=CONFIG_DEFAULT)
|
||||
|
||||
window.show()
|
||||
sys.exit(app.exec())
|
||||
@@ -1,926 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1561</width>
|
||||
<height>748</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>1409</width>
|
||||
<height>748</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Motor Controller</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout" stretch="8,5,8">
|
||||
<item>
|
||||
<widget class="GraphicsLayoutWidget" name="glw">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QFrame" name="Controls">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>221</width>
|
||||
<height>471</height>
|
||||
</size>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_6" stretch="1,1,1,0,1">
|
||||
<property name="spacing">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetMinimumSize</enum>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="motorSelection">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>261</width>
|
||||
<height>145</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Motor Selection</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_4">
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_8">
|
||||
<property name="text">
|
||||
<string>Motor Y</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="comboBox_motor_x"/>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QComboBox" name="comboBox_motor_y"/>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
<string>Motor X</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="2">
|
||||
<widget class="QPushButton" name="pushButton_connecMotors">
|
||||
<property name="text">
|
||||
<string>Connect Motors</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Minimum</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>18</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="motorControl">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>261</width>
|
||||
<height>339</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Motor Relative</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_enableArrows">
|
||||
<property name="text">
|
||||
<string>Move with arrow keys</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_same_xy">
|
||||
<property name="text">
|
||||
<string>Step [X] = Step [Y]</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QGridLayout" name="step_grid">
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_step_y">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>111</width>
|
||||
<height>19</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Step [Y]</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>111</width>
|
||||
<height>19</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Decimal</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QDoubleSpinBox" name="spinBox_step_x">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>19</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>0.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>99.000000000000000</double>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<double>0.100000000000000</double>
|
||||
</property>
|
||||
<property name="value">
|
||||
<double>1.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_step_x">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>111</width>
|
||||
<height>19</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Step [X]</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QDoubleSpinBox" name="spinBox_step_y">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>19</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>0.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>99.000000000000000</double>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<double>0.100000000000000</double>
|
||||
</property>
|
||||
<property name="value">
|
||||
<double>1.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QSpinBox" name="spinBox_precision">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>19</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>8</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>2</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QGridLayout" name="direction_grid">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetDefaultConstraint</enum>
|
||||
</property>
|
||||
<item row="1" column="2" alignment="Qt::AlignHCenter|Qt::AlignVCenter">
|
||||
<widget class="QToolButton" name="toolButton_up">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>26</width>
|
||||
<height>26</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="arrowType">
|
||||
<enum>Qt::UpArrow</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="4">
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="3" column="2" alignment="Qt::AlignHCenter|Qt::AlignVCenter">
|
||||
<widget class="QToolButton" name="toolButton_down">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>26</width>
|
||||
<height>26</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="arrowType">
|
||||
<enum>Qt::DownArrow</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QToolButton" name="toolButton_left">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>26</width>
|
||||
<height>26</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="arrowType">
|
||||
<enum>Qt::LeftArrow</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="2" column="3">
|
||||
<widget class="QToolButton" name="toolButton_right">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>26</width>
|
||||
<height>26</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="arrowType">
|
||||
<enum>Qt::RightArrow</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="4" column="2">
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_4">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Minimum</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>18</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="motorControl_absolute">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>261</width>
|
||||
<height>195</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Move Absolute</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_save_with_go">
|
||||
<property name="text">
|
||||
<string>Save position with Go</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<item row="1" column="1">
|
||||
<widget class="QDoubleSpinBox" name="spinBox_absolute_y">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-500.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>500.000000000000000</double>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<double>0.100000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QDoubleSpinBox" name="spinBox_absolute_x">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-500.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>500.000000000000000</double>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<double>0.100000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLabel" name="label_7">
|
||||
<property name="text">
|
||||
<string>Y</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_6">
|
||||
<property name="text">
|
||||
<string>X</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_save">
|
||||
<property name="text">
|
||||
<string>Save</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_set">
|
||||
<property name="text">
|
||||
<string>Set</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_go_absolute">
|
||||
<property name="text">
|
||||
<string>Go</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_stop">
|
||||
<property name="text">
|
||||
<string>Stop Movement</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTabWidget" name="tabWidget_tables">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="tab_coordinates">
|
||||
<attribute name="title">
|
||||
<string>Coordinates</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Entries Mode:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="comboBox_mode">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Individual</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Start/Stop</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTableWidget" name="tableWidget_coordinates">
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::MultiSelection</enum>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Show</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Move</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Tag</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>X</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Y</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="1" column="1">
|
||||
<widget class="QPushButton" name="pushButton_resize_table">
|
||||
<property name="text">
|
||||
<string>Resize Table</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QCheckBox" name="checkBox_resize_auto">
|
||||
<property name="text">
|
||||
<string>Resize Auto</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QPushButton" name="pushButton_importCSV">
|
||||
<property name="text">
|
||||
<string>Import CSV</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QPushButton" name="pushButton_exportCSV">
|
||||
<property name="text">
|
||||
<string>Export CSV</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QPushButton" name="pushButton_help">
|
||||
<property name="text">
|
||||
<string>Help</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QPushButton" name="pushButton_duplicate">
|
||||
<property name="text">
|
||||
<string>Duplicate Last Entry</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_settings">
|
||||
<attribute name="title">
|
||||
<string>Settings</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="motorLimits">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Motor Limits</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="2" column="1">
|
||||
<widget class="QPushButton" name="pushButton_updateLimits">
|
||||
<property name="text">
|
||||
<string>Update</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLabel" name="label_Y_max">
|
||||
<property name="text">
|
||||
<string>+ Y</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QLabel" name="label_Y_min">
|
||||
<property name="text">
|
||||
<string>- Y</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_X_min">
|
||||
<property name="text">
|
||||
<string>- X</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QLabel" name="label_X_max">
|
||||
<property name="text">
|
||||
<string>+ X</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QDoubleSpinBox" name="spinBox_y_max">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-1000.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>1000.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QDoubleSpinBox" name="spinBox_y_min">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-1000.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>1000.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QDoubleSpinBox" name="spinBox_x_min">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-1000.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>1000.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="2">
|
||||
<widget class="QDoubleSpinBox" name="spinBox_x_max">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-1000.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>1000.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_2">
|
||||
<property name="title">
|
||||
<string>Plotting Options</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_5">
|
||||
<item row="0" column="1" colspan="2">
|
||||
<widget class="QSpinBox" name="spinBox_max_points">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>100</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>10000</number>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<number>100</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>5000</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_15">
|
||||
<property name="text">
|
||||
<string>Max Points</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1" colspan="2">
|
||||
<widget class="QSpinBox" name="spinBox_scatter_size">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>15</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>5</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_11">
|
||||
<property name="text">
|
||||
<string>Scatter Size</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="3">
|
||||
<widget class="QPushButton" name="pushButton_update_config">
|
||||
<property name="text">
|
||||
<string>Update Settings</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1" colspan="2">
|
||||
<widget class="QSpinBox" name="spinBox_num_dim_points">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>1000</number>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>100</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_16">
|
||||
<property name="text">
|
||||
<string>N dim</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0" colspan="3">
|
||||
<widget class="QPushButton" name="pushButton_enableGUI">
|
||||
<property name="text">
|
||||
<string>Enable Control GUI</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_queue">
|
||||
<attribute name="title">
|
||||
<string>Queue</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Work in progress</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_5">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Reset Queue</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTableWidget" name="tableWidget_2">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>queueID</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>scan_id</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>is_scan</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>type</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>scan_number</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>IQ status</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>GraphicsLayoutWidget</class>
|
||||
<extends>QGraphicsView</extends>
|
||||
<header>pyqtgraph.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
4
bec_widgets/readme.md
Normal file
@@ -0,0 +1,4 @@
|
||||
Add/modify the path in the following variable to make the plugin avaiable in Qt Designer:
|
||||
```
|
||||
$ export PYQTDESIGNERPATH=/<path to repo>/bec/bec_qtplugin:$PYQTDESIGNERPATH
|
||||
```
|
||||
106
bec_widgets/scan_plot.py
Normal file
@@ -0,0 +1,106 @@
|
||||
import itertools
|
||||
|
||||
import pyqtgraph as pg
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSlot
|
||||
|
||||
from bec_lib.core.logger import bec_logger
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
pg.setConfigOptions(background="w", foreground="k", antialias=True)
|
||||
COLORS = ["#fd7f6f", "#7eb0d5", "#b2e061", "#bd7ebe", "#ffb55a"]
|
||||
|
||||
|
||||
class BECScanPlot(pg.PlotWidget):
|
||||
def __init__(self, parent=None, background="default"):
|
||||
super().__init__(parent, background)
|
||||
|
||||
self._x_channel = ""
|
||||
self._y_channel_list = []
|
||||
|
||||
self.scan_curves = {}
|
||||
self.dap_curves = {}
|
||||
|
||||
def initialize(self):
|
||||
plot_item = self.getPlotItem()
|
||||
plot_item.addLegend()
|
||||
colors = itertools.cycle(COLORS)
|
||||
|
||||
for y_chan in self.y_channel_list:
|
||||
if y_chan.startswith("dap."):
|
||||
y_chan = y_chan.partition("dap.")[-1]
|
||||
curves = self.dap_curves
|
||||
else:
|
||||
curves = self.scan_curves
|
||||
|
||||
curves[y_chan] = plot_item.plot(
|
||||
x=[], y=[], pen=pg.mkPen(color=next(colors), width=2), name=y_chan
|
||||
)
|
||||
|
||||
plot_item.setLabel("bottom", self._x_channel)
|
||||
if len(self.scan_curves) == 1:
|
||||
plot_item.setLabel("left", next(iter(self.scan_curves)))
|
||||
|
||||
@pyqtSlot()
|
||||
def clearData(self):
|
||||
for plot_curve in {**self.scan_curves, **self.dap_curves}.values():
|
||||
plot_curve.setData(x=[], y=[])
|
||||
|
||||
@pyqtSlot(dict)
|
||||
def redraw_scan(self, data):
|
||||
if not self.x_channel:
|
||||
return
|
||||
|
||||
if self.x_channel not in data:
|
||||
logger.warning(f"Unknown channel `{self.x_channel}` for X data in {self.objectName()}")
|
||||
return
|
||||
|
||||
x_new = data[self.x_channel][self.x_channel]["value"]
|
||||
for chan, plot_curve in self.scan_curves.items():
|
||||
if not chan:
|
||||
continue
|
||||
|
||||
if chan not in data:
|
||||
logger.warning(f"Unknown channel `{chan}` for Y data in {self.objectName()}")
|
||||
continue
|
||||
|
||||
y_new = data[chan][chan]["value"]
|
||||
x, y = plot_curve.getData() # TODO: is it a good approach?
|
||||
if x is None:
|
||||
x = []
|
||||
if y is None:
|
||||
y = []
|
||||
|
||||
plot_curve.setData(x=[*x, x_new], y=[*y, y_new])
|
||||
|
||||
@pyqtSlot(dict)
|
||||
def redraw_dap(self, data):
|
||||
for chan, plot_curve in self.dap_curves.items():
|
||||
if not chan:
|
||||
continue
|
||||
|
||||
if chan not in data:
|
||||
logger.warning(f"Unknown channel `{chan}` for DAP data in {self.objectName()}")
|
||||
continue
|
||||
|
||||
x_new = data[chan]["x"]
|
||||
y_new = data[chan]["y"]
|
||||
|
||||
plot_curve.setData(x=x_new, y=y_new)
|
||||
|
||||
@pyqtProperty("QStringList")
|
||||
def y_channel_list(self):
|
||||
return self._y_channel_list
|
||||
|
||||
@y_channel_list.setter
|
||||
def y_channel_list(self, new_list):
|
||||
self._y_channel_list = new_list
|
||||
|
||||
@pyqtProperty(str)
|
||||
def x_channel(self):
|
||||
return self._x_channel
|
||||
|
||||
@x_channel.setter
|
||||
def x_channel(self, new_val):
|
||||
self._x_channel = new_val
|
||||
55
bec_widgets/scan_plot_plugin.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from PyQt5.QtDesigner import QPyDesignerCustomWidgetPlugin
|
||||
from PyQt5.QtGui import QIcon
|
||||
from scan_plot import BECScanPlot
|
||||
|
||||
|
||||
class BECScanPlotPlugin(QPyDesignerCustomWidgetPlugin):
|
||||
def __init__(self, parent=None):
|
||||
super(BECScanPlotPlugin, self).__init__(parent)
|
||||
|
||||
self._initialized = False
|
||||
|
||||
def initialize(self, formEditor):
|
||||
if self._initialized:
|
||||
return
|
||||
|
||||
self._initialized = True
|
||||
|
||||
def isInitialized(self):
|
||||
return self._initialized
|
||||
|
||||
def createWidget(self, parent):
|
||||
return BECScanPlot(parent)
|
||||
|
||||
def name(self):
|
||||
return "BECScanPlot"
|
||||
|
||||
def group(self):
|
||||
return "BEC widgets"
|
||||
|
||||
def icon(self):
|
||||
return QIcon()
|
||||
|
||||
def toolTip(self):
|
||||
return "BEC plot for scans"
|
||||
|
||||
def whatsThis(self):
|
||||
return "BEC plot for scans"
|
||||
|
||||
def isContainer(self):
|
||||
return False
|
||||
|
||||
def domXml(self):
|
||||
return (
|
||||
'<widget class="BECScanPlot" name="BECScanPlot">\n'
|
||||
' <property name="toolTip" >\n'
|
||||
" <string>BEC plot for scans</string>\n"
|
||||
" </property>\n"
|
||||
' <property name="whatsThis" >\n'
|
||||
" <string>BEC plot for scans in Python using PyQt.</string>\n"
|
||||
" </property>\n"
|
||||
"</widget>\n"
|
||||
)
|
||||
|
||||
def includeFile(self):
|
||||
return "scan_plot"
|
||||
@@ -1,11 +0,0 @@
|
||||
from .bec_connector import BECConnector, ConnectionConfig
|
||||
from .bec_dispatcher import BECDispatcher
|
||||
from .bec_table import BECTable
|
||||
from .colors import Colors
|
||||
from .container_utils import WidgetContainerUtils
|
||||
from .crosshair import Crosshair
|
||||
from .entry_validator import EntryValidator
|
||||
from .layout_manager import GridLayoutManager
|
||||
from .rpc_decorator import register_rpc_methods, rpc_public
|
||||
from .ui_loader import UILoader
|
||||
from .validator_delegate import DoubleValidationDelegate
|
||||
@@ -1,171 +0,0 @@
|
||||
# pylint: disable = no-name-in-module,missing-module-docstring
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import Optional, Type
|
||||
|
||||
from bec_lib.utils.import_utils import lazy_import, lazy_import_from
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from qtpy.QtCore import Slot as pyqtSlot
|
||||
|
||||
from bec_widgets.cli.rpc_register import RPCRegister
|
||||
|
||||
BECDispatcher = lazy_import_from("bec_widgets.utils.bec_dispatcher", ("BECDispatcher",))
|
||||
|
||||
|
||||
class ConnectionConfig(BaseModel):
|
||||
"""Configuration for BECConnector mixin class"""
|
||||
|
||||
widget_class: str = Field(default="NonSpecifiedWidget", description="The class of the widget.")
|
||||
gui_id: Optional[str] = Field(
|
||||
default=None, validate_default=True, description="The GUI ID of the widget."
|
||||
)
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
|
||||
@field_validator("gui_id")
|
||||
def generate_gui_id(cls, v, values):
|
||||
"""Generate a GUI ID if none is provided."""
|
||||
if v is None:
|
||||
widget_class = values.data["widget_class"]
|
||||
v = f"{widget_class}_{str(time.time())}"
|
||||
return v
|
||||
return v
|
||||
|
||||
|
||||
class BECConnector:
|
||||
"""Connection mixin class for all BEC widgets, to handle BEC client and device manager"""
|
||||
|
||||
USER_ACCESS = ["config_dict", "get_all_rpc"]
|
||||
|
||||
def __init__(self, client=None, config: ConnectionConfig = None, gui_id: str = None):
|
||||
# BEC related connections
|
||||
self.bec_dispatcher = BECDispatcher(client=client)
|
||||
self.client = self.bec_dispatcher.client if client is None else client
|
||||
|
||||
if config:
|
||||
self.config = config
|
||||
self.config.widget_class = self.__class__.__name__
|
||||
else:
|
||||
print(
|
||||
f"No initial config found for {self.__class__.__name__}.\n"
|
||||
f"Initializing with default config."
|
||||
)
|
||||
self.config = ConnectionConfig(widget_class=self.__class__.__name__)
|
||||
|
||||
if gui_id:
|
||||
self.config.gui_id = gui_id
|
||||
self.gui_id = gui_id
|
||||
else:
|
||||
self.gui_id = self.config.gui_id
|
||||
|
||||
# register widget to rpc register
|
||||
self.rpc_register = RPCRegister()
|
||||
self.rpc_register.add_rpc(self)
|
||||
|
||||
def get_all_rpc(self) -> dict:
|
||||
"""Get all registered RPC objects."""
|
||||
all_connections = self.rpc_register.list_all_connections()
|
||||
return dict(all_connections)
|
||||
|
||||
@property
|
||||
def rpc_id(self) -> str:
|
||||
"""Get the RPC ID of the widget."""
|
||||
return self.gui_id
|
||||
|
||||
@rpc_id.setter
|
||||
def rpc_id(self, rpc_id: str) -> None:
|
||||
"""Set the RPC ID of the widget."""
|
||||
self.gui_id = rpc_id
|
||||
|
||||
@property
|
||||
def config_dict(self) -> dict:
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
|
||||
Returns:
|
||||
dict: The configuration of the widget.
|
||||
"""
|
||||
return self.config.model_dump()
|
||||
|
||||
@config_dict.setter
|
||||
def config_dict(self, config: BaseModel) -> None:
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
|
||||
Returns:
|
||||
dict: The configuration of the widget.
|
||||
"""
|
||||
self.config = config
|
||||
|
||||
@pyqtSlot(str)
|
||||
def set_gui_id(self, gui_id: str) -> None:
|
||||
"""
|
||||
Set the GUI ID for the widget.
|
||||
|
||||
Args:
|
||||
gui_id(str): GUI ID
|
||||
"""
|
||||
self.config.gui_id = gui_id
|
||||
self.gui_id = gui_id
|
||||
|
||||
def get_obj_by_id(self, obj_id: str):
|
||||
if obj_id == self.gui_id:
|
||||
return self
|
||||
|
||||
def get_bec_shortcuts(self):
|
||||
"""Get BEC shortcuts for the widget."""
|
||||
self.dev = self.client.device_manager.devices
|
||||
self.scans = self.client.scans
|
||||
self.queue = self.client.queue
|
||||
self.scan_storage = self.queue.scan_storage
|
||||
self.dap = self.client.dap
|
||||
|
||||
def update_client(self, client) -> None:
|
||||
"""Update the client and device manager from BEC and create object for BEC shortcuts.
|
||||
|
||||
Args:
|
||||
client: BEC client
|
||||
"""
|
||||
self.client = client
|
||||
self.get_bec_shortcuts()
|
||||
|
||||
@pyqtSlot(ConnectionConfig) # TODO can be also dict
|
||||
def on_config_update(self, config: ConnectionConfig | dict) -> None:
|
||||
"""
|
||||
Update the configuration for the widget.
|
||||
|
||||
Args:
|
||||
config(ConnectionConfig): Configuration settings.
|
||||
"""
|
||||
if isinstance(config, dict):
|
||||
config = ConnectionConfig(**config)
|
||||
# TODO add error handler
|
||||
|
||||
self.config = config
|
||||
|
||||
def get_config(self, dict_output: bool = True) -> dict | BaseModel:
|
||||
"""
|
||||
Get the configuration of the widget.
|
||||
|
||||
Args:
|
||||
dict_output(bool): If True, return the configuration as a dictionary. If False, return the configuration as a pydantic model.
|
||||
|
||||
Returns:
|
||||
dict: The configuration of the plot widget.
|
||||
"""
|
||||
if dict_output:
|
||||
return self.config.model_dump()
|
||||
else:
|
||||
return self.config
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup the widget."""
|
||||
self.rpc_register.remove_rpc(self)
|
||||
all_connections = self.rpc_register.list_all_connections()
|
||||
if len(all_connections) == 0:
|
||||
print("No more connections. Shutting down GUI BEC client.")
|
||||
self.client.shutdown()
|
||||
|
||||
# def closeEvent(self, event):
|
||||
# self.cleanup()
|
||||
# super().closeEvent(event)
|
||||
@@ -1,155 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import collections
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, Union
|
||||
|
||||
import redis
|
||||
from bec_lib.client import BECClient
|
||||
from bec_lib.redis_connector import MessageObject, RedisConnector
|
||||
from bec_lib.service_config import ServiceConfig
|
||||
from qtpy.QtCore import QObject
|
||||
from qtpy.QtCore import Signal as pyqtSignal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bec_lib.endpoints import EndpointInfo
|
||||
|
||||
|
||||
class QtThreadSafeCallback(QObject):
|
||||
cb_signal = pyqtSignal(dict, dict)
|
||||
|
||||
def __init__(self, cb):
|
||||
super().__init__()
|
||||
|
||||
self.cb = cb
|
||||
self.cb_signal.connect(self.cb)
|
||||
|
||||
def __hash__(self):
|
||||
# make 2 differents QtThreadSafeCallback to look
|
||||
# identical when used as dictionary keys, if the
|
||||
# callback is the same
|
||||
return id(self.cb)
|
||||
|
||||
def __call__(self, msg_content, metadata):
|
||||
self.cb_signal.emit(msg_content, metadata)
|
||||
|
||||
|
||||
class QtRedisConnector(RedisConnector):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _execute_callback(self, cb, msg, kwargs):
|
||||
if not isinstance(cb, QtThreadSafeCallback):
|
||||
return super()._execute_callback(cb, msg, kwargs)
|
||||
# if msg.msg_type == "bundle_message":
|
||||
# # big warning: how to handle bundle messages?
|
||||
# # message with messages inside ; which slot to call?
|
||||
# # bundle_msg = msg
|
||||
# # for msg in bundle_msg:
|
||||
# # ...
|
||||
# # for now, only consider the 1st message
|
||||
# msg = msg[0]
|
||||
# raise RuntimeError(f"
|
||||
if isinstance(msg, MessageObject):
|
||||
if isinstance(msg.value, list):
|
||||
msg = msg.value[0]
|
||||
else:
|
||||
msg = msg.value
|
||||
|
||||
# we can notice kwargs are lost when passed to Qt slot
|
||||
metadata = msg.metadata
|
||||
cb(msg.content, metadata)
|
||||
else:
|
||||
# from stream
|
||||
msg = msg["data"]
|
||||
cb(msg.content, msg.metadata)
|
||||
|
||||
|
||||
class BECDispatcher:
|
||||
"""Utility class to keep track of slots connected to a particular redis connector"""
|
||||
|
||||
_instance = None
|
||||
_initialized = False
|
||||
|
||||
def __new__(cls, client=None, config: str = None, *args, **kwargs):
|
||||
if cls._instance is None:
|
||||
cls._instance = super(BECDispatcher, cls).__new__(cls)
|
||||
cls._initialized = False
|
||||
return cls._instance
|
||||
|
||||
def __init__(self, client=None, config: str = None):
|
||||
if self._initialized:
|
||||
return
|
||||
|
||||
self._slots = collections.defaultdict(set)
|
||||
self.client = client
|
||||
|
||||
if self.client is None:
|
||||
if config is not None:
|
||||
host, port = config.split(":")
|
||||
redis_config = {"host": host, "port": port}
|
||||
self.client = BECClient(
|
||||
config=ServiceConfig(redis=redis_config), connector_cls=QtRedisConnector
|
||||
) # , forced=True)
|
||||
else:
|
||||
self.client = BECClient(connector_cls=QtRedisConnector) # , forced=True)
|
||||
else:
|
||||
if self.client.started:
|
||||
# have to reinitialize client to use proper connector
|
||||
self.client.shutdown()
|
||||
self.client._BECClient__init_params["connector_cls"] = QtRedisConnector
|
||||
|
||||
try:
|
||||
self.client.start()
|
||||
except redis.exceptions.ConnectionError:
|
||||
print("Could not connect to Redis, skipping start of BECClient.")
|
||||
|
||||
self._initialized = True
|
||||
|
||||
@classmethod
|
||||
def reset_singleton(cls):
|
||||
cls._instance = None
|
||||
cls._initialized = False
|
||||
|
||||
def connect_slot(
|
||||
self, slot: Callable, topics: Union[EndpointInfo, str, list[Union[EndpointInfo, str]]]
|
||||
) -> None:
|
||||
"""Connect widget's pyqt slot, so that it is called on new pub/sub topic message.
|
||||
|
||||
Args:
|
||||
slot (Callable): A slot method/function that accepts two inputs: content and metadata of
|
||||
the corresponding pub/sub message
|
||||
topics (EndpointInfo | str | list): A topic or list of topics that can typically be acquired via bec_lib.MessageEndpoints
|
||||
"""
|
||||
slot = QtThreadSafeCallback(slot)
|
||||
self.client.connector.register(topics, cb=slot)
|
||||
topics_str, _ = self.client.connector._convert_endpointinfo(topics)
|
||||
self._slots[slot].update(set(topics_str))
|
||||
|
||||
def disconnect_slot(self, slot: Callable, topics: Union[str, list]):
|
||||
# find the right slot to disconnect from ;
|
||||
# slot callbacks are wrapped in QtThreadSafeCallback objects,
|
||||
# but the slot we receive here is the original callable
|
||||
for connected_slot in self._slots:
|
||||
if connected_slot.cb == slot:
|
||||
break
|
||||
else:
|
||||
return
|
||||
self.client.connector.unregister(topics, cb=connected_slot)
|
||||
topics_str, _ = self.client.connector._convert_endpointinfo(topics)
|
||||
self._slots[slot].difference_update(set(topics_str))
|
||||
if not self._slots[slot]:
|
||||
del self._slots[slot]
|
||||
|
||||
def disconnect_topics(self, topics: Union[str, list]):
|
||||
self.client.connector.unregister(topics)
|
||||
topics_str, _ = self.client.connector._convert_endpointinfo(topics)
|
||||
for slot in list(self._slots.keys()):
|
||||
slot_topics = self._slots[slot]
|
||||
slot_topics.difference_update(set(topics_str))
|
||||
if not slot_topics:
|
||||
del self._slots[slot]
|
||||
|
||||
def disconnect_all(self, *args, **kwargs):
|
||||
self.disconnect_topics(self.client.connector._topics_cb)
|
||||
@@ -1,21 +0,0 @@
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtWidgets import QTableWidget
|
||||
|
||||
|
||||
class BECTable(QTableWidget):
|
||||
"""Table widget with custom keyPressEvent to delete rows with backspace or delete key"""
|
||||
|
||||
def keyPressEvent(self, event) -> None:
|
||||
"""
|
||||
Delete selected rows with backspace or delete key
|
||||
|
||||
Args:
|
||||
event: keyPressEvent
|
||||
"""
|
||||
if event.key() in (Qt.Key_Backspace, Qt.Key_Delete):
|
||||
selected_ranges = self.selectedRanges()
|
||||
for selected_range in selected_ranges:
|
||||
for row in range(selected_range.topRow(), selected_range.bottomRow() + 1):
|
||||
self.removeRow(row)
|
||||
else:
|
||||
super().keyPressEvent(event)
|
||||
@@ -1,276 +0,0 @@
|
||||
import re
|
||||
from typing import Literal
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from pydantic_core import PydanticCustomError
|
||||
from qtpy.QtGui import QColor
|
||||
|
||||
|
||||
class Colors:
|
||||
|
||||
@staticmethod
|
||||
def golden_ratio(num: int) -> list:
|
||||
"""Calculate the golden ratio for a given number of angles.
|
||||
|
||||
Args:
|
||||
num (int): Number of angles
|
||||
|
||||
Returns:
|
||||
list: List of angles calculated using the golden ratio.
|
||||
"""
|
||||
phi = 2 * np.pi * ((1 + np.sqrt(5)) / 2)
|
||||
angles = []
|
||||
for ii in range(num):
|
||||
x = np.cos(ii * phi)
|
||||
y = np.sin(ii * phi)
|
||||
angle = np.arctan2(y, x)
|
||||
angles.append(angle)
|
||||
return angles
|
||||
|
||||
@staticmethod
|
||||
def golden_angle_color(
|
||||
colormap: str, num: int, format: Literal["QColor", "HEX", "RGB"] = "QColor"
|
||||
) -> list:
|
||||
"""
|
||||
Extract num colors from the specified colormap following golden angle distribution and return them in the specified format.
|
||||
|
||||
Args:
|
||||
colormap (str): Name of the colormap.
|
||||
num (int): Number of requested colors.
|
||||
format (Literal["QColor","HEX","RGB"]): The format of the returned colors ('RGB', 'HEX', 'QColor').
|
||||
|
||||
Returns:
|
||||
list: List of colors in the specified format.
|
||||
|
||||
Raises:
|
||||
ValueError: If the number of requested colors is greater than the number of colors in the colormap.
|
||||
"""
|
||||
cmap = pg.colormap.get(colormap)
|
||||
cmap_colors = cmap.getColors(mode="float")
|
||||
if num > len(cmap_colors):
|
||||
raise ValueError(
|
||||
f"Number of colors requested ({num}) is greater than the number of colors in the colormap ({len(cmap_colors)})"
|
||||
)
|
||||
angles = Colors.golden_ratio(len(cmap_colors))
|
||||
color_selection = np.round(np.interp(angles, (-np.pi, np.pi), (0, len(cmap_colors))))
|
||||
colors = []
|
||||
for ii in color_selection[:num]:
|
||||
color = cmap_colors[int(ii)]
|
||||
if format.upper() == "HEX":
|
||||
colors.append(QColor.fromRgbF(*color).name())
|
||||
elif format.upper() == "RGB":
|
||||
colors.append(tuple((np.array(color) * 255).astype(int)))
|
||||
elif format.upper() == "QCOLOR":
|
||||
colors.append(QColor.fromRgbF(*color))
|
||||
else:
|
||||
raise ValueError("Unsupported format. Please choose 'RGB', 'HEX', or 'QColor'.")
|
||||
return colors
|
||||
|
||||
@staticmethod
|
||||
def validate_color(color: tuple | str) -> tuple | str:
|
||||
"""
|
||||
Validate the color input if it is HEX or RGBA compatible. Can be used in any pydantic model as a field validator.
|
||||
|
||||
Args:
|
||||
color(tuple|str): The color to be validated. Can be a tuple of RGBA values or a HEX string.
|
||||
|
||||
Returns:
|
||||
tuple|str: The validated color.
|
||||
"""
|
||||
CSS_COLOR_NAMES = {
|
||||
"aliceblue",
|
||||
"antiquewhite",
|
||||
"aqua",
|
||||
"aquamarine",
|
||||
"azure",
|
||||
"beige",
|
||||
"bisque",
|
||||
"black",
|
||||
"blanchedalmond",
|
||||
"blue",
|
||||
"blueviolet",
|
||||
"brown",
|
||||
"burlywood",
|
||||
"cadetblue",
|
||||
"chartreuse",
|
||||
"chocolate",
|
||||
"coral",
|
||||
"cornflowerblue",
|
||||
"cornsilk",
|
||||
"crimson",
|
||||
"cyan",
|
||||
"darkblue",
|
||||
"darkcyan",
|
||||
"darkgoldenrod",
|
||||
"darkgray",
|
||||
"darkgreen",
|
||||
"darkgrey",
|
||||
"darkkhaki",
|
||||
"darkmagenta",
|
||||
"darkolivegreen",
|
||||
"darkorange",
|
||||
"darkorchid",
|
||||
"darkred",
|
||||
"darksalmon",
|
||||
"darkseagreen",
|
||||
"darkslateblue",
|
||||
"darkslategray",
|
||||
"darkslategrey",
|
||||
"darkturquoise",
|
||||
"darkviolet",
|
||||
"deeppink",
|
||||
"deepskyblue",
|
||||
"dimgray",
|
||||
"dimgrey",
|
||||
"dodgerblue",
|
||||
"firebrick",
|
||||
"floralwhite",
|
||||
"forestgreen",
|
||||
"fuchsia",
|
||||
"gainsboro",
|
||||
"ghostwhite",
|
||||
"gold",
|
||||
"goldenrod",
|
||||
"gray",
|
||||
"green",
|
||||
"greenyellow",
|
||||
"grey",
|
||||
"honeydew",
|
||||
"hotpink",
|
||||
"indianred",
|
||||
"indigo",
|
||||
"ivory",
|
||||
"khaki",
|
||||
"lavender",
|
||||
"lavenderblush",
|
||||
"lawngreen",
|
||||
"lemonchiffon",
|
||||
"lightblue",
|
||||
"lightcoral",
|
||||
"lightcyan",
|
||||
"lightgoldenrodyellow",
|
||||
"lightgray",
|
||||
"lightgreen",
|
||||
"lightgrey",
|
||||
"lightpink",
|
||||
"lightsalmon",
|
||||
"lightseagreen",
|
||||
"lightskyblue",
|
||||
"lightslategray",
|
||||
"lightslategrey",
|
||||
"lightsteelblue",
|
||||
"lightyellow",
|
||||
"lime",
|
||||
"limegreen",
|
||||
"linen",
|
||||
"magenta",
|
||||
"maroon",
|
||||
"mediumaquamarine",
|
||||
"mediumblue",
|
||||
"mediumorchid",
|
||||
"mediumpurple",
|
||||
"mediumseagreen",
|
||||
"mediumslateblue",
|
||||
"mediumspringgreen",
|
||||
"mediumturquoise",
|
||||
"mediumvioletred",
|
||||
"midnightblue",
|
||||
"mintcream",
|
||||
"mistyrose",
|
||||
"moccasin",
|
||||
"navajowhite",
|
||||
"navy",
|
||||
"oldlace",
|
||||
"olive",
|
||||
"olivedrab",
|
||||
"orange",
|
||||
"orangered",
|
||||
"orchid",
|
||||
"palegoldenrod",
|
||||
"palegreen",
|
||||
"paleturquoise",
|
||||
"palevioletred",
|
||||
"papayawhip",
|
||||
"peachpuff",
|
||||
"peru",
|
||||
"pink",
|
||||
"plum",
|
||||
"powderblue",
|
||||
"purple",
|
||||
"red",
|
||||
"rosybrown",
|
||||
"royalblue",
|
||||
"saddlebrown",
|
||||
"salmon",
|
||||
"sandybrown",
|
||||
"seagreen",
|
||||
"seashell",
|
||||
"sienna",
|
||||
"silver",
|
||||
"skyblue",
|
||||
"slateblue",
|
||||
"slategray",
|
||||
"slategrey",
|
||||
"snow",
|
||||
"springgreen",
|
||||
"steelblue",
|
||||
"tan",
|
||||
"teal",
|
||||
"thistle",
|
||||
"tomato",
|
||||
"turquoise",
|
||||
"violet",
|
||||
"wheat",
|
||||
"white",
|
||||
"whitesmoke",
|
||||
"yellow",
|
||||
"yellowgreen",
|
||||
}
|
||||
if isinstance(color, str):
|
||||
hex_pattern = re.compile(r"^#(?:[0-9a-fA-F]{3}){1,2}$")
|
||||
if hex_pattern.match(color):
|
||||
return color
|
||||
elif color.lower() in CSS_COLOR_NAMES:
|
||||
return color
|
||||
else:
|
||||
raise PydanticCustomError(
|
||||
"unsupported color",
|
||||
"The color must be a valid HEX string or CSS Color.",
|
||||
{"wrong_value": color},
|
||||
)
|
||||
elif isinstance(color, tuple):
|
||||
if len(color) != 4:
|
||||
raise PydanticCustomError(
|
||||
"unsupported color",
|
||||
"The color must be a tuple of 4 elements (R, G, B, A).",
|
||||
{"wrong_value": color},
|
||||
)
|
||||
for value in color:
|
||||
if not 0 <= value <= 255:
|
||||
raise PydanticCustomError(
|
||||
"unsupported color",
|
||||
f"The color values must be between 0 and 255 in RGBA format (R,G,B,A)",
|
||||
{"wrong_value": color},
|
||||
)
|
||||
return color
|
||||
|
||||
@staticmethod
|
||||
def validate_color_map(color_map: str) -> str:
|
||||
"""
|
||||
Validate the colormap input if it is supported by pyqtgraph. Can be used in any pydantic model as a field validator. If validation fails it prints all available colormaps from pyqtgraph instance.
|
||||
|
||||
Args:
|
||||
color_map(str): The colormap to be validated.
|
||||
|
||||
Returns:
|
||||
str: The validated colormap.
|
||||
"""
|
||||
available_colormaps = pg.colormap.listMaps()
|
||||
if color_map not in available_colormaps:
|
||||
raise PydanticCustomError(
|
||||
"unsupported colormap",
|
||||
f"Colormap '{color_map}' not found in the current installation of pyqtgraph. Choose on the following: {available_colormaps}.",
|
||||
{"wrong_value": color_map},
|
||||
)
|
||||
return color_map
|
||||
@@ -1,48 +0,0 @@
|
||||
import itertools
|
||||
from typing import Type
|
||||
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
|
||||
class WidgetContainerUtils:
|
||||
|
||||
@staticmethod
|
||||
def generate_unique_widget_id(container: dict, prefix: str = "widget") -> str:
|
||||
"""
|
||||
Generate a unique widget ID.
|
||||
|
||||
Args:
|
||||
container(dict): The container of widgets.
|
||||
prefix(str): The prefix of the widget ID.
|
||||
|
||||
Returns:
|
||||
widget_id(str): The unique widget ID.
|
||||
"""
|
||||
existing_ids = set(container.keys())
|
||||
for i in itertools.count(1):
|
||||
widget_id = f"{prefix}_{i}"
|
||||
if widget_id not in existing_ids:
|
||||
return widget_id
|
||||
|
||||
@staticmethod
|
||||
def find_first_widget_by_class(
|
||||
container: dict, widget_class: Type[QWidget], can_fail: bool = True
|
||||
) -> QWidget | None:
|
||||
"""
|
||||
Find the first widget of a given class in the figure.
|
||||
|
||||
Args:
|
||||
container(dict): The container of widgets.
|
||||
widget_class(Type): The class of the widget to find.
|
||||
can_fail(bool): If True, the method will return None if no widget is found. If False, it will raise an error.
|
||||
|
||||
Returns:
|
||||
widget: The widget of the given class.
|
||||
"""
|
||||
for widget_id, widget in container.items():
|
||||
if isinstance(widget, widget_class):
|
||||
return widget
|
||||
if can_fail:
|
||||
return None
|
||||
else:
|
||||
raise ValueError(f"No widget of class {widget_class} found.")
|
||||
@@ -1,235 +0,0 @@
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
|
||||
# from qtpy.QtCore import QObject, pyqtSignal
|
||||
from qtpy.QtCore import QObject
|
||||
from qtpy.QtCore import Signal as pyqtSignal
|
||||
|
||||
|
||||
class Crosshair(QObject):
|
||||
# Signal for 1D plot
|
||||
coordinatesChanged1D = pyqtSignal(tuple)
|
||||
coordinatesClicked1D = pyqtSignal(tuple)
|
||||
# Signal for 2D plot
|
||||
coordinatesChanged2D = pyqtSignal(tuple)
|
||||
coordinatesClicked2D = pyqtSignal(tuple)
|
||||
|
||||
def __init__(self, plot_item: pg.PlotItem, precision: int = 3, parent=None):
|
||||
"""
|
||||
Crosshair for 1D and 2D plots.
|
||||
|
||||
Args:
|
||||
plot_item (pyqtgraph.PlotItem): The plot item to which the crosshair will be attached.
|
||||
precision (int, optional): Number of decimal places to round the coordinates to. Defaults to None.
|
||||
parent (QObject, optional): Parent object for the QObject. Defaults to None.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.is_log_y = None
|
||||
self.is_log_x = None
|
||||
self.plot_item = plot_item
|
||||
self.precision = precision
|
||||
self.v_line = pg.InfiniteLine(angle=90, movable=False)
|
||||
self.h_line = pg.InfiniteLine(angle=0, movable=False)
|
||||
self.plot_item.addItem(self.v_line, ignoreBounds=True)
|
||||
self.plot_item.addItem(self.h_line, ignoreBounds=True)
|
||||
self.proxy = pg.SignalProxy(
|
||||
self.plot_item.scene().sigMouseMoved, rateLimit=60, slot=self.mouse_moved
|
||||
)
|
||||
self.plot_item.scene().sigMouseClicked.connect(self.mouse_clicked)
|
||||
|
||||
# Initialize markers
|
||||
self.marker_moved_1d = []
|
||||
self.marker_clicked_1d = []
|
||||
self.marker_2d = None
|
||||
self.update_markers()
|
||||
|
||||
def update_markers(self):
|
||||
"""Update the markers for the crosshair, creating new ones if necessary."""
|
||||
|
||||
# Clear existing markers
|
||||
for marker in self.marker_moved_1d + self.marker_clicked_1d:
|
||||
self.plot_item.removeItem(marker)
|
||||
if self.marker_2d:
|
||||
self.plot_item.removeItem(self.marker_2d)
|
||||
|
||||
# Create new markers
|
||||
self.marker_moved_1d = []
|
||||
self.marker_clicked_1d = []
|
||||
self.marker_2d = None
|
||||
for item in self.plot_item.items:
|
||||
if isinstance(item, pg.PlotDataItem): # 1D plot
|
||||
pen = item.opts["pen"]
|
||||
color = pen.color() if hasattr(pen, "color") else pg.mkColor(pen)
|
||||
marker_moved = pg.ScatterPlotItem(
|
||||
size=10, pen=pg.mkPen(color), brush=pg.mkBrush(None)
|
||||
)
|
||||
marker_clicked = pg.ScatterPlotItem(
|
||||
size=10, pen=pg.mkPen(None), brush=pg.mkBrush(color)
|
||||
)
|
||||
self.marker_moved_1d.append(marker_moved)
|
||||
self.plot_item.addItem(marker_moved)
|
||||
# Create glowing effect markers for clicked events
|
||||
marker_clicked_list = []
|
||||
for size, alpha in [(18, 64), (14, 128), (10, 255)]:
|
||||
marker_clicked = pg.ScatterPlotItem(
|
||||
size=size,
|
||||
pen=pg.mkPen(None),
|
||||
brush=pg.mkBrush(color.red(), color.green(), color.blue(), alpha),
|
||||
)
|
||||
marker_clicked_list.append(marker_clicked)
|
||||
self.plot_item.addItem(marker_clicked)
|
||||
|
||||
self.marker_clicked_1d.append(marker_clicked_list)
|
||||
elif isinstance(item, pg.ImageItem): # 2D plot
|
||||
self.marker_2d = pg.ROI(
|
||||
[0, 0], size=[1, 1], pen=pg.mkPen("r", width=2), movable=False
|
||||
)
|
||||
self.plot_item.addItem(self.marker_2d)
|
||||
|
||||
def snap_to_data(self, x, y) -> tuple:
|
||||
"""
|
||||
Finds the nearest data points to the given x and y coordinates.
|
||||
|
||||
Args:
|
||||
x: The x-coordinate
|
||||
y: The y-coordinate
|
||||
|
||||
Returns:
|
||||
tuple: The nearest x and y values
|
||||
"""
|
||||
y_values_1d = []
|
||||
x_values_1d = []
|
||||
image_2d = None
|
||||
|
||||
# Iterate through items in the plot
|
||||
for item in self.plot_item.items:
|
||||
if isinstance(item, pg.PlotDataItem): # 1D plot
|
||||
x_data, y_data = item.xData, item.yData
|
||||
if x_data is not None and y_data is not None:
|
||||
if self.is_log_x:
|
||||
min_x_data = np.min(x_data[x_data > 0])
|
||||
else:
|
||||
min_x_data = np.min(x_data)
|
||||
max_x_data = np.max(x_data)
|
||||
if x < min_x_data or x > max_x_data:
|
||||
return None, None
|
||||
closest_x, closest_y = self.closest_x_y_value(x, x_data, y_data)
|
||||
y_values_1d.append(closest_y)
|
||||
x_values_1d.append(closest_x)
|
||||
elif isinstance(item, pg.ImageItem): # 2D plot
|
||||
image_2d = item.image
|
||||
|
||||
# Handle 1D plot
|
||||
if y_values_1d:
|
||||
if all(v is None for v in x_values_1d) or all(v is None for v in y_values_1d):
|
||||
return None, None
|
||||
closest_x = min(x_values_1d, key=lambda xi: abs(xi - x)) # Snap x to closest data point
|
||||
return closest_x, y_values_1d
|
||||
|
||||
# Handle 2D plot
|
||||
if image_2d is not None:
|
||||
x_idx = int(np.clip(x, 0, image_2d.shape[0] - 1))
|
||||
y_idx = int(np.clip(y, 0, image_2d.shape[1] - 1))
|
||||
return x_idx, y_idx
|
||||
|
||||
return None, None
|
||||
|
||||
def closest_x_y_value(self, input_value: float, list_x: list, list_y: list) -> tuple:
|
||||
"""
|
||||
Find the closest x and y value to the input value.
|
||||
|
||||
Args:
|
||||
input_value (float): Input value
|
||||
list_x (list): List of x values
|
||||
list_y (list): List of y values
|
||||
|
||||
Returns:
|
||||
tuple: Closest x and y value
|
||||
"""
|
||||
arr = np.asarray(list_x)
|
||||
i = (np.abs(arr - input_value)).argmin()
|
||||
return list_x[i], list_y[i]
|
||||
|
||||
def mouse_moved(self, event):
|
||||
"""Handles the mouse moved event, updating the crosshair position and emitting signals.
|
||||
|
||||
Args:
|
||||
event: The mouse moved event
|
||||
"""
|
||||
self.check_log()
|
||||
pos = event[0]
|
||||
if self.plot_item.vb.sceneBoundingRect().contains(pos):
|
||||
mouse_point = self.plot_item.vb.mapSceneToView(pos)
|
||||
self.v_line.setPos(mouse_point.x())
|
||||
self.h_line.setPos(mouse_point.y())
|
||||
|
||||
x, y = mouse_point.x(), mouse_point.y()
|
||||
if self.is_log_x:
|
||||
x = 10**x
|
||||
if self.is_log_y:
|
||||
y = 10**y
|
||||
x, y_values = self.snap_to_data(x, y)
|
||||
|
||||
for item in self.plot_item.items:
|
||||
if isinstance(item, pg.PlotDataItem):
|
||||
if x is None or all(v is None for v in y_values):
|
||||
return
|
||||
coordinate_to_emit = (
|
||||
round(x, self.precision),
|
||||
[round(y_val, self.precision) for y_val in y_values],
|
||||
)
|
||||
self.coordinatesChanged1D.emit(coordinate_to_emit)
|
||||
for i, y_val in enumerate(y_values):
|
||||
self.marker_moved_1d[i].setData(
|
||||
[x if not self.is_log_x else np.log10(x)],
|
||||
[y_val if not self.is_log_y else np.log10(y_val)],
|
||||
)
|
||||
elif isinstance(item, pg.ImageItem):
|
||||
if x is None or y_values is None:
|
||||
return
|
||||
coordinate_to_emit = (x, y_values)
|
||||
self.coordinatesChanged2D.emit(coordinate_to_emit)
|
||||
|
||||
def mouse_clicked(self, event):
|
||||
"""Handles the mouse clicked event, updating the crosshair position and emitting signals.
|
||||
|
||||
Args:
|
||||
event: The mouse clicked event
|
||||
"""
|
||||
self.check_log()
|
||||
if self.plot_item.vb.sceneBoundingRect().contains(event._scenePos):
|
||||
mouse_point = self.plot_item.vb.mapSceneToView(event._scenePos)
|
||||
x, y = mouse_point.x(), mouse_point.y()
|
||||
|
||||
if self.is_log_x:
|
||||
x = 10**x
|
||||
if self.is_log_y:
|
||||
y = 10**y
|
||||
x, y_values = self.snap_to_data(x, y)
|
||||
|
||||
for item in self.plot_item.items:
|
||||
if isinstance(item, pg.PlotDataItem):
|
||||
if x is None or all(v is None for v in y_values):
|
||||
return
|
||||
coordinate_to_emit = (
|
||||
round(x, self.precision),
|
||||
[round(y_val, self.precision) for y_val in y_values],
|
||||
)
|
||||
self.coordinatesClicked1D.emit(coordinate_to_emit)
|
||||
for i, y_val in enumerate(y_values):
|
||||
for marker in self.marker_clicked_1d[i]:
|
||||
marker.setData(
|
||||
[x if not self.is_log_x else np.log10(x)],
|
||||
[y_val if not self.is_log_y else np.log10(y_val)],
|
||||
)
|
||||
elif isinstance(item, pg.ImageItem):
|
||||
if x is None or y_values is None:
|
||||
return
|
||||
coordinate_to_emit = (x, y_values)
|
||||
self.coordinatesClicked2D.emit(coordinate_to_emit)
|
||||
self.marker_2d.setPos([x, y_values])
|
||||
|
||||
def check_log(self):
|
||||
"""Checks if the x or y axis is in log scale and updates the internal state accordingly."""
|
||||
self.is_log_x = self.plot_item.ctrl.logXCheck.isChecked()
|
||||
self.is_log_y = self.plot_item.ctrl.logYCheck.isChecked()
|
||||
@@ -1,42 +0,0 @@
|
||||
class EntryValidator:
|
||||
def __init__(self, devices):
|
||||
self.devices = devices
|
||||
|
||||
def validate_signal(self, name: str, entry: str = None) -> str:
|
||||
"""
|
||||
Validate a signal entry for a given device. If the entry is not provided, the first signal entry will be used from the device hints.
|
||||
|
||||
Args:
|
||||
name(str): Device name
|
||||
entry(str): Signal entry
|
||||
|
||||
Returns:
|
||||
str: Signal entry
|
||||
"""
|
||||
if name not in self.devices:
|
||||
raise ValueError(f"Device '{name}' not found in current BEC session")
|
||||
|
||||
device = self.devices[name]
|
||||
description = device.describe()
|
||||
|
||||
if entry is None:
|
||||
entry = next(iter(device._hints), name) if hasattr(device, "_hints") else name
|
||||
if entry not in description:
|
||||
raise ValueError(f"Entry '{entry}' not found in device '{name}' signals")
|
||||
|
||||
return entry
|
||||
|
||||
def validate_monitor(self, monitor: str) -> str:
|
||||
"""
|
||||
Validate a monitor entry for a given device.
|
||||
|
||||
Args:
|
||||
monitor(str): Monitor entry
|
||||
|
||||
Returns:
|
||||
str: Monitor entry
|
||||
"""
|
||||
if monitor not in self.devices:
|
||||
raise ValueError(f"Device '{monitor}' not found in current BEC session")
|
||||
|
||||
return monitor
|
||||
@@ -1,121 +0,0 @@
|
||||
from collections import OrderedDict
|
||||
from typing import Literal
|
||||
|
||||
from qtpy.QtWidgets import QGridLayout, QWidget
|
||||
|
||||
|
||||
class GridLayoutManager:
|
||||
"""
|
||||
GridLayoutManager class is used to manage widgets in a QGridLayout and extend its functionality.
|
||||
|
||||
The GridLayoutManager class provides methods to add, move, and check the position of widgets in a QGridLayout.
|
||||
It also provides a method to get the positions of all widgets in the layout.
|
||||
|
||||
Args:
|
||||
layout(QGridLayout): The layout to manage.
|
||||
"""
|
||||
|
||||
def __init__(self, layout: QGridLayout):
|
||||
self.layout = layout
|
||||
|
||||
def is_position_occupied(self, row: int, col: int) -> bool:
|
||||
"""
|
||||
Check if the position in the layout is occupied by a widget.
|
||||
|
||||
Args:
|
||||
row(int): The row to check.
|
||||
col(int): The column to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the position is occupied, False otherwise.
|
||||
"""
|
||||
for i in range(self.layout.count()):
|
||||
widget_row, widget_col, _, _ = self.layout.getItemPosition(i)
|
||||
if widget_row == row and widget_col == col:
|
||||
return True
|
||||
return False
|
||||
|
||||
def shift_widgets(
|
||||
self,
|
||||
direction: Literal["down", "up", "left", "right"] = "down",
|
||||
start_row: int = 0,
|
||||
start_col: int = 0,
|
||||
):
|
||||
"""
|
||||
Shift widgets in the layout in the specified direction starting from the specified position.
|
||||
|
||||
Args:
|
||||
direction(str): The direction to shift the widgets. Can be "down", "up", "left", or "right".
|
||||
start_row(int): The row to start shifting from. Default is 0.
|
||||
start_col(int): The column to start shifting from. Default is 0.
|
||||
"""
|
||||
for i in reversed(range(self.layout.count())):
|
||||
widget_item = self.layout.itemAt(i)
|
||||
widget = widget_item.widget()
|
||||
row, col, rowspan, colspan = self.layout.getItemPosition(i)
|
||||
if direction == "down" and row >= start_row:
|
||||
self.layout.addWidget(widget, row + 1, col, rowspan, colspan)
|
||||
elif direction == "up" and row > start_row:
|
||||
self.layout.addWidget(widget, row - 1, col, rowspan, colspan)
|
||||
elif direction == "right" and col >= start_col:
|
||||
self.layout.addWidget(widget, row, col + 1, rowspan, colspan)
|
||||
elif direction == "left" and col > start_col:
|
||||
self.layout.addWidget(widget, row, col - 1, rowspan, colspan)
|
||||
|
||||
def move_widget(self, widget: QWidget, new_row: int, new_col: int):
|
||||
"""
|
||||
Move a widget to a new position in the layout.
|
||||
|
||||
Args:
|
||||
widget(QWidget): The widget to move.
|
||||
new_row(int): The new row to move the widget to.
|
||||
new_col(int): The new column to move the widget to.
|
||||
"""
|
||||
self.layout.removeWidget(widget)
|
||||
self.layout.addWidget(widget, new_row, new_col)
|
||||
|
||||
def add_widget(
|
||||
self,
|
||||
widget: QWidget,
|
||||
row=None,
|
||||
col=0,
|
||||
rowspan=1,
|
||||
colspan=1,
|
||||
shift: Literal["down", "up", "left", "right"] = "down",
|
||||
):
|
||||
"""
|
||||
Add a widget to the layout at the specified position.
|
||||
|
||||
Args:
|
||||
widget(QWidget): The widget to add.
|
||||
row(int): The row to add the widget to. If None, the widget will be added to the next available row.
|
||||
col(int): The column to add the widget to. Default is 0.
|
||||
rowspan(int): The number of rows the widget will span. Default is 1.
|
||||
colspan(int): The number of columns the widget will span. Default is 1.
|
||||
shift(str): The direction to shift the widgets if the position is occupied. Can be "down", "up", "left", or "right".
|
||||
"""
|
||||
if row is None:
|
||||
row = self.layout.rowCount()
|
||||
if self.is_position_occupied(row, col):
|
||||
self.shift_widgets(shift, start_row=row)
|
||||
self.layout.addWidget(widget, row, col, rowspan, colspan)
|
||||
|
||||
def get_widgets_positions(self) -> dict:
|
||||
"""
|
||||
Get the positions of all widgets in the layout.
|
||||
Returns:
|
||||
dict: A dictionary with the positions of the widgets in the layout.
|
||||
|
||||
"""
|
||||
positions = []
|
||||
for i in range(self.layout.count()):
|
||||
widget_item = self.layout.itemAt(i)
|
||||
widget = widget_item.widget()
|
||||
if widget:
|
||||
position = self.layout.getItemPosition(i)
|
||||
positions.append((position, widget))
|
||||
positions.sort(key=lambda x: (x[0][0], x[0][1], x[0][2], x[0][3]))
|
||||
ordered_positions = OrderedDict()
|
||||
for pos, widget in positions:
|
||||
ordered_positions[pos] = widget
|
||||
return ordered_positions
|
||||
@@ -1,40 +0,0 @@
|
||||
import inspect
|
||||
|
||||
from bec_lib.plugin_helper import _get_available_plugins
|
||||
|
||||
from bec_widgets.utils import BECConnector
|
||||
|
||||
|
||||
def get_plugin_widgets() -> dict[str, BECConnector]:
|
||||
"""
|
||||
Get all available widgets from the plugin directory. Widgets are classes that inherit from BECConnector.
|
||||
The plugins are provided through python plugins and specified in the respective pyproject.toml file using
|
||||
the following key:
|
||||
|
||||
[project.entry-points."bec.widgets.user_widgets"]
|
||||
plugin_widgets = "path.to.plugin.module"
|
||||
|
||||
e.g.
|
||||
[project.entry-points."bec.widgets.user_widgets"]
|
||||
plugin_widgets = "pxiii_bec.bec_widgets.widgets"
|
||||
|
||||
assuming that the widgets module for the package pxiii_bec is located at pxiii_bec/bec_widgets/widgets and
|
||||
contains the widgets to be loaded within the pxiii_bec/bec_widgets/widgets/__init__.py file.
|
||||
|
||||
Returns:
|
||||
dict[str, BECConnector]: A dictionary of widget names and their respective classes.
|
||||
"""
|
||||
modules = _get_available_plugins("bec.widgets.user_widgets")
|
||||
loaded_plugins = {}
|
||||
print(modules)
|
||||
for module in modules:
|
||||
mods = inspect.getmembers(module, predicate=_filter_plugins)
|
||||
for name, mod_cls in mods:
|
||||
if name in loaded_plugins:
|
||||
print(f"Duplicated widgets plugin {name}.")
|
||||
loaded_plugins[name] = mod_cls
|
||||
return loaded_plugins
|
||||
|
||||
|
||||
def _filter_plugins(obj):
|
||||
return inspect.isclass(obj) and issubclass(obj, BECConnector)
|
||||
@@ -1,15 +0,0 @@
|
||||
def rpc_public(func):
|
||||
func.rpc_public = True # Mark the function for later processing by the class decorator
|
||||
return func
|
||||
|
||||
|
||||
def register_rpc_methods(cls):
|
||||
"""
|
||||
Class decorator to scan for rpc_public methods and add them to USER_ACCESS.
|
||||
"""
|
||||
if not hasattr(cls, "USER_ACCESS"):
|
||||
cls.USER_ACCESS = set()
|
||||
for name, method in cls.__dict__.items():
|
||||
if getattr(method, "rpc_public", False):
|
||||
cls.USER_ACCESS.add(name)
|
||||
return cls
|
||||
@@ -1,37 +0,0 @@
|
||||
import threading
|
||||
|
||||
|
||||
class ThreadTracker:
|
||||
def __init__(self, exclude_names=None):
|
||||
self.exclude_names = exclude_names if exclude_names else []
|
||||
self.initial_threads = self._capture_threads()
|
||||
|
||||
def _capture_threads(self):
|
||||
return set(
|
||||
th
|
||||
for th in threading.enumerate()
|
||||
if not any(ex_name in th.name for ex_name in self.exclude_names)
|
||||
and th is not threading.main_thread()
|
||||
)
|
||||
|
||||
def _thread_info(self, threads):
|
||||
return ", \n".join(f"{th.name}(ID: {th.ident})" for th in threads)
|
||||
|
||||
def check_unfinished_threads(self):
|
||||
current_threads = self._capture_threads()
|
||||
additional_threads = current_threads - self.initial_threads
|
||||
closed_threads = self.initial_threads - current_threads
|
||||
if additional_threads:
|
||||
raise Exception(
|
||||
f"###### Initial threads ######:\n {self._thread_info(self.initial_threads)}\n"
|
||||
f"###### Current threads ######:\n {self._thread_info(current_threads)}\n"
|
||||
f"###### Closed threads ######:\n {self._thread_info(closed_threads)}\n"
|
||||
f"###### Unfinished threads ######:\n {self._thread_info(additional_threads)}"
|
||||
)
|
||||
else:
|
||||
print(
|
||||
"All threads properly closed.\n"
|
||||
f"###### Initial threads ######:\n {self._thread_info(self.initial_threads)}\n"
|
||||
f"###### Current threads ######:\n {self._thread_info(current_threads)}\n"
|
||||
f"###### Closed threads ######:\n {self._thread_info(closed_threads)}"
|
||||
)
|
||||
@@ -1,58 +0,0 @@
|
||||
from qtpy import QT_VERSION
|
||||
from qtpy.QtCore import QFile, QIODevice
|
||||
|
||||
|
||||
class UILoader:
|
||||
"""Universal UI loader for PyQt5, PyQt6, PySide2, and PySide6."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
self.parent = parent
|
||||
if QT_VERSION.startswith("5"):
|
||||
# PyQt5 or PySide2
|
||||
from qtpy import uic
|
||||
|
||||
self.loader = uic.loadUi
|
||||
elif QT_VERSION.startswith("6"):
|
||||
# PyQt6 or PySide6
|
||||
try:
|
||||
from PySide6.QtUiTools import QUiLoader
|
||||
|
||||
self.loader = self.load_ui_pyside6
|
||||
except ImportError:
|
||||
from PyQt6.uic import loadUi
|
||||
|
||||
self.loader = loadUi
|
||||
|
||||
def load_ui_pyside6(self, ui_file, parent=None):
|
||||
"""
|
||||
Specific loader for PySide6 using QUiLoader.
|
||||
Args:
|
||||
ui_file(str): Path to the .ui file.
|
||||
parent(QWidget): Parent widget.
|
||||
|
||||
Returns:
|
||||
QWidget: The loaded widget.
|
||||
"""
|
||||
from PySide6.QtUiTools import QUiLoader
|
||||
|
||||
loader = QUiLoader(parent)
|
||||
file = QFile(ui_file)
|
||||
if not file.open(QIODevice.ReadOnly):
|
||||
raise IOError(f"Cannot open file: {ui_file}")
|
||||
widget = loader.load(file, parent)
|
||||
file.close()
|
||||
return widget
|
||||
|
||||
def load_ui(self, ui_file, parent=None):
|
||||
"""
|
||||
Universal UI loader method.
|
||||
Args:
|
||||
ui_file(str): Path to the .ui file.
|
||||
parent(QWidget): Parent widget.
|
||||
|
||||
Returns:
|
||||
QWidget: The loaded widget.
|
||||
"""
|
||||
if parent is None:
|
||||
parent = self.parent
|
||||
return self.loader(ui_file, parent)
|
||||
@@ -1,13 +0,0 @@
|
||||
# from qtpy.QtGui import QDoubleValidator
|
||||
# from qtpy.QtWidgets import QStyledItemDelegate, QLineEdit
|
||||
|
||||
from qtpy.QtGui import QDoubleValidator
|
||||
from qtpy.QtWidgets import QLineEdit, QStyledItemDelegate
|
||||
|
||||
|
||||
class DoubleValidationDelegate(QStyledItemDelegate):
|
||||
def createEditor(self, parent, option, index):
|
||||
editor = QLineEdit(parent)
|
||||
validator = QDoubleValidator()
|
||||
editor.setValidator(validator)
|
||||
return editor
|
||||
@@ -1,325 +0,0 @@
|
||||
# pylint: disable=no-name-in-module
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
QCheckBox,
|
||||
QComboBox,
|
||||
QDoubleSpinBox,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QSpinBox,
|
||||
QTableWidget,
|
||||
QTableWidgetItem,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
|
||||
class WidgetHandler(ABC):
|
||||
"""Abstract base class for all widget handlers."""
|
||||
|
||||
@abstractmethod
|
||||
def get_value(self, widget: QWidget):
|
||||
"""Retrieve value from the widget instance."""
|
||||
|
||||
@abstractmethod
|
||||
def set_value(self, widget: QWidget, value):
|
||||
"""Set a value on the widget instance."""
|
||||
|
||||
|
||||
class LineEditHandler(WidgetHandler):
|
||||
"""Handler for QLineEdit widgets."""
|
||||
|
||||
def get_value(self, widget: QLineEdit) -> str:
|
||||
return widget.text()
|
||||
|
||||
def set_value(self, widget: QLineEdit, value: str) -> None:
|
||||
widget.setText(value)
|
||||
|
||||
|
||||
class ComboBoxHandler(WidgetHandler):
|
||||
"""Handler for QComboBox widgets."""
|
||||
|
||||
def get_value(self, widget: QComboBox) -> int:
|
||||
return widget.currentIndex()
|
||||
|
||||
def set_value(self, widget: QComboBox, value: int) -> None:
|
||||
widget.setCurrentIndex(value)
|
||||
|
||||
|
||||
class TableWidgetHandler(WidgetHandler):
|
||||
"""Handler for QTableWidget widgets."""
|
||||
|
||||
def get_value(self, widget: QTableWidget) -> list:
|
||||
return [
|
||||
[
|
||||
widget.item(row, col).text() if widget.item(row, col) else ""
|
||||
for col in range(widget.columnCount())
|
||||
]
|
||||
for row in range(widget.rowCount())
|
||||
]
|
||||
|
||||
def set_value(self, widget: QTableWidget, value) -> None:
|
||||
for row, row_values in enumerate(value):
|
||||
for col, cell_value in enumerate(row_values):
|
||||
item = QTableWidgetItem(str(cell_value))
|
||||
widget.setItem(row, col, item)
|
||||
|
||||
|
||||
class SpinBoxHandler(WidgetHandler):
|
||||
"""Handler for QSpinBox and QDoubleSpinBox widgets."""
|
||||
|
||||
def get_value(self, widget):
|
||||
return widget.value()
|
||||
|
||||
def set_value(self, widget, value):
|
||||
widget.setValue(value)
|
||||
|
||||
|
||||
class CheckBoxHandler(WidgetHandler):
|
||||
"""Handler for QCheckBox widgets."""
|
||||
|
||||
def get_value(self, widget):
|
||||
return widget.isChecked()
|
||||
|
||||
def set_value(self, widget, value):
|
||||
widget.setChecked(value)
|
||||
|
||||
|
||||
class LabelHandler(WidgetHandler):
|
||||
"""Handler for QLabel widgets."""
|
||||
|
||||
def get_value(self, widget):
|
||||
return widget.text()
|
||||
|
||||
def set_value(self, widget, value):
|
||||
widget.setText(value)
|
||||
|
||||
|
||||
class WidgetIO:
|
||||
"""Public interface for getting and setting values using handler mapping"""
|
||||
|
||||
_handlers = {
|
||||
QLineEdit: LineEditHandler,
|
||||
QComboBox: ComboBoxHandler,
|
||||
QTableWidget: TableWidgetHandler,
|
||||
QSpinBox: SpinBoxHandler,
|
||||
QDoubleSpinBox: SpinBoxHandler,
|
||||
QCheckBox: CheckBoxHandler,
|
||||
QLabel: LabelHandler,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_value(widget, ignore_errors=False):
|
||||
"""
|
||||
Retrieve value from the widget instance.
|
||||
|
||||
Args:
|
||||
widget: Widget instance.
|
||||
ignore_errors(bool, optional): Whether to ignore if no handler is found.
|
||||
"""
|
||||
handler_class = WidgetIO._handlers.get(type(widget))
|
||||
if handler_class:
|
||||
return handler_class().get_value(widget) # Instantiate the handler
|
||||
if not ignore_errors:
|
||||
raise ValueError(f"No handler for widget type: {type(widget)}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def set_value(widget, value, ignore_errors=False):
|
||||
"""
|
||||
Set a value on the widget instance.
|
||||
|
||||
Args:
|
||||
widget: Widget instance.
|
||||
value: Value to set.
|
||||
ignore_errors(bool, optional): Whether to ignore if no handler is found.
|
||||
"""
|
||||
handler_class = WidgetIO._handlers.get(type(widget))
|
||||
if handler_class:
|
||||
handler_class().set_value(widget, value) # Instantiate the handler
|
||||
elif not ignore_errors:
|
||||
raise ValueError(f"No handler for widget type: {type(widget)}")
|
||||
|
||||
|
||||
################## for exporting and importing widget hierarchies ##################
|
||||
|
||||
|
||||
class WidgetHierarchy:
|
||||
@staticmethod
|
||||
def print_widget_hierarchy(
|
||||
widget,
|
||||
indent: int = 0,
|
||||
grab_values: bool = False,
|
||||
prefix: str = "",
|
||||
exclude_internal_widgets: bool = True,
|
||||
) -> None:
|
||||
"""
|
||||
Print the widget hierarchy to the console.
|
||||
|
||||
Args:
|
||||
widget: Widget to print the hierarchy of
|
||||
indent(int, optional): Level of indentation.
|
||||
grab_values(bool,optional): Whether to grab the values of the widgets.
|
||||
prefix(stc,optional): Custom string prefix for indentation.
|
||||
exclude_internal_widgets(bool,optional): Whether to exclude internal widgets (e.g. QComboBox in PyQt6).
|
||||
"""
|
||||
widget_info = f"{widget.__class__.__name__} ({widget.objectName()})"
|
||||
if grab_values:
|
||||
value = WidgetIO.get_value(widget, ignore_errors=True)
|
||||
value_str = f" [value: {value}]" if value is not None else ""
|
||||
widget_info += value_str
|
||||
|
||||
print(prefix + widget_info)
|
||||
|
||||
children = widget.children()
|
||||
for child in children:
|
||||
if (
|
||||
exclude_internal_widgets
|
||||
and isinstance(widget, QComboBox)
|
||||
and child.__class__.__name__ in ["QFrame", "QBoxLayout", "QListView"]
|
||||
):
|
||||
continue
|
||||
child_prefix = prefix + " "
|
||||
arrow = "├─ " if child != children[-1] else "└─ "
|
||||
WidgetHierarchy.print_widget_hierarchy(
|
||||
child, indent + 1, grab_values, prefix=child_prefix + arrow
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def export_config_to_dict(
|
||||
widget: QWidget,
|
||||
config: dict = None,
|
||||
indent: int = 0,
|
||||
grab_values: bool = False,
|
||||
print_hierarchy: bool = False,
|
||||
save_all: bool = True,
|
||||
exclude_internal_widgets: bool = True,
|
||||
) -> dict:
|
||||
"""
|
||||
Export the widget hierarchy to a dictionary.
|
||||
|
||||
Args:
|
||||
widget: Widget to print the hierarchy of.
|
||||
config(dict,optional): Dictionary to export the hierarchy to.
|
||||
indent(int,optional): Level of indentation.
|
||||
grab_values(bool,optional): Whether to grab the values of the widgets.
|
||||
print_hierarchy(bool,optional): Whether to print the hierarchy to the console.
|
||||
save_all(bool,optional): Whether to save all widgets or only those with values.
|
||||
exclude_internal_widgets(bool,optional): Whether to exclude internal widgets (e.g. QComboBox in PyQt6).
|
||||
Returns:
|
||||
config(dict): Dictionary containing the widget hierarchy.
|
||||
"""
|
||||
if config is None:
|
||||
config = {}
|
||||
widget_info = f"{widget.__class__.__name__} ({widget.objectName()})"
|
||||
|
||||
# if grab_values and type(widget) in WidgetIO._handlers:
|
||||
if grab_values:
|
||||
value = WidgetIO.get_value(widget, ignore_errors=True)
|
||||
if value is not None or save_all:
|
||||
if widget_info not in config:
|
||||
config[widget_info] = {}
|
||||
if value is not None:
|
||||
config[widget_info]["value"] = value
|
||||
|
||||
if print_hierarchy:
|
||||
WidgetHierarchy.print_widget_hierarchy(widget, indent, grab_values)
|
||||
|
||||
for child in widget.children():
|
||||
# Skip internal widgets of QComboBox in PyQt6
|
||||
if (
|
||||
exclude_internal_widgets
|
||||
and isinstance(widget, QComboBox)
|
||||
and child.__class__.__name__ in ["QFrame", "QBoxLayout", "QListView"]
|
||||
):
|
||||
continue
|
||||
child_config = WidgetHierarchy.export_config_to_dict(
|
||||
child, None, indent + 1, grab_values, print_hierarchy, save_all
|
||||
)
|
||||
if child_config or save_all:
|
||||
if widget_info not in config:
|
||||
config[widget_info] = {}
|
||||
config[widget_info].update(child_config)
|
||||
|
||||
return config
|
||||
|
||||
@staticmethod
|
||||
def import_config_from_dict(widget, config: dict, set_values: bool = False) -> None:
|
||||
"""
|
||||
Import the widget hierarchy from a dictionary.
|
||||
|
||||
Args:
|
||||
widget: Widget to import the hierarchy to.
|
||||
config:
|
||||
set_values:
|
||||
"""
|
||||
widget_name = f"{widget.__class__.__name__} ({widget.objectName()})"
|
||||
widget_config = config.get(widget_name, {})
|
||||
for child in widget.children():
|
||||
child_name = f"{child.__class__.__name__} ({child.objectName()})"
|
||||
child_config = widget_config.get(child_name)
|
||||
if child_config is not None:
|
||||
value = child_config.get("value")
|
||||
if set_values and value is not None:
|
||||
WidgetIO.set_value(child, value)
|
||||
WidgetHierarchy.import_config_from_dict(child, widget_config, set_values)
|
||||
|
||||
|
||||
# Example application to demonstrate the usage of the functions
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
app = QApplication([])
|
||||
|
||||
# Create instance of WidgetHierarchy
|
||||
widget_hierarchy = WidgetHierarchy()
|
||||
|
||||
# Create a simple widget hierarchy for demonstration purposes
|
||||
main_widget = QWidget()
|
||||
layout = QVBoxLayout(main_widget)
|
||||
line_edit = QLineEdit(main_widget)
|
||||
combo_box = QComboBox(main_widget)
|
||||
table_widget = QTableWidget(2, 2, main_widget)
|
||||
spin_box = QSpinBox(main_widget)
|
||||
layout.addWidget(line_edit)
|
||||
layout.addWidget(combo_box)
|
||||
layout.addWidget(table_widget)
|
||||
layout.addWidget(spin_box)
|
||||
|
||||
# Add text items to the combo box
|
||||
combo_box.addItems(["Option 1", "Option 2", "Option 3"])
|
||||
|
||||
main_widget.show()
|
||||
|
||||
# Hierarchy of original widget
|
||||
print(30 * "#")
|
||||
print(f"Widget hierarchy for {main_widget.objectName()}:")
|
||||
print(30 * "#")
|
||||
config_dict = widget_hierarchy.export_config_to_dict(
|
||||
main_widget, grab_values=True, print_hierarchy=True
|
||||
)
|
||||
print(30 * "#")
|
||||
print(f"Config dict: {config_dict}")
|
||||
|
||||
# Hierarchy of new widget and set values
|
||||
new_config_dict = {
|
||||
"QWidget ()": {
|
||||
"QLineEdit ()": {"value": "New Text"},
|
||||
"QComboBox ()": {"value": 1},
|
||||
"QTableWidget ()": {"value": [["a", "b"], ["c", "d"]]},
|
||||
"QSpinBox ()": {"value": 10},
|
||||
}
|
||||
}
|
||||
widget_hierarchy.import_config_from_dict(main_widget, new_config_dict, set_values=True)
|
||||
print(30 * "#")
|
||||
config_dict_new = widget_hierarchy.export_config_to_dict(
|
||||
main_widget, grab_values=True, print_hierarchy=True
|
||||
)
|
||||
config_dict_new_reduced = widget_hierarchy.export_config_to_dict(
|
||||
main_widget, grab_values=True, print_hierarchy=True, save_all=False
|
||||
)
|
||||
print(30 * "#")
|
||||
print(f"Config dict new FULL: {config_dict_new}")
|
||||
print(f"Config dict new REDUCED: {config_dict_new_reduced}")
|
||||
|
||||
app.exec()
|
||||
@@ -1,64 +0,0 @@
|
||||
# pylint: disable=no-name-in-module
|
||||
|
||||
from typing import Union
|
||||
|
||||
import yaml
|
||||
from qtpy.QtWidgets import QFileDialog
|
||||
|
||||
|
||||
def load_yaml(instance) -> Union[dict, None]:
|
||||
"""
|
||||
Load YAML file from disk.
|
||||
|
||||
Args:
|
||||
instance: Instance of the calling widget.
|
||||
|
||||
Returns:
|
||||
dict: Configuration data loaded from the YAML file.
|
||||
"""
|
||||
options = QFileDialog.Options()
|
||||
file_path, _ = QFileDialog.getOpenFileName(
|
||||
instance, "Load Settings", "", "YAML Files (*.yaml *.yml);;All Files (*)", options=options
|
||||
)
|
||||
|
||||
if not file_path:
|
||||
return None
|
||||
try:
|
||||
with open(file_path, "r") as file:
|
||||
config = yaml.safe_load(file)
|
||||
return config
|
||||
|
||||
except FileNotFoundError:
|
||||
print(f"The file {file_path} was not found.")
|
||||
except PermissionError:
|
||||
print(f"Permission denied for file {file_path}.")
|
||||
except yaml.YAMLError as e:
|
||||
print(f"Error parsing YAML file {file_path}: {e}")
|
||||
except Exception as e:
|
||||
print(f"An error occurred while loading the settings from {file_path}: {e}")
|
||||
|
||||
|
||||
def save_yaml(instance, config: dict) -> None:
|
||||
"""
|
||||
Save YAML file to disk.
|
||||
|
||||
Args:
|
||||
instance: Instance of the calling widget.
|
||||
config: Configuration data to be saved.
|
||||
"""
|
||||
options = QFileDialog.Options()
|
||||
file_path, _ = QFileDialog.getSaveFileName(
|
||||
instance, "Save Settings", "", "YAML Files (*.yaml *.yml);;All Files (*)", options=options
|
||||
)
|
||||
|
||||
if not file_path:
|
||||
return
|
||||
try:
|
||||
if not (file_path.endswith(".yaml") or file_path.endswith(".yml")):
|
||||
file_path += ".yaml"
|
||||
|
||||
with open(file_path, "w") as file:
|
||||
yaml.dump(config, file)
|
||||
print(f"Settings saved to {file_path}")
|
||||
except Exception as e:
|
||||
print(f"An error occurred while saving the settings to {file_path}: {e}")
|
||||
@@ -1,4 +0,0 @@
|
||||
from .dock import BECDock, BECDockArea
|
||||
from .figure import BECFigure, FigureConfig
|
||||
from .scan_control import ScanControl
|
||||
from .spiral_progress_bar import SpiralProgressBar
|
||||
@@ -1,2 +0,0 @@
|
||||
from .dock import BECDock
|
||||
from .dock_area import BECDockArea
|
||||
@@ -1,283 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Literal, Optional
|
||||
|
||||
from pydantic import Field
|
||||
from pyqtgraph.dockarea import Dock
|
||||
|
||||
from bec_widgets.cli.rpc_wigdet_handler import RPCWidgetHandler
|
||||
from bec_widgets.utils import BECConnector, ConnectionConfig, GridLayoutManager
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.widgets import BECDockArea
|
||||
|
||||
|
||||
class DockConfig(ConnectionConfig):
|
||||
widgets: dict[str, Any] = Field({}, description="The widgets in the dock.")
|
||||
position: Literal["bottom", "top", "left", "right", "above", "below"] = Field(
|
||||
"bottom", description="The position of the dock."
|
||||
)
|
||||
parent_dock_area: Optional[str] = Field(
|
||||
None, description="The GUI ID of parent dock area of the dock."
|
||||
)
|
||||
|
||||
|
||||
class BECDock(BECConnector, Dock):
|
||||
USER_ACCESS = [
|
||||
"config_dict",
|
||||
"rpc_id",
|
||||
"widget_list",
|
||||
"show_title_bar",
|
||||
"hide_title_bar",
|
||||
"get_widgets_positions",
|
||||
"set_title",
|
||||
"add_widget",
|
||||
"list_eligible_widgets",
|
||||
"move_widget",
|
||||
"remove_widget",
|
||||
"remove",
|
||||
"attach",
|
||||
"detach",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: QWidget | None = None,
|
||||
parent_dock_area: BECDockArea | None = None,
|
||||
config: DockConfig | None = None,
|
||||
name: str | None = None,
|
||||
client=None,
|
||||
gui_id: str | None = None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
if config is None:
|
||||
config = DockConfig(
|
||||
widget_class=self.__class__.__name__, parent_dock_area=parent_dock_area.gui_id
|
||||
)
|
||||
else:
|
||||
if isinstance(config, dict):
|
||||
config = DockConfig(**config)
|
||||
self.config = config
|
||||
super().__init__(client=client, config=config, gui_id=gui_id)
|
||||
Dock.__init__(self, name=name, **kwargs)
|
||||
|
||||
self.parent_dock_area = parent_dock_area
|
||||
|
||||
# Layout Manager
|
||||
self.layout_manager = GridLayoutManager(self.layout)
|
||||
|
||||
def dropEvent(self, event):
|
||||
source = event.source()
|
||||
old_area = source.area
|
||||
self.setOrientation("horizontal", force=True)
|
||||
super().dropEvent(event)
|
||||
if old_area in self.parent_dock_area.tempAreas and old_area != self.parent_dock_area:
|
||||
self.parent_dock_area.removeTempArea(old_area)
|
||||
|
||||
def float(self):
|
||||
"""
|
||||
Float the dock.
|
||||
Overwrites the default pyqtgraph dock float.
|
||||
"""
|
||||
|
||||
# need to check if the dock is temporary and if it is the only dock in the area
|
||||
# fixes bug in pyqtgraph detaching
|
||||
if self.area.temporary == True and len(self.area.docks) <= 1:
|
||||
return
|
||||
elif self.area.temporary == True and len(self.area.docks) > 1:
|
||||
self.area.docks.pop(self.name(), None)
|
||||
super().float()
|
||||
else:
|
||||
super().float()
|
||||
|
||||
@property
|
||||
def widget_list(self) -> list[BECConnector]:
|
||||
"""
|
||||
Get the widgets in the dock.
|
||||
|
||||
Returns:
|
||||
widgets(list): The widgets in the dock.
|
||||
"""
|
||||
return self.widgets
|
||||
|
||||
@widget_list.setter
|
||||
def widget_list(self, value: list[BECConnector]):
|
||||
self.widgets = value
|
||||
|
||||
def hide_title_bar(self):
|
||||
"""
|
||||
Hide the title bar of the dock.
|
||||
"""
|
||||
# self.hideTitleBar() #TODO pyqtgraph looks bugged ATM, doing my implementation
|
||||
self.label.hide()
|
||||
self.labelHidden = True
|
||||
|
||||
def show_title_bar(self):
|
||||
"""
|
||||
Hide the title bar of the dock.
|
||||
"""
|
||||
# self.showTitleBar() #TODO pyqtgraph looks bugged ATM, doing my implementation
|
||||
self.label.show()
|
||||
self.labelHidden = False
|
||||
|
||||
def set_title(self, title: str):
|
||||
"""
|
||||
Set the title of the dock.
|
||||
|
||||
Args:
|
||||
title(str): The title of the dock.
|
||||
"""
|
||||
self.parent_dock_area.docks[title] = self.parent_dock_area.docks.pop(self.name())
|
||||
self.setTitle(title)
|
||||
self._name = title
|
||||
|
||||
def get_widgets_positions(self) -> dict:
|
||||
"""
|
||||
Get the positions of the widgets in the dock.
|
||||
|
||||
Returns:
|
||||
dict: The positions of the widgets in the dock as dict -> {(row, col, rowspan, colspan):widget}
|
||||
"""
|
||||
return self.layout_manager.get_widgets_positions()
|
||||
|
||||
def list_eligible_widgets(
|
||||
self,
|
||||
) -> list: # TODO can be moved to some util mixin like container class for rpc widgets
|
||||
"""
|
||||
List all widgets that can be added to the dock.
|
||||
|
||||
Returns:
|
||||
list: The list of eligible widgets.
|
||||
"""
|
||||
return list(RPCWidgetHandler.widget_classes.keys())
|
||||
|
||||
# def add_widget_bec(
|
||||
# self,
|
||||
# widget_type: str,
|
||||
# row=None,
|
||||
# col=0,
|
||||
# rowspan=1,
|
||||
# colspan=1,
|
||||
# shift: Literal["down", "up", "left", "right"] = "down",
|
||||
# ):
|
||||
# """
|
||||
# Add a widget to the dock.
|
||||
#
|
||||
# Args:
|
||||
# widget_type(str): The widget to add. Only BEC RPC widgets from RPCWidgetHandler are allowed.
|
||||
# row(int): The row to add the widget to. If None, the widget will be added to the next available row.
|
||||
# col(int): The column to add the widget to.
|
||||
# rowspan(int): The number of rows the widget should span.
|
||||
# colspan(int): The number of columns the widget should span.
|
||||
# shift(Literal["down", "up", "left", "right"]): The direction to shift the widgets if the position is occupied.
|
||||
# """
|
||||
# if row is None:
|
||||
# row = self.layout.rowCount()
|
||||
#
|
||||
# if self.layout_manager.is_position_occupied(row, col):
|
||||
# self.layout_manager.shift_widgets(shift, start_row=row)
|
||||
#
|
||||
# widget = RPCWidgetHandler.create_widget(widget_type)
|
||||
# self.addWidget(widget, row=row, col=col, rowspan=rowspan, colspan=colspan)
|
||||
# self.config.widgets[widget.gui_id] = widget.config
|
||||
#
|
||||
# return widget
|
||||
|
||||
def add_widget(
|
||||
self,
|
||||
widget: BECConnector | str,
|
||||
row=None,
|
||||
col=0,
|
||||
rowspan=1,
|
||||
colspan=1,
|
||||
shift: Literal["down", "up", "left", "right"] = "down",
|
||||
) -> BECConnector:
|
||||
"""
|
||||
Add a widget to the dock.
|
||||
|
||||
Args:
|
||||
widget(QWidget): The widget to add.
|
||||
row(int): The row to add the widget to. If None, the widget will be added to the next available row.
|
||||
col(int): The column to add the widget to.
|
||||
rowspan(int): The number of rows the widget should span.
|
||||
colspan(int): The number of columns the widget should span.
|
||||
shift(Literal["down", "up", "left", "right"]): The direction to shift the widgets if the position is occupied.
|
||||
"""
|
||||
if row is None:
|
||||
row = self.layout.rowCount()
|
||||
|
||||
if self.layout_manager.is_position_occupied(row, col):
|
||||
self.layout_manager.shift_widgets(shift, start_row=row)
|
||||
|
||||
if isinstance(widget, str):
|
||||
widget = RPCWidgetHandler.create_widget(widget)
|
||||
else:
|
||||
widget = widget
|
||||
|
||||
self.addWidget(widget, row=row, col=col, rowspan=rowspan, colspan=colspan)
|
||||
|
||||
if hasattr(widget, "config"):
|
||||
self.config.widgets[widget.gui_id] = widget.config
|
||||
|
||||
return widget
|
||||
|
||||
def move_widget(self, widget: QWidget, new_row: int, new_col: int):
|
||||
"""
|
||||
Move a widget to a new position in the layout.
|
||||
|
||||
Args:
|
||||
widget(QWidget): The widget to move.
|
||||
new_row(int): The new row to move the widget to.
|
||||
new_col(int): The new column to move the widget to.
|
||||
"""
|
||||
self.layout_manager.move_widget(widget, new_row, new_col)
|
||||
|
||||
def attach(self):
|
||||
"""
|
||||
Attach the dock to the parent dock area.
|
||||
"""
|
||||
self.parent_dock_area.removeTempArea(self.area)
|
||||
|
||||
def detach(self):
|
||||
"""
|
||||
Detach the dock from the parent dock area.
|
||||
"""
|
||||
self.float()
|
||||
|
||||
def remove_widget(self, widget_rpc_id: str):
|
||||
"""
|
||||
Remove a widget from the dock.
|
||||
|
||||
Args:
|
||||
widget_rpc_id(str): The ID of the widget to remove.
|
||||
"""
|
||||
widget = self.rpc_register.get_rpc_by_id(widget_rpc_id)
|
||||
self.layout.removeWidget(widget)
|
||||
self.config.widgets.pop(widget_rpc_id, None)
|
||||
widget.close()
|
||||
|
||||
def remove(self):
|
||||
"""
|
||||
Remove the dock from the parent dock area.
|
||||
"""
|
||||
# self.cleanup()
|
||||
self.parent_dock_area.remove_dock(self.name())
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
Clean up the dock, including all its widgets.
|
||||
"""
|
||||
for widget in self.widgets:
|
||||
if hasattr(widget, "cleanup"):
|
||||
widget.cleanup()
|
||||
super().cleanup()
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Close the dock area and cleanup.
|
||||
Has to be implemented to overwrite pyqtgraph event accept in Container close.
|
||||
"""
|
||||
self.cleanup()
|
||||
super().close()
|
||||
@@ -1,240 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal, Optional
|
||||
from weakref import WeakValueDictionary
|
||||
|
||||
from pydantic import Field
|
||||
from pyqtgraph.dockarea.DockArea import DockArea
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtGui import QPainter, QPaintEvent
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils import BECConnector, ConnectionConfig, WidgetContainerUtils
|
||||
|
||||
from .dock import BECDock, DockConfig
|
||||
|
||||
|
||||
class DockAreaConfig(ConnectionConfig):
|
||||
docks: dict[str, DockConfig] = Field({}, description="The docks in the dock area.")
|
||||
docks_state: Optional[dict] = Field(
|
||||
None, description="The state of the docks in the dock area."
|
||||
)
|
||||
|
||||
|
||||
class BECDockArea(BECConnector, DockArea):
|
||||
USER_ACCESS = [
|
||||
"config_dict",
|
||||
"panels",
|
||||
"save_state",
|
||||
"remove_dock",
|
||||
"restore_state",
|
||||
"add_dock",
|
||||
"clear_all",
|
||||
"detach_dock",
|
||||
"attach_all",
|
||||
"get_all_rpc",
|
||||
"temp_areas",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: QWidget | None = None,
|
||||
config: DockAreaConfig | None = None,
|
||||
client=None,
|
||||
gui_id: str = None,
|
||||
) -> None:
|
||||
if config is None:
|
||||
config = DockAreaConfig(widget_class=self.__class__.__name__)
|
||||
else:
|
||||
if isinstance(config, dict):
|
||||
config = DockAreaConfig(**config)
|
||||
self.config = config
|
||||
super().__init__(client=client, config=config, gui_id=gui_id)
|
||||
DockArea.__init__(self, parent=parent)
|
||||
|
||||
self._instructions_visible = True
|
||||
|
||||
def paintEvent(self, event: QPaintEvent):
|
||||
super().paintEvent(event)
|
||||
if self._instructions_visible:
|
||||
painter = QPainter(self)
|
||||
painter.drawText(self.rect(), Qt.AlignCenter, "Add docks using 'add_dock' method")
|
||||
|
||||
@property
|
||||
def panels(self) -> dict[str, BECDock]:
|
||||
"""
|
||||
Get the docks in the dock area.
|
||||
Returns:
|
||||
dock_dict(dict): The docks in the dock area.
|
||||
"""
|
||||
return dict(self.docks)
|
||||
|
||||
@panels.setter
|
||||
def panels(self, value: dict[str, BECDock]):
|
||||
self.docks = WeakValueDictionary(value)
|
||||
|
||||
@property
|
||||
def temp_areas(self) -> list:
|
||||
"""
|
||||
Get the temporary areas in the dock area.
|
||||
|
||||
Returns:
|
||||
list: The temporary areas in the dock area.
|
||||
"""
|
||||
return list(map(str, self.tempAreas))
|
||||
|
||||
@temp_areas.setter
|
||||
def temp_areas(self, value: list):
|
||||
self.tempAreas = list(map(str, value))
|
||||
|
||||
def restore_state(
|
||||
self, state: dict = None, missing: Literal["ignore", "error"] = "ignore", extra="bottom"
|
||||
):
|
||||
"""
|
||||
Restore the state of the dock area. If no state is provided, the last state is restored.
|
||||
|
||||
Args:
|
||||
state(dict): The state to restore.
|
||||
missing(Literal['ignore','error']): What to do if a dock is missing.
|
||||
extra(str): Extra docks that are in the dockarea but that are not mentioned in state will be added to the bottom of the dockarea, unless otherwise specified by the extra argument.
|
||||
"""
|
||||
if state is None:
|
||||
state = self.config.docks_state
|
||||
self.restoreState(state, missing=missing, extra=extra)
|
||||
|
||||
def save_state(self) -> dict:
|
||||
"""
|
||||
Save the state of the dock area.
|
||||
|
||||
Returns:
|
||||
dict: The state of the dock area.
|
||||
"""
|
||||
last_state = self.saveState()
|
||||
self.config.docks_state = last_state
|
||||
return last_state
|
||||
|
||||
def remove_dock(self, name: str):
|
||||
"""
|
||||
Remove a dock by name and ensure it is properly closed and cleaned up.
|
||||
|
||||
Args:
|
||||
name(str): The name of the dock to remove.
|
||||
"""
|
||||
dock = self.docks.pop(name, None)
|
||||
self.config.docks.pop(name, None)
|
||||
if dock:
|
||||
dock.close()
|
||||
if len(self.docks) <= 1:
|
||||
for dock in self.docks.values():
|
||||
dock.hide_title_bar()
|
||||
|
||||
else:
|
||||
raise ValueError(f"Dock with name {name} does not exist.")
|
||||
|
||||
def add_dock(
|
||||
self,
|
||||
name: str = None,
|
||||
position: Literal["bottom", "top", "left", "right", "above", "below"] = None,
|
||||
relative_to: BECDock | None = None,
|
||||
closable: bool = False,
|
||||
prefix: str = "dock",
|
||||
widget: str | QWidget | None = None,
|
||||
row: int = None,
|
||||
col: int = 0,
|
||||
rowspan: int = 1,
|
||||
colspan: int = 1,
|
||||
) -> BECDock:
|
||||
"""
|
||||
Add a dock to the dock area. Dock has QGridLayout as layout manager by default.
|
||||
|
||||
Args:
|
||||
name(str): The name of the dock to be displayed and for further references. Has to be unique.
|
||||
position(Literal["bottom", "top", "left", "right", "above", "below"]): The position of the dock.
|
||||
relative_to(BECDock): The dock to which the new dock should be added relative to.
|
||||
closable(bool): Whether the dock is closable.
|
||||
prefix(str): The prefix for the dock name if no name is provided.
|
||||
widget(str|QWidget|None): The widget to be added to the dock. While using RPC, only BEC RPC widgets from RPCWidgetHandler are allowed.
|
||||
row(int): The row of the added widget.
|
||||
col(int): The column of the added widget.
|
||||
rowspan(int): The rowspan of the added widget.
|
||||
colspan(int): The colspan of the added widget.
|
||||
|
||||
Returns:
|
||||
BECDock: The created dock.
|
||||
"""
|
||||
if name is None:
|
||||
name = WidgetContainerUtils.generate_unique_widget_id(
|
||||
container=self.docks, prefix=prefix
|
||||
)
|
||||
|
||||
if name in set(self.docks.keys()):
|
||||
raise ValueError(f"Dock with name {name} already exists.")
|
||||
|
||||
if position is None:
|
||||
position = "bottom"
|
||||
|
||||
dock = BECDock(name=name, parent_dock_area=self, closable=closable)
|
||||
dock.config.position = position
|
||||
self.config.docks[name] = dock.config
|
||||
|
||||
self.addDock(dock=dock, position=position, relativeTo=relative_to)
|
||||
|
||||
if len(self.docks) <= 1:
|
||||
dock.hide_title_bar()
|
||||
elif len(self.docks) > 1:
|
||||
for dock in self.docks.values():
|
||||
dock.show_title_bar()
|
||||
|
||||
if widget is not None and isinstance(widget, str):
|
||||
dock.add_widget(widget=widget, row=row, col=col, rowspan=rowspan, colspan=colspan)
|
||||
elif widget is not None and isinstance(widget, QWidget):
|
||||
dock.addWidget(widget, row=row, col=col, rowspan=rowspan, colspan=colspan)
|
||||
if self._instructions_visible:
|
||||
self._instructions_visible = False
|
||||
self.update()
|
||||
return dock
|
||||
|
||||
def detach_dock(self, dock_name: str) -> BECDock:
|
||||
"""
|
||||
Undock a dock from the dock area.
|
||||
|
||||
Args:
|
||||
dock_name(str): The dock to undock.
|
||||
|
||||
Returns:
|
||||
BECDock: The undocked dock.
|
||||
"""
|
||||
dock = self.docks[dock_name]
|
||||
dock.detach()
|
||||
return dock
|
||||
|
||||
def attach_all(self):
|
||||
"""
|
||||
Return all floating docks to the dock area.
|
||||
"""
|
||||
while self.tempAreas:
|
||||
for temp_area in self.tempAreas:
|
||||
self.removeTempArea(temp_area)
|
||||
|
||||
def clear_all(self):
|
||||
"""
|
||||
Close all docks and remove all temp areas.
|
||||
"""
|
||||
self.attach_all()
|
||||
for dock in dict(self.docks).values():
|
||||
dock.remove()
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
Cleanup the dock area.
|
||||
"""
|
||||
self.clear_all()
|
||||
super().cleanup()
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Close the dock area and cleanup.
|
||||
Has to be implemented to overwrite pyqtgraph event accept in Container close.
|
||||
"""
|
||||
self.cleanup()
|
||||
super().close()
|
||||
@@ -1 +0,0 @@
|
||||
from .figure import BECFigure, FigureConfig
|
||||
@@ -1,866 +0,0 @@
|
||||
# pylint: disable = no-name-in-module,missing-module-docstring
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from collections import defaultdict
|
||||
from typing import Literal, Optional
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
import qdarktheme
|
||||
from pydantic import Field
|
||||
from qtpy.QtCore import Signal as pyqtSignal
|
||||
from qtpy.QtWidgets import QWidget
|
||||
from typeguard import typechecked
|
||||
|
||||
from bec_widgets.utils import BECConnector, ConnectionConfig, WidgetContainerUtils
|
||||
from bec_widgets.widgets.figure.plots.image.image import BECImageShow, ImageConfig
|
||||
from bec_widgets.widgets.figure.plots.motor_map.motor_map import BECMotorMap, MotorMapConfig
|
||||
from bec_widgets.widgets.figure.plots.plot_base import BECPlotBase, SubplotConfig
|
||||
from bec_widgets.widgets.figure.plots.waveform.waveform import BECWaveform, Waveform1DConfig
|
||||
|
||||
|
||||
class FigureConfig(ConnectionConfig):
|
||||
"""Configuration for BECFigure. Inheriting from ConnectionConfig widget_class and gui_id"""
|
||||
|
||||
theme: Literal["dark", "light"] = Field("dark", description="The theme of the figure widget.")
|
||||
num_cols: int = Field(1, description="The number of columns in the figure widget.")
|
||||
num_rows: int = Field(1, description="The number of rows in the figure widget.")
|
||||
widgets: dict[str, Waveform1DConfig | ImageConfig | MotorMapConfig | SubplotConfig] = Field(
|
||||
{}, description="The list of widgets to be added to the figure widget."
|
||||
)
|
||||
|
||||
|
||||
class WidgetHandler:
|
||||
"""Factory for creating and configuring BEC widgets for BECFigure."""
|
||||
|
||||
def __init__(self):
|
||||
self.widget_factory = {
|
||||
"PlotBase": (BECPlotBase, SubplotConfig),
|
||||
"Waveform1D": (BECWaveform, Waveform1DConfig),
|
||||
"ImShow": (BECImageShow, ImageConfig),
|
||||
"MotorMap": (BECMotorMap, MotorMapConfig),
|
||||
}
|
||||
|
||||
def create_widget(
|
||||
self,
|
||||
widget_type: str,
|
||||
widget_id: str,
|
||||
parent_figure,
|
||||
parent_id: str,
|
||||
config: dict = None,
|
||||
**axis_kwargs,
|
||||
) -> BECPlotBase:
|
||||
"""
|
||||
Create and configure a widget based on its type.
|
||||
|
||||
Args:
|
||||
widget_type (str): The type of the widget to create.
|
||||
widget_id (str): Unique identifier for the widget.
|
||||
parent_id (str): Identifier of the parent figure.
|
||||
config (dict, optional): Additional configuration for the widget.
|
||||
**axis_kwargs: Additional axis properties to set on the widget after creation.
|
||||
|
||||
Returns:
|
||||
BECPlotBase: The created and configured widget instance.
|
||||
"""
|
||||
entry = self.widget_factory.get(widget_type)
|
||||
if not entry:
|
||||
raise ValueError(f"Unsupported widget type: {widget_type}")
|
||||
|
||||
widget_class, config_class = entry
|
||||
if config is not None and isinstance(config, config_class):
|
||||
config = config.model_dump()
|
||||
widget_config_dict = {
|
||||
"widget_class": widget_class.__name__,
|
||||
"parent_id": parent_id,
|
||||
"gui_id": widget_id,
|
||||
**(config if config is not None else {}),
|
||||
}
|
||||
widget_config = config_class(**widget_config_dict)
|
||||
widget = widget_class(
|
||||
config=widget_config, parent_figure=parent_figure, client=parent_figure.client
|
||||
)
|
||||
|
||||
if axis_kwargs:
|
||||
widget.set(**axis_kwargs)
|
||||
|
||||
return widget
|
||||
|
||||
|
||||
class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
||||
USER_ACCESS = [
|
||||
"rpc_id",
|
||||
"config_dict",
|
||||
"axes",
|
||||
"widgets",
|
||||
"add_plot",
|
||||
"add_image",
|
||||
"add_motor_map",
|
||||
"plot",
|
||||
"image",
|
||||
"motor_map",
|
||||
"remove",
|
||||
"change_layout",
|
||||
"change_theme",
|
||||
"clear_all",
|
||||
"get_all_rpc",
|
||||
"widget_list",
|
||||
]
|
||||
|
||||
clean_signal = pyqtSignal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: Optional[QWidget] = None,
|
||||
config: Optional[FigureConfig] = None,
|
||||
client=None,
|
||||
gui_id: Optional[str] = None,
|
||||
) -> None:
|
||||
if config is None:
|
||||
config = FigureConfig(widget_class=self.__class__.__name__)
|
||||
else:
|
||||
if isinstance(config, dict):
|
||||
config = FigureConfig(**config)
|
||||
self.config = config
|
||||
super().__init__(client=client, config=config, gui_id=gui_id)
|
||||
pg.GraphicsLayoutWidget.__init__(self, parent)
|
||||
|
||||
self.widget_handler = WidgetHandler()
|
||||
|
||||
# Widget container to reference widgets by 'widget_id'
|
||||
self._widgets = defaultdict(dict)
|
||||
|
||||
# Container to keep track of the grid
|
||||
self.grid = []
|
||||
|
||||
def __getitem__(self, key: tuple | str):
|
||||
if isinstance(key, tuple) and len(key) == 2:
|
||||
return self.axes(*key)
|
||||
elif isinstance(key, str):
|
||||
widget = self._widgets.get(key)
|
||||
if widget is None:
|
||||
raise KeyError(f"No widget with ID {key}")
|
||||
return self._widgets.get(key)
|
||||
else:
|
||||
raise TypeError(
|
||||
"Key must be a string (widget id) or a tuple of two integers (grid coordinates)"
|
||||
)
|
||||
|
||||
@property
|
||||
def widget_list(self) -> list[BECPlotBase]:
|
||||
"""
|
||||
Access all widget in BECFigure as a list
|
||||
Returns:
|
||||
list[BECPlotBase]: List of all widgets in the figure.
|
||||
"""
|
||||
axes = [value for value in self._widgets.values() if isinstance(value, BECPlotBase)]
|
||||
return axes
|
||||
|
||||
@widget_list.setter
|
||||
def widget_list(self, value: list[BECPlotBase]):
|
||||
"""
|
||||
Access all widget in BECFigure as a list
|
||||
Returns:
|
||||
list[BECPlotBase]: List of all widgets in the figure.
|
||||
"""
|
||||
self._axes = value
|
||||
|
||||
@property
|
||||
def widgets(self) -> dict:
|
||||
"""
|
||||
All widgets within the figure with gui ids as keys.
|
||||
Returns:
|
||||
dict: All widgets within the figure.
|
||||
"""
|
||||
return self._widgets
|
||||
|
||||
@widgets.setter
|
||||
def widgets(self, value: dict):
|
||||
"""
|
||||
All widgets within the figure with gui ids as keys.
|
||||
Returns:
|
||||
dict: All widgets within the figure.
|
||||
"""
|
||||
self._widgets = value
|
||||
|
||||
def _init_waveform(
|
||||
self,
|
||||
waveform,
|
||||
x_name: str = None,
|
||||
y_name: str = None,
|
||||
z_name: str = None,
|
||||
x_entry: str = None,
|
||||
y_entry: str = None,
|
||||
z_entry: str = None,
|
||||
x: list | np.ndarray = None,
|
||||
y: list | np.ndarray = None,
|
||||
color: Optional[str] = None,
|
||||
color_map_z: Optional[str] = "plasma",
|
||||
label: Optional[str] = None,
|
||||
validate: bool = True,
|
||||
):
|
||||
"""
|
||||
Configure the waveform based on the provided parameters.
|
||||
|
||||
Args:
|
||||
waveform (BECWaveform): The waveform to configure.
|
||||
x (list | np.ndarray): Custom x data to plot.
|
||||
y (list | np.ndarray): Custom y data to plot.
|
||||
x_name (str): The name of the device for the x-axis.
|
||||
y_name (str): The name of the device for the y-axis.
|
||||
z_name (str): The name of the device for the z-axis.
|
||||
x_entry (str): The name of the entry for the x-axis.
|
||||
y_entry (str): The name of the entry for the y-axis.
|
||||
z_entry (str): The name of the entry for the z-axis.
|
||||
color (str): The color of the curve.
|
||||
color_map_z (str): The color map to use for the z-axis.
|
||||
label (str): The label of the curve.
|
||||
validate (bool): If True, validate the device names and entries.
|
||||
"""
|
||||
if x is not None and y is None:
|
||||
if isinstance(x, np.ndarray):
|
||||
if x.ndim == 1:
|
||||
y = np.arange(x.size)
|
||||
waveform.add_curve_custom(x=np.arange(x.size), y=x, color=color, label=label)
|
||||
return waveform
|
||||
if x.ndim == 2:
|
||||
waveform.add_curve_custom(x=x[:, 0], y=x[:, 1], color=color, label=label)
|
||||
return waveform
|
||||
elif isinstance(x, list):
|
||||
y = np.arange(len(x))
|
||||
waveform.add_curve_custom(x=np.arange(len(x)), y=x, color=color, label=label)
|
||||
return waveform
|
||||
else:
|
||||
raise ValueError(
|
||||
"Invalid input. Provide either device names (x_name, y_name) or custom data."
|
||||
)
|
||||
if x is not None and y is not None:
|
||||
waveform.add_curve_custom(x=x, y=y, color=color, label=label)
|
||||
return waveform
|
||||
# User wants to add scan curve -> 1D Waveform
|
||||
if x_name is not None and y_name is not None and z_name is None and x is None and y is None:
|
||||
waveform.add_curve_scan(
|
||||
x_name=x_name,
|
||||
y_name=y_name,
|
||||
x_entry=x_entry,
|
||||
y_entry=y_entry,
|
||||
validate=validate,
|
||||
color=color,
|
||||
label=label,
|
||||
)
|
||||
# User wants to add scan curve -> 2D Waveform Scatter
|
||||
if (
|
||||
x_name is not None
|
||||
and y_name is not None
|
||||
and z_name is not None
|
||||
and x is None
|
||||
and y is None
|
||||
):
|
||||
waveform.add_curve_scan(
|
||||
x_name=x_name,
|
||||
y_name=y_name,
|
||||
z_name=z_name,
|
||||
x_entry=x_entry,
|
||||
y_entry=y_entry,
|
||||
z_entry=z_entry,
|
||||
color=color,
|
||||
color_map_z=color_map_z,
|
||||
label=label,
|
||||
validate=validate,
|
||||
)
|
||||
# User wants to add custom curve
|
||||
elif x is not None and y is not None and x_name is None and y_name is None:
|
||||
waveform.add_curve_custom(x=x, y=y, color=color, label=label)
|
||||
|
||||
return waveform
|
||||
|
||||
def add_plot(
|
||||
self,
|
||||
x: list | np.ndarray = None,
|
||||
y: list | np.ndarray = None,
|
||||
x_name: str = None,
|
||||
y_name: str = None,
|
||||
z_name: str = None,
|
||||
x_entry: str = None,
|
||||
y_entry: str = None,
|
||||
z_entry: str = None,
|
||||
color: Optional[str] = None,
|
||||
color_map_z: Optional[str] = "plasma",
|
||||
label: Optional[str] = None,
|
||||
validate: bool = True,
|
||||
row: int = None,
|
||||
col: int = None,
|
||||
config=None,
|
||||
**axis_kwargs,
|
||||
) -> BECWaveform:
|
||||
"""
|
||||
Add a Waveform1D plot to the figure at the specified position.
|
||||
|
||||
Args:
|
||||
x(list | np.ndarray): Custom x data to plot.
|
||||
y(list | np.ndarray): Custom y data to plot.
|
||||
x_name(str): The name of the device for the x-axis.
|
||||
y_name(str): The name of the device for the y-axis.
|
||||
z_name(str): The name of the device for the z-axis.
|
||||
x_entry(str): The name of the entry for the x-axis.
|
||||
y_entry(str): The name of the entry for the y-axis.
|
||||
z_entry(str): The name of the entry for the z-axis.
|
||||
color(str): The color of the curve.
|
||||
color_map_z(str): The color map to use for the z-axis.
|
||||
label(str): The label of the curve.
|
||||
validate(bool): If True, validate the device names and entries.
|
||||
row(int): The row coordinate of the widget in the figure. If not provided, the next empty row will be used.
|
||||
col(int): The column coordinate of the widget in the figure. If not provided, the next empty column will be used.
|
||||
config(dict): Additional configuration for the widget.
|
||||
**axis_kwargs(dict): Additional axis properties to set on the widget after creation.
|
||||
"""
|
||||
widget_id = str(uuid.uuid4())
|
||||
waveform = self.add_widget(
|
||||
widget_type="Waveform1D",
|
||||
widget_id=widget_id,
|
||||
row=row,
|
||||
col=col,
|
||||
config=config,
|
||||
**axis_kwargs,
|
||||
)
|
||||
|
||||
waveform = self._init_waveform(
|
||||
waveform=waveform,
|
||||
x=x,
|
||||
y=y,
|
||||
x_name=x_name,
|
||||
y_name=y_name,
|
||||
z_name=z_name,
|
||||
x_entry=x_entry,
|
||||
y_entry=y_entry,
|
||||
z_entry=z_entry,
|
||||
color=color,
|
||||
color_map_z=color_map_z,
|
||||
label=label,
|
||||
validate=validate,
|
||||
)
|
||||
return waveform
|
||||
|
||||
@typechecked
|
||||
def plot(
|
||||
self,
|
||||
x: list | np.ndarray | None = None,
|
||||
y: list | np.ndarray | None = None,
|
||||
x_name: str | None = None,
|
||||
y_name: str | None = None,
|
||||
z_name: str | None = None,
|
||||
x_entry: str | None = None,
|
||||
y_entry: str | None = None,
|
||||
z_entry: str | None = None,
|
||||
color: str | None = None,
|
||||
color_map_z: str | None = "plasma",
|
||||
label: str | None = None,
|
||||
validate: bool = True,
|
||||
**axis_kwargs,
|
||||
) -> BECWaveform:
|
||||
"""
|
||||
Add a 1D waveform plot to the figure. Always access the first waveform widget in the figure.
|
||||
|
||||
Args:
|
||||
x(list | np.ndarray): Custom x data to plot.
|
||||
y(list | np.ndarray): Custom y data to plot.
|
||||
x_name(str): The name of the device for the x-axis.
|
||||
y_name(str): The name of the device for the y-axis.
|
||||
z_name(str): The name of the device for the z-axis.
|
||||
x_entry(str): The name of the entry for the x-axis.
|
||||
y_entry(str): The name of the entry for the y-axis.
|
||||
z_entry(str): The name of the entry for the z-axis.
|
||||
color(str): The color of the curve.
|
||||
color_map_z(str): The color map to use for the z-axis.
|
||||
label(str): The label of the curve.
|
||||
validate(bool): If True, validate the device names and entries.
|
||||
**axis_kwargs: Additional axis properties to set on the widget after creation.
|
||||
|
||||
Returns:
|
||||
BECWaveform: The waveform plot widget.
|
||||
"""
|
||||
waveform = WidgetContainerUtils.find_first_widget_by_class(
|
||||
self._widgets, BECWaveform, can_fail=True
|
||||
)
|
||||
if waveform is not None:
|
||||
if axis_kwargs:
|
||||
waveform.set(**axis_kwargs)
|
||||
else:
|
||||
waveform = self.add_plot(**axis_kwargs)
|
||||
|
||||
waveform = self._init_waveform(
|
||||
waveform=waveform,
|
||||
x=x,
|
||||
y=y,
|
||||
x_name=x_name,
|
||||
y_name=y_name,
|
||||
z_name=z_name,
|
||||
x_entry=x_entry,
|
||||
y_entry=y_entry,
|
||||
z_entry=z_entry,
|
||||
color=color,
|
||||
color_map_z=color_map_z,
|
||||
label=label,
|
||||
validate=validate,
|
||||
)
|
||||
# TODO remove repetition from .plot method
|
||||
return waveform
|
||||
|
||||
def _init_image(
|
||||
self,
|
||||
image,
|
||||
monitor: str = None,
|
||||
color_bar: Literal["simple", "full"] = "full",
|
||||
color_map: str = "magma",
|
||||
data: np.ndarray = None,
|
||||
vrange: tuple[float, float] = None,
|
||||
) -> BECImageShow:
|
||||
"""
|
||||
Configure the image based on the provided parameters.
|
||||
|
||||
Args:
|
||||
image (BECImageShow): The image to configure.
|
||||
monitor (str): The name of the monitor to display.
|
||||
color_bar (Literal["simple","full"]): The type of color bar to display.
|
||||
color_map (str): The color map to use for the image.
|
||||
data (np.ndarray): Custom data to display.
|
||||
"""
|
||||
if monitor is not None and data is None:
|
||||
image.add_monitor_image(
|
||||
monitor=monitor, color_map=color_map, vrange=vrange, color_bar=color_bar
|
||||
)
|
||||
elif data is not None and monitor is None:
|
||||
image.add_custom_image(
|
||||
name="custom", data=data, color_map=color_map, vrange=vrange, color_bar=color_bar
|
||||
)
|
||||
elif data is None and monitor is None:
|
||||
# Setting appearance
|
||||
if vrange is not None:
|
||||
image.set_vrange(vmin=vrange[0], vmax=vrange[1])
|
||||
if color_map is not None:
|
||||
image.set_color_map(color_map)
|
||||
else:
|
||||
raise ValueError("Invalid input. Provide either monitor name or custom data.")
|
||||
return image
|
||||
|
||||
def image(
|
||||
self,
|
||||
monitor: str = None,
|
||||
color_bar: Literal["simple", "full"] = "full",
|
||||
color_map: str = "magma",
|
||||
data: np.ndarray = None,
|
||||
vrange: tuple[float, float] = None,
|
||||
**axis_kwargs,
|
||||
) -> BECImageShow:
|
||||
"""
|
||||
Add an image to the figure. Always access the first image widget in the figure.
|
||||
|
||||
Args:
|
||||
monitor(str): The name of the monitor to display.
|
||||
color_bar(Literal["simple","full"]): The type of color bar to display.
|
||||
color_map(str): The color map to use for the image.
|
||||
data(np.ndarray): Custom data to display.
|
||||
vrange(tuple[float, float]): The range of values to display.
|
||||
**axis_kwargs: Additional axis properties to set on the widget after creation.
|
||||
|
||||
Returns:
|
||||
BECImageShow: The image widget.
|
||||
"""
|
||||
image = WidgetContainerUtils.find_first_widget_by_class(
|
||||
self._widgets, BECImageShow, can_fail=True
|
||||
)
|
||||
if image is not None:
|
||||
if axis_kwargs:
|
||||
image.set(**axis_kwargs)
|
||||
else:
|
||||
image = self.add_image(color_bar=color_bar, **axis_kwargs)
|
||||
|
||||
image = self._init_image(
|
||||
image=image,
|
||||
monitor=monitor,
|
||||
color_bar=color_bar,
|
||||
color_map=color_map,
|
||||
data=data,
|
||||
vrange=vrange,
|
||||
)
|
||||
return image
|
||||
|
||||
def add_image(
|
||||
self,
|
||||
monitor: str = None,
|
||||
color_bar: Literal["simple", "full"] = "full",
|
||||
color_map: str = "magma",
|
||||
data: np.ndarray = None,
|
||||
vrange: tuple[float, float] = None,
|
||||
row: int = None,
|
||||
col: int = None,
|
||||
config=None,
|
||||
**axis_kwargs,
|
||||
) -> BECImageShow:
|
||||
"""
|
||||
Add an image to the figure at the specified position.
|
||||
|
||||
Args:
|
||||
monitor(str): The name of the monitor to display.
|
||||
color_bar(Literal["simple","full"]): The type of color bar to display.
|
||||
color_map(str): The color map to use for the image.
|
||||
data(np.ndarray): Custom data to display.
|
||||
vrange(tuple[float, float]): The range of values to display.
|
||||
row(int): The row coordinate of the widget in the figure. If not provided, the next empty row will be used.
|
||||
col(int): The column coordinate of the widget in the figure. If not provided, the next empty column will be used.
|
||||
config(dict): Additional configuration for the widget.
|
||||
**axis_kwargs: Additional axis properties to set on the widget after creation.
|
||||
|
||||
Returns:
|
||||
BECImageShow: The image widget.
|
||||
"""
|
||||
|
||||
widget_id = str(uuid.uuid4())
|
||||
if config is None:
|
||||
config = ImageConfig(
|
||||
widget_class="BECImageShow",
|
||||
gui_id=widget_id,
|
||||
parent_id=self.gui_id,
|
||||
color_map=color_map,
|
||||
color_bar=color_bar,
|
||||
vrange=vrange,
|
||||
)
|
||||
image = self.add_widget(
|
||||
widget_type="ImShow",
|
||||
widget_id=widget_id,
|
||||
row=row,
|
||||
col=col,
|
||||
config=config,
|
||||
**axis_kwargs,
|
||||
)
|
||||
image = self._init_image(
|
||||
image=image,
|
||||
monitor=monitor,
|
||||
color_bar=color_bar,
|
||||
color_map=color_map,
|
||||
data=data,
|
||||
vrange=vrange,
|
||||
)
|
||||
return image
|
||||
|
||||
def motor_map(self, motor_x: str = None, motor_y: str = None, **axis_kwargs) -> BECMotorMap:
|
||||
"""
|
||||
Add a motor map to the figure. Always access the first motor map widget in the figure.
|
||||
|
||||
Args:
|
||||
motor_x(str): The name of the motor for the X axis.
|
||||
motor_y(str): The name of the motor for the Y axis.
|
||||
**axis_kwargs: Additional axis properties to set on the widget after creation.
|
||||
|
||||
Returns:
|
||||
BECMotorMap: The motor map widget.
|
||||
"""
|
||||
motor_map = WidgetContainerUtils.find_first_widget_by_class(
|
||||
self._widgets, BECMotorMap, can_fail=True
|
||||
)
|
||||
if motor_map is not None:
|
||||
if axis_kwargs:
|
||||
motor_map.set(**axis_kwargs)
|
||||
else:
|
||||
motor_map = self.add_motor_map(**axis_kwargs)
|
||||
|
||||
if motor_x is not None and motor_y is not None:
|
||||
motor_map.change_motors(motor_x, motor_y)
|
||||
|
||||
return motor_map
|
||||
|
||||
def add_motor_map(
|
||||
self,
|
||||
motor_x: str = None,
|
||||
motor_y: str = None,
|
||||
row: int = None,
|
||||
col: int = None,
|
||||
config=None,
|
||||
**axis_kwargs,
|
||||
) -> BECMotorMap:
|
||||
"""
|
||||
|
||||
Args:
|
||||
motor_x(str): The name of the motor for the X axis.
|
||||
motor_y(str): The name of the motor for the Y axis.
|
||||
row(int): The row coordinate of the widget in the figure. If not provided, the next empty row will be used.
|
||||
col(int): The column coordinate of the widget in the figure. If not provided, the next empty column will be used.
|
||||
config(dict): Additional configuration for the widget.
|
||||
**axis_kwargs:
|
||||
|
||||
Returns:
|
||||
BECMotorMap: The motor map widget.
|
||||
"""
|
||||
widget_id = str(uuid.uuid4())
|
||||
if config is None:
|
||||
config = MotorMapConfig(
|
||||
widget_class="BECMotorMap", gui_id=widget_id, parent_id=self.gui_id
|
||||
)
|
||||
motor_map = self.add_widget(
|
||||
widget_type="MotorMap",
|
||||
widget_id=widget_id,
|
||||
row=row,
|
||||
col=col,
|
||||
config=config,
|
||||
**axis_kwargs,
|
||||
)
|
||||
|
||||
if motor_x is not None and motor_y is not None:
|
||||
motor_map.change_motors(motor_x, motor_y)
|
||||
|
||||
return motor_map
|
||||
|
||||
def add_widget(
|
||||
self,
|
||||
widget_type: Literal["PlotBase", "Waveform1D", "ImShow"] = "PlotBase",
|
||||
widget_id: str = None,
|
||||
row: int = None,
|
||||
col: int = None,
|
||||
config=None,
|
||||
**axis_kwargs,
|
||||
) -> BECPlotBase:
|
||||
"""
|
||||
Add a widget to the figure at the specified position.
|
||||
|
||||
Args:
|
||||
widget_type(Literal["PlotBase","Waveform1D"]): The type of the widget to add.
|
||||
widget_id(str): The unique identifier of the widget. If not provided, a unique ID will be generated.
|
||||
row(int): The row coordinate of the widget in the figure. If not provided, the next empty row will be used.
|
||||
col(int): The column coordinate of the widget in the figure. If not provided, the next empty column will be used.
|
||||
config(dict): Additional configuration for the widget.
|
||||
**axis_kwargs(dict): Additional axis properties to set on the widget after creation.
|
||||
"""
|
||||
if not widget_id:
|
||||
widget_id = str(uuid.uuid4())
|
||||
if widget_id in self._widgets:
|
||||
raise ValueError(f"Widget with ID '{widget_id}' already exists.")
|
||||
|
||||
widget = self.widget_handler.create_widget(
|
||||
widget_type=widget_type,
|
||||
widget_id=widget_id,
|
||||
parent_figure=self,
|
||||
parent_id=self.gui_id,
|
||||
config=config,
|
||||
**axis_kwargs,
|
||||
)
|
||||
|
||||
# Check if position is occupied
|
||||
if row is not None and col is not None:
|
||||
if self.getItem(row, col):
|
||||
raise ValueError(f"Position at row {row} and column {col} is already occupied.")
|
||||
else:
|
||||
widget.config.row = row
|
||||
widget.config.col = col
|
||||
|
||||
# Add widget to the figure
|
||||
self.addItem(widget, row=row, col=col)
|
||||
else:
|
||||
row, col = self._find_next_empty_position()
|
||||
widget.config.row = row
|
||||
widget.config.col = col
|
||||
|
||||
# Add widget to the figure
|
||||
self.addItem(widget, row=row, col=col)
|
||||
|
||||
# Update num_cols and num_rows based on the added widget
|
||||
self.config.num_rows = max(self.config.num_rows, row + 1)
|
||||
self.config.num_cols = max(self.config.num_cols, col + 1)
|
||||
|
||||
# Saving config for future referencing
|
||||
self.config.widgets[widget_id] = widget.config
|
||||
self._widgets[widget_id] = widget
|
||||
|
||||
# Reflect the grid coordinates
|
||||
self._change_grid(widget_id, row, col)
|
||||
|
||||
return widget
|
||||
|
||||
def remove(
|
||||
self,
|
||||
row: int = None,
|
||||
col: int = None,
|
||||
widget_id: str = None,
|
||||
coordinates: tuple[int, int] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Remove a widget from the figure. Can be removed by its unique identifier or by its coordinates.
|
||||
|
||||
Args:
|
||||
row(int): The row coordinate of the widget to remove.
|
||||
col(int): The column coordinate of the widget to remove.
|
||||
widget_id(str): The unique identifier of the widget to remove.
|
||||
coordinates(tuple[int, int], optional): The coordinates of the widget to remove.
|
||||
"""
|
||||
if widget_id:
|
||||
self._remove_by_id(widget_id)
|
||||
elif row is not None and col is not None:
|
||||
self._remove_by_coordinates(row, col)
|
||||
elif coordinates:
|
||||
self._remove_by_coordinates(*coordinates)
|
||||
else:
|
||||
raise ValueError("Must provide either widget_id or coordinates for removal.")
|
||||
|
||||
def change_theme(self, theme: Literal["dark", "light"]) -> None:
|
||||
"""
|
||||
Change the theme of the figure widget.
|
||||
|
||||
Args:
|
||||
theme(Literal["dark","light"]): The theme to set for the figure widget.
|
||||
"""
|
||||
qdarktheme.setup_theme(theme)
|
||||
self.setBackground("k" if theme == "dark" else "w")
|
||||
self.config.theme = theme
|
||||
|
||||
def _remove_by_coordinates(self, row: int, col: int) -> None:
|
||||
"""
|
||||
Remove a widget from the figure by its coordinates.
|
||||
|
||||
Args:
|
||||
row(int): The row coordinate of the widget to remove.
|
||||
col(int): The column coordinate of the widget to remove.
|
||||
"""
|
||||
widget = self.axes(row, col)
|
||||
if widget:
|
||||
widget_id = widget.config.gui_id
|
||||
if widget_id in self._widgets:
|
||||
self._remove_by_id(widget_id)
|
||||
|
||||
def _remove_by_id(self, widget_id: str) -> None:
|
||||
"""
|
||||
Remove a widget from the figure by its unique identifier.
|
||||
|
||||
Args:
|
||||
widget_id(str): The unique identifier of the widget to remove.
|
||||
"""
|
||||
if widget_id in self._widgets:
|
||||
widget = self._widgets.pop(widget_id)
|
||||
widget.cleanup()
|
||||
self.removeItem(widget)
|
||||
self.grid[widget.config.row][widget.config.col] = None
|
||||
self._reindex_grid()
|
||||
if widget_id in self.config.widgets:
|
||||
self.config.widgets.pop(widget_id)
|
||||
else:
|
||||
raise ValueError(f"Widget with ID '{widget_id}' does not exist.")
|
||||
|
||||
def axes(self, row: int, col: int) -> BECPlotBase:
|
||||
"""
|
||||
Get widget by its coordinates in the figure.
|
||||
|
||||
Args:
|
||||
row(int): the row coordinate
|
||||
col(int): the column coordinate
|
||||
|
||||
Returns:
|
||||
BECPlotBase: the widget at the given coordinates
|
||||
"""
|
||||
widget = self.getItem(row, col)
|
||||
if widget is None:
|
||||
raise ValueError(f"No widget at coordinates ({row}, {col})")
|
||||
return widget
|
||||
|
||||
def _find_next_empty_position(self):
|
||||
"""Find the next empty position (new row) in the figure."""
|
||||
row, col = 0, 0
|
||||
while self.getItem(row, col):
|
||||
row += 1
|
||||
return row, col
|
||||
|
||||
def _change_grid(self, widget_id: str, row: int, col: int):
|
||||
"""
|
||||
Change the grid to reflect the new position of the widget.
|
||||
|
||||
Args:
|
||||
widget_id(str): The unique identifier of the widget.
|
||||
row(int): The new row coordinate of the widget in the figure.
|
||||
col(int): The new column coordinate of the widget in the figure.
|
||||
"""
|
||||
while len(self.grid) <= row:
|
||||
self.grid.append([])
|
||||
row = self.grid[row]
|
||||
while len(row) <= col:
|
||||
row.append(None)
|
||||
row[col] = widget_id
|
||||
|
||||
def _reindex_grid(self):
|
||||
"""Reindex the grid to remove empty rows and columns."""
|
||||
new_grid = []
|
||||
for row in self.grid:
|
||||
new_row = [widget for widget in row if widget is not None]
|
||||
if new_row:
|
||||
new_grid.append(new_row)
|
||||
#
|
||||
# Update the config of each object to reflect its new position
|
||||
for row_idx, row in enumerate(new_grid):
|
||||
for col_idx, widget in enumerate(row):
|
||||
self._widgets[widget].config.row, self._widgets[widget].config.col = (
|
||||
row_idx,
|
||||
col_idx,
|
||||
)
|
||||
|
||||
self.grid = new_grid
|
||||
self._replot_layout()
|
||||
|
||||
def _replot_layout(self):
|
||||
"""Replot the layout based on the current grid configuration."""
|
||||
self.clear()
|
||||
for row_idx, row in enumerate(self.grid):
|
||||
for col_idx, widget in enumerate(row):
|
||||
self.addItem(self._widgets[widget], row=row_idx, col=col_idx)
|
||||
|
||||
def change_layout(self, max_columns=None, max_rows=None):
|
||||
"""
|
||||
Reshuffle the layout of the figure to adjust to a new number of max_columns or max_rows.
|
||||
If both max_columns and max_rows are provided, max_rows is ignored.
|
||||
|
||||
Args:
|
||||
max_columns (Optional[int]): The new maximum number of columns in the figure.
|
||||
max_rows (Optional[int]): The new maximum number of rows in the figure.
|
||||
"""
|
||||
# Calculate total number of widgets
|
||||
total_widgets = len(self._widgets)
|
||||
|
||||
if max_columns:
|
||||
# Calculate the required number of rows based on max_columns
|
||||
required_rows = (total_widgets + max_columns - 1) // max_columns
|
||||
new_grid = [[None for _ in range(max_columns)] for _ in range(required_rows)]
|
||||
elif max_rows:
|
||||
# Calculate the required number of columns based on max_rows
|
||||
required_columns = (total_widgets + max_rows - 1) // max_rows
|
||||
new_grid = [[None for _ in range(required_columns)] for _ in range(max_rows)]
|
||||
else:
|
||||
# If neither max_columns nor max_rows is specified, just return without changing the layout
|
||||
return
|
||||
|
||||
# Populate the new grid with widgets' IDs
|
||||
current_idx = 0
|
||||
for widget_id, widget in self._widgets.items():
|
||||
row = current_idx // len(new_grid[0])
|
||||
col = current_idx % len(new_grid[0])
|
||||
new_grid[row][col] = widget_id
|
||||
current_idx += 1
|
||||
|
||||
self.config.num_rows = row
|
||||
self.config.num_cols = col
|
||||
|
||||
# Update widgets' positions and replot them according to the new grid
|
||||
self.grid = new_grid
|
||||
self._reindex_grid() # This method should be updated to handle reshuffling correctly
|
||||
self._replot_layout() # Assumes this method re-adds widgets to the layout based on self.grid
|
||||
|
||||
def clear_all(self):
|
||||
"""Clear all widgets from the figure and reset to default state"""
|
||||
for widget in list(self._widgets.values()):
|
||||
widget.remove()
|
||||
# self.clear()
|
||||
self._widgets = defaultdict(dict)
|
||||
self.grid = []
|
||||
theme = self.config.theme
|
||||
self.config = FigureConfig(
|
||||
widget_class=self.__class__.__name__, gui_id=self.gui_id, theme=theme
|
||||
)
|
||||
|
||||
def cleanup(self):
|
||||
self.clear_all()
|
||||
super().cleanup()
|
||||
@@ -1,559 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from typing import Any, Literal, Optional
|
||||
|
||||
import numpy as np
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from pydantic import Field, ValidationError
|
||||
from qtpy.QtCore import QThread
|
||||
from qtpy.QtCore import Slot as pyqtSlot
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils import EntryValidator
|
||||
from bec_widgets.widgets.figure.plots.image.image_item import BECImageItem, ImageItemConfig
|
||||
from bec_widgets.widgets.figure.plots.image.image_processor import ImageProcessor, ProcessorWorker
|
||||
from bec_widgets.widgets.figure.plots.plot_base import BECPlotBase, SubplotConfig
|
||||
|
||||
|
||||
class ImageConfig(SubplotConfig):
|
||||
images: dict[str, ImageItemConfig] = Field(
|
||||
{},
|
||||
description="The configuration of the images. The key is the name of the image (source).",
|
||||
)
|
||||
|
||||
|
||||
class BECImageShow(BECPlotBase):
|
||||
USER_ACCESS = [
|
||||
"rpc_id",
|
||||
"config_dict",
|
||||
"add_image_by_config",
|
||||
"get_image_config",
|
||||
"get_image_dict",
|
||||
"add_monitor_image",
|
||||
"add_custom_image",
|
||||
"set_vrange",
|
||||
"set_color_map",
|
||||
"set_autorange",
|
||||
"set_monitor",
|
||||
"set_processing",
|
||||
"set_image_properties",
|
||||
"set_fft",
|
||||
"set_log",
|
||||
"set_rotation",
|
||||
"set_transpose",
|
||||
"toggle_threading",
|
||||
"set",
|
||||
"set_title",
|
||||
"set_x_label",
|
||||
"set_y_label",
|
||||
"set_x_scale",
|
||||
"set_y_scale",
|
||||
"set_x_lim",
|
||||
"set_y_lim",
|
||||
"set_grid",
|
||||
"lock_aspect_ratio",
|
||||
"remove",
|
||||
"images",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: Optional[QWidget] = None,
|
||||
parent_figure=None,
|
||||
config: Optional[ImageConfig] = None,
|
||||
client=None,
|
||||
gui_id: Optional[str] = None,
|
||||
):
|
||||
if config is None:
|
||||
config = ImageConfig(widget_class=self.__class__.__name__)
|
||||
super().__init__(
|
||||
parent=parent, parent_figure=parent_figure, config=config, client=client, gui_id=gui_id
|
||||
)
|
||||
# Get bec shortcuts dev, scans, queue, scan_storage, dap
|
||||
self.get_bec_shortcuts()
|
||||
self.entry_validator = EntryValidator(self.dev)
|
||||
self._images = defaultdict(dict)
|
||||
self.apply_config(self.config)
|
||||
self.processor = ImageProcessor()
|
||||
self.use_threading = False # TODO WILL be moved to the init method and to figure method
|
||||
|
||||
def _create_thread_worker(self, device: str, image: np.ndarray):
|
||||
thread = QThread()
|
||||
worker = ProcessorWorker(self.processor)
|
||||
worker.moveToThread(thread)
|
||||
|
||||
# Connect signals and slots
|
||||
thread.started.connect(lambda: worker.process_image(device, image))
|
||||
worker.processed.connect(self.update_image)
|
||||
worker.finished.connect(thread.quit)
|
||||
worker.finished.connect(thread.wait)
|
||||
worker.finished.connect(worker.deleteLater)
|
||||
thread.finished.connect(thread.deleteLater)
|
||||
|
||||
thread.start()
|
||||
|
||||
def find_image_by_monitor(self, item_id: str) -> BECImageItem:
|
||||
"""
|
||||
Find the widget by its gui_id.
|
||||
|
||||
Args:
|
||||
item_id(str): The gui_id of the widget.
|
||||
|
||||
Returns:
|
||||
BECImageItem: The widget with the given gui_id.
|
||||
"""
|
||||
for source, images in self._images.items():
|
||||
for key, value in images.items():
|
||||
if key == item_id and isinstance(value, BECImageItem):
|
||||
return value
|
||||
elif isinstance(value, dict):
|
||||
result = self.find_image_by_monitor(item_id)
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
def apply_config(self, config: dict | SubplotConfig):
|
||||
"""
|
||||
Apply the configuration to the 1D waveform widget.
|
||||
|
||||
Args:
|
||||
config(dict|SubplotConfig): Configuration settings.
|
||||
replot_last_scan(bool, optional): If True, replot the last scan. Defaults to False.
|
||||
"""
|
||||
if isinstance(config, dict):
|
||||
try:
|
||||
config = ImageConfig(**config)
|
||||
except ValidationError as e:
|
||||
print(f"Validation error when applying config to BECImageShow: {e}")
|
||||
return
|
||||
self.config = config
|
||||
self.plot_item.clear()
|
||||
|
||||
self.apply_axis_config()
|
||||
self._images = defaultdict(dict)
|
||||
|
||||
# TODO extend by adding image by config
|
||||
|
||||
def change_gui_id(self, new_gui_id: str):
|
||||
"""
|
||||
Change the GUI ID of the image widget and update the parent_id in all associated curves.
|
||||
|
||||
Args:
|
||||
new_gui_id (str): The new GUI ID to be set for the image widget.
|
||||
"""
|
||||
self.gui_id = new_gui_id
|
||||
self.config.gui_id = new_gui_id
|
||||
|
||||
for source, images in self._images.items():
|
||||
for id, image_item in images.items():
|
||||
image_item.config.parent_id = new_gui_id
|
||||
|
||||
def add_image_by_config(self, config: ImageItemConfig | dict) -> BECImageItem:
|
||||
"""
|
||||
Add an image to the widget by configuration.
|
||||
|
||||
Args:
|
||||
config(ImageItemConfig|dict): The configuration of the image.
|
||||
|
||||
Returns:
|
||||
BECImageItem: The image object.
|
||||
"""
|
||||
if isinstance(config, dict):
|
||||
config = ImageItemConfig(**config)
|
||||
config.parent_id = self.gui_id
|
||||
name = config.monitor if config.monitor is not None else config.gui_id
|
||||
image = self._add_image_object(source=config.source, name=name, config=config)
|
||||
return image
|
||||
|
||||
def get_image_config(self, image_id, dict_output: bool = True) -> ImageItemConfig | dict:
|
||||
"""
|
||||
Get the configuration of the image.
|
||||
|
||||
Args:
|
||||
image_id(str): The ID of the image.
|
||||
dict_output(bool): Whether to return the configuration as a dictionary. Defaults to True.
|
||||
|
||||
Returns:
|
||||
ImageItemConfig|dict: The configuration of the image.
|
||||
"""
|
||||
for source, images in self._images.items():
|
||||
for id, image in images.items():
|
||||
if id == image_id:
|
||||
if dict_output:
|
||||
return image.config.dict()
|
||||
else:
|
||||
return image.config # TODO check if this works
|
||||
|
||||
@property
|
||||
def images(self) -> list[BECImageItem]:
|
||||
"""
|
||||
Get the list of images.
|
||||
Returns:
|
||||
list[BECImageItem]: The list of images.
|
||||
"""
|
||||
images = []
|
||||
for source, images_dict in self._images.items():
|
||||
for id, image in images_dict.items():
|
||||
images.append(image)
|
||||
return images
|
||||
|
||||
@images.setter
|
||||
def images(self, value: dict[str, dict[str, BECImageItem]]):
|
||||
"""
|
||||
Set the images from a dictionary.
|
||||
|
||||
Args:
|
||||
value (dict[str, dict[str, BECImageItem]]): The images to set, organized by source and id.
|
||||
"""
|
||||
self._images = value
|
||||
|
||||
def get_image_dict(self) -> dict[str, dict[str, BECImageItem]]:
|
||||
"""
|
||||
Get all images.
|
||||
|
||||
Returns:
|
||||
dict[str, dict[str, BECImageItem]]: The dictionary of images.
|
||||
"""
|
||||
return self._images
|
||||
|
||||
def add_monitor_image(
|
||||
self,
|
||||
monitor: str,
|
||||
color_map: Optional[str] = "magma",
|
||||
color_bar: Optional[Literal["simple", "full"]] = "simple",
|
||||
downsample: Optional[bool] = True,
|
||||
opacity: Optional[float] = 1.0,
|
||||
vrange: Optional[tuple[int, int]] = None,
|
||||
# post_processing: Optional[PostProcessingConfig] = None,
|
||||
**kwargs,
|
||||
) -> BECImageItem:
|
||||
image_source = "device_monitor"
|
||||
|
||||
image_exits = self._check_image_id(monitor, self._images)
|
||||
if image_exits:
|
||||
raise ValueError(
|
||||
f"Monitor with ID '{monitor}' already exists in widget '{self.gui_id}'."
|
||||
)
|
||||
|
||||
monitor = self.entry_validator.validate_monitor(monitor)
|
||||
|
||||
image_config = ImageItemConfig(
|
||||
widget_class="BECImageItem",
|
||||
parent_id=self.gui_id,
|
||||
color_map=color_map,
|
||||
color_bar=color_bar,
|
||||
downsample=downsample,
|
||||
opacity=opacity,
|
||||
vrange=vrange,
|
||||
# post_processing=post_processing,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
image = self._add_image_object(source=image_source, name=monitor, config=image_config)
|
||||
self._connect_device_monitor(monitor)
|
||||
return image
|
||||
|
||||
def add_custom_image(
|
||||
self,
|
||||
name: str,
|
||||
data: Optional[np.ndarray] = None,
|
||||
color_map: Optional[str] = "magma",
|
||||
color_bar: Optional[Literal["simple", "full"]] = "simple",
|
||||
downsample: Optional[bool] = True,
|
||||
opacity: Optional[float] = 1.0,
|
||||
vrange: Optional[tuple[int, int]] = None,
|
||||
# post_processing: Optional[PostProcessingConfig] = None,
|
||||
**kwargs,
|
||||
):
|
||||
image_source = "device_monitor"
|
||||
|
||||
image_exits = self._check_curve_id(name, self._images)
|
||||
if image_exits:
|
||||
raise ValueError(f"Monitor with ID '{name}' already exists in widget '{self.gui_id}'.")
|
||||
|
||||
image_config = ImageItemConfig(
|
||||
widget_class="BECImageItem",
|
||||
parent_id=self.gui_id,
|
||||
monitor=name,
|
||||
color_map=color_map,
|
||||
color_bar=color_bar,
|
||||
downsample=downsample,
|
||||
opacity=opacity,
|
||||
vrange=vrange,
|
||||
# post_processing=post_processing,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
image = self._add_image_object(source=image_source, config=image_config, data=data)
|
||||
return image
|
||||
|
||||
def apply_setting_to_images(
|
||||
self, setting_method_name: str, args: list, kwargs: dict, image_id: str = None
|
||||
):
|
||||
"""
|
||||
Apply a setting to all images or a specific image by its ID.
|
||||
|
||||
Args:
|
||||
setting_method_name (str): The name of the method to apply (e.g., 'set_color_map').
|
||||
args (list): Positional arguments for the setting method.
|
||||
kwargs (dict): Keyword arguments for the setting method.
|
||||
image_id (str, optional): The ID of the specific image to apply the setting to. If None, applies to all images.
|
||||
"""
|
||||
if image_id:
|
||||
image = self.find_image_by_monitor(image_id)
|
||||
if image:
|
||||
getattr(image, setting_method_name)(*args, **kwargs)
|
||||
else:
|
||||
for source, images in self._images.items():
|
||||
for _, image in images.items():
|
||||
getattr(image, setting_method_name)(*args, **kwargs)
|
||||
|
||||
def set_vrange(self, vmin: float, vmax: float, name: str = None):
|
||||
"""
|
||||
Set the range of the color bar.
|
||||
If name is not specified, then set vrange for all images.
|
||||
|
||||
Args:
|
||||
vmin(float): Minimum value of the color bar.
|
||||
vmax(float): Maximum value of the color bar.
|
||||
name(str): The name of the image. If None, apply to all images.
|
||||
"""
|
||||
self.apply_setting_to_images("set_vrange", args=[vmin, vmax], kwargs={}, image_id=name)
|
||||
|
||||
def set_color_map(self, cmap: str, name: str = None):
|
||||
"""
|
||||
Set the color map of the image.
|
||||
If name is not specified, then set color map for all images.
|
||||
|
||||
Args:
|
||||
cmap(str): The color map of the image.
|
||||
name(str): The name of the image. If None, apply to all images.
|
||||
"""
|
||||
self.apply_setting_to_images("set_color_map", args=[cmap], kwargs={}, image_id=name)
|
||||
|
||||
def set_autorange(self, enable: bool = False, name: str = None):
|
||||
"""
|
||||
Set the autoscale of the image.
|
||||
|
||||
Args:
|
||||
enable(bool): Whether to autoscale the color bar.
|
||||
name(str): The name of the image. If None, apply to all images.
|
||||
"""
|
||||
self.apply_setting_to_images("set_autorange", args=[enable], kwargs={}, image_id=name)
|
||||
|
||||
def set_monitor(self, monitor: str, name: str = None):
|
||||
"""
|
||||
Set the monitor of the image.
|
||||
If name is not specified, then set monitor for all images.
|
||||
|
||||
Args:
|
||||
monitor(str): The name of the monitor.
|
||||
name(str): The name of the image. If None, apply to all images.
|
||||
"""
|
||||
self.apply_setting_to_images("set_monitor", args=[monitor], kwargs={}, image_id=name)
|
||||
|
||||
def set_processing(self, name: str = None, **kwargs):
|
||||
"""
|
||||
Set the post processing of the image.
|
||||
If name is not specified, then set post processing for all images.
|
||||
|
||||
Args:
|
||||
name(str): The name of the image. If None, apply to all images.
|
||||
**kwargs: Keyword arguments for the properties to be set.
|
||||
Possible properties:
|
||||
- fft: bool
|
||||
- log: bool
|
||||
- rot: int
|
||||
- transpose: bool
|
||||
"""
|
||||
self.apply_setting_to_images("set", args=[], kwargs=kwargs, image_id=name)
|
||||
|
||||
def set_image_properties(self, name: str = None, **kwargs):
|
||||
"""
|
||||
Set the properties of the image.
|
||||
|
||||
Args:
|
||||
name(str): The name of the image. If None, apply to all images.
|
||||
**kwargs: Keyword arguments for the properties to be set.
|
||||
Possible properties:
|
||||
- downsample: bool
|
||||
- color_map: str
|
||||
- monitor: str
|
||||
- opacity: float
|
||||
- vrange: tuple[int,int]
|
||||
- fft: bool
|
||||
- log: bool
|
||||
- rot: int
|
||||
- transpose: bool
|
||||
"""
|
||||
self.apply_setting_to_images("set", args=[], kwargs=kwargs, image_id=name)
|
||||
|
||||
def set_fft(self, enable: bool = False, name: str = None):
|
||||
"""
|
||||
Set the FFT of the image.
|
||||
If name is not specified, then set FFT for all images.
|
||||
|
||||
Args:
|
||||
enable(bool): Whether to perform FFT on the monitor data.
|
||||
name(str): The name of the image. If None, apply to all images.
|
||||
"""
|
||||
self.apply_setting_to_images("set_fft", args=[enable], kwargs={}, image_id=name)
|
||||
|
||||
def set_log(self, enable: bool = False, name: str = None):
|
||||
"""
|
||||
Set the log of the image.
|
||||
If name is not specified, then set log for all images.
|
||||
|
||||
Args:
|
||||
enable(bool): Whether to perform log on the monitor data.
|
||||
name(str): The name of the image. If None, apply to all images.
|
||||
"""
|
||||
self.apply_setting_to_images("set_log", args=[enable], kwargs={}, image_id=name)
|
||||
|
||||
def set_rotation(self, deg_90: int = 0, name: str = None):
|
||||
"""
|
||||
Set the rotation of the image.
|
||||
If name is not specified, then set rotation for all images.
|
||||
|
||||
Args:
|
||||
deg_90(int): The rotation angle of the monitor data before displaying.
|
||||
name(str): The name of the image. If None, apply to all images.
|
||||
"""
|
||||
self.apply_setting_to_images("set_rotation", args=[deg_90], kwargs={}, image_id=name)
|
||||
|
||||
def set_transpose(self, enable: bool = False, name: str = None):
|
||||
"""
|
||||
Set the transpose of the image.
|
||||
If name is not specified, then set transpose for all images.
|
||||
|
||||
Args:
|
||||
enable(bool): Whether to transpose the monitor data before displaying.
|
||||
name(str): The name of the image. If None, apply to all images.
|
||||
"""
|
||||
self.apply_setting_to_images("set_transpose", args=[enable], kwargs={}, image_id=name)
|
||||
|
||||
def toggle_threading(self, use_threading: bool):
|
||||
"""
|
||||
Toggle threading for the widgets postprocessing and updating.
|
||||
|
||||
Args:
|
||||
use_threading(bool): Whether to use threading.
|
||||
"""
|
||||
self.use_threading = use_threading
|
||||
if self.use_threading is False and self.thread.isRunning():
|
||||
self.cleanup()
|
||||
|
||||
@pyqtSlot(dict)
|
||||
def on_image_update(self, msg: dict):
|
||||
"""
|
||||
Update the image of the device monitor from bec.
|
||||
|
||||
Args:
|
||||
msg(dict): The message from bec.
|
||||
"""
|
||||
data = msg["data"]
|
||||
device = msg["device"]
|
||||
image_to_update = self._images["device_monitor"][device]
|
||||
processing_config = image_to_update.config.processing
|
||||
self.processor.set_config(processing_config)
|
||||
if self.use_threading:
|
||||
self._create_thread_worker(device, data)
|
||||
else:
|
||||
data = self.processor.process_image(data)
|
||||
self.update_image(device, data)
|
||||
|
||||
@pyqtSlot(str, np.ndarray)
|
||||
def update_image(self, device: str, data: np.ndarray):
|
||||
"""
|
||||
Update the image of the device monitor.
|
||||
|
||||
Args:
|
||||
device(str): The name of the device.
|
||||
data(np.ndarray): The data to be updated.
|
||||
"""
|
||||
image_to_update = self._images["device_monitor"][device]
|
||||
image_to_update.updateImage(data, autoLevels=image_to_update.config.autorange)
|
||||
|
||||
def _connect_device_monitor(self, monitor: str):
|
||||
"""
|
||||
Connect to the device monitor.
|
||||
|
||||
Args:
|
||||
monitor(str): The name of the monitor.
|
||||
"""
|
||||
image_item = self.find_image_by_monitor(monitor)
|
||||
try:
|
||||
previous_monitor = image_item.config.monitor
|
||||
except AttributeError:
|
||||
previous_monitor = None
|
||||
if previous_monitor != monitor:
|
||||
if previous_monitor:
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.on_image_update, MessageEndpoints.device_monitor(previous_monitor)
|
||||
)
|
||||
if monitor:
|
||||
self.bec_dispatcher.connect_slot(
|
||||
self.on_image_update, MessageEndpoints.device_monitor(monitor)
|
||||
)
|
||||
image_item.set_monitor(monitor)
|
||||
|
||||
def _add_image_object(
|
||||
self, source: str, name: str, config: ImageItemConfig, data=None
|
||||
) -> BECImageItem: # TODO fix types
|
||||
config.parent_id = self.gui_id
|
||||
image = BECImageItem(config=config, parent_image=self)
|
||||
self.plot_item.addItem(image)
|
||||
self._images[source][name] = image
|
||||
self.config.images[name] = config
|
||||
if data is not None:
|
||||
image.setImage(data)
|
||||
return image
|
||||
|
||||
def _check_image_id(self, val: Any, dict_to_check: dict) -> bool:
|
||||
"""
|
||||
Check if val is in the values of the dict_to_check or in the values of the nested dictionaries.
|
||||
|
||||
Args:
|
||||
val(Any): Value to check.
|
||||
dict_to_check(dict): Dictionary to check.
|
||||
|
||||
Returns:
|
||||
bool: True if val is in the values of the dict_to_check or in the values of the nested dictionaries, False otherwise.
|
||||
"""
|
||||
if val in dict_to_check.keys():
|
||||
return True
|
||||
for key in dict_to_check:
|
||||
if isinstance(dict_to_check[key], dict):
|
||||
if self._check_image_id(val, dict_to_check[key]):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _validate_monitor(self, monitor: str, validate_bec: bool = True):
|
||||
"""
|
||||
Validate the monitor name.
|
||||
|
||||
Args:
|
||||
monitor(str): The name of the monitor.
|
||||
validate_bec(bool): Whether to validate the monitor name with BEC.
|
||||
|
||||
Returns:
|
||||
bool: True if the monitor name is valid, False otherwise.
|
||||
"""
|
||||
if not monitor or monitor == "":
|
||||
return False
|
||||
if validate_bec:
|
||||
return monitor in self.dev
|
||||
return True
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
Clean up the widget.
|
||||
"""
|
||||
for monitor in self._images["device_monitor"]:
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.on_image_update, MessageEndpoints.device_monitor(monitor)
|
||||
)
|
||||
for image in self.images:
|
||||
image.cleanup()
|
||||
|
||||
super().cleanup()
|
||||
@@ -1,277 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Literal, Optional
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from pydantic import Field
|
||||
|
||||
from bec_widgets.utils import BECConnector, ConnectionConfig
|
||||
from bec_widgets.widgets.figure.plots.image.image_processor import ProcessingConfig
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bec_widgets.widgets.figure.plots.image.image import BECImageShow
|
||||
|
||||
|
||||
class ImageItemConfig(ConnectionConfig):
|
||||
parent_id: Optional[str] = Field(None, description="The parent plot of the image.")
|
||||
monitor: Optional[str] = Field(None, description="The name of the monitor.")
|
||||
source: Optional[str] = Field(None, description="The source of the curve.")
|
||||
color_map: Optional[str] = Field("magma", description="The color map of the image.")
|
||||
downsample: Optional[bool] = Field(True, description="Whether to downsample the image.")
|
||||
opacity: Optional[float] = Field(1.0, description="The opacity of the image.")
|
||||
vrange: Optional[tuple[int, int]] = Field(
|
||||
None, description="The range of the color bar. If None, the range is automatically set."
|
||||
)
|
||||
color_bar: Optional[Literal["simple", "full"]] = Field(
|
||||
"simple", description="The type of the color bar."
|
||||
)
|
||||
autorange: Optional[bool] = Field(True, description="Whether to autorange the color bar.")
|
||||
processing: ProcessingConfig = Field(
|
||||
default_factory=ProcessingConfig, description="The post processing of the image."
|
||||
)
|
||||
|
||||
|
||||
class BECImageItem(BECConnector, pg.ImageItem):
|
||||
USER_ACCESS = [
|
||||
"rpc_id",
|
||||
"config_dict",
|
||||
"set",
|
||||
"set_fft",
|
||||
"set_log",
|
||||
"set_rotation",
|
||||
"set_transpose",
|
||||
"set_opacity",
|
||||
"set_autorange",
|
||||
"set_color_map",
|
||||
"set_auto_downsample",
|
||||
"set_monitor",
|
||||
"set_vrange",
|
||||
"get_data",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: Optional[ImageItemConfig] = None,
|
||||
gui_id: Optional[str] = None,
|
||||
parent_image: Optional[BECImageShow] = None,
|
||||
**kwargs,
|
||||
):
|
||||
if config is None:
|
||||
config = ImageItemConfig(widget_class=self.__class__.__name__)
|
||||
self.config = config
|
||||
else:
|
||||
self.config = config
|
||||
super().__init__(config=config, gui_id=gui_id)
|
||||
pg.ImageItem.__init__(self)
|
||||
|
||||
self.parent_image = parent_image
|
||||
self.colorbar_bar = None
|
||||
|
||||
self._add_color_bar(
|
||||
self.config.color_bar, self.config.vrange
|
||||
) # TODO can also support None to not have any colorbar
|
||||
self.apply_config()
|
||||
if kwargs:
|
||||
self.set(**kwargs)
|
||||
|
||||
def apply_config(self):
|
||||
"""
|
||||
Apply current configuration.
|
||||
"""
|
||||
self.set_color_map(self.config.color_map)
|
||||
self.set_auto_downsample(self.config.downsample)
|
||||
if self.config.vrange is not None:
|
||||
self.set_vrange(vrange=self.config.vrange)
|
||||
|
||||
def set(self, **kwargs):
|
||||
"""
|
||||
Set the properties of the image.
|
||||
|
||||
Args:
|
||||
**kwargs: Keyword arguments for the properties to be set.
|
||||
|
||||
Possible properties:
|
||||
- downsample
|
||||
- color_map
|
||||
- monitor
|
||||
- opacity
|
||||
- vrange
|
||||
- fft
|
||||
- log
|
||||
- rot
|
||||
- transpose
|
||||
"""
|
||||
method_map = {
|
||||
"downsample": self.set_auto_downsample,
|
||||
"color_map": self.set_color_map,
|
||||
"monitor": self.set_monitor,
|
||||
"opacity": self.set_opacity,
|
||||
"vrange": self.set_vrange,
|
||||
"fft": self.set_fft,
|
||||
"log": self.set_log,
|
||||
"rot": self.set_rotation,
|
||||
"transpose": self.set_transpose,
|
||||
}
|
||||
for key, value in kwargs.items():
|
||||
if key in method_map:
|
||||
method_map[key](value)
|
||||
else:
|
||||
print(f"Warning: '{key}' is not a recognized property.")
|
||||
|
||||
def set_fft(self, enable: bool = False):
|
||||
"""
|
||||
Set the FFT of the image.
|
||||
|
||||
Args:
|
||||
enable(bool): Whether to perform FFT on the monitor data.
|
||||
"""
|
||||
self.config.processing.fft = enable
|
||||
|
||||
def set_log(self, enable: bool = False):
|
||||
"""
|
||||
Set the log of the image.
|
||||
|
||||
Args:
|
||||
enable(bool): Whether to perform log on the monitor data.
|
||||
"""
|
||||
self.config.processing.log = enable
|
||||
if enable and self.color_bar and self.config.color_bar == "full":
|
||||
self.color_bar.autoHistogramRange()
|
||||
|
||||
def set_rotation(self, deg_90: int = 0):
|
||||
"""
|
||||
Set the rotation of the image.
|
||||
|
||||
Args:
|
||||
deg_90(int): The rotation angle of the monitor data before displaying.
|
||||
"""
|
||||
self.config.processing.rotation = deg_90
|
||||
|
||||
def set_transpose(self, enable: bool = False):
|
||||
"""
|
||||
Set the transpose of the image.
|
||||
|
||||
Args:
|
||||
enable(bool): Whether to transpose the image.
|
||||
"""
|
||||
self.config.processing.transpose = enable
|
||||
|
||||
def set_opacity(self, opacity: float = 1.0):
|
||||
"""
|
||||
Set the opacity of the image.
|
||||
|
||||
Args:
|
||||
opacity(float): The opacity of the image.
|
||||
"""
|
||||
self.setOpacity(opacity)
|
||||
self.config.opacity = opacity
|
||||
|
||||
def set_autorange(self, autorange: bool = False):
|
||||
"""
|
||||
Set the autorange of the color bar.
|
||||
|
||||
Args:
|
||||
autorange(bool): Whether to autorange the color bar.
|
||||
"""
|
||||
self.config.autorange = autorange
|
||||
if self.color_bar is not None:
|
||||
self.color_bar.autoHistogramRange()
|
||||
|
||||
def set_color_map(self, cmap: str = "magma"):
|
||||
"""
|
||||
Set the color map of the image.
|
||||
|
||||
Args:
|
||||
cmap(str): The color map of the image.
|
||||
"""
|
||||
self.setColorMap(cmap)
|
||||
if self.color_bar is not None:
|
||||
if self.config.color_bar == "simple":
|
||||
self.color_bar.setColorMap(cmap)
|
||||
elif self.config.color_bar == "full":
|
||||
self.color_bar.gradient.loadPreset(cmap)
|
||||
self.config.color_map = cmap
|
||||
|
||||
def set_auto_downsample(self, auto: bool = True):
|
||||
"""
|
||||
Set the auto downsample of the image.
|
||||
|
||||
Args:
|
||||
auto(bool): Whether to downsample the image.
|
||||
"""
|
||||
self.setAutoDownsample(auto)
|
||||
self.config.downsample = auto
|
||||
|
||||
def set_monitor(self, monitor: str):
|
||||
"""
|
||||
Set the monitor of the image.
|
||||
|
||||
Args:
|
||||
monitor(str): The name of the monitor.
|
||||
"""
|
||||
self.config.monitor = monitor
|
||||
|
||||
def set_vrange(self, vmin: float = None, vmax: float = None, vrange: tuple[int, int] = None):
|
||||
"""
|
||||
Set the range of the color bar.
|
||||
|
||||
Args:
|
||||
vmin(float): Minimum value of the color bar.
|
||||
vmax(float): Maximum value of the color bar.
|
||||
"""
|
||||
if vrange is not None:
|
||||
vmin, vmax = vrange
|
||||
self.setLevels([vmin, vmax])
|
||||
self.config.vrange = (vmin, vmax)
|
||||
self.config.autorange = False
|
||||
if self.color_bar is not None:
|
||||
if self.config.color_bar == "simple":
|
||||
self.color_bar.setLevels(low=vmin, high=vmax)
|
||||
elif self.config.color_bar == "full":
|
||||
self.color_bar.setLevels(min=vmin, max=vmax)
|
||||
self.color_bar.setHistogramRange(vmin - 0.1 * vmin, vmax + 0.1 * vmax)
|
||||
|
||||
def get_data(self) -> np.ndarray:
|
||||
"""
|
||||
Get the data of the image.
|
||||
Returns:
|
||||
np.ndarray: The data of the image.
|
||||
"""
|
||||
return self.image
|
||||
|
||||
def _add_color_bar(
|
||||
self, color_bar_style: str = "simple", vrange: Optional[tuple[int, int]] = None
|
||||
):
|
||||
"""
|
||||
Add color bar to the layout.
|
||||
|
||||
Args:
|
||||
style(Literal["simple,full"]): The style of the color bar.
|
||||
vrange(tuple[int,int]): The range of the color bar.
|
||||
"""
|
||||
if color_bar_style == "simple":
|
||||
self.color_bar = pg.ColorBarItem(colorMap=self.config.color_map)
|
||||
if vrange is not None:
|
||||
self.color_bar.setLevels(low=vrange[0], high=vrange[1])
|
||||
self.color_bar.setImageItem(self)
|
||||
self.parent_image.addItem(self.color_bar) # , row=0, col=1)
|
||||
self.config.color_bar = "simple"
|
||||
elif color_bar_style == "full":
|
||||
# Setting histogram
|
||||
self.color_bar = pg.HistogramLUTItem()
|
||||
self.color_bar.setImageItem(self)
|
||||
self.color_bar.gradient.loadPreset(self.config.color_map)
|
||||
if vrange is not None:
|
||||
self.color_bar.setLevels(min=vrange[0], max=vrange[1])
|
||||
self.color_bar.setHistogramRange(
|
||||
vrange[0] - 0.1 * vrange[0], vrange[1] + 0.1 * vrange[1]
|
||||
)
|
||||
|
||||
# Adding histogram to the layout
|
||||
self.parent_image.addItem(self.color_bar) # , row=0, col=1)
|
||||
|
||||
# save settings
|
||||
self.config.color_bar = "full"
|
||||
else:
|
||||
raise ValueError("style should be 'simple' or 'full'")
|
||||
@@ -1,153 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
from pydantic import BaseModel, Field
|
||||
from qtpy.QtCore import QObject, Signal, Slot
|
||||
|
||||
|
||||
class ProcessingConfig(BaseModel):
|
||||
fft: Optional[bool] = Field(False, description="Whether to perform FFT on the monitor data.")
|
||||
log: Optional[bool] = Field(False, description="Whether to perform log on the monitor data.")
|
||||
center_of_mass: Optional[bool] = Field(
|
||||
False, description="Whether to calculate the center of mass of the monitor data."
|
||||
)
|
||||
transpose: Optional[bool] = Field(
|
||||
False, description="Whether to transpose the monitor data before displaying."
|
||||
)
|
||||
rotation: Optional[int] = Field(
|
||||
None, description="The rotation angle of the monitor data before displaying."
|
||||
)
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
|
||||
|
||||
class ImageProcessor:
|
||||
"""
|
||||
Class for processing the image data.
|
||||
"""
|
||||
|
||||
def __init__(self, config: ProcessingConfig = None):
|
||||
if config is None:
|
||||
config = ProcessingConfig()
|
||||
self.config = config
|
||||
|
||||
def set_config(self, config: ProcessingConfig):
|
||||
"""
|
||||
Set the configuration of the processor.
|
||||
|
||||
Args:
|
||||
config(ProcessingConfig): The configuration of the processor.
|
||||
"""
|
||||
self.config = config
|
||||
|
||||
def FFT(self, data: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Perform FFT on the data.
|
||||
|
||||
Args:
|
||||
data(np.ndarray): The data to be processed.
|
||||
|
||||
Returns:
|
||||
np.ndarray: The processed data.
|
||||
"""
|
||||
return np.abs(np.fft.fftshift(np.fft.fft2(data)))
|
||||
|
||||
def rotation(self, data: np.ndarray, rotate_90: int) -> np.ndarray:
|
||||
"""
|
||||
Rotate the data by 90 degrees n times.
|
||||
|
||||
Args:
|
||||
data(np.ndarray): The data to be processed.
|
||||
rotate_90(int): The number of 90 degree rotations.
|
||||
|
||||
Returns:
|
||||
np.ndarray: The processed data.
|
||||
"""
|
||||
return np.rot90(data, k=rotate_90, axes=(0, 1))
|
||||
|
||||
def transpose(self, data: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Transpose the data.
|
||||
|
||||
Args:
|
||||
data(np.ndarray): The data to be processed.
|
||||
|
||||
Returns:
|
||||
np.ndarray: The processed data.
|
||||
"""
|
||||
return np.transpose(data)
|
||||
|
||||
def log(self, data: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Perform log on the data.
|
||||
|
||||
Args:
|
||||
data(np.ndarray): The data to be processed.
|
||||
|
||||
Returns:
|
||||
np.ndarray: The processed data.
|
||||
"""
|
||||
# TODO this is not final solution -> data should stay as int16
|
||||
data = data.astype(np.float32)
|
||||
offset = 1e-6
|
||||
data_offset = data + offset
|
||||
return np.log10(data_offset)
|
||||
|
||||
# def center_of_mass(self, data: np.ndarray) -> tuple: # TODO check functionality
|
||||
# return np.unravel_index(np.argmax(data), data.shape)
|
||||
|
||||
def process_image(self, data: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Process the data according to the configuration.
|
||||
|
||||
Args:
|
||||
data(np.ndarray): The data to be processed.
|
||||
|
||||
Returns:
|
||||
np.ndarray: The processed data.
|
||||
"""
|
||||
if self.config.fft:
|
||||
data = self.FFT(data)
|
||||
if self.config.rotation is not None:
|
||||
data = self.rotation(data, self.config.rotation)
|
||||
if self.config.transpose:
|
||||
data = self.transpose(data)
|
||||
if self.config.log:
|
||||
data = self.log(data)
|
||||
return data
|
||||
|
||||
|
||||
class ProcessorWorker(QObject):
|
||||
"""
|
||||
Worker for processing the image data.
|
||||
"""
|
||||
|
||||
processed = Signal(str, np.ndarray)
|
||||
stopRequested = Signal()
|
||||
finished = Signal()
|
||||
|
||||
def __init__(self, processor):
|
||||
super().__init__()
|
||||
self.processor = processor
|
||||
self._isRunning = False
|
||||
self.stopRequested.connect(self.stop)
|
||||
|
||||
@Slot(str, np.ndarray)
|
||||
def process_image(self, device: str, image: np.ndarray):
|
||||
"""
|
||||
Process the image data.
|
||||
|
||||
Args:
|
||||
device(str): The name of the device.
|
||||
image(np.ndarray): The image data.
|
||||
"""
|
||||
self._isRunning = True
|
||||
processed_image = self.processor.process_image(image)
|
||||
self._isRunning = False
|
||||
if not self._isRunning:
|
||||
self.processed.emit(device, processed_image)
|
||||
self.finished.emit()
|
||||
|
||||
def stop(self):
|
||||
self._isRunning = False
|
||||
@@ -1,435 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from typing import Optional, Union
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from pydantic import Field
|
||||
from qtpy import QtCore, QtGui
|
||||
from qtpy.QtCore import Signal as pyqtSignal
|
||||
from qtpy.QtCore import Slot as pyqtSlot
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils import EntryValidator
|
||||
from bec_widgets.widgets.figure.plots.plot_base import BECPlotBase, SubplotConfig
|
||||
from bec_widgets.widgets.figure.plots.waveform.waveform import Signal, SignalData
|
||||
|
||||
|
||||
class MotorMapConfig(SubplotConfig):
|
||||
signals: Optional[Signal] = Field(None, description="Signals of the motor map")
|
||||
color_map: Optional[str] = Field(
|
||||
"Greys", description="Color scheme of the motor position gradient."
|
||||
) # TODO decide if useful for anything, or just keep GREYS always
|
||||
scatter_size: Optional[int] = Field(5, description="Size of the scatter points.")
|
||||
max_points: Optional[int] = Field(1000, description="Maximum number of points to display.")
|
||||
num_dim_points: Optional[int] = Field(
|
||||
100,
|
||||
description="Number of points to dim before the color remains same for older recorded position.",
|
||||
)
|
||||
precision: Optional[int] = Field(2, description="Decimal precision of the motor position.")
|
||||
background_value: Optional[int] = Field(
|
||||
25, description="Background value of the motor map."
|
||||
) # TODO can be percentage from 255 calculated
|
||||
|
||||
|
||||
class BECMotorMap(BECPlotBase):
|
||||
USER_ACCESS = [
|
||||
"config_dict",
|
||||
"change_motors",
|
||||
"set_max_points",
|
||||
"set_precision",
|
||||
"set_num_dim_points",
|
||||
"set_background_value",
|
||||
"set_scatter_size",
|
||||
"get_data",
|
||||
]
|
||||
|
||||
# QT Signals
|
||||
update_signal = pyqtSignal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: Optional[QWidget] = None,
|
||||
parent_figure=None,
|
||||
config: Optional[MotorMapConfig] = None,
|
||||
client=None,
|
||||
gui_id: Optional[str] = None,
|
||||
):
|
||||
if config is None:
|
||||
config = MotorMapConfig(widget_class=self.__class__.__name__)
|
||||
super().__init__(
|
||||
parent=parent, parent_figure=parent_figure, config=config, client=client, gui_id=gui_id
|
||||
)
|
||||
|
||||
# Get bec shortcuts dev, scans, queue, scan_storage, dap
|
||||
self.get_bec_shortcuts()
|
||||
self.entry_validator = EntryValidator(self.dev)
|
||||
|
||||
self.motor_x = None
|
||||
self.motor_y = None
|
||||
self.database_buffer = {"x": [], "y": []}
|
||||
self.plot_components = defaultdict(dict) # container for plot components
|
||||
|
||||
# connect update signal to update plot
|
||||
self.proxy_update_plot = pg.SignalProxy(
|
||||
self.update_signal, rateLimit=25, slot=self._update_plot
|
||||
)
|
||||
|
||||
# TODO decide if needed to implement, maybe there will be no children widgets for motormap for now...
|
||||
# def find_widget_by_id(self, item_id: str) -> BECCurve:
|
||||
# """
|
||||
# Find the curve by its ID.
|
||||
# Args:
|
||||
# item_id(str): ID of the curve.
|
||||
#
|
||||
# Returns:
|
||||
# BECCurve: The curve object.
|
||||
# """
|
||||
# for curve in self.plot_item.curves:
|
||||
# if curve.gui_id == item_id:
|
||||
# return curve
|
||||
|
||||
@pyqtSlot(str, str, str, str, bool)
|
||||
def change_motors(
|
||||
self,
|
||||
motor_x: str,
|
||||
motor_y: str,
|
||||
motor_x_entry: str = None,
|
||||
motor_y_entry: str = None,
|
||||
validate_bec: bool = True,
|
||||
) -> None:
|
||||
"""
|
||||
Change the active motors for the plot.
|
||||
|
||||
Args:
|
||||
motor_x(str): Motor name for the X axis.
|
||||
motor_y(str): Motor name for the Y axis.
|
||||
motor_x_entry(str): Motor entry for the X axis.
|
||||
motor_y_entry(str): Motor entry for the Y axis.
|
||||
validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True.
|
||||
"""
|
||||
motor_x_entry, motor_y_entry = self._validate_signal_entries(
|
||||
motor_x, motor_y, motor_x_entry, motor_y_entry, validate_bec
|
||||
)
|
||||
|
||||
motor_x_limit = self._get_motor_limit(motor_x)
|
||||
motor_y_limit = self._get_motor_limit(motor_y)
|
||||
|
||||
signal = Signal(
|
||||
source="device_readback",
|
||||
x=SignalData(name=motor_x, entry=motor_x_entry, limits=motor_x_limit),
|
||||
y=SignalData(name=motor_y, entry=motor_y_entry, limits=motor_y_limit),
|
||||
)
|
||||
self.config.signals = signal
|
||||
|
||||
# reconnect the signals
|
||||
self._connect_motor_to_slots()
|
||||
|
||||
# Redraw the motor map
|
||||
self._make_motor_map()
|
||||
|
||||
def get_data(self) -> dict:
|
||||
"""
|
||||
Get the data of the motor map.
|
||||
Returns:
|
||||
dict: Data of the motor map.
|
||||
"""
|
||||
data = {"x": self.database_buffer["x"], "y": self.database_buffer["y"]}
|
||||
return data
|
||||
|
||||
# TODO setup all visual properties
|
||||
def set_max_points(self, max_points: int) -> None:
|
||||
"""
|
||||
Set the maximum number of points to display.
|
||||
|
||||
Args:
|
||||
max_points(int): Maximum number of points to display.
|
||||
"""
|
||||
self.config.max_points = max_points
|
||||
|
||||
def set_precision(self, precision: int) -> None:
|
||||
"""
|
||||
Set the decimal precision of the motor position.
|
||||
|
||||
Args:
|
||||
precision(int): Decimal precision of the motor position.
|
||||
"""
|
||||
self.config.precision = precision
|
||||
|
||||
def set_num_dim_points(self, num_dim_points: int) -> None:
|
||||
"""
|
||||
Set the number of dim points for the motor map.
|
||||
|
||||
Args:
|
||||
num_dim_points(int): Number of dim points.
|
||||
"""
|
||||
self.config.num_dim_points = num_dim_points
|
||||
|
||||
def set_background_value(self, background_value: int) -> None:
|
||||
"""
|
||||
Set the background value of the motor map.
|
||||
|
||||
Args:
|
||||
background_value(int): Background value of the motor map.
|
||||
"""
|
||||
self.config.background_value = background_value
|
||||
|
||||
def set_scatter_size(self, scatter_size: int) -> None:
|
||||
"""
|
||||
Set the scatter size of the motor map plot.
|
||||
|
||||
Args:
|
||||
scatter_size(int): Size of the scatter points.
|
||||
"""
|
||||
self.config.scatter_size = scatter_size
|
||||
|
||||
def _disconnect_current_motors(self):
|
||||
"""Disconnect the current motors from the slots."""
|
||||
if self.motor_x is not None and self.motor_y is not None:
|
||||
endpoints = [
|
||||
MessageEndpoints.device_readback(self.motor_x),
|
||||
MessageEndpoints.device_readback(self.motor_y),
|
||||
]
|
||||
self.bec_dispatcher.disconnect_slot(self.on_device_readback, endpoints)
|
||||
|
||||
def _connect_motor_to_slots(self):
|
||||
"""Connect motors to slots."""
|
||||
self._disconnect_current_motors()
|
||||
|
||||
self.motor_x = self.config.signals.x.name
|
||||
self.motor_y = self.config.signals.y.name
|
||||
|
||||
endpoints = [
|
||||
MessageEndpoints.device_readback(self.motor_x),
|
||||
MessageEndpoints.device_readback(self.motor_y),
|
||||
]
|
||||
|
||||
self.bec_dispatcher.connect_slot(self.on_device_readback, endpoints)
|
||||
|
||||
def _make_motor_map(self):
|
||||
"""
|
||||
Create the motor map plot.
|
||||
"""
|
||||
# Create limit map
|
||||
motor_x_limit = self.config.signals.x.limits
|
||||
motor_y_limit = self.config.signals.y.limits
|
||||
self.plot_components["limit_map"] = self._make_limit_map(motor_x_limit, motor_y_limit)
|
||||
self.plot_item.addItem(self.plot_components["limit_map"])
|
||||
self.plot_components["limit_map"].setZValue(-1)
|
||||
|
||||
# Create scatter plot
|
||||
scatter_size = self.config.scatter_size
|
||||
self.plot_components["scatter"] = pg.ScatterPlotItem(
|
||||
size=scatter_size, brush=pg.mkBrush(255, 255, 255, 255)
|
||||
)
|
||||
self.plot_item.addItem(self.plot_components["scatter"])
|
||||
self.plot_components["scatter"].setZValue(0)
|
||||
|
||||
# Enable Grid
|
||||
self.set_grid(True, True)
|
||||
|
||||
# Add the crosshair for initial motor coordinates
|
||||
initial_position_x = self._get_motor_init_position(
|
||||
self.motor_x, self.config.signals.x.entry, self.config.precision
|
||||
)
|
||||
initial_position_y = self._get_motor_init_position(
|
||||
self.motor_y, self.config.signals.y.entry, self.config.precision
|
||||
)
|
||||
|
||||
self.database_buffer["x"] = [initial_position_x]
|
||||
self.database_buffer["y"] = [initial_position_y]
|
||||
|
||||
self.plot_components["scatter"].setData([initial_position_x], [initial_position_y])
|
||||
self._add_coordinantes_crosshair(initial_position_x, initial_position_y)
|
||||
|
||||
# Set default labels for the plot
|
||||
self.set(x_label=f"Motor X ({self.motor_x})", y_label=f"Motor Y ({self.motor_y})")
|
||||
|
||||
def _add_coordinantes_crosshair(self, x: float, y: float) -> None:
|
||||
"""
|
||||
Add crosshair to the plot to highlight the current position.
|
||||
|
||||
Args:
|
||||
x(float): X coordinate.
|
||||
y(float): Y coordinate.
|
||||
"""
|
||||
|
||||
# Crosshair to highlight the current position
|
||||
highlight_H = pg.InfiniteLine(
|
||||
angle=0, movable=False, pen=pg.mkPen(color="r", width=1, style=QtCore.Qt.DashLine)
|
||||
)
|
||||
highlight_V = pg.InfiniteLine(
|
||||
angle=90, movable=False, pen=pg.mkPen(color="r", width=1, style=QtCore.Qt.DashLine)
|
||||
)
|
||||
|
||||
# Add crosshair to the curve list for future referencing
|
||||
self.plot_components["highlight_H"] = highlight_H
|
||||
self.plot_components["highlight_V"] = highlight_V
|
||||
|
||||
# Add crosshair to the plot
|
||||
self.plot_item.addItem(highlight_H)
|
||||
self.plot_item.addItem(highlight_V)
|
||||
|
||||
highlight_V.setPos(x)
|
||||
highlight_H.setPos(y)
|
||||
|
||||
def _make_limit_map(self, limits_x: list, limits_y: list) -> pg.ImageItem:
|
||||
"""
|
||||
Create a limit map for the motor map plot.
|
||||
|
||||
Args:
|
||||
limits_x(list): Motor limits for the x axis.
|
||||
limits_y(list): Motor limits for the y axis.
|
||||
|
||||
Returns:
|
||||
pg.ImageItem: Limit map.
|
||||
"""
|
||||
limit_x_min, limit_x_max = limits_x
|
||||
limit_y_min, limit_y_max = limits_y
|
||||
|
||||
map_width = int(limit_x_max - limit_x_min + 1)
|
||||
map_height = int(limit_y_max - limit_y_min + 1)
|
||||
|
||||
# Create limits map
|
||||
background_value = self.config.background_value
|
||||
limit_map_data = np.full((map_width, map_height), background_value, dtype=np.float32)
|
||||
limit_map = pg.ImageItem()
|
||||
limit_map.setImage(limit_map_data)
|
||||
|
||||
# Translate and scale the image item to match the motor coordinates
|
||||
tr = QtGui.QTransform()
|
||||
tr.translate(limit_x_min, limit_y_min)
|
||||
limit_map.setTransform(tr)
|
||||
|
||||
return limit_map
|
||||
|
||||
def _get_motor_init_position(self, name: str, entry: str, precision: int) -> float:
|
||||
"""
|
||||
Get the motor initial position from the config.
|
||||
|
||||
Args:
|
||||
name(str): Motor name.
|
||||
entry(str): Motor entry.
|
||||
precision(int): Decimal precision of the motor position.
|
||||
|
||||
Returns:
|
||||
float: Motor initial position.
|
||||
"""
|
||||
init_position = round(float(self.dev[name].read()[entry]["value"]), precision)
|
||||
return init_position
|
||||
|
||||
def _validate_signal_entries(
|
||||
self,
|
||||
x_name: str,
|
||||
y_name: str,
|
||||
x_entry: str | None,
|
||||
y_entry: str | None,
|
||||
validate_bec: bool = True,
|
||||
) -> tuple[str, str]:
|
||||
"""
|
||||
Validate the signal name and entry.
|
||||
|
||||
Args:
|
||||
x_name(str): Name of the x signal.
|
||||
y_name(str): Name of the y signal.
|
||||
x_entry(str|None): Entry of the x signal.
|
||||
y_entry(str|None): Entry of the y signal.
|
||||
validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True.
|
||||
|
||||
Returns:
|
||||
tuple[str,str]: Validated x and y entries.
|
||||
"""
|
||||
if validate_bec:
|
||||
x_entry = self.entry_validator.validate_signal(x_name, x_entry)
|
||||
y_entry = self.entry_validator.validate_signal(y_name, y_entry)
|
||||
else:
|
||||
x_entry = x_name if x_entry is None else x_entry
|
||||
y_entry = y_name if y_entry is None else y_entry
|
||||
return x_entry, y_entry
|
||||
|
||||
def _get_motor_limit(self, motor: str) -> Union[list | None]: # TODO check if works correctly
|
||||
"""
|
||||
Get the motor limit from the config.
|
||||
|
||||
Args:
|
||||
motor(str): Motor name.
|
||||
|
||||
Returns:
|
||||
float: Motor limit.
|
||||
"""
|
||||
try:
|
||||
limits = self.dev[motor].limits
|
||||
if limits == [0, 0]:
|
||||
return None
|
||||
return limits
|
||||
except AttributeError: # TODO maybe not needed, if no limits it returns [0,0]
|
||||
# If the motor doesn't have a 'limits' attribute, return a default value or raise a custom exception
|
||||
print(f"The device '{motor}' does not have defined limits.")
|
||||
return None
|
||||
|
||||
def _update_plot(self):
|
||||
"""Update the motor map plot."""
|
||||
x = self.database_buffer["x"]
|
||||
y = self.database_buffer["y"]
|
||||
|
||||
# Setup gradient brush for history
|
||||
brushes = [pg.mkBrush(50, 50, 50, 255)] * len(x)
|
||||
|
||||
# Calculate the decrement step based on self.num_dim_points
|
||||
num_dim_points = self.config.num_dim_points
|
||||
decrement_step = (255 - 50) / num_dim_points
|
||||
for i in range(1, min(num_dim_points + 1, len(x) + 1)):
|
||||
brightness = max(60, 255 - decrement_step * (i - 1))
|
||||
brushes[-i] = pg.mkBrush(brightness, brightness, brightness, 255)
|
||||
brushes[-1] = pg.mkBrush(255, 255, 255, 255) # Newest point is always full brightness
|
||||
scatter_size = self.config.scatter_size
|
||||
|
||||
# Update the scatter plot
|
||||
self.plot_components["scatter"].setData(
|
||||
x=x, y=y, brush=brushes, pen=None, size=scatter_size
|
||||
)
|
||||
|
||||
# Get last know position for crosshair
|
||||
current_x = x[-1]
|
||||
current_y = y[-1]
|
||||
|
||||
# Update the crosshair
|
||||
self.plot_components["highlight_V"].setPos(current_x)
|
||||
self.plot_components["highlight_H"].setPos(current_y)
|
||||
|
||||
# TODO not update title but some label
|
||||
# Update plot title
|
||||
precision = self.config.precision
|
||||
self.set_title(
|
||||
f"Motor position: ({round(float(current_x),precision)}, {round(float(current_y),precision)})"
|
||||
)
|
||||
|
||||
@pyqtSlot(dict)
|
||||
def on_device_readback(self, msg: dict) -> None:
|
||||
"""
|
||||
Update the motor map plot with the new motor position.
|
||||
|
||||
Args:
|
||||
msg(dict): Message from the device readback.
|
||||
"""
|
||||
if self.motor_x is None or self.motor_y is None:
|
||||
return
|
||||
|
||||
if self.motor_x in msg["signals"]:
|
||||
x = msg["signals"][self.motor_x]["value"]
|
||||
self.database_buffer["x"].append(x)
|
||||
self.database_buffer["y"].append(self.database_buffer["y"][-1])
|
||||
|
||||
elif self.motor_y in msg["signals"]:
|
||||
y = msg["signals"][self.motor_y]["value"]
|
||||
self.database_buffer["y"].append(y)
|
||||
self.database_buffer["x"].append(self.database_buffer["x"][-1])
|
||||
|
||||
self.update_signal.emit()
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup the widget."""
|
||||
self._disconnect_current_motors()
|
||||
super().cleanup()
|
||||
@@ -1,248 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal, Optional
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from pydantic import BaseModel, Field
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils import BECConnector, ConnectionConfig
|
||||
|
||||
|
||||
class AxisConfig(BaseModel):
|
||||
title: Optional[str] = Field(None, description="The title of the axes.")
|
||||
x_label: Optional[str] = Field(None, description="The label for the x-axis.")
|
||||
y_label: Optional[str] = Field(None, description="The label for the y-axis.")
|
||||
x_scale: Literal["linear", "log"] = Field("linear", description="The scale of the x-axis.")
|
||||
y_scale: Literal["linear", "log"] = Field("linear", description="The scale of the y-axis.")
|
||||
x_lim: Optional[tuple] = Field(None, description="The limits of the x-axis.")
|
||||
y_lim: Optional[tuple] = Field(None, description="The limits of the y-axis.")
|
||||
x_grid: bool = Field(False, description="Show grid on the x-axis.")
|
||||
y_grid: bool = Field(False, description="Show grid on the y-axis.")
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
|
||||
|
||||
class SubplotConfig(ConnectionConfig):
|
||||
parent_id: Optional[str] = Field(None, description="The parent figure of the plot.")
|
||||
|
||||
# Coordinates in the figure
|
||||
row: int = Field(0, description="The row coordinate in the figure.")
|
||||
col: int = Field(0, description="The column coordinate in the figure.")
|
||||
|
||||
# Appearance settings
|
||||
axis: AxisConfig = Field(
|
||||
default_factory=AxisConfig, description="The axis configuration of the plot."
|
||||
)
|
||||
|
||||
|
||||
class BECPlotBase(BECConnector, pg.GraphicsLayout):
|
||||
USER_ACCESS = [
|
||||
"config_dict",
|
||||
"set",
|
||||
"set_title",
|
||||
"set_x_label",
|
||||
"set_y_label",
|
||||
"set_x_scale",
|
||||
"set_y_scale",
|
||||
"set_x_lim",
|
||||
"set_y_lim",
|
||||
"set_grid",
|
||||
"lock_aspect_ratio",
|
||||
"remove",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: Optional[QWidget] = None, # TODO decide if needed for this class
|
||||
parent_figure=None,
|
||||
config: Optional[SubplotConfig] = None,
|
||||
client=None,
|
||||
gui_id: Optional[str] = None,
|
||||
):
|
||||
if config is None:
|
||||
config = SubplotConfig(widget_class=self.__class__.__name__)
|
||||
super().__init__(client=client, config=config, gui_id=gui_id)
|
||||
pg.GraphicsLayout.__init__(self, parent)
|
||||
|
||||
self.figure = parent_figure
|
||||
self.plot_item = self.addPlot(row=0, col=0)
|
||||
|
||||
self.add_legend()
|
||||
|
||||
def set(self, **kwargs) -> None:
|
||||
"""
|
||||
Set the properties of the plot widget.
|
||||
|
||||
Args:
|
||||
**kwargs: Keyword arguments for the properties to be set.
|
||||
|
||||
Possible properties:
|
||||
- title: str
|
||||
- x_label: str
|
||||
- y_label: str
|
||||
- x_scale: Literal["linear", "log"]
|
||||
- y_scale: Literal["linear", "log"]
|
||||
- x_lim: tuple
|
||||
- y_lim: tuple
|
||||
"""
|
||||
# Mapping of keywords to setter methods
|
||||
method_map = {
|
||||
"title": self.set_title,
|
||||
"x_label": self.set_x_label,
|
||||
"y_label": self.set_y_label,
|
||||
"x_scale": self.set_x_scale,
|
||||
"y_scale": self.set_y_scale,
|
||||
"x_lim": self.set_x_lim,
|
||||
"y_lim": self.set_y_lim,
|
||||
}
|
||||
for key, value in kwargs.items():
|
||||
if key in method_map:
|
||||
method_map[key](value)
|
||||
else:
|
||||
print(f"Warning: '{key}' is not a recognized property.")
|
||||
|
||||
def apply_axis_config(self):
|
||||
"""Apply the axis configuration to the plot widget."""
|
||||
config_mappings = {
|
||||
"title": self.config.axis.title,
|
||||
"x_label": self.config.axis.x_label,
|
||||
"y_label": self.config.axis.y_label,
|
||||
"x_scale": self.config.axis.x_scale,
|
||||
"y_scale": self.config.axis.y_scale,
|
||||
"x_lim": self.config.axis.x_lim,
|
||||
"y_lim": self.config.axis.y_lim,
|
||||
}
|
||||
|
||||
self.set(**{k: v for k, v in config_mappings.items() if v is not None})
|
||||
|
||||
def set_title(self, title: str):
|
||||
"""
|
||||
Set the title of the plot widget.
|
||||
|
||||
Args:
|
||||
title(str): Title of the plot widget.
|
||||
"""
|
||||
self.plot_item.setTitle(title)
|
||||
self.config.axis.title = title
|
||||
|
||||
def set_x_label(self, label: str):
|
||||
"""
|
||||
Set the label of the x-axis.
|
||||
|
||||
Args:
|
||||
label(str): Label of the x-axis.
|
||||
"""
|
||||
self.plot_item.setLabel("bottom", label)
|
||||
self.config.axis.x_label = label
|
||||
|
||||
def set_y_label(self, label: str):
|
||||
"""
|
||||
Set the label of the y-axis.
|
||||
|
||||
Args:
|
||||
label(str): Label of the y-axis.
|
||||
"""
|
||||
self.plot_item.setLabel("left", label)
|
||||
self.config.axis.y_label = label
|
||||
|
||||
def set_x_scale(self, scale: Literal["linear", "log"] = "linear"):
|
||||
"""
|
||||
Set the scale of the x-axis.
|
||||
|
||||
Args:
|
||||
scale(Literal["linear", "log"]): Scale of the x-axis.
|
||||
"""
|
||||
self.plot_item.setLogMode(x=(scale == "log"))
|
||||
self.config.axis.x_scale = scale
|
||||
|
||||
def set_y_scale(self, scale: Literal["linear", "log"] = "linear"):
|
||||
"""
|
||||
Set the scale of the y-axis.
|
||||
|
||||
Args:
|
||||
scale(Literal["linear", "log"]): Scale of the y-axis.
|
||||
"""
|
||||
self.plot_item.setLogMode(y=(scale == "log"))
|
||||
self.config.axis.y_scale = scale
|
||||
|
||||
def set_x_lim(self, *args) -> None:
|
||||
"""
|
||||
Set the limits of the x-axis. This method can accept either two separate arguments
|
||||
for the minimum and maximum x-axis values, or a single tuple containing both limits.
|
||||
|
||||
Usage:
|
||||
set_x_lim(x_min, x_max)
|
||||
set_x_lim((x_min, x_max))
|
||||
|
||||
Args:
|
||||
*args: A variable number of arguments. Can be two integers (x_min and x_max)
|
||||
or a single tuple with two integers.
|
||||
"""
|
||||
if len(args) == 1 and isinstance(args[0], tuple):
|
||||
x_min, x_max = args[0]
|
||||
elif len(args) == 2:
|
||||
x_min, x_max = args
|
||||
else:
|
||||
raise ValueError("set_x_lim expects either two separate arguments or a single tuple")
|
||||
|
||||
self.plot_item.setXRange(x_min, x_max)
|
||||
self.config.axis.x_lim = (x_min, x_max)
|
||||
|
||||
def set_y_lim(self, *args) -> None:
|
||||
"""
|
||||
Set the limits of the y-axis. This method can accept either two separate arguments
|
||||
for the minimum and maximum y-axis values, or a single tuple containing both limits.
|
||||
|
||||
Usage:
|
||||
set_y_lim(y_min, y_max)
|
||||
set_y_lim((y_min, y_max))
|
||||
|
||||
Args:
|
||||
*args: A variable number of arguments. Can be two integers (y_min and y_max)
|
||||
or a single tuple with two integers.
|
||||
"""
|
||||
if len(args) == 1 and isinstance(args[0], tuple):
|
||||
y_min, y_max = args[0]
|
||||
elif len(args) == 2:
|
||||
y_min, y_max = args
|
||||
else:
|
||||
raise ValueError("set_y_lim expects either two separate arguments or a single tuple")
|
||||
|
||||
self.plot_item.setYRange(y_min, y_max)
|
||||
self.config.axis.y_lim = (y_min, y_max)
|
||||
|
||||
def set_grid(self, x: bool = False, y: bool = False):
|
||||
"""
|
||||
Set the grid of the plot widget.
|
||||
|
||||
Args:
|
||||
x(bool): Show grid on the x-axis.
|
||||
y(bool): Show grid on the y-axis.
|
||||
"""
|
||||
self.plot_item.showGrid(x, y)
|
||||
self.config.axis.x_grid = x
|
||||
self.config.axis.y_grid = y
|
||||
|
||||
def add_legend(self):
|
||||
"""Add legend to the plot"""
|
||||
self.plot_item.addLegend()
|
||||
|
||||
def lock_aspect_ratio(self, lock):
|
||||
"""
|
||||
Lock aspect ratio.
|
||||
|
||||
Args:
|
||||
lock(bool): True to lock, False to unlock.
|
||||
"""
|
||||
self.plot_item.setAspectLocked(lock)
|
||||
|
||||
def remove(self):
|
||||
"""Remove the plot widget from the figure."""
|
||||
if self.figure is not None:
|
||||
self.cleanup()
|
||||
self.figure.remove(widget_id=self.gui_id)
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup the plot widget."""
|
||||
super().cleanup()
|
||||
@@ -1,665 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from typing import Any, Literal, Optional
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.scan_data import ScanData
|
||||
from pydantic import Field, ValidationError
|
||||
from qtpy.QtCore import Signal as pyqtSignal
|
||||
from qtpy.QtCore import Slot as pyqtSlot
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils import Colors, EntryValidator
|
||||
from bec_widgets.widgets.figure.plots.plot_base import BECPlotBase, SubplotConfig
|
||||
from bec_widgets.widgets.figure.plots.waveform.waveform_curve import (
|
||||
BECCurve,
|
||||
CurveConfig,
|
||||
Signal,
|
||||
SignalData,
|
||||
)
|
||||
|
||||
|
||||
class Waveform1DConfig(SubplotConfig):
|
||||
color_palette: Literal["plasma", "viridis", "inferno", "magma"] = Field(
|
||||
"plasma", description="The color palette of the figure widget."
|
||||
) # TODO can be extended to all colormaps from current pyqtgraph session
|
||||
curves: dict[str, CurveConfig] = Field(
|
||||
{}, description="The list of curves to be added to the 1D waveform widget."
|
||||
)
|
||||
|
||||
|
||||
class BECWaveform(BECPlotBase):
|
||||
USER_ACCESS = [
|
||||
"rpc_id",
|
||||
"config_dict",
|
||||
"plot",
|
||||
"remove_curve",
|
||||
"scan_history",
|
||||
"curves",
|
||||
"get_curve",
|
||||
"get_curve_config",
|
||||
"apply_config",
|
||||
"get_all_data",
|
||||
"set",
|
||||
"set_title",
|
||||
"set_x_label",
|
||||
"set_y_label",
|
||||
"set_x_scale",
|
||||
"set_y_scale",
|
||||
"set_x_lim",
|
||||
"set_y_lim",
|
||||
"set_grid",
|
||||
"lock_aspect_ratio",
|
||||
"plot",
|
||||
"remove",
|
||||
]
|
||||
scan_signal_update = pyqtSignal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: Optional[QWidget] = None,
|
||||
parent_figure=None,
|
||||
config: Optional[Waveform1DConfig] = None,
|
||||
client=None,
|
||||
gui_id: Optional[str] = None,
|
||||
):
|
||||
if config is None:
|
||||
config = Waveform1DConfig(widget_class=self.__class__.__name__)
|
||||
super().__init__(
|
||||
parent=parent, parent_figure=parent_figure, config=config, client=client, gui_id=gui_id
|
||||
)
|
||||
|
||||
self._curves_data = defaultdict(dict)
|
||||
self.scan_id = None
|
||||
|
||||
# Scan segment update proxy
|
||||
self.proxy_update_plot = pg.SignalProxy(
|
||||
self.scan_signal_update, rateLimit=25, slot=self._update_scan_segment_plot
|
||||
)
|
||||
|
||||
# Get bec shortcuts dev, scans, queue, scan_storage, dap
|
||||
self.get_bec_shortcuts()
|
||||
|
||||
# Connect dispatcher signals
|
||||
self.bec_dispatcher.connect_slot(self.on_scan_segment, MessageEndpoints.scan_segment())
|
||||
|
||||
self.entry_validator = EntryValidator(self.dev)
|
||||
|
||||
self.add_legend()
|
||||
self.apply_config(self.config)
|
||||
|
||||
def apply_config(self, config: dict | SubplotConfig, replot_last_scan: bool = False):
|
||||
"""
|
||||
Apply the configuration to the 1D waveform widget.
|
||||
|
||||
Args:
|
||||
config(dict|SubplotConfig): Configuration settings.
|
||||
replot_last_scan(bool, optional): If True, replot the last scan. Defaults to False.
|
||||
"""
|
||||
if isinstance(config, dict):
|
||||
try:
|
||||
config = Waveform1DConfig(**config)
|
||||
except ValidationError as e:
|
||||
print(f"Validation error when applying config to BECWaveform1D: {e}")
|
||||
return
|
||||
|
||||
self.config = config
|
||||
self.plot_item.clear() # TODO not sure if on the plot or layout level
|
||||
|
||||
self.apply_axis_config()
|
||||
# Reset curves
|
||||
self._curves_data = defaultdict(dict)
|
||||
self._curves = self.plot_item.curves
|
||||
for curve_id, curve_config in self.config.curves.items():
|
||||
self.add_curve_by_config(curve_config)
|
||||
if replot_last_scan:
|
||||
self.scan_history(scan_index=-1)
|
||||
|
||||
def change_gui_id(self, new_gui_id: str):
|
||||
"""
|
||||
Change the GUI ID of the waveform widget and update the parent_id in all associated curves.
|
||||
|
||||
Args:
|
||||
new_gui_id (str): The new GUI ID to be set for the waveform widget.
|
||||
"""
|
||||
# Update the gui_id in the waveform widget itself
|
||||
self.gui_id = new_gui_id
|
||||
self.config.gui_id = new_gui_id
|
||||
|
||||
for curve in self.curves:
|
||||
curve.config.parent_id = new_gui_id
|
||||
|
||||
def add_curve_by_config(self, curve_config: CurveConfig | dict) -> BECCurve:
|
||||
"""
|
||||
Add a curve to the plot widget by its configuration.
|
||||
|
||||
Args:
|
||||
curve_config(CurveConfig|dict): Configuration of the curve to be added.
|
||||
|
||||
Returns:
|
||||
BECCurve: The curve object.
|
||||
"""
|
||||
if isinstance(curve_config, dict):
|
||||
curve_config = CurveConfig(**curve_config)
|
||||
curve = self._add_curve_object(
|
||||
name=curve_config.label, source=curve_config.source, config=curve_config
|
||||
)
|
||||
return curve
|
||||
|
||||
def get_curve_config(self, curve_id: str, dict_output: bool = True) -> CurveConfig | dict:
|
||||
"""
|
||||
Get the configuration of a curve by its ID.
|
||||
|
||||
Args:
|
||||
curve_id(str): ID of the curve.
|
||||
|
||||
Returns:
|
||||
CurveConfig|dict: Configuration of the curve.
|
||||
"""
|
||||
for source, curves in self._curves_data.items():
|
||||
if curve_id in curves:
|
||||
if dict_output:
|
||||
return curves[curve_id].config.model_dump()
|
||||
else:
|
||||
return curves[curve_id].config
|
||||
|
||||
@property
|
||||
def curves(self) -> list[BECCurve]:
|
||||
"""
|
||||
Get the curves of the plot widget as a list
|
||||
Returns:
|
||||
list: List of curves.
|
||||
"""
|
||||
return self._curves
|
||||
|
||||
@curves.setter
|
||||
def curves(self, value: list[BECCurve]):
|
||||
self._curves = value
|
||||
|
||||
def get_curve(self, identifier) -> BECCurve:
|
||||
"""
|
||||
Get the curve by its index or ID.
|
||||
|
||||
Args:
|
||||
identifier(int|str): Identifier of the curve. Can be either an integer (index) or a string (curve_id).
|
||||
|
||||
Returns:
|
||||
BECCurve: The curve object.
|
||||
"""
|
||||
if isinstance(identifier, int):
|
||||
return self.plot_item.curves[identifier]
|
||||
elif isinstance(identifier, str):
|
||||
for source_type, curves in self._curves_data.items():
|
||||
if identifier in curves:
|
||||
return curves[identifier]
|
||||
raise ValueError(f"Curve with ID '{identifier}' not found.")
|
||||
else:
|
||||
raise ValueError("Identifier must be either an integer (index) or a string (curve_id).")
|
||||
|
||||
def plot(
|
||||
self,
|
||||
x: list | np.ndarray | None = None,
|
||||
y: list | np.ndarray | None = None,
|
||||
x_name: str | None = None,
|
||||
y_name: str | None = None,
|
||||
z_name: str | None = None,
|
||||
x_entry: str | None = None,
|
||||
y_entry: str | None = None,
|
||||
z_entry: str | None = None,
|
||||
color: str | None = None,
|
||||
color_map_z: str | None = "plasma",
|
||||
label: str | None = None,
|
||||
validate: bool = True,
|
||||
) -> BECCurve:
|
||||
"""
|
||||
Plot a curve to the plot widget.
|
||||
Args:
|
||||
x(list | np.ndarray): Custom x data to plot.
|
||||
y(list | np.ndarray): Custom y data to plot.
|
||||
x_name(str): The name of the device for the x-axis.
|
||||
y_name(str): The name of the device for the y-axis.
|
||||
z_name(str): The name of the device for the z-axis.
|
||||
x_entry(str): The name of the entry for the x-axis.
|
||||
y_entry(str): The name of the entry for the y-axis.
|
||||
z_entry(str): The name of the entry for the z-axis.
|
||||
color(str): The color of the curve.
|
||||
color_map_z(str): The color map to use for the z-axis.
|
||||
label(str): The label of the curve.
|
||||
validate(bool): If True, validate the device names and entries.
|
||||
|
||||
Returns:
|
||||
BECCurve: The curve object.
|
||||
"""
|
||||
|
||||
if x is not None and y is not None:
|
||||
return self.add_curve_custom(x=x, y=y, label=label, color=color)
|
||||
else:
|
||||
return self.add_curve_scan(
|
||||
x_name=x_name,
|
||||
y_name=y_name,
|
||||
z_name=z_name,
|
||||
x_entry=x_entry,
|
||||
y_entry=y_entry,
|
||||
z_entry=z_entry,
|
||||
color=color,
|
||||
color_map_z=color_map_z,
|
||||
label=label,
|
||||
validate_bec=validate,
|
||||
)
|
||||
|
||||
def add_curve_custom(
|
||||
self,
|
||||
x: list | np.ndarray,
|
||||
y: list | np.ndarray,
|
||||
label: str = None,
|
||||
color: str = None,
|
||||
**kwargs,
|
||||
) -> BECCurve:
|
||||
"""
|
||||
Add a custom data curve to the plot widget.
|
||||
|
||||
Args:
|
||||
x(list|np.ndarray): X data of the curve.
|
||||
y(list|np.ndarray): Y data of the curve.
|
||||
label(str, optional): Label of the curve. Defaults to None.
|
||||
color(str, optional): Color of the curve. Defaults to None.
|
||||
**kwargs: Additional keyword arguments for the curve configuration.
|
||||
|
||||
Returns:
|
||||
BECCurve: The curve object.
|
||||
"""
|
||||
curve_source = "custom"
|
||||
curve_id = label or f"Curve {len(self.plot_item.curves) + 1}"
|
||||
|
||||
curve_exits = self._check_curve_id(curve_id, self._curves_data)
|
||||
if curve_exits:
|
||||
raise ValueError(
|
||||
f"Curve with ID '{curve_id}' already exists in widget '{self.gui_id}'."
|
||||
)
|
||||
|
||||
color = (
|
||||
color
|
||||
or Colors.golden_angle_color(
|
||||
colormap=self.config.color_palette, num=len(self.plot_item.curves) + 1, format="HEX"
|
||||
)[-1]
|
||||
)
|
||||
|
||||
# Create curve by config
|
||||
curve_config = CurveConfig(
|
||||
widget_class="BECCurve",
|
||||
parent_id=self.gui_id,
|
||||
label=curve_id,
|
||||
color=color,
|
||||
source=curve_source,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
curve = self._add_curve_object(
|
||||
name=curve_id, source=curve_source, config=curve_config, data=(x, y)
|
||||
)
|
||||
return curve
|
||||
|
||||
def add_curve_scan(
|
||||
self,
|
||||
x_name: str,
|
||||
y_name: str,
|
||||
z_name: Optional[str] = None,
|
||||
x_entry: Optional[str] = None,
|
||||
y_entry: Optional[str] = None,
|
||||
z_entry: Optional[str] = None,
|
||||
color: Optional[str] = None,
|
||||
color_map_z: Optional[str] = "plasma",
|
||||
label: Optional[str] = None,
|
||||
validate_bec: bool = True,
|
||||
**kwargs,
|
||||
) -> BECCurve:
|
||||
"""
|
||||
Add a curve to the plot widget from the scan segment.
|
||||
|
||||
Args:
|
||||
x_name(str): Name of the x signal.
|
||||
x_entry(str): Entry of the x signal.
|
||||
y_name(str): Name of the y signal.
|
||||
y_entry(str): Entry of the y signal.
|
||||
z_name(str): Name of the z signal.
|
||||
z_entry(str): Entry of the z signal.
|
||||
color(str, optional): Color of the curve. Defaults to None.
|
||||
color_map_z(str): The color map to use for the z-axis.
|
||||
label(str, optional): Label of the curve. Defaults to None.
|
||||
**kwargs: Additional keyword arguments for the curve configuration.
|
||||
|
||||
Returns:
|
||||
BECCurve: The curve object.
|
||||
"""
|
||||
# Check if curve already exists
|
||||
curve_source = "scan_segment"
|
||||
|
||||
# Get entry if not provided and validate
|
||||
x_entry, y_entry, z_entry = self._validate_signal_entries(
|
||||
x_name, y_name, z_name, x_entry, y_entry, z_entry, validate_bec
|
||||
)
|
||||
|
||||
if z_name is not None and z_entry is not None:
|
||||
label = label or f"{z_name}-{z_entry}"
|
||||
else:
|
||||
label = label or f"{y_name}-{y_entry}"
|
||||
|
||||
curve_exits = self._check_curve_id(label, self._curves_data)
|
||||
if curve_exits:
|
||||
raise ValueError(f"Curve with ID '{label}' already exists in widget '{self.gui_id}'.")
|
||||
|
||||
color = (
|
||||
color
|
||||
or Colors.golden_angle_color(
|
||||
colormap=self.config.color_palette, num=len(self.plot_item.curves) + 1, format="HEX"
|
||||
)[-1]
|
||||
)
|
||||
|
||||
# Create curve by config
|
||||
curve_config = CurveConfig(
|
||||
widget_class="BECCurve",
|
||||
parent_id=self.gui_id,
|
||||
label=label,
|
||||
color=color,
|
||||
color_map_z=color_map_z,
|
||||
source=curve_source,
|
||||
signals=Signal(
|
||||
source=curve_source,
|
||||
x=SignalData(name=x_name, entry=x_entry),
|
||||
y=SignalData(name=y_name, entry=y_entry),
|
||||
z=SignalData(name=z_name, entry=z_entry) if z_name else None,
|
||||
),
|
||||
**kwargs,
|
||||
)
|
||||
curve = self._add_curve_object(name=label, source=curve_source, config=curve_config)
|
||||
return curve
|
||||
|
||||
def _add_curve_object(
|
||||
self,
|
||||
name: str,
|
||||
source: str,
|
||||
config: CurveConfig,
|
||||
data: tuple[list | np.ndarray, list | np.ndarray] = None,
|
||||
) -> BECCurve:
|
||||
"""
|
||||
Add a curve object to the plot widget.
|
||||
|
||||
Args:
|
||||
name(str): ID of the curve.
|
||||
source(str): Source of the curve.
|
||||
config(CurveConfig): Configuration of the curve.
|
||||
data(tuple[list|np.ndarray,list|np.ndarray], optional): Data (x,y) to be plotted. Defaults to None.
|
||||
|
||||
Returns:
|
||||
BECCurve: The curve object.
|
||||
"""
|
||||
curve = BECCurve(config=config, name=name, parent_item=self)
|
||||
self._curves_data[source][name] = curve
|
||||
self.plot_item.addItem(curve)
|
||||
self.config.curves[name] = curve.config
|
||||
if data is not None:
|
||||
curve.setData(data[0], data[1])
|
||||
return curve
|
||||
|
||||
def _validate_signal_entries(
|
||||
self,
|
||||
x_name: str,
|
||||
y_name: str,
|
||||
z_name: str | None,
|
||||
x_entry: str | None,
|
||||
y_entry: str | None,
|
||||
z_entry: str | None,
|
||||
validate_bec: bool = True,
|
||||
) -> tuple[str, str, str | None]:
|
||||
"""
|
||||
Validate the signal name and entry.
|
||||
|
||||
Args:
|
||||
x_name(str): Name of the x signal.
|
||||
y_name(str): Name of the y signal.
|
||||
z_name(str): Name of the z signal.
|
||||
x_entry(str|None): Entry of the x signal.
|
||||
y_entry(str|None): Entry of the y signal.
|
||||
z_entry(str|None): Entry of the z signal.
|
||||
validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True.
|
||||
|
||||
Returns:
|
||||
tuple[str,str,str|None]: Validated x, y, z entries.
|
||||
"""
|
||||
if validate_bec:
|
||||
x_entry = self.entry_validator.validate_signal(x_name, x_entry)
|
||||
y_entry = self.entry_validator.validate_signal(y_name, y_entry)
|
||||
if z_name:
|
||||
z_entry = self.entry_validator.validate_signal(z_name, z_entry)
|
||||
else:
|
||||
x_entry = x_name if x_entry is None else x_entry
|
||||
y_entry = y_name if y_entry is None else y_entry
|
||||
z_entry = z_name if z_entry is None else z_entry
|
||||
return x_entry, y_entry, z_entry
|
||||
|
||||
def _check_curve_id(self, val: Any, dict_to_check: dict) -> bool:
|
||||
"""
|
||||
Check if val is in the values of the dict_to_check or in the values of the nested dictionaries.
|
||||
|
||||
Args:
|
||||
val(Any): Value to check.
|
||||
dict_to_check(dict): Dictionary to check.
|
||||
|
||||
Returns:
|
||||
bool: True if val is in the values of the dict_to_check or in the values of the nested dictionaries, False otherwise.
|
||||
"""
|
||||
if val in dict_to_check.keys():
|
||||
return True
|
||||
for key in dict_to_check:
|
||||
if isinstance(dict_to_check[key], dict):
|
||||
if self._check_curve_id(val, dict_to_check[key]):
|
||||
return True
|
||||
return False
|
||||
|
||||
def remove_curve(self, *identifiers):
|
||||
"""
|
||||
Remove a curve from the plot widget.
|
||||
|
||||
Args:
|
||||
*identifiers: Identifier of the curve to be removed. Can be either an integer (index) or a string (curve_id).
|
||||
"""
|
||||
for identifier in identifiers:
|
||||
if isinstance(identifier, int):
|
||||
self._remove_curve_by_order(identifier)
|
||||
elif isinstance(identifier, str):
|
||||
self._remove_curve_by_id(identifier)
|
||||
else:
|
||||
raise ValueError(
|
||||
"Each identifier must be either an integer (index) or a string (curve_id)."
|
||||
)
|
||||
|
||||
def _remove_curve_by_id(self, curve_id):
|
||||
"""
|
||||
Remove a curve by its ID from the plot widget.
|
||||
|
||||
Args:
|
||||
curve_id(str): ID of the curve to be removed.
|
||||
"""
|
||||
for source, curves in self._curves_data.items():
|
||||
if curve_id in curves:
|
||||
curve = curves.pop(curve_id)
|
||||
self.plot_item.removeItem(curve)
|
||||
del self.config.curves[curve_id]
|
||||
if curve in self.plot_item.curves:
|
||||
self.plot_item.curves.remove(curve)
|
||||
return
|
||||
raise KeyError(f"Curve with ID '{curve_id}' not found.")
|
||||
|
||||
def _remove_curve_by_order(self, N):
|
||||
"""
|
||||
Remove a curve by its order from the plot widget.
|
||||
|
||||
Args:
|
||||
N(int): Order of the curve to be removed.
|
||||
"""
|
||||
if N < len(self.plot_item.curves):
|
||||
curve = self.plot_item.curves[N]
|
||||
curve_id = curve.name() # Assuming curve's name is used as its ID
|
||||
self.plot_item.removeItem(curve)
|
||||
del self.config.curves[curve_id]
|
||||
# Remove from self.curve_data
|
||||
for source, curves in self._curves_data.items():
|
||||
if curve_id in curves:
|
||||
del curves[curve_id]
|
||||
break
|
||||
else:
|
||||
raise IndexError(f"Curve order {N} out of range.")
|
||||
|
||||
@pyqtSlot(dict, dict)
|
||||
def on_scan_segment(self, msg: dict, metadata: dict):
|
||||
"""
|
||||
Handle new scan segments and saves data to a dictionary. Linked through bec_dispatcher.
|
||||
|
||||
Args:
|
||||
msg (dict): Message received with scan data.
|
||||
metadata (dict): Metadata of the scan.
|
||||
"""
|
||||
current_scan_id = msg.get("scan_id", None)
|
||||
if current_scan_id is None:
|
||||
return
|
||||
|
||||
if current_scan_id != self.scan_id:
|
||||
self.scan_id = current_scan_id
|
||||
self.scan_segment_data = self.queue.scan_storage.find_scan_by_ID(
|
||||
self.scan_id
|
||||
) # TODO do scan access through BECFigure
|
||||
|
||||
self.scan_signal_update.emit()
|
||||
|
||||
def _update_scan_segment_plot(self):
|
||||
"""Update the plot with the data from the scan segment."""
|
||||
data = self.scan_segment_data.data
|
||||
self._update_scan_curves(data)
|
||||
|
||||
def _update_scan_curves(self, data: ScanData):
|
||||
"""
|
||||
Update the scan curves with the data from the scan segment.
|
||||
|
||||
Args:
|
||||
data(ScanData): Data from the scan segment.
|
||||
"""
|
||||
data_x = None
|
||||
data_y = None
|
||||
data_z = None
|
||||
for curve_id, curve in self._curves_data["scan_segment"].items():
|
||||
x_name = curve.config.signals.x.name
|
||||
x_entry = curve.config.signals.x.entry
|
||||
y_name = curve.config.signals.y.name
|
||||
y_entry = curve.config.signals.y.entry
|
||||
if curve.config.signals.z:
|
||||
z_name = curve.config.signals.z.name
|
||||
z_entry = curve.config.signals.z.entry
|
||||
|
||||
try:
|
||||
data_x = data[x_name][x_entry].val
|
||||
data_y = data[y_name][y_entry].val
|
||||
if curve.config.signals.z:
|
||||
data_z = data[z_name][z_entry].val
|
||||
color_z = self._make_z_gradient(
|
||||
data_z, curve.config.color_map_z
|
||||
) # TODO decide how to implement custom gradient
|
||||
except TypeError:
|
||||
continue
|
||||
|
||||
if data_z is not None and color_z is not None:
|
||||
curve.setData(x=data_x, y=data_y, symbolBrush=color_z)
|
||||
else:
|
||||
curve.setData(data_x, data_y)
|
||||
|
||||
def _make_z_gradient(self, data_z: list | np.ndarray, colormap: str) -> list | None:
|
||||
"""
|
||||
Make a gradient color for the z values.
|
||||
|
||||
Args:
|
||||
data_z(list|np.ndarray): Z values.
|
||||
colormap(str): Colormap for the gradient color.
|
||||
|
||||
Returns:
|
||||
list: List of colors for the z values.
|
||||
"""
|
||||
# Normalize z_values for color mapping
|
||||
z_min, z_max = np.min(data_z), np.max(data_z)
|
||||
|
||||
if z_max != z_min: # Ensure that there is a range in the z values
|
||||
z_values_norm = (data_z - z_min) / (z_max - z_min)
|
||||
colormap = pg.colormap.get(colormap) # using colormap from global settings
|
||||
colors = [colormap.map(z, mode="qcolor") for z in z_values_norm]
|
||||
return colors
|
||||
else:
|
||||
return None
|
||||
|
||||
def scan_history(self, scan_index: int = None, scan_id: str = None):
|
||||
"""
|
||||
Update the scan curves with the data from the scan storage.
|
||||
Provide only one of scan_id or scan_index.
|
||||
|
||||
Args:
|
||||
scan_id(str, optional): ScanID of the scan to be updated. Defaults to None.
|
||||
scan_index(int, optional): Index of the scan to be updated. Defaults to None.
|
||||
"""
|
||||
if scan_index is not None and scan_id is not None:
|
||||
raise ValueError("Only one of scan_id or scan_index can be provided.")
|
||||
|
||||
if scan_index is not None:
|
||||
self.scan_id = self.queue.scan_storage.storage[scan_index].scan_id
|
||||
data = self.queue.scan_storage.find_scan_by_ID(self.scan_id).data
|
||||
elif scan_id is not None:
|
||||
self.scan_id = scan_id
|
||||
data = self.queue.scan_storage.find_scan_by_ID(self.scan_id).data
|
||||
|
||||
self._update_scan_curves(data)
|
||||
|
||||
def get_all_data(self, output: Literal["dict", "pandas"] = "dict") -> dict | pd.DataFrame:
|
||||
"""
|
||||
Extract all curve data into a dictionary or a pandas DataFrame.
|
||||
|
||||
Args:
|
||||
output (Literal["dict", "pandas"]): Format of the output data.
|
||||
|
||||
Returns:
|
||||
dict | pd.DataFrame: Data of all curves in the specified format.
|
||||
"""
|
||||
data = {}
|
||||
try:
|
||||
import pandas as pd
|
||||
except ImportError:
|
||||
pd = None
|
||||
if output == "pandas":
|
||||
print(
|
||||
"Pandas is not installed. "
|
||||
"Please install pandas using 'pip install pandas'."
|
||||
"Output will be dictionary instead."
|
||||
)
|
||||
output = "dict"
|
||||
|
||||
for curve in self.plot_item.curves:
|
||||
x_data, y_data = curve.get_data()
|
||||
if x_data is not None or y_data is not None:
|
||||
if output == "dict":
|
||||
data[curve.name()] = {"x": x_data.tolist(), "y": y_data.tolist()}
|
||||
elif output == "pandas" and pd is not None:
|
||||
data[curve.name()] = pd.DataFrame({"x": x_data, "y": y_data})
|
||||
|
||||
if output == "pandas" and pd is not None:
|
||||
combined_data = pd.concat(
|
||||
[data[curve.name()] for curve in self.plot_item.curves],
|
||||
axis=1,
|
||||
keys=[curve.name() for curve in self.plot_item.curves],
|
||||
)
|
||||
return combined_data
|
||||
return data
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup the widget connection from BECDispatcher."""
|
||||
self.bec_dispatcher.disconnect_slot(self.on_scan_segment, MessageEndpoints.scan_segment())
|
||||
for curve in self.curves:
|
||||
curve.cleanup()
|
||||
super().cleanup()
|
||||
@@ -1,245 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Literal, Optional
|
||||
|
||||
import pyqtgraph as pg
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from pydantic_core import PydanticCustomError
|
||||
from qtpy import QtCore
|
||||
|
||||
from bec_widgets.utils import BECConnector, Colors, ConnectionConfig
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bec_widgets.widgets.figure.plots.waveform import BECWaveform1D
|
||||
|
||||
|
||||
class SignalData(BaseModel):
|
||||
"""The data configuration of a signal in the 1D waveform widget for x and y axis."""
|
||||
|
||||
name: str
|
||||
entry: str
|
||||
unit: Optional[str] = None # todo implement later
|
||||
modifier: Optional[str] = None # todo implement later
|
||||
limits: Optional[list[float]] = None # todo implement later
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
|
||||
|
||||
class Signal(BaseModel):
|
||||
"""The configuration of a signal in the 1D waveform widget."""
|
||||
|
||||
source: str
|
||||
x: SignalData # TODO maybe add metadata for config gui later
|
||||
y: SignalData
|
||||
z: Optional[SignalData] = None
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
|
||||
|
||||
class CurveConfig(ConnectionConfig):
|
||||
parent_id: Optional[str] = Field(None, description="The parent plot of the curve.")
|
||||
label: Optional[str] = Field(None, description="The label of the curve.")
|
||||
color: Optional[str | tuple] = Field(None, description="The color of the curve.")
|
||||
symbol: Optional[str] = Field("o", description="The symbol of the curve.")
|
||||
symbol_color: Optional[str | tuple] = Field(
|
||||
None, description="The color of the symbol of the curve."
|
||||
)
|
||||
symbol_size: Optional[int] = Field(5, description="The size of the symbol of the curve.")
|
||||
pen_width: Optional[int] = Field(2, description="The width of the pen of the curve.")
|
||||
pen_style: Optional[Literal["solid", "dash", "dot", "dashdot"]] = Field(
|
||||
"solid", description="The style of the pen of the curve."
|
||||
)
|
||||
source: Optional[str] = Field(None, description="The source of the curve.")
|
||||
signals: Optional[Signal] = Field(None, description="The signal of the curve.")
|
||||
color_map_z: Optional[str] = Field(
|
||||
"plasma", description="The colormap of the curves z gradient.", validate_default=True
|
||||
)
|
||||
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
|
||||
_validate_color_map_z = field_validator("color_map_z")(Colors.validate_color_map)
|
||||
_validate_color = field_validator("color")(Colors.validate_color)
|
||||
_validate_symbol_color = field_validator("symbol_color")(Colors.validate_color)
|
||||
|
||||
|
||||
class BECCurve(BECConnector, pg.PlotDataItem):
|
||||
USER_ACCESS = [
|
||||
"remove",
|
||||
"rpc_id",
|
||||
"config_dict",
|
||||
"set",
|
||||
"set_data",
|
||||
"set_color",
|
||||
"set_colormap_z",
|
||||
"set_symbol",
|
||||
"set_symbol_color",
|
||||
"set_symbol_size",
|
||||
"set_pen_width",
|
||||
"set_pen_style",
|
||||
"get_data",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: Optional[str] = None,
|
||||
config: Optional[CurveConfig] = None,
|
||||
gui_id: Optional[str] = None,
|
||||
parent_item: Optional[BECWaveform1D] = None,
|
||||
**kwargs,
|
||||
):
|
||||
if config is None:
|
||||
config = CurveConfig(label=name, widget_class=self.__class__.__name__)
|
||||
self.config = config
|
||||
else:
|
||||
self.config = config
|
||||
# config.widget_class = self.__class__.__name__
|
||||
super().__init__(config=config, gui_id=gui_id)
|
||||
pg.PlotDataItem.__init__(self, name=name)
|
||||
|
||||
self.parent_item = parent_item
|
||||
self.apply_config()
|
||||
if kwargs:
|
||||
self.set(**kwargs)
|
||||
|
||||
def apply_config(self):
|
||||
pen_style_map = {
|
||||
"solid": QtCore.Qt.SolidLine,
|
||||
"dash": QtCore.Qt.DashLine,
|
||||
"dot": QtCore.Qt.DotLine,
|
||||
"dashdot": QtCore.Qt.DashDotLine,
|
||||
}
|
||||
pen_style = pen_style_map.get(self.config.pen_style, QtCore.Qt.SolidLine)
|
||||
|
||||
pen = pg.mkPen(color=self.config.color, width=self.config.pen_width, style=pen_style)
|
||||
self.setPen(pen)
|
||||
|
||||
if self.config.symbol:
|
||||
symbol_color = self.config.symbol_color or self.config.color
|
||||
brush = pg.mkBrush(color=symbol_color)
|
||||
|
||||
self.setSymbolBrush(brush)
|
||||
self.setSymbolSize(self.config.symbol_size)
|
||||
self.setSymbol(self.config.symbol)
|
||||
|
||||
def set_data(self, x, y):
|
||||
if self.config.source == "custom":
|
||||
self.setData(x, y)
|
||||
else:
|
||||
raise ValueError(f"Source {self.config.source} do not allow custom data setting.")
|
||||
|
||||
def set(self, **kwargs):
|
||||
"""
|
||||
Set the properties of the curve.
|
||||
|
||||
Args:
|
||||
**kwargs: Keyword arguments for the properties to be set.
|
||||
|
||||
Possible properties:
|
||||
- color: str
|
||||
- symbol: str
|
||||
- symbol_color: str
|
||||
- symbol_size: int
|
||||
- pen_width: int
|
||||
- pen_style: Literal["solid", "dash", "dot", "dashdot"]
|
||||
"""
|
||||
|
||||
# Mapping of keywords to setter methods
|
||||
method_map = {
|
||||
"color": self.set_color,
|
||||
"color_map_z": self.set_color_map_z,
|
||||
"symbol": self.set_symbol,
|
||||
"symbol_color": self.set_symbol_color,
|
||||
"symbol_size": self.set_symbol_size,
|
||||
"pen_width": self.set_pen_width,
|
||||
"pen_style": self.set_pen_style,
|
||||
}
|
||||
for key, value in kwargs.items():
|
||||
if key in method_map:
|
||||
method_map[key](value)
|
||||
else:
|
||||
print(f"Warning: '{key}' is not a recognized property.")
|
||||
|
||||
def set_color(self, color: str, symbol_color: Optional[str] = None):
|
||||
"""
|
||||
Change the color of the curve.
|
||||
|
||||
Args:
|
||||
color(str): Color of the curve.
|
||||
symbol_color(str, optional): Color of the symbol. Defaults to None.
|
||||
"""
|
||||
self.config.color = color
|
||||
self.config.symbol_color = symbol_color or color
|
||||
self.apply_config()
|
||||
|
||||
def set_symbol(self, symbol: str):
|
||||
"""
|
||||
Change the symbol of the curve.
|
||||
|
||||
Args:
|
||||
symbol(str): Symbol of the curve.
|
||||
"""
|
||||
self.config.symbol = symbol
|
||||
self.apply_config()
|
||||
|
||||
def set_symbol_color(self, symbol_color: str):
|
||||
"""
|
||||
Change the symbol color of the curve.
|
||||
|
||||
Args:
|
||||
symbol_color(str): Color of the symbol.
|
||||
"""
|
||||
self.config.symbol_color = symbol_color
|
||||
self.apply_config()
|
||||
|
||||
def set_symbol_size(self, symbol_size: int):
|
||||
"""
|
||||
Change the symbol size of the curve.
|
||||
|
||||
Args:
|
||||
symbol_size(int): Size of the symbol.
|
||||
"""
|
||||
self.config.symbol_size = symbol_size
|
||||
self.apply_config()
|
||||
|
||||
def set_pen_width(self, pen_width: int):
|
||||
"""
|
||||
Change the pen width of the curve.
|
||||
|
||||
Args:
|
||||
pen_width(int): Width of the pen.
|
||||
"""
|
||||
self.config.pen_width = pen_width
|
||||
self.apply_config()
|
||||
|
||||
def set_pen_style(self, pen_style: Literal["solid", "dash", "dot", "dashdot"]):
|
||||
"""
|
||||
Change the pen style of the curve.
|
||||
|
||||
Args:
|
||||
pen_style(Literal["solid", "dash", "dot", "dashdot"]): Style of the pen.
|
||||
"""
|
||||
self.config.pen_style = pen_style
|
||||
self.apply_config()
|
||||
|
||||
def set_color_map_z(self, colormap: str):
|
||||
"""
|
||||
Set the colormap for the scatter plot z gradient.
|
||||
|
||||
Args:
|
||||
colormap(str): Colormap for the scatter plot.
|
||||
"""
|
||||
self.config.color_map_z = colormap
|
||||
self.apply_config()
|
||||
self.parent_item.scan_history(-1)
|
||||
|
||||
def get_data(self) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""
|
||||
Get the data of the curve.
|
||||
Returns:
|
||||
tuple[np.ndarray,np.ndarray]: X and Y data of the curve.
|
||||
"""
|
||||
x_data, y_data = self.getData()
|
||||
return x_data, y_data
|
||||
|
||||
def remove(self):
|
||||
"""Remove the curve from the plot."""
|
||||
self.parent_item.removeItem(self)
|
||||
self.cleanup()
|
||||
@@ -1,72 +0,0 @@
|
||||
from bec_ipython_client.main import BECIPythonClient
|
||||
from qtconsole.inprocess import QtInProcessKernelManager
|
||||
from qtconsole.manager import QtKernelManager
|
||||
from qtconsole.rich_jupyter_widget import RichJupyterWidget
|
||||
from qtpy.QtWidgets import QApplication, QMainWindow
|
||||
|
||||
|
||||
class BECJupyterConsole(RichJupyterWidget): # pragma: no cover:
|
||||
def __init__(self, inprocess: bool = False):
|
||||
super().__init__()
|
||||
|
||||
self.inprocess = None
|
||||
|
||||
self.kernel_manager, self.kernel_client = self._init_kernel(inprocess=inprocess)
|
||||
self.set_default_style("linux")
|
||||
self._init_bec()
|
||||
|
||||
def _init_kernel(self, inprocess: bool = False, kernel_name: str = "python3"):
|
||||
self.inprocess = inprocess
|
||||
if inprocess is True:
|
||||
print("starting inprocess kernel")
|
||||
kernel_manager = QtInProcessKernelManager()
|
||||
else:
|
||||
kernel_manager = QtKernelManager(kernel_name=kernel_name)
|
||||
kernel_manager.start_kernel()
|
||||
kernel_client = kernel_manager.client()
|
||||
kernel_client.start_channels()
|
||||
return kernel_manager, kernel_client
|
||||
|
||||
def _init_bec(self):
|
||||
if self.inprocess is True:
|
||||
self._init_bec_inprocess()
|
||||
else:
|
||||
self._init_bec_kernel()
|
||||
|
||||
def _init_bec_inprocess(self):
|
||||
self.client = BECIPythonClient()
|
||||
self.client.start()
|
||||
|
||||
self.kernel_manager.kernel.shell.push(
|
||||
{
|
||||
"bec": self.client,
|
||||
"dev": self.client.device_manager.devices,
|
||||
"scans": self.client.scans,
|
||||
}
|
||||
)
|
||||
|
||||
def _init_bec_kernel(self):
|
||||
self.execute(
|
||||
"""
|
||||
from bec_ipython_client.main import BECIPythonClient
|
||||
bec = BECIPythonClient()
|
||||
bec.start()
|
||||
dev = bec.device_manager.devices if bec else None
|
||||
scans = bec.scans if bec else None
|
||||
"""
|
||||
)
|
||||
|
||||
def shutdown_kernel(self):
|
||||
self.kernel_client.stop_channels()
|
||||
self.kernel_manager.shutdown_kernel()
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
win = QMainWindow()
|
||||
win.setCentralWidget(BECJupyterConsole(True))
|
||||
win.show()
|
||||
|
||||
sys.exit(app.exec_())
|
||||
@@ -1,252 +0,0 @@
|
||||
# pylint: disable = no-name-in-module,missing-module-docstring
|
||||
from enum import Enum
|
||||
|
||||
from bec_lib.alarm_handler import AlarmBase
|
||||
from bec_lib.device import Positioner
|
||||
from qtpy.QtCore import QThread
|
||||
from qtpy.QtCore import Signal as pyqtSignal
|
||||
from qtpy.QtCore import Slot as pyqtSlot
|
||||
from qtpy.QtWidgets import QMessageBox, QWidget
|
||||
|
||||
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
||||
|
||||
CONFIG_DEFAULT = {
|
||||
"motor_control": {
|
||||
"motor_x": "samx",
|
||||
"motor_y": "samy",
|
||||
"step_size_x": 3,
|
||||
"step_size_y": 50,
|
||||
"precision": 4,
|
||||
"step_x_y_same": False,
|
||||
"move_with_arrows": False,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class MotorControlWidget(QWidget):
|
||||
"""Base class for motor control widgets."""
|
||||
|
||||
def __init__(self, parent=None, client=None, motor_thread=None, config=None):
|
||||
super().__init__(parent)
|
||||
self.client = client
|
||||
self.motor_thread = motor_thread
|
||||
self.config = config
|
||||
|
||||
self.motor_x = None
|
||||
self.motor_y = None
|
||||
|
||||
if not self.client:
|
||||
bec_dispatcher = BECDispatcher()
|
||||
self.client = bec_dispatcher.client
|
||||
|
||||
if not self.motor_thread:
|
||||
self.motor_thread = MotorThread(client=self.client)
|
||||
|
||||
self._load_ui()
|
||||
|
||||
if self.config is None:
|
||||
print(f"No initial config found for {self.__class__.__name__}")
|
||||
self._init_ui()
|
||||
else:
|
||||
self.on_config_update(self.config)
|
||||
|
||||
def _load_ui(self):
|
||||
"""Load the UI from the .ui file."""
|
||||
|
||||
def _init_ui(self):
|
||||
"""Initialize the UI components specific to the widget."""
|
||||
|
||||
@pyqtSlot(dict)
|
||||
def on_config_update(self, config):
|
||||
"""Handle configuration updates."""
|
||||
self.config = config
|
||||
self._init_ui()
|
||||
|
||||
|
||||
class MotorControlErrors:
|
||||
"""Class for displaying formatted error messages."""
|
||||
|
||||
@staticmethod
|
||||
def display_error_message(error_message: str) -> None:
|
||||
"""
|
||||
Display a critical error message.
|
||||
Args:
|
||||
error_message(str): Error message to display.
|
||||
"""
|
||||
# Create a QMessageBox
|
||||
msg = QMessageBox()
|
||||
msg.setIcon(QMessageBox.Critical)
|
||||
msg.setWindowTitle("Critical Error")
|
||||
|
||||
# Format the message
|
||||
formatted_message = MotorControlErrors._format_error_message(error_message)
|
||||
msg.setText(formatted_message)
|
||||
|
||||
# Display the message box
|
||||
msg.exec_()
|
||||
|
||||
@staticmethod
|
||||
def _format_error_message(error_message: str) -> str:
|
||||
"""
|
||||
Format the error message.
|
||||
Args:
|
||||
error_message(str): Error message to format.
|
||||
|
||||
Returns:
|
||||
str: Formatted error message.
|
||||
"""
|
||||
# Split the message into lines
|
||||
lines = error_message.split("\n")
|
||||
formatted_lines = [
|
||||
f"<b>{line.strip()}</b>" if i == 0 else line.strip()
|
||||
for i, line in enumerate(lines)
|
||||
if line.strip()
|
||||
]
|
||||
|
||||
# Join the lines with double breaks for empty lines in between
|
||||
formatted_message = "<br><br>".join(formatted_lines)
|
||||
|
||||
return formatted_message
|
||||
|
||||
|
||||
class MotorActions(Enum):
|
||||
"""Enum for motor actions."""
|
||||
|
||||
MOVE_ABSOLUTE = "move_absolute"
|
||||
MOVE_RELATIVE = "move_relative"
|
||||
|
||||
|
||||
class MotorThread(QThread):
|
||||
"""
|
||||
QThread subclass for controlling motor actions asynchronously.
|
||||
|
||||
Signals:
|
||||
coordinates_updated (pyqtSignal): Signal to emit current coordinates.
|
||||
motor_error (pyqtSignal): Signal to emit when there is an error with the motors.
|
||||
lock_gui (pyqtSignal): Signal to lock/unlock the GUI.
|
||||
"""
|
||||
|
||||
coordinates_updated = pyqtSignal(float, float) # Signal to emit current coordinates
|
||||
motor_error = pyqtSignal(str) # Signal to emit when there is an error with the motors
|
||||
lock_gui = pyqtSignal(bool) # Signal to lock/unlock the GUI
|
||||
|
||||
def __init__(self, parent=None, client=None):
|
||||
super().__init__(parent)
|
||||
|
||||
bec_dispatcher = BECDispatcher()
|
||||
self.client = bec_dispatcher.client if client is None else client
|
||||
self.dev = self.client.device_manager.devices
|
||||
self.scans = self.client.scans
|
||||
self.queue = self.client.queue
|
||||
self.action = None
|
||||
|
||||
self.motor = None
|
||||
self.motor_x = None
|
||||
self.motor_y = None
|
||||
self.target_coordinates = None
|
||||
self.value = None
|
||||
|
||||
def get_all_motors_names(self) -> list:
|
||||
"""
|
||||
Get all the motors names.
|
||||
Returns:
|
||||
list: List of all the motors names.
|
||||
"""
|
||||
all_devices = self.client.device_manager.devices.enabled_devices
|
||||
all_motors_names = [motor.name for motor in all_devices if isinstance(motor, Positioner)]
|
||||
return all_motors_names
|
||||
|
||||
def get_coordinates(self, motor_x: str, motor_y: str) -> tuple:
|
||||
"""
|
||||
Get the current coordinates of the motors.
|
||||
Args:
|
||||
motor_x(str): Motor X to get positions from.
|
||||
motor_y(str): Motor Y to get positions from.
|
||||
|
||||
Returns:
|
||||
tuple: Current coordinates of the motors.
|
||||
"""
|
||||
x = self.dev[motor_x].readback.get()
|
||||
y = self.dev[motor_y].readback.get()
|
||||
return x, y
|
||||
|
||||
def move_absolute(self, motor_x: str, motor_y: str, target_coordinates: tuple) -> None:
|
||||
"""
|
||||
Wrapper for moving the motor to the target coordinates.
|
||||
Args:
|
||||
motor_x(str): Motor X to move.
|
||||
motor_y(str): Motor Y to move.
|
||||
target_coordinates(tuple): Target coordinates.
|
||||
"""
|
||||
self.action = MotorActions.MOVE_ABSOLUTE
|
||||
self.motor_x = motor_x
|
||||
self.motor_y = motor_y
|
||||
self.target_coordinates = target_coordinates
|
||||
self.start()
|
||||
|
||||
def move_relative(self, motor: str, value: float) -> None:
|
||||
"""
|
||||
Wrapper for moving the motor relative to the current position.
|
||||
Args:
|
||||
motor(str): Motor to move.
|
||||
value(float): Value to move.
|
||||
"""
|
||||
self.action = MotorActions.MOVE_RELATIVE
|
||||
self.motor = motor
|
||||
self.value = value
|
||||
self.start()
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Run the thread.
|
||||
Possible actions:
|
||||
- Move to coordinates
|
||||
- Move relative
|
||||
"""
|
||||
if self.action == MotorActions.MOVE_ABSOLUTE:
|
||||
self._move_motor_absolute(self.motor_x, self.motor_y, self.target_coordinates)
|
||||
elif self.action == MotorActions.MOVE_RELATIVE:
|
||||
self._move_motor_relative(self.motor, self.value)
|
||||
|
||||
def _move_motor_absolute(self, motor_x: str, motor_y: str, target_coordinates: tuple) -> None:
|
||||
"""
|
||||
Move the motor to the target coordinates.
|
||||
Args:
|
||||
motor_x(str): Motor X to move.
|
||||
motor_y(str): Motor Y to move.
|
||||
target_coordinates(tuple): Target coordinates.
|
||||
"""
|
||||
self.lock_gui.emit(False)
|
||||
try:
|
||||
status = self.scans.mv(
|
||||
self.dev[motor_x],
|
||||
target_coordinates[0],
|
||||
self.dev[motor_y],
|
||||
target_coordinates[1],
|
||||
relative=False,
|
||||
)
|
||||
status.wait()
|
||||
except AlarmBase as e:
|
||||
self.motor_error.emit(str(e))
|
||||
finally:
|
||||
self.lock_gui.emit(True)
|
||||
|
||||
def _move_motor_relative(self, motor, value: float) -> None:
|
||||
"""
|
||||
Move the motor relative to the current position.
|
||||
Args:
|
||||
motor(str): Motor to move.
|
||||
value(float): Value to move.
|
||||
"""
|
||||
self.lock_gui.emit(False)
|
||||
try:
|
||||
status = self.scans.mv(self.dev[motor], value, relative=True)
|
||||
status.wait()
|
||||
except AlarmBase as e:
|
||||
self.motor_error.emit(str(e))
|
||||
finally:
|
||||
self.lock_gui.emit(True)
|
||||
|
||||
def stop_movement(self):
|
||||
self.queue.request_scan_abortion()
|
||||
self.queue.request_queue_reset()
|
||||
@@ -1,484 +0,0 @@
|
||||
# pylint: disable = no-name-in-module,missing-module-docstring
|
||||
import os
|
||||
|
||||
from qtpy import uic
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtCore import Signal as pyqtSignal
|
||||
from qtpy.QtCore import Slot as pyqtSlot
|
||||
from qtpy.QtGui import QDoubleValidator, QKeySequence
|
||||
from qtpy.QtWidgets import (
|
||||
QCheckBox,
|
||||
QLineEdit,
|
||||
QMessageBox,
|
||||
QPushButton,
|
||||
QShortcut,
|
||||
QTableWidget,
|
||||
QTableWidgetItem,
|
||||
)
|
||||
|
||||
from bec_widgets.utils import UILoader
|
||||
from bec_widgets.widgets.motor_control.motor_control import MotorControlWidget
|
||||
|
||||
|
||||
class MotorCoordinateTable(MotorControlWidget):
|
||||
"""
|
||||
Widget to save coordinates from motor, display them in the table and move back to them.
|
||||
There are two modes of operation:
|
||||
- Individual: Each row is a single coordinate.
|
||||
- Start/Stop: Each pair of rows is a start and end coordinate.
|
||||
Signals:
|
||||
plot_coordinates_signal (pyqtSignal(list, str, str)): Signal to plot the coordinates in the MotorMap.
|
||||
Slots:
|
||||
add_coordinate (pyqtSlot(tuple)): Slot to add a coordinate to the table.
|
||||
mode_switch (pyqtSlot): Slot to switch between individual and start/stop mode.
|
||||
"""
|
||||
|
||||
plot_coordinates_signal = pyqtSignal(list, str, str)
|
||||
|
||||
def _load_ui(self):
|
||||
"""Load the UI for the coordinate table."""
|
||||
current_path = os.path.dirname(__file__)
|
||||
self.ui = UILoader().load_ui(os.path.join(current_path, "motor_table.ui"), self)
|
||||
|
||||
def _init_ui(self):
|
||||
"""Initialize the UI"""
|
||||
# Setup table behaviour
|
||||
self._setup_table()
|
||||
self.ui.table.setSelectionBehavior(QTableWidget.SelectRows)
|
||||
|
||||
# for tag columns default tag
|
||||
self.tag_counter = 1
|
||||
|
||||
# Connect signals and slots
|
||||
self.ui.checkBox_resize_auto.stateChanged.connect(self.resize_table_auto)
|
||||
self.ui.comboBox_mode.currentIndexChanged.connect(self.mode_switch)
|
||||
|
||||
# Keyboard shortcuts for deleting a row
|
||||
self.delete_shortcut = QShortcut(QKeySequence(Qt.Key_Delete), self.ui.table)
|
||||
self.delete_shortcut.activated.connect(self.delete_selected_row)
|
||||
self.backspace_shortcut = QShortcut(QKeySequence(Qt.Key_Backspace), self.ui.table)
|
||||
self.backspace_shortcut.activated.connect(self.delete_selected_row)
|
||||
|
||||
# Warning message for mode switch enable/disable
|
||||
self.warning_message = True
|
||||
|
||||
@pyqtSlot(dict)
|
||||
def on_config_update(self, config: dict) -> None:
|
||||
"""
|
||||
Update config dict
|
||||
Args:
|
||||
config(dict): New config dict
|
||||
"""
|
||||
self.config = config
|
||||
|
||||
# Get motor names
|
||||
self.motor_x, self.motor_y = (
|
||||
self.config["motor_control"]["motor_x"],
|
||||
self.config["motor_control"]["motor_y"],
|
||||
)
|
||||
|
||||
# Decimal precision of the table coordinates
|
||||
self.precision = self.config["motor_control"].get("precision", 3)
|
||||
|
||||
# Mode switch default option
|
||||
self.mode = self.config["motor_control"].get("mode", "Individual")
|
||||
|
||||
# Set combobox to default mode
|
||||
self.ui.comboBox_mode.setCurrentText(self.mode)
|
||||
|
||||
self._init_ui()
|
||||
|
||||
def _setup_table(self):
|
||||
"""Setup the table with appropriate headers and configurations."""
|
||||
mode = self.ui.comboBox_mode.currentText()
|
||||
|
||||
if mode == "Individual":
|
||||
self._setup_individual_mode()
|
||||
elif mode == "Start/Stop":
|
||||
self._setup_start_stop_mode()
|
||||
self.start_stop_counter = 0 # TODO: remove this??
|
||||
|
||||
self.wipe_motor_map_coordinates()
|
||||
|
||||
def _setup_individual_mode(self):
|
||||
"""Setup the table for individual mode."""
|
||||
self.ui.table.setColumnCount(5)
|
||||
self.ui.table.setHorizontalHeaderLabels(["Show", "Move", "Tag", "X", "Y"])
|
||||
self.ui.table.verticalHeader().setVisible(False)
|
||||
|
||||
def _setup_start_stop_mode(self):
|
||||
"""Setup the table for start/stop mode."""
|
||||
self.ui.table.setColumnCount(8)
|
||||
self.ui.table.setHorizontalHeaderLabels(
|
||||
[
|
||||
"Show",
|
||||
"Move [start]",
|
||||
"Move [end]",
|
||||
"Tag",
|
||||
"X [start]",
|
||||
"Y [start]",
|
||||
"X [end]",
|
||||
"Y [end]",
|
||||
]
|
||||
)
|
||||
self.ui.table.verticalHeader().setVisible(False)
|
||||
# Set flag to track if the coordinate is stat or the end of the entry
|
||||
self.is_next_entry_end = False
|
||||
|
||||
def mode_switch(self):
|
||||
"""Switch between individual and start/stop mode."""
|
||||
last_selected_index = self.ui.comboBox_mode.currentIndex()
|
||||
|
||||
if self.ui.table.rowCount() > 0 and self.warning_message is True:
|
||||
msgBox = QMessageBox()
|
||||
msgBox.setIcon(QMessageBox.Critical)
|
||||
msgBox.setText(
|
||||
"Switching modes will delete all table entries. Do you want to continue?"
|
||||
)
|
||||
msgBox.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)
|
||||
returnValue = msgBox.exec()
|
||||
|
||||
if returnValue is QMessageBox.Cancel:
|
||||
self.ui.comboBox_mode.blockSignals(True) # Block signals
|
||||
self.ui.comboBox_mode.setCurrentIndex(last_selected_index)
|
||||
self.ui.comboBox_mode.blockSignals(False) # Unblock signals
|
||||
return
|
||||
|
||||
# Wipe table
|
||||
self.wipe_motor_map_coordinates()
|
||||
|
||||
# Initiate new table with new mode
|
||||
self._setup_table()
|
||||
|
||||
@pyqtSlot(tuple)
|
||||
def add_coordinate(self, coordinates: tuple):
|
||||
"""
|
||||
Add a coordinate to the table.
|
||||
Args:
|
||||
coordinates(tuple): Coordinates (x,y) to add to the table.
|
||||
"""
|
||||
tag = f"Pos {self.tag_counter}"
|
||||
self.tag_counter += 1
|
||||
x, y = coordinates
|
||||
self._add_row(tag, x, y)
|
||||
|
||||
def _add_row(self, tag: str, x: float, y: float) -> None:
|
||||
"""
|
||||
Add a row to the table.
|
||||
Args:
|
||||
tag(str): Tag of the coordinate.
|
||||
x(float): X coordinate.
|
||||
y(float): Y coordinate.
|
||||
"""
|
||||
|
||||
mode = self.ui.comboBox_mode.currentText()
|
||||
if mode == "Individual":
|
||||
checkbox_pos = 0
|
||||
button_pos = 1
|
||||
tag_pos = 2
|
||||
x_pos = 3
|
||||
y_pos = 4
|
||||
coordinate_reference = "Individual"
|
||||
color = "green"
|
||||
|
||||
# Add new row -> new entry
|
||||
row_count = self.ui.table.rowCount()
|
||||
self.ui.table.insertRow(row_count)
|
||||
|
||||
# Add Widgets
|
||||
self._add_widgets(
|
||||
tag,
|
||||
x,
|
||||
y,
|
||||
row_count,
|
||||
checkbox_pos,
|
||||
tag_pos,
|
||||
button_pos,
|
||||
x_pos,
|
||||
y_pos,
|
||||
coordinate_reference,
|
||||
color,
|
||||
)
|
||||
|
||||
if mode == "Start/Stop":
|
||||
# These positions are always fixed
|
||||
checkbox_pos = 0
|
||||
tag_pos = 3
|
||||
|
||||
if self.is_next_entry_end is False: # It is the start position of the entry
|
||||
print("Start position")
|
||||
button_pos = 1
|
||||
x_pos = 4
|
||||
y_pos = 5
|
||||
coordinate_reference = "Start"
|
||||
color = "blue"
|
||||
|
||||
# Add new row -> new entry
|
||||
row_count = self.ui.table.rowCount()
|
||||
self.ui.table.insertRow(row_count)
|
||||
|
||||
# Add Widgets
|
||||
self._add_widgets(
|
||||
tag,
|
||||
x,
|
||||
y,
|
||||
row_count,
|
||||
checkbox_pos,
|
||||
tag_pos,
|
||||
button_pos,
|
||||
x_pos,
|
||||
y_pos,
|
||||
coordinate_reference,
|
||||
color,
|
||||
)
|
||||
|
||||
# Next entry will be the end of the current entry
|
||||
self.is_next_entry_end = True
|
||||
|
||||
elif self.is_next_entry_end is True: # It is the end position of the entry
|
||||
print("End position")
|
||||
row_count = self.ui.table.rowCount() - 1 # Current row
|
||||
button_pos = 2
|
||||
x_pos = 6
|
||||
y_pos = 7
|
||||
coordinate_reference = "Stop"
|
||||
color = "red"
|
||||
|
||||
# Add Widgets
|
||||
self._add_widgets(
|
||||
tag,
|
||||
x,
|
||||
y,
|
||||
row_count,
|
||||
checkbox_pos,
|
||||
tag_pos,
|
||||
button_pos,
|
||||
x_pos,
|
||||
y_pos,
|
||||
coordinate_reference,
|
||||
color,
|
||||
)
|
||||
self.is_next_entry_end = False # Next entry will be the start of the new entry
|
||||
|
||||
# Auto table resize
|
||||
self.resize_table_auto()
|
||||
|
||||
def _add_widgets(
|
||||
self,
|
||||
tag: str,
|
||||
x: float,
|
||||
y: float,
|
||||
row: int,
|
||||
checkBox_pos: int,
|
||||
tag_pos: int,
|
||||
button_pos: int,
|
||||
x_pos: int,
|
||||
y_pos: int,
|
||||
coordinate_reference: str,
|
||||
color: str,
|
||||
) -> None:
|
||||
"""
|
||||
Add widgets to the table.
|
||||
Args:
|
||||
tag(str): Tag of the coordinate.
|
||||
x(float): X coordinate.
|
||||
y(float): Y coordinate.
|
||||
row(int): Row of the QTableWidget where to add the widgets.
|
||||
checkBox_pos(int): Column where to put CheckBox.
|
||||
tag_pos(int): Column where to put Tag.
|
||||
button_pos(int): Column where to put Move button.
|
||||
x_pos(int): Column where to link x coordinate.
|
||||
y_pos(int): Column where to link y coordinate.
|
||||
coordinate_reference(str): Reference to the coordinate for MotorMap.
|
||||
color(str): Color of the coordinate for MotorMap.
|
||||
"""
|
||||
# Add widgets
|
||||
self._add_checkbox(row, checkBox_pos, x_pos, y_pos)
|
||||
self._add_move_button(row, button_pos, x_pos, y_pos)
|
||||
self.ui.table.setItem(row, tag_pos, QTableWidgetItem(tag))
|
||||
self._add_line_edit(x, row, x_pos, x_pos, y_pos, coordinate_reference, color)
|
||||
self._add_line_edit(y, row, y_pos, x_pos, y_pos, coordinate_reference, color)
|
||||
|
||||
# # Emit the coordinates to be plotted
|
||||
self.emit_plot_coordinates(x_pos, y_pos, coordinate_reference, color)
|
||||
|
||||
# Connect item edit to emit coordinates
|
||||
self.ui.table.itemChanged.connect(
|
||||
lambda: print(f"item changed from {coordinate_reference} slot \n {x}-{y}-{color}")
|
||||
)
|
||||
self.ui.table.itemChanged.connect(
|
||||
lambda: self.emit_plot_coordinates(x_pos, y_pos, coordinate_reference, color)
|
||||
)
|
||||
|
||||
def _add_checkbox(self, row: int, checkBox_pos: int, x_pos: int, y_pos: int):
|
||||
"""
|
||||
Add a checkbox to the table.
|
||||
Args:
|
||||
row(int): Row of QTableWidget where to add the checkbox.
|
||||
checkBox_pos(int): Column where to put CheckBox.
|
||||
x_pos(int): Column where to link x coordinate.
|
||||
y_pos(int): Column where to link y coordinate.
|
||||
"""
|
||||
show_checkbox = QCheckBox()
|
||||
show_checkbox.setChecked(True)
|
||||
show_checkbox.stateChanged.connect(lambda: self.emit_plot_coordinates(x_pos, y_pos))
|
||||
self.ui.table.setCellWidget(row, checkBox_pos, show_checkbox)
|
||||
|
||||
def _add_move_button(self, row: int, button_pos: int, x_pos: int, y_pos: int) -> None:
|
||||
"""
|
||||
Add a move button to the table.
|
||||
Args:
|
||||
row(int): Row of QTableWidget where to add the move button.
|
||||
button_pos(int): Column where to put move button.
|
||||
x_pos(int): Column where to link x coordinate.
|
||||
y_pos(int): Column where to link y coordinate.
|
||||
"""
|
||||
move_button = QPushButton("Move")
|
||||
move_button.clicked.connect(lambda: self.handle_move_button_click(x_pos, y_pos))
|
||||
self.ui.table.setCellWidget(row, button_pos, move_button)
|
||||
|
||||
def _add_line_edit(
|
||||
self,
|
||||
value: float,
|
||||
row: int,
|
||||
line_pos: int,
|
||||
x_pos: int,
|
||||
y_pos: int,
|
||||
coordinate_reference: str,
|
||||
color: str,
|
||||
) -> None:
|
||||
"""
|
||||
Add a QLineEdit to the table.
|
||||
Args:
|
||||
value(float): Initial value of the QLineEdit.
|
||||
row(int): Row of QTableWidget where to add the QLineEdit.
|
||||
line_pos(int): Column where to put QLineEdit.
|
||||
x_pos(int): Column where to link x coordinate.
|
||||
y_pos(int): Column where to link y coordinate.
|
||||
coordinate_reference(str): Reference to the coordinate for MotorMap.
|
||||
color(str): Color of the coordinate for MotorMap.
|
||||
"""
|
||||
# Adding validator
|
||||
validator = QDoubleValidator()
|
||||
validator.setDecimals(self.precision)
|
||||
|
||||
# Create line edit
|
||||
edit = QLineEdit(str(f"{value:.{self.precision}f}"))
|
||||
edit.setValidator(validator)
|
||||
edit.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
# Add line edit to the table
|
||||
self.ui.table.setCellWidget(row, line_pos, edit)
|
||||
edit.textChanged.connect(
|
||||
lambda: self.emit_plot_coordinates(x_pos, y_pos, coordinate_reference, color)
|
||||
)
|
||||
|
||||
def wipe_motor_map_coordinates(self):
|
||||
"""Wipe the motor map coordinates."""
|
||||
try:
|
||||
self.ui.table.itemChanged.disconnect() # Disconnect all previous connections
|
||||
except TypeError:
|
||||
print("No previous connections to disconnect")
|
||||
self.ui.table.setRowCount(0)
|
||||
reference_tags = ["Individual", "Start", "Stop"]
|
||||
for reference_tag in reference_tags:
|
||||
self.plot_coordinates_signal.emit([], reference_tag, "green")
|
||||
|
||||
def handle_move_button_click(self, x_pos: int, y_pos: int) -> None:
|
||||
"""
|
||||
Handle the move button click.
|
||||
Args:
|
||||
x_pos(int): X position of the coordinate.
|
||||
y_pos(int): Y position of the coordinate.
|
||||
"""
|
||||
button = self.sender()
|
||||
row = self.ui.table.indexAt(button.pos()).row()
|
||||
|
||||
x = self.get_coordinate(row, x_pos)
|
||||
y = self.get_coordinate(row, y_pos)
|
||||
self.move_motor(x, y)
|
||||
|
||||
def emit_plot_coordinates(self, x_pos: float, y_pos: float, reference_tag: str, color: str):
|
||||
"""
|
||||
Emit the coordinates to be plotted.
|
||||
Args:
|
||||
x_pos(float): X position of the coordinate.
|
||||
y_pos(float): Y position of the coordinate.
|
||||
reference_tag(str): Reference tag of the coordinate.
|
||||
color(str): Color of the coordinate.
|
||||
"""
|
||||
print(
|
||||
f"Emitting plot coordinates: x_pos={x_pos}, y_pos={y_pos}, reference_tag={reference_tag}, color={color}"
|
||||
)
|
||||
coordinates = []
|
||||
for row in range(self.ui.table.rowCount()):
|
||||
show = self.ui.table.cellWidget(row, 0).isChecked()
|
||||
x = self.get_coordinate(row, x_pos)
|
||||
y = self.get_coordinate(row, y_pos)
|
||||
|
||||
coordinates.append((x, y, show)) # (x, y, show_flag)
|
||||
self.plot_coordinates_signal.emit(coordinates, reference_tag, color)
|
||||
|
||||
def get_coordinate(self, row: int, column: int) -> float:
|
||||
"""
|
||||
Helper function to get the coordinate from the table QLineEdit cells.
|
||||
Args:
|
||||
row(int): Row of the table.
|
||||
column(int): Column of the table.
|
||||
Returns:
|
||||
float: Value of the coordinate.
|
||||
"""
|
||||
edit = self.ui.table.cellWidget(row, column)
|
||||
value = float(edit.text()) if edit and edit.text() != "" else None
|
||||
if value:
|
||||
return value
|
||||
|
||||
def delete_selected_row(self):
|
||||
"""Delete the selected row from the table."""
|
||||
selected_rows = self.ui.table.selectionModel().selectedRows()
|
||||
for row in selected_rows:
|
||||
self.ui.table.removeRow(row.row())
|
||||
if self.ui.comboBox_mode.currentText() == "Start/Stop":
|
||||
self.emit_plot_coordinates(x_pos=4, y_pos=5, reference_tag="Start", color="blue")
|
||||
self.emit_plot_coordinates(x_pos=6, y_pos=7, reference_tag="Stop", color="red")
|
||||
self.is_next_entry_end = False
|
||||
elif self.ui.comboBox_mode.currentText() == "Individual":
|
||||
self.emit_plot_coordinates(x_pos=3, y_pos=4, reference_tag="Individual", color="green")
|
||||
|
||||
def resize_table_auto(self):
|
||||
"""Resize the table to fit the contents."""
|
||||
if self.ui.checkBox_resize_auto.isChecked():
|
||||
self.ui.table.resizeColumnsToContents()
|
||||
|
||||
def move_motor(self, x: float, y: float) -> None:
|
||||
"""
|
||||
Move the motor to the target coordinates.
|
||||
Args:
|
||||
x(float): Target x coordinate.
|
||||
y(float): Target y coordinate.
|
||||
"""
|
||||
self.motor_thread.move_absolute(self.motor_x, self.motor_y, (x, y))
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def change_motors(self, motor_x: str, motor_y: str) -> None:
|
||||
"""
|
||||
Change the active motors and update config.
|
||||
Can be connected to the selected_motors_signal from MotorControlSelection.
|
||||
Args:
|
||||
motor_x(str): New motor X to be controlled.
|
||||
motor_y(str): New motor Y to be controlled.
|
||||
"""
|
||||
self.motor_x = motor_x
|
||||
self.motor_y = motor_y
|
||||
self.config["motor_control"]["motor_x"] = motor_x
|
||||
self.config["motor_control"]["motor_y"] = motor_y
|
||||
|
||||
@pyqtSlot(int)
|
||||
def set_precision(self, precision: int) -> None:
|
||||
"""
|
||||
Set the precision of the coordinates.
|
||||
Args:
|
||||
precision(int): Precision of the coordinates.
|
||||
"""
|
||||
self.precision = precision
|
||||
self.config["motor_control"]["precision"] = precision
|
||||
@@ -1,113 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>676</width>
|
||||
<height>667</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Motor Coordinates Table</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_4">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_resize_auto">
|
||||
<property name="text">
|
||||
<string>Resize Auto</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_editColumns">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Edit Custom Column</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Entries Mode:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="comboBox_mode">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Individual</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Start/Stop</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTableWidget" name="table">
|
||||
<property name="gridStyle">
|
||||
<enum>Qt::SolidLine</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_importCSV">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Import CSV</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_exportCSV">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Export CSV</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -1,159 +0,0 @@
|
||||
import os
|
||||
|
||||
from qtpy import uic
|
||||
from qtpy.QtCore import Signal as pyqtSignal
|
||||
from qtpy.QtCore import Slot as pyqtSlot
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils import UILoader
|
||||
from bec_widgets.widgets.motor_control.motor_control import MotorControlErrors, MotorControlWidget
|
||||
|
||||
|
||||
class MotorControlAbsolute(MotorControlWidget):
|
||||
"""
|
||||
Widget for controlling the motors to absolute coordinates.
|
||||
|
||||
Signals:
|
||||
coordinates_signal (pyqtSignal(tuple)): Signal to emit the coordinates.
|
||||
Slots:
|
||||
change_motors (pyqtSlot): Slot to change the active motors.
|
||||
enable_motor_controls (pyqtSlot(bool)): Slot to enable/disable the motor controls.
|
||||
"""
|
||||
|
||||
coordinates_signal = pyqtSignal(tuple)
|
||||
|
||||
def _load_ui(self):
|
||||
"""Load the UI from the .ui file."""
|
||||
current_path = os.path.dirname(__file__)
|
||||
self.ui = UILoader().load_ui(os.path.join(current_path, "movement_absolute.ui"), self)
|
||||
|
||||
def _init_ui(self):
|
||||
"""Initialize the UI."""
|
||||
|
||||
# Check if there are any motors connected
|
||||
if self.motor_x is None or self.motor_y is None:
|
||||
self.ui.motorControl_absolute.setEnabled(False)
|
||||
return
|
||||
|
||||
# Move to absolute coordinates
|
||||
self.ui.pushButton_go_absolute.clicked.connect(
|
||||
lambda: self.move_motor_absolute(
|
||||
self.ui.spinBox_absolute_x.value(), self.ui.spinBox_absolute_y.value()
|
||||
)
|
||||
)
|
||||
|
||||
self.ui.pushButton_set.clicked.connect(self.save_absolute_coordinates)
|
||||
self.ui.pushButton_save.clicked.connect(self.save_current_coordinates)
|
||||
self.ui.pushButton_stop.clicked.connect(self.motor_thread.stop_movement)
|
||||
|
||||
# Enable/Disable GUI
|
||||
self.motor_thread.lock_gui.connect(self.enable_motor_controls)
|
||||
|
||||
# Error messages
|
||||
self.motor_thread.motor_error.connect(
|
||||
lambda error: MotorControlErrors.display_error_message(error)
|
||||
)
|
||||
|
||||
# Keyboard shortcuts
|
||||
self._init_keyboard_shortcuts()
|
||||
|
||||
@pyqtSlot(dict)
|
||||
def on_config_update(self, config: dict) -> None:
|
||||
"""Update config dict"""
|
||||
self.config = config
|
||||
|
||||
# Get motor names
|
||||
self.motor_x, self.motor_y = (
|
||||
self.config["motor_control"]["motor_x"],
|
||||
self.config["motor_control"]["motor_y"],
|
||||
)
|
||||
|
||||
# Update step precision
|
||||
self.precision = self.config["motor_control"]["precision"]
|
||||
|
||||
self._init_ui()
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def enable_motor_controls(self, enable: bool) -> None:
|
||||
"""
|
||||
Enable or disable the motor controls.
|
||||
Args:
|
||||
enable(bool): True to enable, False to disable.
|
||||
"""
|
||||
|
||||
# Disable or enable all controls within the motorControl_absolute group box
|
||||
for widget in self.ui.motorControl_absolute.findChildren(QWidget):
|
||||
widget.setEnabled(enable)
|
||||
|
||||
# Enable the pushButton_stop if the motor is moving
|
||||
self.ui.pushButton_stop.setEnabled(True)
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def change_motors(self, motor_x: str, motor_y: str):
|
||||
"""
|
||||
Change the active motors and update config.
|
||||
Can be connected to the selected_motors_signal from MotorControlSelection.
|
||||
Args:
|
||||
motor_x(str): New motor X to be controlled.
|
||||
motor_y(str): New motor Y to be controlled.
|
||||
"""
|
||||
self.motor_x = motor_x
|
||||
self.motor_y = motor_y
|
||||
self.config["motor_control"]["motor_x"] = motor_x
|
||||
self.config["motor_control"]["motor_y"] = motor_y
|
||||
|
||||
@pyqtSlot(int)
|
||||
def set_precision(self, precision: int) -> None:
|
||||
"""
|
||||
Set the precision of the coordinates.
|
||||
Args:
|
||||
precision(int): Precision of the coordinates.
|
||||
"""
|
||||
self.precision = precision
|
||||
self.config["motor_control"]["precision"] = precision
|
||||
self.ui.spinBox_absolute_x.setDecimals(precision)
|
||||
self.ui.spinBox_absolute_y.setDecimals(precision)
|
||||
|
||||
def move_motor_absolute(self, x: float, y: float) -> None:
|
||||
"""
|
||||
Move the motor to the target coordinates.
|
||||
Args:
|
||||
x(float): Target x coordinate.
|
||||
y(float): Target y coordinate.
|
||||
"""
|
||||
# self._enable_motor_controls(False)
|
||||
target_coordinates = (x, y)
|
||||
self.motor_thread.move_absolute(self.motor_x, self.motor_y, target_coordinates)
|
||||
if self.ui.checkBox_save_with_go.isChecked():
|
||||
self.save_absolute_coordinates()
|
||||
|
||||
def _init_keyboard_shortcuts(self):
|
||||
"""Initialize the keyboard shortcuts."""
|
||||
# Go absolute button
|
||||
self.ui.pushButton_go_absolute.setShortcut("Ctrl+G")
|
||||
self.ui.pushButton_go_absolute.setToolTip("Ctrl+G")
|
||||
|
||||
# Set absolute coordinates
|
||||
self.ui.pushButton_set.setShortcut("Ctrl+D")
|
||||
self.ui.pushButton_set.setToolTip("Ctrl+D")
|
||||
|
||||
# Save Current coordinates
|
||||
self.ui.pushButton_save.setShortcut("Ctrl+S")
|
||||
self.ui.pushButton_save.setToolTip("Ctrl+S")
|
||||
|
||||
# Stop Button
|
||||
self.ui.pushButton_stop.setShortcut("Ctrl+X")
|
||||
self.ui.pushButton_stop.setToolTip("Ctrl+X")
|
||||
|
||||
def save_absolute_coordinates(self):
|
||||
"""Emit the setup coordinates from the spinboxes"""
|
||||
|
||||
x, y = round(self.ui.spinBox_absolute_x.value(), self.precision), round(
|
||||
self.ui.spinBox_absolute_y.value(), self.precision
|
||||
)
|
||||
self.coordinates_signal.emit((x, y))
|
||||
|
||||
def save_current_coordinates(self):
|
||||
"""Emit the current coordinates from the motor thread"""
|
||||
x, y = self.motor_thread.get_coordinates(self.motor_x, self.motor_y)
|
||||
self.coordinates_signal.emit((round(x, self.precision), round(y, self.precision)))
|
||||
@@ -1,149 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>285</width>
|
||||
<height>220</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>285</width>
|
||||
<height>220</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>285</width>
|
||||
<height>220</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Move Movement Absolute</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="motorControl_absolute">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>261</width>
|
||||
<height>195</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>261</width>
|
||||
<height>195</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Move Movement Absolute</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_save_with_go">
|
||||
<property name="text">
|
||||
<string>Save position with Go</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<item row="1" column="1">
|
||||
<widget class="QDoubleSpinBox" name="spinBox_absolute_y">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-500.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>500.000000000000000</double>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<double>0.100000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QDoubleSpinBox" name="spinBox_absolute_x">
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-500.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>500.000000000000000</double>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<double>0.100000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLabel" name="label_7">
|
||||
<property name="text">
|
||||
<string>Y</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_6">
|
||||
<property name="text">
|
||||
<string>X</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_save">
|
||||
<property name="text">
|
||||
<string>Save</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_set">
|
||||
<property name="text">
|
||||
<string>Set</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_go_absolute">
|
||||
<property name="text">
|
||||
<string>Go</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_stop">
|
||||
<property name="text">
|
||||
<string>Stop Movement</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -1,230 +0,0 @@
|
||||
import os
|
||||
|
||||
from qtpy import uic
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtCore import Signal as pyqtSignal
|
||||
from qtpy.QtCore import Slot as pyqtSlot
|
||||
from qtpy.QtGui import QKeySequence
|
||||
from qtpy.QtWidgets import QDoubleSpinBox, QShortcut, QWidget
|
||||
|
||||
from bec_widgets.utils import UILoader
|
||||
from bec_widgets.widgets.motor_control.motor_control import MotorControlWidget
|
||||
|
||||
|
||||
class MotorControlRelative(MotorControlWidget):
|
||||
"""
|
||||
Widget for controlling the motors to relative coordinates.
|
||||
|
||||
Signals:
|
||||
precision_signal (pyqtSignal): Signal to emit the precision of the coordinates.
|
||||
Slots:
|
||||
change_motors (pyqtSlot(str,str)): Slot to change the active motors.
|
||||
enable_motor_controls (pyqtSlot): Slot to enable/disable the motor controls.
|
||||
"""
|
||||
|
||||
precision_signal = pyqtSignal(int)
|
||||
|
||||
def _load_ui(self):
|
||||
"""Load the UI from the .ui file."""
|
||||
# Loading UI
|
||||
current_path = os.path.dirname(__file__)
|
||||
self.ui = UILoader().load_ui(os.path.join(current_path, "movement_relative.ui"), self)
|
||||
|
||||
def _init_ui(self):
|
||||
"""Initialize the UI."""
|
||||
self._init_ui_motor_control()
|
||||
self._init_keyboard_shortcuts()
|
||||
|
||||
@pyqtSlot(dict)
|
||||
def on_config_update(self, config: dict) -> None:
|
||||
"""
|
||||
Update config dict
|
||||
Args:
|
||||
config(dict): New config dict
|
||||
"""
|
||||
self.config = config
|
||||
|
||||
# Get motor names
|
||||
self.motor_x, self.motor_y = (
|
||||
self.config["motor_control"]["motor_x"],
|
||||
self.config["motor_control"]["motor_y"],
|
||||
)
|
||||
|
||||
# Update step precision
|
||||
self.precision = self.config["motor_control"]["precision"]
|
||||
self.ui.spinBox_precision.setValue(self.precision)
|
||||
|
||||
# Update step sizes
|
||||
self.ui.spinBox_step_x.setValue(self.config["motor_control"]["step_size_x"])
|
||||
self.ui.spinBox_step_y.setValue(self.config["motor_control"]["step_size_y"])
|
||||
|
||||
# Checkboxes for keyboard shortcuts and x/y step size link
|
||||
self.ui.checkBox_same_xy.setChecked(self.config["motor_control"]["step_x_y_same"])
|
||||
self.ui.checkBox_enableArrows.setChecked(self.config["motor_control"]["move_with_arrows"])
|
||||
|
||||
self._init_ui()
|
||||
|
||||
def _init_ui_motor_control(self) -> None:
|
||||
"""Initialize the motor control elements"""
|
||||
|
||||
# Connect checkbox and spinBoxes
|
||||
self.ui.checkBox_same_xy.stateChanged.connect(self._sync_step_sizes)
|
||||
self.ui.spinBox_step_x.valueChanged.connect(self._update_step_size_x)
|
||||
self.ui.spinBox_step_y.valueChanged.connect(self._update_step_size_y)
|
||||
|
||||
self.ui.toolButton_right.clicked.connect(
|
||||
lambda: self.move_motor_relative(self.motor_x, "x", 1)
|
||||
)
|
||||
self.ui.toolButton_left.clicked.connect(
|
||||
lambda: self.move_motor_relative(self.motor_x, "x", -1)
|
||||
)
|
||||
self.ui.toolButton_up.clicked.connect(
|
||||
lambda: self.move_motor_relative(self.motor_y, "y", 1)
|
||||
)
|
||||
self.ui.toolButton_down.clicked.connect(
|
||||
lambda: self.move_motor_relative(self.motor_y, "y", -1)
|
||||
)
|
||||
|
||||
# Switch between key shortcuts active
|
||||
self.ui.checkBox_enableArrows.stateChanged.connect(self._update_arrow_key_shortcuts)
|
||||
self._update_arrow_key_shortcuts()
|
||||
|
||||
# Enable/Disable GUI
|
||||
self.motor_thread.lock_gui.connect(self.enable_motor_controls)
|
||||
|
||||
# Precision update
|
||||
self.ui.spinBox_precision.valueChanged.connect(lambda x: self._update_precision(x))
|
||||
|
||||
# Error messages
|
||||
self.motor_thread.motor_error.connect(
|
||||
lambda error: MotorControlErrors.display_error_message(error)
|
||||
)
|
||||
|
||||
# Stop Button
|
||||
self.ui.pushButton_stop.clicked.connect(self.motor_thread.stop_movement)
|
||||
|
||||
def _init_keyboard_shortcuts(self) -> None:
|
||||
"""Initialize the keyboard shortcuts"""
|
||||
|
||||
# Increase/decrease step size for X motor
|
||||
increase_x_shortcut = QShortcut(QKeySequence("Ctrl+A"), self)
|
||||
decrease_x_shortcut = QShortcut(QKeySequence("Ctrl+Z"), self)
|
||||
increase_x_shortcut.activated.connect(
|
||||
lambda: self._change_step_size(self.ui.spinBox_step_x, 2)
|
||||
)
|
||||
decrease_x_shortcut.activated.connect(
|
||||
lambda: self._change_step_size(self.ui.spinBox_step_x, 0.5)
|
||||
)
|
||||
self.ui.spinBox_step_x.setToolTip("Increase step size: Ctrl+A\nDecrease step size: Ctrl+Z")
|
||||
|
||||
# Increase/decrease step size for Y motor
|
||||
increase_y_shortcut = QShortcut(QKeySequence("Alt+A"), self)
|
||||
decrease_y_shortcut = QShortcut(QKeySequence("Alt+Z"), self)
|
||||
increase_y_shortcut.activated.connect(
|
||||
lambda: self._change_step_size(self.ui.spinBox_step_y, 2)
|
||||
)
|
||||
decrease_y_shortcut.activated.connect(
|
||||
lambda: self._change_step_size(self.ui.spinBox_step_y, 0.5)
|
||||
)
|
||||
self.ui.spinBox_step_y.setToolTip("Increase step size: Alt+A\nDecrease step size: Alt+Z")
|
||||
|
||||
# Stop Button
|
||||
self.ui.pushButton_stop.setShortcut("Ctrl+X")
|
||||
self.ui.pushButton_stop.setToolTip("Ctrl+X")
|
||||
|
||||
def _update_arrow_key_shortcuts(self) -> None:
|
||||
"""Update the arrow key shortcuts based on the checkbox state."""
|
||||
if self.ui.checkBox_enableArrows.isChecked():
|
||||
# Set the arrow key shortcuts for motor movement
|
||||
self.ui.toolButton_right.setShortcut(Qt.Key_Right)
|
||||
self.ui.toolButton_left.setShortcut(Qt.Key_Left)
|
||||
self.ui.toolButton_up.setShortcut(Qt.Key_Up)
|
||||
self.ui.toolButton_down.setShortcut(Qt.Key_Down)
|
||||
else:
|
||||
# Clear the shortcuts
|
||||
self.ui.toolButton_right.setShortcut("")
|
||||
self.ui.toolButton_left.setShortcut("")
|
||||
self.ui.toolButton_up.setShortcut("")
|
||||
self.ui.toolButton_down.setShortcut("")
|
||||
|
||||
def _update_precision(self, precision: int) -> None:
|
||||
"""
|
||||
Update the precision of the coordinates.
|
||||
Args:
|
||||
precision(int): Precision of the coordinates.
|
||||
"""
|
||||
self.ui.spinBox_step_x.setDecimals(precision)
|
||||
self.ui.spinBox_step_y.setDecimals(precision)
|
||||
self.precision_signal.emit(precision)
|
||||
|
||||
def _change_step_size(self, spinBox: QDoubleSpinBox, factor: float) -> None:
|
||||
"""
|
||||
Change the step size of the spinbox.
|
||||
Args:
|
||||
spinBox(QDoubleSpinBox): Spinbox to change the step size.
|
||||
factor(float): Factor to change the step size.
|
||||
"""
|
||||
old_step = spinBox.value()
|
||||
new_step = old_step * factor
|
||||
spinBox.setValue(new_step)
|
||||
|
||||
def _sync_step_sizes(self):
|
||||
"""Sync step sizes based on checkbox state."""
|
||||
if self.ui.checkBox_same_xy.isChecked():
|
||||
value = self.ui.spinBox_step_x.value()
|
||||
self.ui.spinBox_step_y.setValue(value)
|
||||
|
||||
def _update_step_size_x(self):
|
||||
"""Update step size for x if checkbox is checked."""
|
||||
if self.ui.checkBox_same_xy.isChecked():
|
||||
value = self.ui.spinBox_step_x.value()
|
||||
self.ui.spinBox_step_y.setValue(value)
|
||||
|
||||
def _update_step_size_y(self):
|
||||
"""Update step size for y if checkbox is checked."""
|
||||
if self.ui.checkBox_same_xy.isChecked():
|
||||
value = self.ui.spinBox_step_y.value()
|
||||
self.ui.spinBox_step_x.setValue(value)
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def change_motors(self, motor_x: str, motor_y: str):
|
||||
"""
|
||||
Change the active motors and update config.
|
||||
Can be connected to the selected_motors_signal from MotorControlSelection.
|
||||
Args:
|
||||
motor_x(str): New motor X to be controlled.
|
||||
motor_y(str): New motor Y to be controlled.
|
||||
"""
|
||||
self.motor_x = motor_x
|
||||
self.motor_y = motor_y
|
||||
self.config["motor_control"]["motor_x"] = motor_x
|
||||
self.config["motor_control"]["motor_y"] = motor_y
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def enable_motor_controls(self, disable: bool) -> None:
|
||||
"""
|
||||
Enable or disable the motor controls.
|
||||
Args:
|
||||
disable(bool): True to disable, False to enable.
|
||||
"""
|
||||
|
||||
# Disable or enable all controls within the motorControl_absolute group box
|
||||
for widget in self.ui.motorControl.findChildren(QWidget):
|
||||
widget.setEnabled(disable)
|
||||
|
||||
# Enable the pushButton_stop if the motor is moving
|
||||
self.ui.pushButton_stop.setEnabled(True)
|
||||
|
||||
def move_motor_relative(self, motor, axis: str, direction: int) -> None:
|
||||
"""
|
||||
Move the motor relative to the current position.
|
||||
Args:
|
||||
motor: Motor to move.
|
||||
axis(str): Axis to move.
|
||||
direction(int): Direction to move. 1 for positive, -1 for negative.
|
||||
"""
|
||||
if axis == "x":
|
||||
step = direction * self.ui.spinBox_step_x.value()
|
||||
elif axis == "y":
|
||||
step = direction * self.ui.spinBox_step_y.value()
|
||||
self.motor_thread.move_relative(motor, step)
|
||||
@@ -1,298 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>285</width>
|
||||
<height>405</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>285</width>
|
||||
<height>405</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Motor Control Relative</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="motorControl">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>261</width>
|
||||
<height>394</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Motor Control Relative</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_enableArrows">
|
||||
<property name="text">
|
||||
<string>Move with arrow keys</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_same_xy">
|
||||
<property name="text">
|
||||
<string>Step [X] = Step [Y]</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QGridLayout" name="step_grid">
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_step_y">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>111</width>
|
||||
<height>19</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Step [Y]</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>111</width>
|
||||
<height>19</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Decimal</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QDoubleSpinBox" name="spinBox_step_x">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>19</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>0.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>99.000000000000000</double>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<double>0.100000000000000</double>
|
||||
</property>
|
||||
<property name="value">
|
||||
<double>1.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_step_x">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>111</width>
|
||||
<height>19</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Step [X]</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QDoubleSpinBox" name="spinBox_step_y">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>19</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>0.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>99.000000000000000</double>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<double>0.100000000000000</double>
|
||||
</property>
|
||||
<property name="value">
|
||||
<double>1.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QSpinBox" name="spinBox_precision">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>19</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>8</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>2</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QGridLayout" name="direction_grid">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetDefaultConstraint</enum>
|
||||
</property>
|
||||
<item row="1" column="2" alignment="Qt::AlignHCenter|Qt::AlignVCenter">
|
||||
<widget class="QToolButton" name="toolButton_up">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>26</width>
|
||||
<height>26</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="arrowType">
|
||||
<enum>Qt::UpArrow</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="4">
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="3" column="2" alignment="Qt::AlignHCenter|Qt::AlignVCenter">
|
||||
<widget class="QToolButton" name="toolButton_down">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>26</width>
|
||||
<height>26</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="arrowType">
|
||||
<enum>Qt::DownArrow</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QToolButton" name="toolButton_left">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>26</width>
|
||||
<height>26</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="arrowType">
|
||||
<enum>Qt::LeftArrow</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="2" column="3">
|
||||
<widget class="QToolButton" name="toolButton_right">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>26</width>
|
||||
<height>26</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>...</string>
|
||||
</property>
|
||||
<property name="arrowType">
|
||||
<enum>Qt::RightArrow</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="4" column="2">
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pushButton_stop">
|
||||
<property name="text">
|
||||
<string>Stop Movement</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -1,110 +0,0 @@
|
||||
# pylint: disable = no-name-in-module,missing-module-docstring
|
||||
import os
|
||||
|
||||
from qtpy import uic
|
||||
from qtpy.QtCore import Signal as pyqtSignal
|
||||
from qtpy.QtCore import Slot as pyqtSlot
|
||||
from qtpy.QtWidgets import QComboBox
|
||||
|
||||
from bec_widgets.widgets.motor_control.motor_control import MotorControlWidget
|
||||
|
||||
|
||||
class MotorControlSelection(MotorControlWidget):
|
||||
"""
|
||||
Widget for selecting the motors to control.
|
||||
|
||||
Signals:
|
||||
selected_motors_signal (pyqtSignal(str,str)): Signal to emit the selected motors.
|
||||
Slots:
|
||||
get_available_motors (pyqtSlot): Slot to populate the available motors in the combo boxes and set the index based on the configuration.
|
||||
enable_motor_controls (pyqtSlot(bool)): Slot to enable/disable the motor controls GUI.
|
||||
on_config_update (pyqtSlot(dict)): Slot to update the config dict.
|
||||
"""
|
||||
|
||||
selected_motors_signal = pyqtSignal(str, str)
|
||||
|
||||
def _load_ui(self):
|
||||
"""Load the UI from the .ui file."""
|
||||
current_path = os.path.dirname(__file__)
|
||||
uic.loadUi(os.path.join(current_path, "selection.ui"), self)
|
||||
|
||||
def _init_ui(self):
|
||||
"""Initialize the UI."""
|
||||
# Lock GUI while motors are moving
|
||||
self.motor_thread.lock_gui.connect(self.enable_motor_controls)
|
||||
|
||||
self.pushButton_connecMotors.clicked.connect(self.select_motor)
|
||||
self.get_available_motors()
|
||||
|
||||
# Connect change signals to change color
|
||||
self.comboBox_motor_x.currentIndexChanged.connect(
|
||||
lambda: self.set_combobox_style(self.comboBox_motor_x, "#ffa700")
|
||||
)
|
||||
self.comboBox_motor_y.currentIndexChanged.connect(
|
||||
lambda: self.set_combobox_style(self.comboBox_motor_y, "#ffa700")
|
||||
)
|
||||
|
||||
@pyqtSlot(dict)
|
||||
def on_config_update(self, config: dict) -> None:
|
||||
"""
|
||||
Update config dict
|
||||
Args:
|
||||
config(dict): New config dict
|
||||
"""
|
||||
self.config = config
|
||||
|
||||
# Get motor names
|
||||
self.motor_x, self.motor_y = (
|
||||
self.config["motor_control"]["motor_x"],
|
||||
self.config["motor_control"]["motor_y"],
|
||||
)
|
||||
|
||||
self._init_ui()
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def enable_motor_controls(self, enable: bool) -> None:
|
||||
"""
|
||||
Enable or disable the motor controls.
|
||||
Args:
|
||||
enable(bool): True to enable, False to disable.
|
||||
"""
|
||||
self.motorSelection.setEnabled(enable)
|
||||
|
||||
@pyqtSlot()
|
||||
def get_available_motors(self) -> None:
|
||||
"""
|
||||
Slot to populate the available motors in the combo boxes and set the index based on the configuration.
|
||||
"""
|
||||
# Get all available motors
|
||||
self.motor_list = self.motor_thread.get_all_motors_names()
|
||||
|
||||
# Populate the combo boxes
|
||||
self.comboBox_motor_x.addItems(self.motor_list)
|
||||
self.comboBox_motor_y.addItems(self.motor_list)
|
||||
|
||||
# Set the index based on the config if provided
|
||||
if self.config:
|
||||
index_x = self.comboBox_motor_x.findText(self.motor_x)
|
||||
index_y = self.comboBox_motor_y.findText(self.motor_y)
|
||||
self.comboBox_motor_x.setCurrentIndex(index_x if index_x != -1 else 0)
|
||||
self.comboBox_motor_y.setCurrentIndex(index_y if index_y != -1 else 0)
|
||||
|
||||
def set_combobox_style(self, combobox, color: str) -> None:
|
||||
"""
|
||||
Set the combobox style to a specific color.
|
||||
Args:
|
||||
combobox(QComboBox): Combobox to change the color.
|
||||
color(str): Color to set the combobox to.
|
||||
"""
|
||||
combobox.setStyleSheet(f"QComboBox {{ background-color: {color}; }}")
|
||||
|
||||
def select_motor(self):
|
||||
"""Emit the selected motors"""
|
||||
motor_x = self.comboBox_motor_x.currentText()
|
||||
motor_y = self.comboBox_motor_y.currentText()
|
||||
|
||||
# Reset the combobox color to normal after selection
|
||||
self.set_combobox_style(self.comboBox_motor_x, "")
|
||||
self.set_combobox_style(self.comboBox_motor_y, "")
|
||||
|
||||
self.selected_motors_signal.emit(motor_x, motor_y)
|
||||
@@ -1,69 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>285</width>
|
||||
<height>156</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>285</width>
|
||||
<height>156</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Motor Control Selection</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="motorSelection">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>261</width>
|
||||
<height>145</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Motor Selection</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_4">
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
<string>Motor X</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="comboBox_motor_x"/>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_8">
|
||||
<property name="text">
|
||||
<string>Motor Y</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="2">
|
||||
<widget class="QPushButton" name="pushButton_connecMotors">
|
||||
<property name="text">
|
||||
<string>Connect Motors</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QComboBox" name="comboBox_motor_y"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -1 +0,0 @@
|
||||
from .scan_control import ScanControl
|
||||
@@ -1,444 +0,0 @@
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
QCheckBox,
|
||||
QComboBox,
|
||||
QDoubleSpinBox,
|
||||
QFrame,
|
||||
QGridLayout,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QHeaderView,
|
||||
QLabel,
|
||||
QLayout,
|
||||
QLineEdit,
|
||||
QPushButton,
|
||||
QSpinBox,
|
||||
QTableWidget,
|
||||
QTableWidgetItem,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
||||
from bec_widgets.utils.widget_io import WidgetIO
|
||||
|
||||
|
||||
class ScanArgType:
|
||||
DEVICE = "device"
|
||||
FLOAT = "float"
|
||||
INT = "int"
|
||||
BOOL = "bool"
|
||||
STR = "str"
|
||||
|
||||
|
||||
class ScanControl(QWidget):
|
||||
WIDGET_HANDLER = {
|
||||
ScanArgType.DEVICE: QLineEdit,
|
||||
ScanArgType.FLOAT: QDoubleSpinBox,
|
||||
ScanArgType.INT: QSpinBox,
|
||||
ScanArgType.BOOL: QCheckBox,
|
||||
ScanArgType.STR: QLineEdit,
|
||||
}
|
||||
|
||||
def __init__(self, parent=None, client=None, allowed_scans=None):
|
||||
super().__init__(parent)
|
||||
|
||||
# Client from BEC + shortcuts to device manager and scans
|
||||
self.client = BECDispatcher().client if client is None else client
|
||||
self.dev = self.client.device_manager.devices
|
||||
self.scans = self.client.scans
|
||||
|
||||
# Scan list - allowed scans for the GUI
|
||||
self.allowed_scans = allowed_scans
|
||||
|
||||
# Create and set main layout
|
||||
self._init_UI()
|
||||
|
||||
def _init_UI(self):
|
||||
self.verticalLayout = QVBoxLayout(self)
|
||||
|
||||
# Scan selection group box
|
||||
self.scan_selection_group = QGroupBox("Scan Selection", self)
|
||||
self.scan_selection_layout = QVBoxLayout(self.scan_selection_group)
|
||||
self.comboBox_scan_selection = QComboBox(self.scan_selection_group)
|
||||
self.button_run_scan = QPushButton("Run Scan", self.scan_selection_group)
|
||||
self.scan_selection_layout.addWidget(self.comboBox_scan_selection)
|
||||
self.scan_selection_layout.addWidget(self.button_run_scan)
|
||||
self.verticalLayout.addWidget(self.scan_selection_group)
|
||||
|
||||
# Scan control group box
|
||||
self.scan_control_group = QGroupBox("Scan Control", self)
|
||||
self.scan_control_layout = QVBoxLayout(self.scan_control_group)
|
||||
self.verticalLayout.addWidget(self.scan_control_group)
|
||||
|
||||
# Kwargs layout - just placeholder
|
||||
self.kwargs_layout = QGridLayout()
|
||||
self.scan_control_layout.addLayout(self.kwargs_layout)
|
||||
|
||||
# 1st Separator
|
||||
self.add_horizontal_separator(self.scan_control_layout)
|
||||
|
||||
# Buttons
|
||||
self.button_layout = QHBoxLayout()
|
||||
self.pushButton_add_bundle = QPushButton("Add Bundle", self.scan_control_group)
|
||||
self.pushButton_add_bundle.clicked.connect(self.add_bundle)
|
||||
self.pushButton_remove_bundle = QPushButton("Remove Bundle", self.scan_control_group)
|
||||
self.pushButton_remove_bundle.clicked.connect(self.remove_bundle)
|
||||
self.button_layout.addWidget(self.pushButton_add_bundle)
|
||||
self.button_layout.addWidget(self.pushButton_remove_bundle)
|
||||
self.scan_control_layout.addLayout(self.button_layout)
|
||||
|
||||
# 2nd Separator
|
||||
self.add_horizontal_separator(self.scan_control_layout)
|
||||
|
||||
# Initialize the QTableWidget for args
|
||||
self.args_table = QTableWidget()
|
||||
self.args_table.verticalHeader().setSectionResizeMode(QHeaderView.Fixed)
|
||||
|
||||
self.scan_control_layout.addWidget(self.args_table)
|
||||
|
||||
# Connect signals
|
||||
self.comboBox_scan_selection.currentIndexChanged.connect(self.on_scan_selected)
|
||||
self.button_run_scan.clicked.connect(self.run_scan)
|
||||
|
||||
# Initialize scan selection
|
||||
self.populate_scans()
|
||||
|
||||
def add_horizontal_separator(self, layout) -> None:
|
||||
"""
|
||||
Adds a horizontal separator to the given layout
|
||||
|
||||
Args:
|
||||
layout: Layout to add the separator to
|
||||
"""
|
||||
separator = QFrame(self.scan_control_group)
|
||||
separator.setFrameShape(QFrame.HLine)
|
||||
separator.setFrameShadow(QFrame.Sunken)
|
||||
layout.addWidget(separator)
|
||||
|
||||
def populate_scans(self):
|
||||
"""Populates the scan selection combo box with available scans"""
|
||||
self.available_scans = self.client.producer.get(MessageEndpoints.available_scans()).resource
|
||||
if self.allowed_scans is None:
|
||||
allowed_scans = self.available_scans.keys()
|
||||
else:
|
||||
allowed_scans = self.allowed_scans
|
||||
# TODO check parent class is ScanBase -> filter out the scans not relevant for GUI
|
||||
self.comboBox_scan_selection.addItems(allowed_scans)
|
||||
|
||||
def on_scan_selected(self):
|
||||
"""Callback for scan selection combo box"""
|
||||
selected_scan_name = self.comboBox_scan_selection.currentText()
|
||||
selected_scan_info = self.available_scans.get(selected_scan_name, {})
|
||||
|
||||
print(selected_scan_info) # TODO remove when widget will be more mature
|
||||
# Generate kwargs input
|
||||
self.generate_kwargs_input_fields(selected_scan_info)
|
||||
|
||||
# Args section
|
||||
self.generate_args_input_fields(selected_scan_info)
|
||||
|
||||
def add_labels_to_layout(self, labels: list, grid_layout: QGridLayout) -> None:
|
||||
"""
|
||||
Adds labels to the given grid layout as a separate row.
|
||||
|
||||
Args:
|
||||
labels (list): List of label names to add.
|
||||
grid_layout (QGridLayout): The grid layout to which labels will be added.
|
||||
"""
|
||||
row_index = grid_layout.rowCount() # Get the next available row
|
||||
for column_index, label_name in enumerate(labels):
|
||||
label = QLabel(label_name.capitalize(), self.scan_control_group)
|
||||
# Add the label to the grid layout at the calculated row and current column
|
||||
grid_layout.addWidget(label, row_index, column_index)
|
||||
|
||||
def add_labels_to_table(
|
||||
self, labels: list, table: QTableWidget
|
||||
) -> None: # TODO could be moved to BECTable
|
||||
"""
|
||||
Adds labels to the given table widget as a header row.
|
||||
|
||||
Args:
|
||||
labels(list): List of label names to add.
|
||||
table(QTableWidget): The table widget to which labels will be added.
|
||||
"""
|
||||
table.setColumnCount(len(labels))
|
||||
table.setHorizontalHeaderLabels(labels)
|
||||
|
||||
def generate_args_input_fields(self, scan_info: dict) -> None:
|
||||
"""
|
||||
Generates input fields for args.
|
||||
|
||||
Args:
|
||||
scan_info(dict): Scan signature dictionary from BEC.
|
||||
"""
|
||||
|
||||
# Setup args table limits
|
||||
self.set_args_table_limits(self.args_table, scan_info)
|
||||
|
||||
# Get arg_input from selected scan
|
||||
self.arg_input = scan_info.get("arg_input", {})
|
||||
|
||||
# Generate labels for table
|
||||
self.add_labels_to_table(list(self.arg_input.keys()), self.args_table)
|
||||
|
||||
# add minimum number of args rows
|
||||
if self.arg_size_min is not None:
|
||||
for i in range(self.arg_size_min):
|
||||
self.add_bundle()
|
||||
|
||||
def generate_kwargs_input_fields(self, scan_info: dict) -> None:
|
||||
"""
|
||||
Generates input fields for kwargs
|
||||
|
||||
Args:
|
||||
scan_info(dict): Scan signature dictionary from BEC.
|
||||
"""
|
||||
# Create a new kwarg layout to replace the old one - this is necessary because otherwise row count is not reseted
|
||||
self.clear_and_delete_layout(self.kwargs_layout)
|
||||
self.kwargs_layout = self.create_new_grid_layout() # Create new grid layout
|
||||
self.scan_control_layout.insertLayout(0, self.kwargs_layout)
|
||||
|
||||
# Get signature
|
||||
signature = scan_info.get("signature", [])
|
||||
|
||||
# Extract kwargs from the converted signature
|
||||
kwargs = [param["name"] for param in signature if param["kind"] == "KEYWORD_ONLY"]
|
||||
|
||||
# Add labels
|
||||
self.add_labels_to_layout(kwargs, self.kwargs_layout)
|
||||
|
||||
# Add widgets
|
||||
widgets = self.generate_widgets_from_signature(kwargs, signature)
|
||||
|
||||
self.add_widgets_row_to_layout(self.kwargs_layout, widgets)
|
||||
|
||||
def generate_widgets_from_signature(self, items: list, signature: dict = None) -> list:
|
||||
"""
|
||||
Generates widgets from the given list of items.
|
||||
|
||||
Args:
|
||||
items(list): List of items to create widgets for.
|
||||
signature(dict, optional): Scan signature dictionary from BEC.
|
||||
|
||||
Returns:
|
||||
list: List of widgets created from the given items.
|
||||
"""
|
||||
widgets = [] # Initialize an empty list to hold the widgets
|
||||
|
||||
for item in items:
|
||||
if signature:
|
||||
# If a signature is provided, extract type and name from it
|
||||
kwarg_info = next((info for info in signature if info["name"] == item), None)
|
||||
if kwarg_info:
|
||||
item_type = kwarg_info.get("annotation", "_empty")
|
||||
item_name = item
|
||||
else:
|
||||
# If no signature is provided, assume the item is a tuple of (name, type)
|
||||
item_name, item_type = item
|
||||
|
||||
widget_class = self.WIDGET_HANDLER.get(item_type, None)
|
||||
if widget_class is None:
|
||||
print(f"Unsupported annotation '{item_type}' for parameter '{item_name}'")
|
||||
continue
|
||||
|
||||
# Instantiate the widget and set some properties if necessary
|
||||
widget = widget_class()
|
||||
|
||||
# set high default range for spin boxes #TODO can be linked to motor/device limits from BEC
|
||||
if isinstance(widget, (QSpinBox, QDoubleSpinBox)):
|
||||
widget.setRange(-9999, 9999)
|
||||
widget.setValue(0)
|
||||
# Add the widget to the list
|
||||
widgets.append(widget)
|
||||
|
||||
return widgets
|
||||
|
||||
def set_args_table_limits(self, table: QTableWidget, scan_info: dict) -> None:
|
||||
# Get bundle info
|
||||
arg_bundle_size = scan_info.get("arg_bundle_size", {})
|
||||
self.arg_size_min = arg_bundle_size.get("min", 1)
|
||||
self.arg_size_max = arg_bundle_size.get("max", None)
|
||||
|
||||
# Clear the previous input fields
|
||||
table.setRowCount(0) # Wipe table
|
||||
|
||||
def add_widgets_row_to_layout(
|
||||
self, grid_layout: QGridLayout, widgets: list, row_index: int = None
|
||||
) -> None:
|
||||
"""
|
||||
Adds a row of widgets to the given grid layout.
|
||||
|
||||
Args:
|
||||
grid_layout (QGridLayout): The grid layout to which widgets will be added.
|
||||
items (list): List of parameter names to create widgets for.
|
||||
row_index (int): The row index where the widgets should be added.
|
||||
"""
|
||||
# If row_index is not specified, add to the next available row
|
||||
if row_index is None:
|
||||
row_index = grid_layout.rowCount()
|
||||
|
||||
for column_index, widget in enumerate(widgets):
|
||||
# Add the widget to the grid layout at the specified row and column
|
||||
grid_layout.addWidget(widget, row_index, column_index)
|
||||
|
||||
def add_widgets_row_to_table(
|
||||
self, table_widget: QTableWidget, widgets: list, row_index: int = None
|
||||
) -> None:
|
||||
"""
|
||||
Adds a row of widgets to the given QTableWidget.
|
||||
|
||||
Args:
|
||||
table_widget (QTableWidget): The table widget to which widgets will be added.
|
||||
widgets (list): List of widgets to add to the table.
|
||||
row_index (int): The row index where the widgets should be added. If None, add to the end.
|
||||
"""
|
||||
# If row_index is not specified, add to the end of the table
|
||||
if row_index is None or row_index > table_widget.rowCount():
|
||||
row_index = table_widget.rowCount()
|
||||
if self.arg_size_max is not None: # ensure the max args size is not exceeded
|
||||
if row_index >= self.arg_size_max:
|
||||
return
|
||||
table_widget.insertRow(row_index)
|
||||
|
||||
for column_index, widget in enumerate(widgets):
|
||||
# If the widget is a subclass of QWidget, use setCellWidget
|
||||
if issubclass(type(widget), QWidget):
|
||||
table_widget.setCellWidget(row_index, column_index, widget)
|
||||
else:
|
||||
# Otherwise, assume it's a string or some other value that should be displayed as text
|
||||
item = QTableWidgetItem(str(widget))
|
||||
table_widget.setItem(row_index, column_index, item)
|
||||
|
||||
# Optionally, adjust the row height based on the content #TODO decide if needed
|
||||
table_widget.setRowHeight(
|
||||
row_index,
|
||||
max(widget.sizeHint().height() for widget in widgets if isinstance(widget, QWidget)),
|
||||
)
|
||||
|
||||
def remove_last_row_from_table(self, table_widget: QTableWidget) -> None:
|
||||
"""
|
||||
Removes the last row from the given QTableWidget until only one row is left.
|
||||
|
||||
Args:
|
||||
table_widget (QTableWidget): The table widget from which the last row will be removed.
|
||||
"""
|
||||
row_count = table_widget.rowCount()
|
||||
if (
|
||||
row_count > self.arg_size_min
|
||||
): # Check to ensure there is a minimum number of rows remaining
|
||||
table_widget.removeRow(row_count - 1)
|
||||
|
||||
def create_new_grid_layout(self):
|
||||
new_layout = QGridLayout()
|
||||
# TODO maybe setup other layouts properties here?
|
||||
return new_layout
|
||||
|
||||
def clear_and_delete_layout(self, layout: QLayout):
|
||||
"""
|
||||
Clears and deletes the given layout and all its child widgets.
|
||||
|
||||
Args:
|
||||
layout(QLayout): Layout to clear and delete
|
||||
"""
|
||||
if layout is not None:
|
||||
while layout.count():
|
||||
item = layout.takeAt(0)
|
||||
widget = item.widget()
|
||||
if widget:
|
||||
widget.deleteLater()
|
||||
else:
|
||||
sub_layout = item.layout()
|
||||
if sub_layout:
|
||||
self.clear_and_delete_layout(sub_layout)
|
||||
layout.deleteLater()
|
||||
|
||||
def add_bundle(self) -> None:
|
||||
"""Adds a new bundle to the scan control layout"""
|
||||
# Get widgets used for particular scan and save them to be able to use for adding bundles
|
||||
args_widgets = self.generate_widgets_from_signature(
|
||||
self.arg_input.items()
|
||||
) # TODO decide if make sense to put widget list into method parameters
|
||||
|
||||
# Add first widgets row to the table
|
||||
self.add_widgets_row_to_table(self.args_table, args_widgets)
|
||||
|
||||
def remove_bundle(self) -> None:
|
||||
"""Removes the last bundle from the scan control layout"""
|
||||
self.remove_last_row_from_table(self.args_table)
|
||||
|
||||
def extract_kwargs_from_grid_row(self, grid_layout: QGridLayout, row: int) -> dict:
|
||||
kwargs = {}
|
||||
for column in range(grid_layout.columnCount()):
|
||||
label_item = grid_layout.itemAtPosition(row, column)
|
||||
if label_item is not None:
|
||||
label_widget = label_item.widget()
|
||||
if isinstance(label_widget, QLabel):
|
||||
key = label_widget.text()
|
||||
|
||||
# The corresponding value widget is in the next row
|
||||
value_item = grid_layout.itemAtPosition(row + 1, column)
|
||||
if value_item is not None:
|
||||
value_widget = value_item.widget()
|
||||
# Use WidgetIO.get_value to extract the value
|
||||
value = WidgetIO.get_value(value_widget)
|
||||
kwargs[key] = value
|
||||
return kwargs
|
||||
|
||||
def extract_args_from_table(self, table: QTableWidget) -> list:
|
||||
"""
|
||||
Extracts the arguments from the given table widget.
|
||||
|
||||
Args:
|
||||
table(QTableWidget): Table widget from which to extract the arguments
|
||||
"""
|
||||
args = []
|
||||
for row in range(table.rowCount()):
|
||||
row_args = []
|
||||
for column in range(table.columnCount()):
|
||||
widget = table.cellWidget(row, column)
|
||||
if widget:
|
||||
if isinstance(widget, QLineEdit): # special case for QLineEdit for Devices
|
||||
value = widget.text().lower()
|
||||
if value in self.dev:
|
||||
value = getattr(self.dev, value)
|
||||
else:
|
||||
raise ValueError(f"The device '{value}' is not recognized.")
|
||||
else:
|
||||
value = WidgetIO.get_value(widget)
|
||||
row_args.append(value)
|
||||
args.extend(row_args)
|
||||
return args
|
||||
|
||||
def run_scan(self):
|
||||
# Extract kwargs for the scan
|
||||
kwargs = {
|
||||
k.lower(): v
|
||||
for k, v in self.extract_kwargs_from_grid_row(self.kwargs_layout, 1).items()
|
||||
}
|
||||
|
||||
# Extract args from the table
|
||||
args = self.extract_args_from_table(self.args_table)
|
||||
|
||||
# Convert args to lowercase if they are strings
|
||||
args = [arg.lower() if isinstance(arg, str) else arg for arg in args]
|
||||
|
||||
# Execute the scan
|
||||
scan_function = getattr(self.scans, self.comboBox_scan_selection.currentText())
|
||||
if callable(scan_function):
|
||||
scan_function(*args, **kwargs)
|
||||
|
||||
|
||||
# Application example
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
# BECclient global variables
|
||||
client = BECDispatcher().client
|
||||
client.start()
|
||||
|
||||
app = QApplication([])
|
||||
scan_control = ScanControl(client=client) # allowed_scans=["line_scan", "grid_scan"])
|
||||
|
||||
window = scan_control
|
||||
window.show()
|
||||
app.exec()
|
||||
@@ -1 +0,0 @@
|
||||
from .spiral_progress_bar import SpiralProgressBar
|
||||
@@ -1,278 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal, Optional
|
||||
|
||||
from bec_lib.endpoints import EndpointInfo
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from pydantic_core import PydanticCustomError
|
||||
from qtpy import QtGui
|
||||
|
||||
from bec_widgets.utils import BECConnector, ConnectionConfig
|
||||
|
||||
|
||||
class RingConnections(BaseModel):
|
||||
slot: Literal["on_scan_progress", "on_device_readback"] = None
|
||||
endpoint: EndpointInfo | str = None
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
|
||||
@field_validator("endpoint")
|
||||
def validate_endpoint(cls, v, values):
|
||||
slot = values.data["slot"]
|
||||
v = v.endpoint if isinstance(v, EndpointInfo) else v
|
||||
if slot == "on_scan_progress":
|
||||
if v != "scans/scan_progress":
|
||||
raise PydanticCustomError(
|
||||
"unsupported endpoint",
|
||||
"For slot 'on_scan_progress', endpoint must be MessageEndpoint.scan_progress or 'scans/scan_progress'.",
|
||||
{"wrong_value": v},
|
||||
)
|
||||
elif slot == "on_device_readback":
|
||||
if not v.startswith("internal/devices/readback/"):
|
||||
raise PydanticCustomError(
|
||||
"unsupported endpoint",
|
||||
"For slot 'on_device_readback', endpoint must be MessageEndpoint.device_readback(device) or 'internal/devices/readback/{device}'.",
|
||||
{"wrong_value": v},
|
||||
)
|
||||
return v
|
||||
|
||||
|
||||
class RingConfig(ConnectionConfig):
|
||||
value: int | float | None = Field(0, description="Value for the progress bars.")
|
||||
direction: int | None = Field(
|
||||
-1, description="Direction of the progress bars. -1 for clockwise, 1 for counter-clockwise."
|
||||
)
|
||||
color: str | tuple | None = Field(
|
||||
(0, 159, 227, 255),
|
||||
description="Color for the progress bars. Can be tuple (R, G, B, A) or string HEX Code.",
|
||||
)
|
||||
background_color: str | tuple | None = Field(
|
||||
(200, 200, 200, 50),
|
||||
description="Background color for the progress bars. Can be tuple (R, G, B, A) or string HEX Code.",
|
||||
)
|
||||
index: int | None = Field(0, description="Index of the progress bar. 0 is outer ring.")
|
||||
line_width: int | None = Field(10, description="Line widths for the progress bars.")
|
||||
start_position: int | None = Field(
|
||||
90,
|
||||
description="Start position for the progress bars in degrees. Default is 90 degrees - corespons to "
|
||||
"the top of the ring.",
|
||||
)
|
||||
min_value: int | float | None = Field(0, description="Minimum value for the progress bars.")
|
||||
max_value: int | float | None = Field(100, description="Maximum value for the progress bars.")
|
||||
precision: int | None = Field(3, description="Precision for the progress bars.")
|
||||
update_behaviour: Literal["manual", "auto"] | None = Field(
|
||||
"auto", description="Update behaviour for the progress bars."
|
||||
)
|
||||
connections: RingConnections | None = Field(
|
||||
default_factory=RingConnections, description="Connections for the progress bars."
|
||||
)
|
||||
|
||||
|
||||
class Ring(BECConnector):
|
||||
USER_ACCESS = [
|
||||
"get_all_rpc",
|
||||
"rpc_id",
|
||||
"config_dict",
|
||||
"set_value",
|
||||
"set_color",
|
||||
"set_background",
|
||||
"set_line_width",
|
||||
"set_min_max_values",
|
||||
"set_start_angle",
|
||||
"set_update",
|
||||
"reset_connection",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
parent_progress_widget=None,
|
||||
config: RingConfig | dict | None = None,
|
||||
client=None,
|
||||
gui_id: Optional[str] = None,
|
||||
):
|
||||
if config is None:
|
||||
config = RingConfig(widget_class=self.__class__.__name__)
|
||||
self.config = config
|
||||
else:
|
||||
if isinstance(config, dict):
|
||||
config = RingConfig(**config)
|
||||
self.config = config
|
||||
super().__init__(client=client, config=config, gui_id=gui_id)
|
||||
|
||||
self.parent_progress_widget = parent_progress_widget
|
||||
self.color = None
|
||||
self.background_color = None
|
||||
self.start_position = None
|
||||
self.config = config
|
||||
self.RID = None
|
||||
self._init_config_params()
|
||||
|
||||
def _init_config_params(self):
|
||||
self.color = self.convert_color(self.config.color)
|
||||
self.background_color = self.convert_color(self.config.background_color)
|
||||
self.set_start_angle(self.config.start_position)
|
||||
if self.config.connections:
|
||||
self.set_connections(self.config.connections.slot, self.config.connections.endpoint)
|
||||
|
||||
def set_value(self, value: int | float):
|
||||
"""
|
||||
Set the value for the ring widget
|
||||
|
||||
Args:
|
||||
value(int | float): Value for the ring widget
|
||||
"""
|
||||
self.config.value = round(
|
||||
float(max(self.config.min_value, min(self.config.max_value, value))),
|
||||
self.config.precision,
|
||||
)
|
||||
|
||||
def set_color(self, color: str | tuple):
|
||||
"""
|
||||
Set the color for the ring widget
|
||||
|
||||
Args:
|
||||
color(str | tuple): Color for the ring widget. Can be HEX code or tuple (R, G, B, A).
|
||||
"""
|
||||
self.config.color = color
|
||||
self.color = self.convert_color(color)
|
||||
|
||||
def set_background(self, color: str | tuple):
|
||||
"""
|
||||
Set the background color for the ring widget
|
||||
|
||||
Args:
|
||||
color(str | tuple): Background color for the ring widget. Can be HEX code or tuple (R, G, B, A).
|
||||
"""
|
||||
self.config.background_color = color
|
||||
self.color = self.convert_color(color)
|
||||
|
||||
def set_line_width(self, width: int):
|
||||
"""
|
||||
Set the line width for the ring widget
|
||||
|
||||
Args:
|
||||
width(int): Line width for the ring widget
|
||||
"""
|
||||
self.config.line_width = width
|
||||
|
||||
def set_min_max_values(self, min_value: int | float, max_value: int | float):
|
||||
"""
|
||||
Set the min and max values for the ring widget.
|
||||
|
||||
Args:
|
||||
min_value(int | float): Minimum value for the ring widget
|
||||
max_value(int | float): Maximum value for the ring widget
|
||||
"""
|
||||
self.config.min_value = min_value
|
||||
self.config.max_value = max_value
|
||||
|
||||
def set_start_angle(self, start_angle: int):
|
||||
"""
|
||||
Set the start angle for the ring widget
|
||||
|
||||
Args:
|
||||
start_angle(int): Start angle for the ring widget in degrees
|
||||
"""
|
||||
self.config.start_position = start_angle
|
||||
self.start_position = start_angle * 16
|
||||
|
||||
@staticmethod
|
||||
def convert_color(color):
|
||||
"""
|
||||
Convert the color to QColor
|
||||
|
||||
Args:
|
||||
color(str | tuple): Color for the ring widget. Can be HEX code or tuple (R, G, B, A).
|
||||
"""
|
||||
converted_color = None
|
||||
if isinstance(color, str):
|
||||
converted_color = QtGui.QColor(color)
|
||||
elif isinstance(color, tuple):
|
||||
converted_color = QtGui.QColor(*color)
|
||||
return converted_color
|
||||
|
||||
def set_update(self, mode: Literal["manual", "scan", "device"], device: str = None):
|
||||
"""
|
||||
Set the update mode for the ring widget.
|
||||
Modes:
|
||||
- "manual": Manual update mode, the value is set by the user.
|
||||
- "scan": Update mode for the scan progress. The value is updated by the current scan progress.
|
||||
- "device": Update mode for the device readback. The value is updated by the device readback. Take into account that user has to set the device name and limits.
|
||||
|
||||
Args:
|
||||
mode(str): Update mode for the ring widget. Can be "manual", "scan" or "device"
|
||||
device(str): Device name for the device readback mode, only used when mode is "device"
|
||||
"""
|
||||
if mode == "manual":
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
getattr(self, self.config.connections.slot), self.config.connections.endpoint
|
||||
)
|
||||
self.config.connections.slot = None
|
||||
self.config.connections.endpoint = None
|
||||
elif mode == "scan":
|
||||
self.set_connections("on_scan_progress", "scans/scan_progress")
|
||||
elif mode == "device":
|
||||
self.set_connections("on_device_readback", f"internal/devices/readback/{device}")
|
||||
|
||||
self.parent_progress_widget.enable_auto_updates(False)
|
||||
|
||||
def set_connections(self, slot: str, endpoint: str | EndpointInfo):
|
||||
"""
|
||||
Set the connections for the ring widget
|
||||
|
||||
Args:
|
||||
slot(str): Slot for the ring widget update. Can be "on_scan_progress" or "on_device_readback".
|
||||
endpoint(str | EndpointInfo): Endpoint for the ring widget update. Endpoint has to match the slot type.
|
||||
"""
|
||||
if self.config.connections.endpoint == endpoint and self.config.connections.slot == slot:
|
||||
return
|
||||
else:
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.config.connections.slot, self.config.connections.endpoint
|
||||
)
|
||||
self.config.connections = RingConnections(slot=slot, endpoint=endpoint)
|
||||
self.bec_dispatcher.connect_slot(getattr(self, slot), endpoint)
|
||||
|
||||
def reset_connection(self):
|
||||
"""
|
||||
Reset the connections for the ring widget. Disconnect the current slot and endpoint.
|
||||
"""
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.config.connections.slot, self.config.connections.endpoint
|
||||
)
|
||||
self.config.connections = RingConnections()
|
||||
|
||||
def on_scan_progress(self, msg, meta):
|
||||
"""
|
||||
Update the ring widget with the scan progress.
|
||||
|
||||
Args:
|
||||
msg(dict): Message with the scan progress
|
||||
meta(dict): Metadata for the message
|
||||
"""
|
||||
current_RID = meta.get("RID", None)
|
||||
if current_RID != self.RID:
|
||||
self.set_min_max_values(0, msg.get("max_value", 100))
|
||||
self.set_value(msg.get("value", 0))
|
||||
self.parent_progress_widget.update()
|
||||
|
||||
def on_device_readback(self, msg, meta):
|
||||
"""
|
||||
Update the ring widget with the device readback.
|
||||
|
||||
Args:
|
||||
msg(dict): Message with the device readback
|
||||
meta(dict): Metadata for the message
|
||||
"""
|
||||
if isinstance(self.config.connections.endpoint, EndpointInfo):
|
||||
endpoint = self.config.connections.endpoint.endpoint
|
||||
else:
|
||||
endpoint = self.config.connections.endpoint
|
||||
device = endpoint.split("/")[-1]
|
||||
value = msg.get("signals").get(device).get("value")
|
||||
self.set_value(value)
|
||||
self.parent_progress_widget.update()
|
||||
|
||||
def cleanup(self):
|
||||
self.reset_connection()
|
||||
super().cleanup()
|
||||
@@ -1,636 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal, Optional
|
||||
|
||||
import pyqtgraph as pg
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from pydantic import Field, field_validator
|
||||
from pydantic_core import PydanticCustomError
|
||||
from qtpy import QtCore, QtGui
|
||||
from qtpy.QtCore import QSize, Slot
|
||||
from qtpy.QtWidgets import QSizePolicy, QWidget
|
||||
|
||||
from bec_widgets.utils import BECConnector, Colors, ConnectionConfig, EntryValidator
|
||||
from bec_widgets.widgets.spiral_progress_bar.ring import Ring, RingConfig
|
||||
|
||||
|
||||
class SpiralProgressBarConfig(ConnectionConfig):
|
||||
color_map: Optional[str] = Field(
|
||||
"magma", description="Color scheme for the progress bars.", validate_default=True
|
||||
)
|
||||
min_number_of_bars: int | None = Field(
|
||||
1, description="Minimum number of progress bars to display."
|
||||
)
|
||||
max_number_of_bars: int | None = Field(
|
||||
10, description="Maximum number of progress bars to display."
|
||||
)
|
||||
num_bars: int | None = Field(1, description="Number of progress bars to display.")
|
||||
gap: int | None = Field(20, description="Gap between progress bars.")
|
||||
auto_updates: bool | None = Field(
|
||||
True, description="Enable or disable updates based on scan queue status."
|
||||
)
|
||||
rings: list[RingConfig] | None = Field([], description="List of ring configurations.")
|
||||
|
||||
@field_validator("num_bars")
|
||||
def validate_num_bars(cls, v, values):
|
||||
min_number_of_bars = values.data.get("min_number_of_bars", None)
|
||||
max_number_of_bars = values.data.get("max_number_of_bars", None)
|
||||
if min_number_of_bars is not None and max_number_of_bars is not None:
|
||||
print(
|
||||
f"Number of bars adjusted to be between defined min:{min_number_of_bars} and max:{max_number_of_bars} number of bars."
|
||||
)
|
||||
v = max(min_number_of_bars, min(v, max_number_of_bars))
|
||||
return v
|
||||
|
||||
@field_validator("rings")
|
||||
def validate_rings(cls, v, values):
|
||||
if v is not None and v is not []:
|
||||
num_bars = values.data.get("num_bars", None)
|
||||
if len(v) != num_bars:
|
||||
raise PydanticCustomError(
|
||||
"different number of configs",
|
||||
f"Length of rings configuration ({len(v)}) does not match the number of bars ({num_bars}).",
|
||||
{"wrong_value": len(v)},
|
||||
)
|
||||
indices = [ring.index for ring in v]
|
||||
if sorted(indices) != list(range(len(indices))):
|
||||
raise PydanticCustomError(
|
||||
"wrong indices",
|
||||
f"Indices of ring configurations must be unique and in order from 0 to num_bars {num_bars}.",
|
||||
{"wrong_value": indices},
|
||||
)
|
||||
return v
|
||||
|
||||
_validate_colormap = field_validator("color_map")(Colors.validate_color_map)
|
||||
|
||||
|
||||
class SpiralProgressBar(BECConnector, QWidget):
|
||||
USER_ACCESS = [
|
||||
"get_all_rpc",
|
||||
"rpc_id",
|
||||
"config_dict",
|
||||
"rings",
|
||||
"update_config",
|
||||
"add_ring",
|
||||
"remove_ring",
|
||||
"set_precision",
|
||||
"set_min_max_values",
|
||||
"set_number_of_bars",
|
||||
"set_value",
|
||||
"set_colors_from_map",
|
||||
"set_colors_directly",
|
||||
"set_line_widths",
|
||||
"set_gap",
|
||||
"set_diameter",
|
||||
"reset_diameter",
|
||||
"enable_auto_updates",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
config: SpiralProgressBarConfig | dict | None = None,
|
||||
client=None,
|
||||
gui_id: str | None = None,
|
||||
num_bars: int | None = None,
|
||||
):
|
||||
if config is None:
|
||||
config = SpiralProgressBarConfig(widget_class=self.__class__.__name__)
|
||||
self.config = config
|
||||
else:
|
||||
if isinstance(config, dict):
|
||||
config = SpiralProgressBarConfig(**config, widget_class=self.__class__.__name__)
|
||||
self.config = config
|
||||
super().__init__(client=client, config=config, gui_id=gui_id)
|
||||
QWidget.__init__(self, parent=None)
|
||||
|
||||
self.get_bec_shortcuts()
|
||||
self.entry_validator = EntryValidator(self.dev)
|
||||
|
||||
self.RID = None
|
||||
|
||||
# For updating bar behaviour
|
||||
self._auto_updates = True
|
||||
self._rings = None
|
||||
|
||||
if num_bars is not None:
|
||||
self.config.num_bars = max(
|
||||
self.config.min_number_of_bars, min(num_bars, self.config.max_number_of_bars)
|
||||
)
|
||||
self.initialize_bars()
|
||||
|
||||
self.enable_auto_updates(self.config.auto_updates)
|
||||
|
||||
@property
|
||||
def rings(self):
|
||||
return self._rings
|
||||
|
||||
@rings.setter
|
||||
def rings(self, value):
|
||||
self._rings = value
|
||||
|
||||
def update_config(self, config: SpiralProgressBarConfig | dict):
|
||||
"""
|
||||
Update the configuration of the widget.
|
||||
|
||||
Args:
|
||||
config(SpiralProgressBarConfig|dict): Configuration to update.
|
||||
"""
|
||||
if isinstance(config, dict):
|
||||
config = SpiralProgressBarConfig(**config, widget_class=self.__class__.__name__)
|
||||
self.config = config
|
||||
self.clear_all()
|
||||
|
||||
def initialize_bars(self):
|
||||
"""
|
||||
Initialize the progress bars.
|
||||
"""
|
||||
start_positions = [90 * 16] * self.config.num_bars
|
||||
directions = [-1] * self.config.num_bars
|
||||
|
||||
self.config.rings = [
|
||||
RingConfig(
|
||||
widget_class="Ring",
|
||||
index=i,
|
||||
start_positions=start_positions[i],
|
||||
directions=directions[i],
|
||||
)
|
||||
for i in range(self.config.num_bars)
|
||||
]
|
||||
self._rings = [
|
||||
Ring(parent_progress_widget=self, config=config) for config in self.config.rings
|
||||
]
|
||||
|
||||
if self.config.color_map:
|
||||
self.set_colors_from_map(self.config.color_map)
|
||||
|
||||
min_size = self._calculate_minimum_size()
|
||||
self.setMinimumSize(min_size)
|
||||
self.update()
|
||||
|
||||
def add_ring(self, **kwargs) -> Ring:
|
||||
"""
|
||||
Add a new progress bar.
|
||||
|
||||
Args:
|
||||
**kwargs: Keyword arguments for the new progress bar.
|
||||
|
||||
Returns:
|
||||
Ring: Ring object.
|
||||
"""
|
||||
if self.config.num_bars < self.config.max_number_of_bars:
|
||||
ring_index = self.config.num_bars
|
||||
ring_config = RingConfig(
|
||||
widget_class="Ring",
|
||||
index=ring_index,
|
||||
start_positions=90 * 16,
|
||||
directions=-1,
|
||||
**kwargs,
|
||||
)
|
||||
ring = Ring(parent_progress_widget=self, config=ring_config)
|
||||
self.config.num_bars += 1
|
||||
self._rings.append(ring)
|
||||
self.config.rings.append(ring.config)
|
||||
if self.config.color_map:
|
||||
self.set_colors_from_map(self.config.color_map)
|
||||
base_line_width = self._rings[ring.config.index].config.line_width
|
||||
self.set_line_widths(base_line_width, ring.config.index)
|
||||
self.update()
|
||||
return ring
|
||||
|
||||
def remove_ring(self, index: int):
|
||||
"""
|
||||
Remove a progress bar by index.
|
||||
|
||||
Args:
|
||||
index(int): Index of the progress bar to remove.
|
||||
"""
|
||||
ring = self._find_ring_by_index(index)
|
||||
ring.cleanup()
|
||||
self._rings.remove(ring)
|
||||
self.config.rings.remove(ring.config)
|
||||
self.config.num_bars -= 1
|
||||
self._reindex_rings()
|
||||
if self.config.color_map:
|
||||
self.set_colors_from_map(self.config.color_map)
|
||||
del ring
|
||||
self.update()
|
||||
|
||||
def _reindex_rings(self):
|
||||
"""
|
||||
Reindex the progress bars.
|
||||
"""
|
||||
for i, ring in enumerate(self._rings):
|
||||
ring.config.index = i
|
||||
|
||||
def set_precision(self, precision: int, bar_index: int = None):
|
||||
"""
|
||||
Set the precision for the progress bars. If bar_index is not provide, the precision will be set for all progress bars.
|
||||
|
||||
Args:
|
||||
precision(int): Precision for the progress bars.
|
||||
bar_index(int): Index of the progress bar to set the precision for. If provided, only a single precision can be set.
|
||||
"""
|
||||
if bar_index is not None:
|
||||
bar_index = self._bar_index_check(bar_index)
|
||||
ring = self._find_ring_by_index(bar_index)
|
||||
ring.config.precision = precision
|
||||
else:
|
||||
for ring in self._rings:
|
||||
ring.config.precision = precision
|
||||
self.update()
|
||||
|
||||
def set_min_max_values(
|
||||
self,
|
||||
min_values: int | float | list[int | float],
|
||||
max_values: int | float | list[int | float],
|
||||
):
|
||||
"""
|
||||
Set the minimum and maximum values for the progress bars.
|
||||
|
||||
Args:
|
||||
min_values(int|float | list[float]): Minimum value(s) for the progress bars. If multiple progress bars are displayed, provide a list of minimum values for each progress bar.
|
||||
max_values(int|float | list[float]): Maximum value(s) for the progress bars. If multiple progress bars are displayed, provide a list of maximum values for each progress bar.
|
||||
"""
|
||||
if isinstance(min_values, int) or isinstance(min_values, float):
|
||||
min_values = [min_values]
|
||||
if isinstance(max_values, int) or isinstance(max_values, float):
|
||||
max_values = [max_values]
|
||||
min_values = self._adjust_list_to_bars(min_values)
|
||||
max_values = self._adjust_list_to_bars(max_values)
|
||||
for ring, min_value, max_value in zip(self._rings, min_values, max_values):
|
||||
ring.set_min_max_values(min_value, max_value)
|
||||
self.update()
|
||||
|
||||
def set_number_of_bars(self, num_bars: int):
|
||||
"""
|
||||
Set the number of progress bars to display.
|
||||
|
||||
Args:
|
||||
num_bars(int): Number of progress bars to display.
|
||||
"""
|
||||
num_bars = max(
|
||||
self.config.min_number_of_bars, min(num_bars, self.config.max_number_of_bars)
|
||||
)
|
||||
current_num_bars = self.config.num_bars
|
||||
|
||||
if num_bars > current_num_bars:
|
||||
for i in range(current_num_bars, num_bars):
|
||||
new_ring_config = RingConfig(
|
||||
widget_class="Ring", index=i, start_positions=90 * 16, directions=-1
|
||||
)
|
||||
self.config.rings.append(new_ring_config)
|
||||
new_ring = Ring(parent_progress_widget=self, config=new_ring_config)
|
||||
self._rings.append(new_ring)
|
||||
|
||||
elif num_bars < current_num_bars:
|
||||
for i in range(current_num_bars - 1, num_bars - 1, -1):
|
||||
self.remove_ring(i)
|
||||
|
||||
self.config.num_bars = num_bars
|
||||
|
||||
if self.config.color_map:
|
||||
self.set_colors_from_map(self.config.color_map)
|
||||
|
||||
base_line_width = self._rings[0].config.line_width
|
||||
self.set_line_widths(base_line_width)
|
||||
|
||||
self.update()
|
||||
|
||||
def set_value(self, values: int | list, ring_index: int = None):
|
||||
"""
|
||||
Set the values for the progress bars.
|
||||
|
||||
Args:
|
||||
values(int | tuple): Value(s) for the progress bars. If multiple progress bars are displayed, provide a tuple of values for each progress bar.
|
||||
ring_index(int): Index of the progress bar to set the value for. If provided, only a single value can be set.
|
||||
|
||||
Examples:
|
||||
>>> SpiralProgressBar.set_value(50)
|
||||
>>> SpiralProgressBar.set_value([30, 40, 50]) # (outer, middle, inner)
|
||||
>>> SpiralProgressBar.set_value(60, bar_index=1) # Set the value for the middle progress bar.
|
||||
"""
|
||||
if ring_index is not None:
|
||||
ring = self._find_ring_by_index(ring_index)
|
||||
if isinstance(values, list):
|
||||
values = values[0]
|
||||
print(
|
||||
f"Warning: Only a single value can be set for a single progress bar. Using the first value in the list {values}"
|
||||
)
|
||||
ring.set_value(values)
|
||||
else:
|
||||
if isinstance(values, int):
|
||||
values = [values]
|
||||
values = self._adjust_list_to_bars(values)
|
||||
for ring, value in zip(self._rings, values):
|
||||
ring.set_value(value)
|
||||
self.update()
|
||||
|
||||
def set_colors_from_map(self, colormap, color_format: Literal["RGB", "HEX"] = "RGB"):
|
||||
"""
|
||||
Set the colors for the progress bars from a colormap.
|
||||
|
||||
Args:
|
||||
colormap(str): Name of the colormap.
|
||||
color_format(Literal["RGB","HEX"]): Format of the returned colors ('RGB', 'HEX').
|
||||
"""
|
||||
if colormap not in pg.colormap.listMaps():
|
||||
raise ValueError(
|
||||
f"Colormap '{colormap}' not found in the current installation of pyqtgraph"
|
||||
)
|
||||
colors = Colors.golden_angle_color(colormap, self.config.num_bars, color_format)
|
||||
self.set_colors_directly(colors)
|
||||
self.config.color_map = colormap
|
||||
self.update()
|
||||
|
||||
def set_colors_directly(self, colors: list[str | tuple] | str | tuple, bar_index: int = None):
|
||||
"""
|
||||
Set the colors for the progress bars directly.
|
||||
|
||||
Args:
|
||||
colors(list[str | tuple] | str | tuple): Color(s) for the progress bars. If multiple progress bars are displayed, provide a list of colors for each progress bar.
|
||||
bar_index(int): Index of the progress bar to set the color for. If provided, only a single color can be set.
|
||||
"""
|
||||
if bar_index is not None and isinstance(colors, (str, tuple)):
|
||||
bar_index = self._bar_index_check(bar_index)
|
||||
ring = self._find_ring_by_index(bar_index)
|
||||
ring.set_color(colors)
|
||||
else:
|
||||
if isinstance(colors, (str, tuple)):
|
||||
colors = [colors]
|
||||
colors = self._adjust_list_to_bars(colors)
|
||||
for ring, color in zip(self._rings, colors):
|
||||
ring.set_color(color)
|
||||
self.update()
|
||||
|
||||
def set_line_widths(self, widths: int | list[int], bar_index: int = None):
|
||||
"""
|
||||
Set the line widths for the progress bars.
|
||||
|
||||
Args:
|
||||
widths(int | list[int]): Line width(s) for the progress bars. If multiple progress bars are displayed, provide a list of line widths for each progress bar.
|
||||
bar_index(int): Index of the progress bar to set the line width for. If provided, only a single line width can be set.
|
||||
"""
|
||||
if bar_index is not None:
|
||||
bar_index = self._bar_index_check(bar_index)
|
||||
ring = self._find_ring_by_index(bar_index)
|
||||
if isinstance(widths, list):
|
||||
widths = widths[0]
|
||||
print(
|
||||
f"Warning: Only a single line width can be set for a single progress bar. Using the first value in the list {widths}"
|
||||
)
|
||||
ring.set_line_width(widths)
|
||||
else:
|
||||
if isinstance(widths, int):
|
||||
widths = [widths]
|
||||
widths = self._adjust_list_to_bars(widths)
|
||||
self.config.gap = max(widths) * 2
|
||||
for ring, width in zip(self._rings, widths):
|
||||
ring.set_line_width(width)
|
||||
min_size = self._calculate_minimum_size()
|
||||
self.setMinimumSize(min_size)
|
||||
self.update()
|
||||
|
||||
def set_gap(self, gap: int):
|
||||
"""
|
||||
Set the gap between the progress bars.
|
||||
|
||||
Args:
|
||||
gap(int): Gap between the progress bars.
|
||||
"""
|
||||
self.config.gap = gap
|
||||
self.update()
|
||||
|
||||
def set_diameter(self, diameter: int):
|
||||
"""
|
||||
Set the diameter of the widget.
|
||||
|
||||
Args:
|
||||
diameter(int): Diameter of the widget.
|
||||
"""
|
||||
size = QSize(diameter, diameter)
|
||||
self.resize(size)
|
||||
self.setFixedSize(size)
|
||||
|
||||
def _find_ring_by_index(self, index: int) -> Ring:
|
||||
"""
|
||||
Find the ring by index.
|
||||
|
||||
Args:
|
||||
index(int): Index of the ring.
|
||||
|
||||
Returns:
|
||||
Ring: Ring object.
|
||||
"""
|
||||
found_ring = None
|
||||
for ring in self._rings:
|
||||
if ring.config.index == index:
|
||||
found_ring = ring
|
||||
break
|
||||
if found_ring is None:
|
||||
raise ValueError(f"Ring with index {index} not found.")
|
||||
return found_ring
|
||||
|
||||
def enable_auto_updates(self, enable: bool = True):
|
||||
"""
|
||||
Enable or disable updates based on scan status. Overrides manual updates.
|
||||
The behaviour of the whole progress bar widget will be driven by the scan queue status.
|
||||
|
||||
Args:
|
||||
enable(bool): True or False.
|
||||
|
||||
Returns:
|
||||
bool: True if scan segment updates are enabled.
|
||||
"""
|
||||
|
||||
self._auto_updates = enable
|
||||
if enable is True:
|
||||
self.bec_dispatcher.connect_slot(
|
||||
self.on_scan_queue_status, MessageEndpoints.scan_queue_status()
|
||||
)
|
||||
else:
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.on_scan_queue_status, MessageEndpoints.scan_queue_status()
|
||||
)
|
||||
return self._auto_updates
|
||||
|
||||
@Slot(dict, dict)
|
||||
def on_scan_queue_status(self, msg, meta):
|
||||
"""
|
||||
Slot to handle scan queue status messages. Decides what update to perform based on the scan queue status.
|
||||
|
||||
Args:
|
||||
msg(dict): Message from the BEC.
|
||||
meta(dict): Metadata from the BEC.
|
||||
"""
|
||||
primary_queue = msg.get("queue").get("primary")
|
||||
info = primary_queue.get("info", None)
|
||||
|
||||
if info:
|
||||
active_request_block = info[0].get("active_request_block", None)
|
||||
if active_request_block:
|
||||
report_instructions = active_request_block.get("report_instructions", None)
|
||||
if report_instructions:
|
||||
instruction_type = list(report_instructions[0].keys())[0]
|
||||
if instruction_type == "scan_progress":
|
||||
self._hook_scan_progress(ring_index=0)
|
||||
elif instruction_type == "readback":
|
||||
devices = report_instructions[0].get("readback").get("devices")
|
||||
start = report_instructions[0].get("readback").get("start")
|
||||
end = report_instructions[0].get("readback").get("end")
|
||||
if self.config.num_bars != len(devices):
|
||||
self.set_number_of_bars(len(devices))
|
||||
for index, device in enumerate(devices):
|
||||
self._hook_readback(index, device, start[index], end[index])
|
||||
else:
|
||||
print(f"{instruction_type} not supported yet.")
|
||||
|
||||
# elif instruction_type == "device_progress":
|
||||
# print("hook device_progress")
|
||||
|
||||
def _hook_scan_progress(self, ring_index: int = None):
|
||||
"""
|
||||
Hook the scan progress to the progress bars.
|
||||
|
||||
Args:
|
||||
ring_index(int): Index of the progress bar to hook the scan progress to.
|
||||
"""
|
||||
if ring_index is not None:
|
||||
ring = self._find_ring_by_index(ring_index)
|
||||
else:
|
||||
ring = self._rings[0]
|
||||
|
||||
if ring.config.connections.slot == "on_scan_progress":
|
||||
return
|
||||
else:
|
||||
ring.set_connections("on_scan_progress", MessageEndpoints.scan_progress())
|
||||
|
||||
def _hook_readback(self, bar_index: int, device: str, min: float | int, max: float | int):
|
||||
"""
|
||||
Hook the readback values to the progress bars.
|
||||
|
||||
Args:
|
||||
bar_index(int): Index of the progress bar to hook the readback values to.
|
||||
device(str): Device to readback values from.
|
||||
min(float|int): Minimum value for the progress bar.
|
||||
max(float|int): Maximum value for the progress bar.
|
||||
"""
|
||||
ring = self._find_ring_by_index(bar_index)
|
||||
ring.set_min_max_values(min, max)
|
||||
endpoint = MessageEndpoints.device_readback(device)
|
||||
ring.set_connections("on_device_readback", endpoint)
|
||||
|
||||
def _adjust_list_to_bars(self, items: list) -> list:
|
||||
"""
|
||||
Utility method to adjust the list of parameters to match the number of progress bars.
|
||||
|
||||
Args:
|
||||
items(list): List of parameters for the progress bars.
|
||||
|
||||
Returns:
|
||||
list: List of parameters for the progress bars.
|
||||
"""
|
||||
if items is None:
|
||||
raise ValueError(
|
||||
"Items cannot be None. Please provide a list for parameters for the progress bars."
|
||||
)
|
||||
if not isinstance(items, list):
|
||||
items = [items]
|
||||
if len(items) < self.config.num_bars:
|
||||
last_item = items[-1]
|
||||
items.extend([last_item] * (self.config.num_bars - len(items)))
|
||||
elif len(items) > self.config.num_bars:
|
||||
items = items[: self.config.num_bars]
|
||||
return items
|
||||
|
||||
def _bar_index_check(self, bar_index: int):
|
||||
"""
|
||||
Utility method to check if the bar index is within the range of the number of progress bars.
|
||||
|
||||
Args:
|
||||
bar_index(int): Index of the progress bar to set the value for.
|
||||
"""
|
||||
if not (0 <= bar_index < self.config.num_bars):
|
||||
raise ValueError(
|
||||
f"bar_index {bar_index} out of range of number of bars {self.config.num_bars}."
|
||||
)
|
||||
return bar_index
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QtGui.QPainter(self)
|
||||
painter.setRenderHint(QtGui.QPainter.Antialiasing)
|
||||
size = min(self.width(), self.height())
|
||||
rect = QtCore.QRect(0, 0, size, size)
|
||||
rect.adjust(
|
||||
max(ring.config.line_width for ring in self._rings),
|
||||
max(ring.config.line_width for ring in self._rings),
|
||||
-max(ring.config.line_width for ring in self._rings),
|
||||
-max(ring.config.line_width for ring in self._rings),
|
||||
)
|
||||
|
||||
for i, ring in enumerate(self._rings):
|
||||
# Background arc
|
||||
painter.setPen(
|
||||
QtGui.QPen(ring.background_color, ring.config.line_width, QtCore.Qt.SolidLine)
|
||||
)
|
||||
offset = self.config.gap * i
|
||||
adjusted_rect = QtCore.QRect(
|
||||
rect.left() + offset,
|
||||
rect.top() + offset,
|
||||
rect.width() - 2 * offset,
|
||||
rect.height() - 2 * offset,
|
||||
)
|
||||
painter.drawArc(adjusted_rect, ring.config.start_position, 360 * 16)
|
||||
|
||||
# Foreground arc
|
||||
pen = QtGui.QPen(ring.color, ring.config.line_width, QtCore.Qt.SolidLine)
|
||||
pen.setCapStyle(QtCore.Qt.RoundCap)
|
||||
painter.setPen(pen)
|
||||
proportion = (ring.config.value - ring.config.min_value) / (
|
||||
(ring.config.max_value - ring.config.min_value) + 1e-3
|
||||
)
|
||||
angle = int(proportion * 360 * 16 * ring.config.direction)
|
||||
painter.drawArc(adjusted_rect, ring.start_position, angle)
|
||||
|
||||
def reset_diameter(self):
|
||||
"""
|
||||
Reset the fixed size of the widget.
|
||||
"""
|
||||
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
|
||||
self.setMinimumSize(self._calculate_minimum_size())
|
||||
self.setMaximumSize(16777215, 16777215)
|
||||
|
||||
def _calculate_minimum_size(self):
|
||||
"""
|
||||
Calculate the minimum size of the widget.
|
||||
"""
|
||||
if not self.config.rings:
|
||||
print("no rings to get size from setting size to 10x10")
|
||||
return QSize(10, 10)
|
||||
ring_widths = [self.config.rings[i].line_width for i in range(self.config.num_bars)]
|
||||
total_width = sum(ring_widths) + self.config.gap * (self.config.num_bars - 1)
|
||||
diameter = total_width * 2
|
||||
if diameter < 50:
|
||||
diameter = 50
|
||||
return QSize(diameter, diameter)
|
||||
|
||||
def sizeHint(self):
|
||||
min_size = self._calculate_minimum_size()
|
||||
return min_size
|
||||
|
||||
def clear_all(self):
|
||||
for ring in self._rings:
|
||||
ring.cleanup()
|
||||
del ring
|
||||
self._rings = []
|
||||
self.update()
|
||||
self.initialize_bars()
|
||||
|
||||
def cleanup(self):
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.on_scan_queue_status, MessageEndpoints.scan_queue_status()
|
||||
)
|
||||
for ring in self._rings:
|
||||
ring.cleanup()
|
||||
del ring
|
||||
super().cleanup()
|
||||
@@ -1 +0,0 @@
|
||||
from .toolbar import ModularToolBar
|
||||
@@ -1,144 +0,0 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
# pylint: disable=no-name-in-module
|
||||
from qtpy.QtCore import QSize, QTimer
|
||||
from qtpy.QtGui import QAction
|
||||
from qtpy.QtWidgets import QApplication, QStyle, QToolBar, QWidget
|
||||
|
||||
|
||||
class ToolBarAction(ABC):
|
||||
"""Abstract base class for action creators for the toolbar."""
|
||||
|
||||
@abstractmethod
|
||||
def create(self, target: QWidget):
|
||||
"""Creates and returns an action to be added to a toolbar.
|
||||
|
||||
This method must be implemented by subclasses.
|
||||
|
||||
Args:
|
||||
target (QWidget): The widget that the action will target.
|
||||
|
||||
Returns:
|
||||
QAction: The action created for the toolbar.
|
||||
"""
|
||||
|
||||
|
||||
class OpenFileAction: # (ToolBarAction):
|
||||
"""Action creator for the 'Open File' action in the toolbar."""
|
||||
|
||||
def create(self, target: QWidget):
|
||||
"""Creates an 'Open File' action for the toolbar.
|
||||
|
||||
Args:
|
||||
target (QWidget): The widget that the 'Open File' action will be targeted.
|
||||
|
||||
Returns:
|
||||
QAction: The 'Open File' action created for the toolbar.
|
||||
"""
|
||||
icon = QApplication.style().standardIcon(QStyle.StandardPixmap.SP_DialogOpenButton)
|
||||
action = QAction(icon, "Open File", target)
|
||||
# action = QAction("Open File", target)
|
||||
action.triggered.connect(target.open_file)
|
||||
return action
|
||||
|
||||
|
||||
class SaveFileAction:
|
||||
"""Action creator for the 'Save File' action in the toolbar."""
|
||||
|
||||
def create(self, target):
|
||||
"""Creates a 'Save File' action for the toolbar.
|
||||
|
||||
Args:
|
||||
target (QWidget): The widget that the 'Save File' action will be targeted.
|
||||
|
||||
Returns:
|
||||
QAction: The 'Save File' action created for the toolbar.
|
||||
"""
|
||||
icon = QApplication.style().standardIcon(QStyle.StandardPixmap.SP_DialogSaveButton)
|
||||
action = QAction(icon, "Save File", target)
|
||||
# action = QAction("Save File", target)
|
||||
action.triggered.connect(target.save_file)
|
||||
return action
|
||||
|
||||
|
||||
class RunScriptAction:
|
||||
"""Action creator for the 'Run Script' action in the toolbar."""
|
||||
|
||||
def create(self, target):
|
||||
"""Creates a 'Run Script' action for the toolbar.
|
||||
|
||||
Args:
|
||||
target (QWidget): The widget that the 'Run Script' action will be targeted.
|
||||
|
||||
Returns:
|
||||
QAction: The 'Run Script' action created for the toolbar.
|
||||
"""
|
||||
icon = QApplication.style().standardIcon(QStyle.StandardPixmap.SP_MediaPlay)
|
||||
action = QAction(icon, "Run Script", target)
|
||||
# action = QAction("Run Script", target)
|
||||
action.triggered.connect(target.run_script)
|
||||
return action
|
||||
|
||||
|
||||
class ModularToolBar(QToolBar):
|
||||
"""Modular toolbar with optional automatic initialization.
|
||||
|
||||
Args:
|
||||
parent (QWidget, optional): The parent widget of the toolbar. Defaults to None.
|
||||
auto_init (bool, optional): If True, automatically populates the toolbar based on the parent widget.
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None, auto_init=True):
|
||||
super().__init__(parent)
|
||||
self.auto_init = auto_init
|
||||
self.handler = {
|
||||
"BECEditor": [OpenFileAction(), SaveFileAction(), RunScriptAction()],
|
||||
# BECMonitor: [SomeOtherAction(), AnotherAction()], # Example for another widget
|
||||
}
|
||||
self.setStyleSheet("QToolBar { background: transparent; }")
|
||||
# Set the icon size for the toolbar
|
||||
self.setIconSize(QSize(20, 20))
|
||||
|
||||
if self.auto_init:
|
||||
QTimer.singleShot(0, self.auto_detect_and_populate)
|
||||
|
||||
def auto_detect_and_populate(self):
|
||||
"""Automatically detects the parent widget and populates the toolbar with relevant actions."""
|
||||
if not self.auto_init:
|
||||
return
|
||||
|
||||
parent_widget = self.parent()
|
||||
if parent_widget is None:
|
||||
return
|
||||
|
||||
parent_widget_class_name = type(parent_widget).__name__
|
||||
for widget_type_name, actions in self.handler.items():
|
||||
if parent_widget_class_name == widget_type_name:
|
||||
self.populate_toolbar(actions, parent_widget)
|
||||
return
|
||||
|
||||
def populate_toolbar(self, actions, target_widget):
|
||||
"""Populates the toolbar with a set of actions.
|
||||
|
||||
Args:
|
||||
actions (list[ToolBarAction]): A list of action creators to populate the toolbar.
|
||||
target_widget (QWidget): The widget that the actions will target.
|
||||
"""
|
||||
self.clear()
|
||||
for action_creator in actions:
|
||||
action = action_creator.create(target_widget)
|
||||
self.addAction(action)
|
||||
|
||||
def set_manual_actions(self, actions, target_widget):
|
||||
"""Manually sets the actions for the toolbar.
|
||||
|
||||
Args:
|
||||
actions (list[QAction or ToolBarAction]): A list of actions or action creators to populate the toolbar.
|
||||
target_widget (QWidget): The widget that the actions will target.
|
||||
"""
|
||||
self.clear()
|
||||
for action in actions:
|
||||
if isinstance(action, QAction):
|
||||
self.addAction(action)
|
||||
elif isinstance(action, ToolBarAction):
|
||||
self.addAction(action.create(target_widget))
|
||||
@@ -1,20 +0,0 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line, and also
|
||||
# from the environment for the first two.
|
||||
SPHINXOPTS ?=
|
||||
SPHINXBUILD ?= sphinx-build
|
||||
SOURCEDIR = .
|
||||
BUILDDIR = _build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
170
docs/_static/custom.css
vendored
34
docs/_templates/custom-class-template.rst
vendored
@@ -1,34 +0,0 @@
|
||||
{{ fullname | escape | underline}}
|
||||
|
||||
.. currentmodule:: {{ module }}
|
||||
|
||||
.. autoclass:: {{ objname }}
|
||||
:members:
|
||||
:show-inheritance:
|
||||
:inherited-members:
|
||||
:special-members: __call__, __add__, __mul__
|
||||
|
||||
{% block methods %}
|
||||
{% if methods %}
|
||||
.. rubric:: {{ _('Methods') }}
|
||||
|
||||
.. autosummary::
|
||||
:nosignatures:
|
||||
{% for item in methods %}
|
||||
{%- if not item.startswith('_') %}
|
||||
~{{ name }}.{{ item }}
|
||||
{%- endif -%}
|
||||
{%- endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block attributes %}
|
||||
{% if attributes %}
|
||||
.. rubric:: {{ _('Attributes') }}
|
||||
|
||||
.. autosummary::
|
||||
{% for item in attributes %}
|
||||
~{{ name }}.{{ item }}
|
||||
{%- endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
66
docs/_templates/custom-module-template.rst
vendored
@@ -1,66 +0,0 @@
|
||||
{{ fullname | escape | underline}}
|
||||
|
||||
.. automodule:: {{ fullname }}
|
||||
|
||||
{% block attributes %}
|
||||
{% if attributes %}
|
||||
.. rubric:: Module attributes
|
||||
|
||||
.. autosummary::
|
||||
:toctree:
|
||||
{% for item in attributes %}
|
||||
{{ item }}
|
||||
{%- endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block functions %}
|
||||
{% if functions %}
|
||||
.. rubric:: {{ _('Functions') }}
|
||||
|
||||
.. autosummary::
|
||||
:toctree:
|
||||
:nosignatures:
|
||||
{% for item in functions %}
|
||||
{{ item }}
|
||||
{%- endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block classes %}
|
||||
{% if classes %}
|
||||
.. rubric:: {{ _('Classes') }}
|
||||
|
||||
.. autosummary::
|
||||
:toctree:
|
||||
:template: custom-class-template.rst
|
||||
:nosignatures:
|
||||
{% for item in classes %}
|
||||
{{ item }}
|
||||
{%- endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block exceptions %}
|
||||
{% if exceptions %}
|
||||
.. rubric:: {{ _('Exceptions') }}
|
||||
|
||||
.. autosummary::
|
||||
:toctree:
|
||||
{% for item in exceptions %}
|
||||
{{ item }}
|
||||
{%- endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block modules %}
|
||||
{% if modules %}
|
||||
.. autosummary::
|
||||
:toctree:
|
||||
:template: custom-module-template.rst
|
||||
:recursive:
|
||||
{% for item in modules %}
|
||||
{{ item }}
|
||||
{%- endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -1,12 +0,0 @@
|
||||
(api_reference)=
|
||||
# API Reference
|
||||
|
||||
```{eval-rst}
|
||||
.. autosummary::
|
||||
:toctree: _autosummary
|
||||
:template: custom-module-template.rst
|
||||
:recursive:
|
||||
|
||||
bec_widgets
|
||||
|
||||
```
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 0 24 24" width="48px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M4 8h4V4H4v4zm6 12h4v-4h-4v4zm-6 0h4v-4H4v4zm0-6h4v-4H4v4zm6 0h4v-4h-4v4zm6-10v4h4V4h-4zm-6 4h4V4h-4v4zm6 6h4v-4h-4v4zm0 6h4v-4h-4v4z"/></svg>
|
||||
|
Before Width: | Height: | Size: 292 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 20 20" height="48px" viewBox="0 0 20 20" width="48px" fill="#000000"><g><rect fill="none" height="20" width="20" x="0"/></g><g><g><path d="M16.5,3h-13C2.67,3,2,3.67,2,4.5v9C2,14.33,2.67,15,3.5,15H7v2h6v-2h3.5c0.83,0,1.5-0.67,1.5-1.5v-9 C18,3.67,17.33,3,16.5,3z M16.5,13.5h-13v-9h13V13.5z"/><rect height="1" width="7" x="5" y="7"/><polygon points="14,8 15,8 15,7 14,7 14,6 13,6 13,9 14,9"/><rect height="1" width="7" x="8" y="10"/><polygon points="6,12 7,12 7,9 6,9 6,10 5,10 5,11 6,11"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 568 B |
@@ -1,97 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="99.058548mm"
|
||||
height="89.967583mm"
|
||||
viewBox="0 0 99.058554 89.967582"
|
||||
version="1.1"
|
||||
id="svg1040"
|
||||
inkscape:version="0.92.4 (f8dce91, 2019-08-02)"
|
||||
sodipodi:docname="index_api.svg">
|
||||
<defs
|
||||
id="defs1034" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.35"
|
||||
inkscape:cx="533.74914"
|
||||
inkscape:cy="10.90433"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:window-width="930"
|
||||
inkscape:window-height="472"
|
||||
inkscape:window-x="2349"
|
||||
inkscape:window-y="267"
|
||||
inkscape:window-maximized="0" />
|
||||
<metadata
|
||||
id="metadata1037">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(195.19933,-1.0492759)">
|
||||
<g
|
||||
id="g1008"
|
||||
transform="matrix(1.094977,0,0,1.094977,-521.5523,-198.34055)">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path899"
|
||||
d="M 324.96812,187.09499 H 303.0455 v 72.1639 h 22.67969"
|
||||
style="fill:none;stroke:#5a5a5a;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path899-3"
|
||||
d="m 361.58921,187.09499 h 21.92262 v 72.1639 h -22.67969"
|
||||
style="fill:none;stroke:#5a5a5a;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<g
|
||||
transform="translate(415.87139,46.162126)"
|
||||
id="g944">
|
||||
<circle
|
||||
style="fill:#5a5a5a;fill-opacity:1;stroke:#5a5a5a;stroke-width:4.53704548;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path918"
|
||||
cx="-84.40152"
|
||||
cy="189.84375"
|
||||
r="2.2293637" />
|
||||
<circle
|
||||
style="fill:#5a5a5a;fill-opacity:1;stroke:#5a5a5a;stroke-width:4.53704548;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path918-5"
|
||||
cx="-72.949402"
|
||||
cy="189.84375"
|
||||
r="2.2293637" />
|
||||
<circle
|
||||
style="fill:#5a5a5a;fill-opacity:1;stroke:#5a5a5a;stroke-width:4.53704548;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path918-6"
|
||||
cx="-61.497284"
|
||||
cy="189.84375"
|
||||
r="2.2293637" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.4 KiB |
@@ -1,76 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="89.624855mm"
|
||||
height="89.96759mm"
|
||||
viewBox="0 0 89.62486 89.96759"
|
||||
version="1.1"
|
||||
id="svg1040"
|
||||
inkscape:version="0.92.4 (f8dce91, 2019-08-02)"
|
||||
sodipodi:docname="index_contribute.svg">
|
||||
<defs
|
||||
id="defs1034" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.35"
|
||||
inkscape:cx="683.11893"
|
||||
inkscape:cy="-59.078181"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:window-width="930"
|
||||
inkscape:window-height="472"
|
||||
inkscape:window-x="2349"
|
||||
inkscape:window-y="267"
|
||||
inkscape:window-maximized="0" />
|
||||
<metadata
|
||||
id="metadata1037">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(234.72009,17.466935)">
|
||||
<g
|
||||
id="g875"
|
||||
transform="matrix(0.99300176,0,0,0.99300176,-133.24106,-172.58804)">
|
||||
<path
|
||||
sodipodi:nodetypes="ccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path869"
|
||||
d="m -97.139881,161.26069 47.247024,40.25446 -47.247024,40.25446"
|
||||
style="fill:none;stroke:#5a5a5a;stroke-width:10;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path871"
|
||||
d="m -49.514879,241.81547 h 32.505951"
|
||||
style="fill:none;stroke:#5a5a5a;stroke-width:10;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.5 KiB |
@@ -1,66 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="101.09389mm"
|
||||
height="89.96759mm"
|
||||
viewBox="0 0 101.09389 89.96759"
|
||||
version="1.1"
|
||||
id="svg1040"
|
||||
inkscape:version="0.92.4 (f8dce91, 2019-08-02)"
|
||||
sodipodi:docname="index_getting_started.svg">
|
||||
<defs
|
||||
id="defs1034" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.35"
|
||||
inkscape:cx="-93.242129"
|
||||
inkscape:cy="-189.9825"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:window-width="1875"
|
||||
inkscape:window-height="1056"
|
||||
inkscape:window-x="1965"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata1037">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(2.9219487,-8.5995374)">
|
||||
<path
|
||||
style="fill:#5a5a5a;fill-opacity:1;stroke-width:0.20233451"
|
||||
d="M 37.270955,98.335591 C 33.358064,97.07991 31.237736,92.52319 32.964256,89.08022 c 0.18139,-0.361738 4.757999,-5.096629 10.17021,-10.521968 l 9.84041,-9.864254 -4.03738,-4.041175 -4.037391,-4.041172 -4.96415,4.916665 c -3.61569,3.581096 -5.238959,5.04997 -5.975818,5.407377 l -1.011682,0.490718 H 17.267525 1.5866055 L 0.65034544,70.96512 C -2.2506745,69.535833 -3.5952145,66.18561 -2.5925745,62.884631 c 0.53525,-1.762217 1.61699004,-3.050074 3.22528014,-3.839847 l 1.15623996,-0.56778 13.2591094,-0.05613 13.259111,-0.05613 11.5262,-11.527539 11.526199,-11.527528 H 40.622647 c -12.145542,0 -12.189222,-0.0046 -13.752801,-1.445851 -2.229871,-2.055423 -2.162799,-5.970551 0.135998,-7.938238 1.475193,-1.262712 1.111351,-1.238469 18.588522,-1.238469 12.899229,0 16.035311,0.05193 16.692589,0.276494 0.641832,0.219264 2.590731,2.051402 9.416301,8.852134 l 8.606941,8.575638 h 6.848168 c 4.837422,0 7.092281,0.07311 7.679571,0.249094 0.48064,0.144008 1.22985,0.634863 1.77578,1.163429 2.383085,2.307333 1.968685,6.539886 -0.804989,8.221882 -0.571871,0.346781 -1.38284,0.687226 -1.80217,0.756523 -0.41933,0.06928 -4.2741,0.127016 -8.56615,0.128238 -6.56998,0.0016 -7.977492,-0.04901 -8.902732,-0.321921 -0.975569,-0.287742 -1.400468,-0.622236 -3.783999,-2.978832 l -2.685021,-2.654679 -5.05411,5.051071 -5.0541,5.051081 3.926292,3.947202 c 2.365399,2.378001 4.114289,4.309171 4.399158,4.857713 0.39266,0.75606 0.47311,1.219412 0.474321,2.731516 0.003,3.083647 0.620779,2.331942 -13.598011,16.531349 -10.273768,10.259761 -12.679778,12.563171 -13.500979,12.92519 -1.267042,0.55857 -3.156169,0.681342 -4.390271,0.285321 z m 40.130741,-65.45839 c -2.212909,-0.579748 -3.782711,-1.498393 -5.51275,-3.226063 -2.522111,-2.518633 -3.633121,-5.181304 -3.633121,-8.707194 0,-3.530699 1.11238,-6.197124 3.631161,-8.704043 4.866751,-4.8438383 12.324781,-4.8550953 17.211791,-0.026 3.908758,3.862461 4.818578,9.377999 2.372188,14.380771 -0.846209,1.730481 -3.39493,4.326384 -5.143839,5.239072 -2.69708,1.407492 -6.042829,1.798628 -8.92543,1.043434 z"
|
||||
id="path1000"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.9 KiB |
@@ -1,67 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="123.72241mm"
|
||||
height="89.96759mm"
|
||||
viewBox="0 0 123.72242 89.96759"
|
||||
version="1.1"
|
||||
id="svg1040"
|
||||
inkscape:version="0.92.4 (f8dce91, 2019-08-02)"
|
||||
sodipodi:docname="index_userguide.svg">
|
||||
<defs
|
||||
id="defs1034" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.35"
|
||||
inkscape:cx="332.26618"
|
||||
inkscape:cy="83.744004"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:window-width="930"
|
||||
inkscape:window-height="472"
|
||||
inkscape:window-x="2349"
|
||||
inkscape:window-y="267"
|
||||
inkscape:window-maximized="0" />
|
||||
<metadata
|
||||
id="metadata1037">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(141.8903,-20.32143)">
|
||||
<path
|
||||
style="fill:#5a5a5a;fill-opacity:1;stroke-width:0.20483544"
|
||||
d="m -139.53374,110.1657 c -0.80428,-0.24884 -1.71513,-1.11296 -2.07107,-1.96486 -0.23905,-0.57214 -0.28453,-6.28104 -0.28453,-35.720988 0,-38.274546 -0.079,-35.840728 1.19849,-36.91568 0.58869,-0.495345 4.63766,-2.187548 8.47998,-3.544073 l 1.58749,-0.560453 v -3.309822 c 0,-3.025538 0.0396,-3.388179 0.46086,-4.222122 0.68808,-1.362003 1.38671,-1.714455 4.60319,-2.322195 4.12797,-0.779966 5.13304,-0.912766 8.81544,-1.16476 11.80964,-0.808168 22.80911,2.509277 30.965439,9.3392 1.750401,1.465747 3.840861,3.5635 5.0903,5.108065 l 0.659122,0.814805 0.659109,-0.814805 c 1.249431,-1.544565 3.33988,-3.642318 5.09029,-5.108065 8.156331,-6.829923 19.155791,-10.147368 30.965441,-9.3392 3.682389,0.251994 4.68748,0.384794 8.81544,1.16476 3.21647,0.60774 3.91511,0.960192 4.60318,2.322195 0.4213,0.833943 0.46087,1.196584 0.46087,4.222122 v 3.309822 l 1.58748,0.560453 c 4.10165,1.448077 7.98852,3.072753 8.5259,3.563743 1.22643,1.120567 1.15258,-1.245868 1.15258,36.927177 0,34.567591 -0.005,35.083151 -0.40663,35.903991 -0.22365,0.45804 -0.73729,1.05665 -1.14143,1.33024 -1.22281,0.82783 -2.17721,0.70485 -5.86813,-0.7561 -9.19595,-3.63998 -18.956011,-6.38443 -26.791332,-7.53353 -3.02827,-0.44412 -9.26189,-0.61543 -11.77821,-0.3237 -5.19357,0.60212 -8.736108,2.05527 -11.700039,4.79936 -0.684501,0.63371 -1.466141,1.23646 -1.736979,1.33942 -0.63859,0.2428 -4.236521,0.2428 -4.875112,0 -0.27083,-0.10296 -1.05247,-0.70571 -1.73696,-1.33942 -2.96395,-2.74409 -6.50648,-4.19724 -11.700058,-4.79936 -2.516312,-0.29173 -8.749941,-0.12042 -11.778201,0.3237 -7.78194,1.14127 -17.39965,3.83907 -26.73341,7.49883 -3.38325,1.32658 -4.15525,1.50926 -5.11851,1.21125 z m 4.2107,-5.34052 c 5.86759,-2.29858 14.40398,-4.922695 20.2018,-6.210065 6.31584,-1.402418 8.5236,-1.646248 14.91592,-1.647338 4.68699,-7.94e-4 6.013661,0.0632 7.257809,0.3497 0.837332,0.19286 1.561052,0.312028 1.60828,0.264819 0.147111,-0.147119 -1.803289,-1.307431 -4.154879,-2.471801 -8.12511,-4.023029 -18.27311,-4.986568 -29.0861,-2.761718 -1.09536,0.22538 -2.32708,0.40827 -2.73715,0.406418 -1.12787,-0.005 -2.3054,-0.76382 -2.84516,-1.8332 l -0.46086,-0.913098 V 62.99179 35.97471 l -0.56331,0.138329 c -0.30981,0.07608 -1.89985,0.665075 -3.5334,1.308881 -2.27551,0.896801 -2.96414,1.252878 -2.94452,1.522563 0.014,0.193604 0.0372,15.284513 0.0512,33.535345 0.014,18.250839 0.0538,33.183322 0.0884,33.183322 0.0346,0 1.02543,-0.3771 2.20198,-0.83801 z m 113.006991,-32.697216 -0.0518,-33.535203 -3.17495,-1.272156 c -1.74623,-0.699685 -3.33627,-1.278755 -3.53341,-1.286819 -0.33966,-0.01389 -0.35847,1.401778 -0.35847,26.980216 v 26.994863 l -0.46087,0.913112 c -0.53976,1.06939 -1.71729,1.828088 -2.84515,1.833189 -0.41008,0.0021 -1.6418,-0.181031 -2.73716,-0.406421 -11.888201,-2.446089 -22.84337,-1.046438 -31.491022,4.02332 -1.68175,0.985941 -2.216748,1.467501 -1.36534,1.228942 1.575181,-0.441362 4.990592,-0.73864 8.524862,-0.742011 5.954408,-0.005 11.43046,0.791951 19.10874,2.78333 3.9516,1.024874 12.1555,3.687454 15.6699,5.085704 1.23926,0.49306 2.36869,0.90517 2.50985,0.9158 0.20489,0.0155 0.2462,-6.745894 0.20483,-33.515866 z m -59.76135,-2.233777 V 40.065438 l -0.95972,-1.357442 c -1.380522,-1.952627 -5.376262,-5.847994 -7.64336,-7.45136 -3.778692,-2.672401 -9.063392,-4.943324 -13.672511,-5.875304 -3.19731,-0.646503 -5.23069,-0.833103 -9.05886,-0.831312 -4.37716,0.0021 -7.70223,0.349169 -11.83461,1.235469 l -1.07538,0.230645 v 31.242342 c 0,26.565778 0.0426,31.226011 0.28429,31.133261 0.15637,-0.06 1.42379,-0.297169 2.81648,-0.527026 12.37657,-2.042634 23.21658,-0.346861 32.521639,5.087596 2.10018,1.226558 5.20202,3.618878 6.880942,5.30692 0.788609,0.792909 1.502978,1.446609 1.587468,1.452679 0.0845,0.006 0.153622,-13.411893 0.153622,-29.817719 z m 5.80221,28.3766 c 6.21476,-6.141601 15.08488,-10.061509 25.025529,-11.05933 4.262419,-0.427849 11.579921,-0.0054 16.017661,0.924912 0.75932,0.15916 1.45259,0.244888 1.54058,0.190498 0.088,-0.05434 0.16003,-14.060382 0.16003,-31.124436 V 26.176883 l -0.52136,-0.198219 c -0.66893,-0.254325 -4.77649,-0.95482 -7.159981,-1.221048 -2.41372,-0.269605 -8.559851,-0.266589 -10.759229,0.0052 -6.458111,0.798299 -12.584091,3.083792 -17.405651,6.49374 -2.267091,1.603366 -6.262831,5.498733 -7.64336,7.45136 l -0.959721,1.357438 v 29.828747 c 0,16.405812 0.0532,29.828746 0.11802,29.828746 0.065,0 0.77928,-0.65347 1.587482,-1.452149 z"
|
||||
id="path845"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="csscccscsssscsssssscscsccsccsccscsscccccccscccccccccsccscscscccscccsccssccsscccscccccsccccsccscsccsscc" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 6.3 KiB |