Compare commits
172 Commits
refactor/r
...
test_branc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1fb318000a | ||
| 216d8c200b | |||
|
|
01a9d607a6
|
||
| 9e48dca5e2 | |||
|
|
dc3d51afc4 | ||
|
|
2a579c957d | ||
|
|
66cd89ed63 | ||
| d06fe65491 | |||
| 189141a047 | |||
| f69e643092 | |||
| 5c263bdb63 | |||
| 9f5254abe2 | |||
| a58c23845f | |||
| c7e11eeabc | |||
| a03e99d615 | |||
| 3c9192d6a5 | |||
| a01593fa4b | |||
|
|
40d6acf431 | ||
| 9e45e927a0 | |||
| 8f7ada2f92 | |||
|
|
b6af93806a | ||
|
|
3db74d9877 | ||
|
|
e6d7e0d82f | ||
| 14713c0d2f | |||
| 3fd3d54003 | |||
| c2cba873d4 | |||
| 7679fa1383 | |||
| cdac29bed1 | |||
| 7a3ce97e30 | |||
|
|
62ecdd37ff | ||
| 256adb14d4 | |||
|
|
3cda1c74b4 | ||
| e3077f6d2f | |||
| a4c1b89395 | |||
|
|
909f0d2c96 | ||
| 4812323e4b | |||
| 809a8834fa | |||
| 98e4364176 | |||
| 173893ec33 | |||
| 1c539a1e9c | |||
| 293e56fba8 | |||
|
|
44a4dfc818 | ||
|
|
53d1798c84 | ||
|
|
a4aa1da5dd | ||
|
|
f332e06240 | ||
|
|
ef76e4b49b | ||
|
|
6734143dc8 | ||
|
|
66622fd75d | ||
|
|
e9fc6930ff | ||
|
|
8ca1f00d1c | ||
|
|
fad4611144 | ||
|
|
c51379a2cc | ||
| 2baaaac1f3 | |||
|
|
8eac0a2c38 | ||
| 2894b1e5c2 | |||
|
|
afd6590360 | ||
| 63ee870c89 | |||
| 36a2e55474 | |||
| 19d117619a | |||
| 447a9c3924 | |||
|
|
17e525689c | ||
| 4907c5db27 | |||
| 0dfcedc47f | |||
| 8f2335396b | |||
| 8a7a005b56 | |||
| e19ce733a2 | |||
|
|
ccaf996512 | ||
|
|
83de8b75db | ||
|
|
829bdc11c1 | ||
|
|
cbb3f9e8bc | ||
|
|
5f819d9835 | ||
|
|
8f55b6dbcb | ||
| b2095f8d7b | |||
|
|
9186853faf | ||
| 4d241d26f3 | |||
| f47ca6e174 | |||
| e86e6ba63c | |||
| 7ff2f7dc1b | |||
| ef1ab749a9 | |||
|
|
ec53bb7000 | ||
|
|
5f1449365d | ||
|
|
9699b39a7e | ||
| 4fa81ddc5c | |||
|
|
79f63b84fc | ||
|
|
5f9f2c8762 | ||
|
|
58960944f8 | ||
|
|
dfb4fa75d9 | ||
| 70b196403b | |||
| f14a5f220e | |||
| a4077be379 | |||
| cf96658625 | |||
| c21d6cf3c6 | |||
| 4f22de66f6 | |||
| dadee3efc1 | |||
|
|
19ac208f73 | ||
| a327d0ccaa | |||
|
|
9107b1f14b | ||
|
|
bb8bf3a072 | ||
| 9c331c13e0 | |||
|
|
8a1951766b | ||
|
|
a70c4588a0 | ||
|
|
55aa7128e2 | ||
| b31a1ea996 | |||
| 820b673144 | |||
| 805d4b5a13 | |||
|
|
011955ca38 | ||
| 04f123ab2b | |||
| 372205957b | |||
|
|
41d574253e | ||
|
|
c9536d9380 | ||
| 831ace2533 | |||
|
|
20a4f4d0bc | ||
|
|
3ae62c8ff9 | ||
|
|
40c460d120 | ||
|
|
0d66d2d364 | ||
|
|
2e3ed9f6f0 | ||
|
|
fff0b8ce41 | ||
|
|
d7308a8b81 | ||
|
|
86e71ca255 | ||
|
|
d3c86b07a0 | ||
|
|
af2a2df55f | ||
|
|
d2e4a8c703 | ||
|
|
a8cc2bcbe0 | ||
|
|
cb16dcfb4c | ||
|
|
4282d4ead6 | ||
|
|
a07a56822f | ||
|
|
07543755d9 | ||
|
|
793eedb3f4 | ||
|
|
dbae426a87 | ||
|
|
9e84c1570c | ||
|
|
2002f2fabf | ||
|
|
fb49295a92 | ||
|
|
85ecf8b55f | ||
|
|
a1eacdf54b | ||
|
|
c9a1fd1cd6 | ||
|
|
54b9a5004a | ||
|
|
972d010954 | ||
|
|
b1f4207738 | ||
|
|
45dfc1305e | ||
|
|
5f90b32210 | ||
|
|
29a8e1b281 | ||
|
|
78b838f516 | ||
|
|
17d4fcb8db | ||
|
|
e03b3b84e0 | ||
|
|
dd2f20fba4 | ||
|
|
d62f398351 | ||
|
|
c17a836858 | ||
|
|
f359e32610 | ||
|
|
b28649e6af | ||
|
|
a0f2269da3 | ||
|
|
143c4dbd65 | ||
|
|
347c63d500 | ||
|
|
f0df487131 | ||
|
|
90449d22ee | ||
|
|
3a06ca484b | ||
|
|
8ba32172ca | ||
|
|
9652545440 | ||
|
|
55f7015834 | ||
|
|
afd5d5548f | ||
|
|
cc04810361 | ||
|
|
fb6ec1c350 | ||
|
|
d32d7401a3 | ||
|
|
c7f9d37c12 | ||
|
|
f3d2100b03 | ||
|
|
79d69c9492 | ||
|
|
e4a2d01488 | ||
| 5648b4dbd6 | |||
|
|
977a2beb4c | ||
|
|
8f9c43ad99 | ||
|
|
8bb89cbf76 | ||
|
|
294d3b0d4e | ||
| 87f002a68b |
9
.copier-answers.yml
Normal file
9
.copier-answers.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
# Do not edit this file!
|
||||
# It is needed to track the repo template version, and editing may break things.
|
||||
# This file will be overwritten by copier on template updates.
|
||||
|
||||
_commit: v1.2.2
|
||||
_src_path: https://github.com/bec-project/plugin_copier_template.git
|
||||
make_commit: false
|
||||
project_name: csaxs_bec
|
||||
widget_plugins_input: []
|
||||
97
.gitea/workflows/ci.yml
Normal file
97
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,97 @@
|
||||
name: CI for csaxs_bec
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
BEC_WIDGETS_BRANCH:
|
||||
description: "Branch of BEC Widgets to install"
|
||||
required: false
|
||||
type: string
|
||||
default: "main"
|
||||
BEC_CORE_BRANCH:
|
||||
description: "Branch of BEC Core to install"
|
||||
required: false
|
||||
type: string
|
||||
default: "main"
|
||||
OPHYD_DEVICES_BRANCH:
|
||||
description: "Branch of Ophyd Devices to install"
|
||||
required: false
|
||||
type: string
|
||||
default: "main"
|
||||
BEC_PLUGIN_REPO_BRANCH:
|
||||
description: "Branch of the BEC Plugin Repository to install"
|
||||
required: false
|
||||
type: string
|
||||
default: "main"
|
||||
PYTHON_VERSION:
|
||||
description: "Python version to use"
|
||||
required: false
|
||||
type: string
|
||||
default: "3.11"
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
QTWEBENGINE_DISABLE_SANDBOX: 1
|
||||
QT_QPA_PLATFORM: "offscreen"
|
||||
|
||||
steps:
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "${{ inputs.PYTHON_VERSION || '3.11' }}"
|
||||
|
||||
- name: Checkout BEC Core
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: bec/bec
|
||||
ref: "${{ inputs.BEC_CORE_BRANCH || 'main' }}"
|
||||
path: ./bec
|
||||
|
||||
- name: Checkout Ophyd Devices
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: bec/ophyd_devices
|
||||
ref: "${{ inputs.OPHYD_DEVICES_BRANCH || 'main' }}"
|
||||
path: ./ophyd_devices
|
||||
|
||||
- name: Checkout BEC Widgets
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: bec/bec_widgets
|
||||
ref: "${{ inputs.BEC_WIDGETS_BRANCH || 'main' }}"
|
||||
path: ./bec_widgets
|
||||
|
||||
- name: Checkout BEC Plugin Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: bec/csaxs_bec
|
||||
ref: "${{ inputs.BEC_PLUGIN_REPO_BRANCH || github.head_ref || github.sha }}"
|
||||
path: ./csaxs_bec
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgl1 libegl1 x11-utils libxkbcommon-x11-0 libdbus-1-3 xvfb
|
||||
sudo apt-get -y install libnss3 libxdamage1 libasound2t64 libatomic1 libxcursor1
|
||||
|
||||
- name: Install Python dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
pip install uv
|
||||
uv pip install --system -e ./ophyd_devices
|
||||
uv pip install --system -e ./bec/bec_lib[dev]
|
||||
uv pip install --system -e ./bec/bec_ipython_client
|
||||
uv pip install --system -e ./bec/bec_server[dev]
|
||||
uv pip install --system -e ./bec_widgets[dev,pyside6]
|
||||
uv pip install --system -e ./csaxs_bec
|
||||
|
||||
- name: Run Pytest with Coverage
|
||||
id: coverage
|
||||
run: pytest --random-order --cov=./csaxs_bec --cov-config=./csaxs_bec/pyproject.toml --cov-branch --cov-report=xml --no-cov-on-fail ./csaxs_bec/tests/ || test $? -eq 5
|
||||
@@ -6,3 +6,15 @@ include:
|
||||
target: "csaxs_bec"
|
||||
branch: $CHILD_PIPELINE_BRANCH
|
||||
|
||||
pages:
|
||||
stage: Deploy
|
||||
needs: []
|
||||
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/csaxs_bec"'
|
||||
script:
|
||||
- curl -X POST -d "branches=$CI_COMMIT_REF_NAME" -d "token=$RTD_TOKEN" https://readthedocs.org/api/v2/webhook/sls-csaxs/270162/
|
||||
|
||||
29
.readthedocs.yaml
Normal file
29
.readthedocs.yaml
Normal file
@@ -0,0 +1,29 @@
|
||||
# .readthedocs.yaml
|
||||
# Read the Docs configuration file
|
||||
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||
|
||||
version: 2
|
||||
|
||||
# Set the version of Python and other tools you might need
|
||||
build:
|
||||
os: ubuntu-20.04
|
||||
tools:
|
||||
python: "3.10"
|
||||
jobs:
|
||||
pre_install:
|
||||
- pip install .
|
||||
|
||||
|
||||
# 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
|
||||
|
||||
29
LICENSE
Normal file
29
LICENSE
Normal file
@@ -0,0 +1,29 @@
|
||||
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2025, Paul Scherrer Institute
|
||||
|
||||
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.
|
||||
1
bin/.gitignore
vendored
Normal file
1
bin/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
# Add anything you don't want to check in to git, e.g. very large files
|
||||
@@ -13,7 +13,6 @@ logger = bec_logger.logger
|
||||
class PilatusConverter:
|
||||
def __init__(self, host: str, port: int) -> None:
|
||||
self._connector = RedisConnector(f"{host}:{port}")
|
||||
self._producer = self._connector.producer()
|
||||
|
||||
def start(self) -> None:
|
||||
"""start the consumer"""
|
||||
@@ -57,10 +56,9 @@ class PilatusConverter:
|
||||
"""
|
||||
Start the consumer.
|
||||
"""
|
||||
file_consumer = self._connector.consumer(
|
||||
self._connector.register(
|
||||
MessageEndpoints.file_event("pilatus_2"), cb=self.on_new_message, parent=self
|
||||
)
|
||||
file_consumer.start()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -280,14 +280,22 @@ class LamNIOpticsMixin:
|
||||
umv(dev.losaz, losaz_out)
|
||||
umv(dev.losay, losay_out)
|
||||
|
||||
def lfzp_info(self):
|
||||
def lfzp_info(self, mokev_val=-1):
|
||||
|
||||
if mokev_val == -1:
|
||||
try:
|
||||
mokev_val = dev.mokev.readback.get()
|
||||
except:
|
||||
print(
|
||||
"Device mokev does not exist. You can specify the energy in keV as an argument instead."
|
||||
)
|
||||
return
|
||||
loptz_val = dev.loptz.read()["loptz"]["value"]
|
||||
distance = -loptz_val + 85.6 + 52
|
||||
print(f"The sample is in a distance of {distance:.1f} mm from the FZP.")
|
||||
|
||||
diameters = [80e-6, 100e-6, 120e-6, 150e-6, 170e-6, 200e-6, 220e-6, 250e-6]
|
||||
|
||||
mokev_val = dev.mokev.read()["mokev"]["value"]
|
||||
console = Console()
|
||||
table = Table(
|
||||
title=f"At the current energy of {mokev_val:.4f} keV we have following options:",
|
||||
|
||||
0
csaxs_bec/bec_ipython_client/plugins/LamNI/load_additional_correction.py
Executable file → Normal file
0
csaxs_bec/bec_ipython_client/plugins/LamNI/load_additional_correction.py
Executable file → Normal file
@@ -0,0 +1,351 @@
|
||||
corr_elements = 175
|
||||
corr_angle[0] = 0.100000
|
||||
corr_angle[1] = 1.001000
|
||||
corr_angle[2] = 1.902000
|
||||
corr_angle[3] = 2.798000
|
||||
corr_angle[4] = 7.305000
|
||||
corr_angle[5] = 8.204000
|
||||
corr_angle[6] = 9.104000
|
||||
corr_angle[7] = 10.005000
|
||||
corr_angle[8] = 14.504000
|
||||
corr_angle[9] = 15.404000
|
||||
corr_angle[10] = 16.304000
|
||||
corr_angle[11] = 17.204000
|
||||
corr_angle[12] = 21.704000
|
||||
corr_angle[13] = 22.604000
|
||||
corr_angle[14] = 23.504000
|
||||
corr_angle[15] = 24.404000
|
||||
corr_angle[16] = 28.904000
|
||||
corr_angle[17] = 29.804000
|
||||
corr_angle[18] = 30.704000
|
||||
corr_angle[19] = 31.604000
|
||||
corr_angle[20] = 36.104000
|
||||
corr_angle[21] = 37.004000
|
||||
corr_angle[22] = 37.904000
|
||||
corr_angle[23] = 38.804000
|
||||
corr_angle[24] = 43.305000
|
||||
corr_angle[25] = 44.205000
|
||||
corr_angle[26] = 45.104000
|
||||
corr_angle[27] = 46.005000
|
||||
corr_angle[28] = 50.504000
|
||||
corr_angle[29] = 51.404000
|
||||
corr_angle[30] = 52.305000
|
||||
corr_angle[31] = 53.205000
|
||||
corr_angle[32] = 57.705000
|
||||
corr_angle[33] = 58.605000
|
||||
corr_angle[34] = 59.505000
|
||||
corr_angle[35] = 60.405000
|
||||
corr_angle[36] = 64.904000
|
||||
corr_angle[37] = 65.804000
|
||||
corr_angle[38] = 66.704000
|
||||
corr_angle[39] = 67.604000
|
||||
corr_angle[40] = 72.104000
|
||||
corr_angle[41] = 73.004000
|
||||
corr_angle[42] = 73.904000
|
||||
corr_angle[43] = 74.804000
|
||||
corr_angle[44] = 79.304000
|
||||
corr_angle[45] = 80.204000
|
||||
corr_angle[46] = 81.104000
|
||||
corr_angle[47] = 82.005000
|
||||
corr_angle[48] = 86.505000
|
||||
corr_angle[49] = 87.404000
|
||||
corr_angle[50] = 88.304000
|
||||
corr_angle[51] = 89.205000
|
||||
corr_angle[52] = 93.704000
|
||||
corr_angle[53] = 94.604000
|
||||
corr_angle[54] = 95.505000
|
||||
corr_angle[55] = 96.404000
|
||||
corr_angle[56] = 100.904000
|
||||
corr_angle[57] = 101.804000
|
||||
corr_angle[58] = 102.704000
|
||||
corr_angle[59] = 103.604000
|
||||
corr_angle[60] = 108.104000
|
||||
corr_angle[61] = 109.004000
|
||||
corr_angle[62] = 109.904000
|
||||
corr_angle[63] = 110.804000
|
||||
corr_angle[64] = 115.304000
|
||||
corr_angle[65] = 116.204000
|
||||
corr_angle[66] = 117.104000
|
||||
corr_angle[67] = 118.004000
|
||||
corr_angle[68] = 122.504000
|
||||
corr_angle[69] = 123.404000
|
||||
corr_angle[70] = 124.304000
|
||||
corr_angle[71] = 125.204000
|
||||
corr_angle[72] = 129.704000
|
||||
corr_angle[73] = 130.604000
|
||||
corr_angle[74] = 131.504000
|
||||
corr_angle[75] = 132.404000
|
||||
corr_angle[76] = 136.904000
|
||||
corr_angle[77] = 137.804000
|
||||
corr_angle[78] = 138.701000
|
||||
corr_angle[79] = 139.604000
|
||||
corr_angle[80] = 144.104000
|
||||
corr_angle[81] = 145.004000
|
||||
corr_angle[82] = 145.904000
|
||||
corr_angle[83] = 146.804000
|
||||
corr_angle[84] = 151.304000
|
||||
corr_angle[85] = 152.204000
|
||||
corr_angle[86] = 153.104000
|
||||
corr_angle[87] = 154.004000
|
||||
corr_angle[88] = 158.504000
|
||||
corr_angle[89] = 159.404000
|
||||
corr_angle[90] = 160.304000
|
||||
corr_angle[91] = 161.204000
|
||||
corr_angle[92] = 165.704000
|
||||
corr_angle[93] = 166.604000
|
||||
corr_angle[94] = 167.504000
|
||||
corr_angle[95] = 168.404000
|
||||
corr_angle[96] = 172.904000
|
||||
corr_angle[97] = 173.805000
|
||||
corr_angle[98] = 174.704000
|
||||
corr_angle[99] = 180.104000
|
||||
corr_angle[100] = 183.704000
|
||||
corr_angle[101] = 184.603000
|
||||
corr_angle[102] = 185.504000
|
||||
corr_angle[103] = 190.904000
|
||||
corr_angle[104] = 191.805000
|
||||
corr_angle[105] = 192.704000
|
||||
corr_angle[106] = 198.104000
|
||||
corr_angle[107] = 199.004000
|
||||
corr_angle[108] = 199.904000
|
||||
corr_angle[109] = 205.304000
|
||||
corr_angle[110] = 206.204000
|
||||
corr_angle[111] = 207.104000
|
||||
corr_angle[112] = 212.504000
|
||||
corr_angle[113] = 213.404000
|
||||
corr_angle[114] = 214.304000
|
||||
corr_angle[115] = 219.704000
|
||||
corr_angle[116] = 220.604000
|
||||
corr_angle[117] = 221.504000
|
||||
corr_angle[118] = 226.904000
|
||||
corr_angle[119] = 227.804000
|
||||
corr_angle[120] = 228.704000
|
||||
corr_angle[121] = 234.104000
|
||||
corr_angle[122] = 235.004000
|
||||
corr_angle[123] = 235.904000
|
||||
corr_angle[124] = 241.304000
|
||||
corr_angle[125] = 242.204000
|
||||
corr_angle[126] = 243.104000
|
||||
corr_angle[127] = 248.504000
|
||||
corr_angle[128] = 249.404000
|
||||
corr_angle[129] = 250.304000
|
||||
corr_angle[130] = 255.704000
|
||||
corr_angle[131] = 256.604000
|
||||
corr_angle[132] = 257.504000
|
||||
corr_angle[133] = 262.904000
|
||||
corr_angle[134] = 263.804000
|
||||
corr_angle[135] = 264.704000
|
||||
corr_angle[136] = 270.104000
|
||||
corr_angle[137] = 271.004000
|
||||
corr_angle[138] = 271.904000
|
||||
corr_angle[139] = 277.304000
|
||||
corr_angle[140] = 278.205000
|
||||
corr_angle[141] = 279.104000
|
||||
corr_angle[142] = 284.504000
|
||||
corr_angle[143] = 285.405000
|
||||
corr_angle[144] = 286.304000
|
||||
corr_angle[145] = 291.703000
|
||||
corr_angle[146] = 292.604000
|
||||
corr_angle[147] = 293.504000
|
||||
corr_angle[148] = 298.904000
|
||||
corr_angle[149] = 299.804000
|
||||
corr_angle[150] = 300.704000
|
||||
corr_angle[151] = 306.104000
|
||||
corr_angle[152] = 307.004000
|
||||
corr_angle[153] = 307.904000
|
||||
corr_angle[154] = 313.304000
|
||||
corr_angle[155] = 314.204000
|
||||
corr_angle[156] = 315.104000
|
||||
corr_angle[157] = 320.504000
|
||||
corr_angle[158] = 321.404000
|
||||
corr_angle[159] = 322.304000
|
||||
corr_angle[160] = 327.704000
|
||||
corr_angle[161] = 328.605000
|
||||
corr_angle[162] = 329.504000
|
||||
corr_angle[163] = 334.904000
|
||||
corr_angle[164] = 335.804000
|
||||
corr_angle[165] = 336.705000
|
||||
corr_angle[166] = 342.104000
|
||||
corr_angle[167] = 343.004000
|
||||
corr_angle[168] = 343.904000
|
||||
corr_angle[169] = 349.304000
|
||||
corr_angle[170] = 350.204000
|
||||
corr_angle[171] = 351.104000
|
||||
corr_angle[172] = 356.504000
|
||||
corr_angle[173] = 357.404000
|
||||
corr_angle[174] = 358.304000
|
||||
corr_pos[0] = 0.012330
|
||||
corr_pos[1] = 0.024870
|
||||
corr_pos[2] = 0.037262
|
||||
corr_pos[3] = 0.049438
|
||||
corr_pos[4] = 0.108462
|
||||
corr_pos[5] = 0.119791
|
||||
corr_pos[6] = 0.130986
|
||||
corr_pos[7] = 0.142044
|
||||
corr_pos[8] = 0.195045
|
||||
corr_pos[9] = 0.205834
|
||||
corr_pos[10] = 0.216589
|
||||
corr_pos[11] = 0.226402
|
||||
corr_pos[12] = 0.271931
|
||||
corr_pos[13] = 0.281684
|
||||
corr_pos[14] = 0.290769
|
||||
corr_pos[15] = 0.299392
|
||||
corr_pos[16] = 0.337986
|
||||
corr_pos[17] = 0.345178
|
||||
corr_pos[18] = 0.352253
|
||||
corr_pos[19] = 0.358528
|
||||
corr_pos[20] = 0.379520
|
||||
corr_pos[21] = 0.382947
|
||||
corr_pos[22] = 0.386110
|
||||
corr_pos[23] = 0.388887
|
||||
corr_pos[24] = 0.395645
|
||||
corr_pos[25] = 0.396154
|
||||
corr_pos[26] = 0.396679
|
||||
corr_pos[27] = 0.396367
|
||||
corr_pos[28] = 0.392400
|
||||
corr_pos[29] = 0.392530
|
||||
corr_pos[30] = 0.391826
|
||||
corr_pos[31] = 0.391325
|
||||
corr_pos[32] = 0.379188
|
||||
corr_pos[33] = 0.376449
|
||||
corr_pos[34] = 0.373195
|
||||
corr_pos[35] = 0.369588
|
||||
corr_pos[36] = 0.343021
|
||||
corr_pos[37] = 0.336754
|
||||
corr_pos[38] = 0.330310
|
||||
corr_pos[39] = 0.323447
|
||||
corr_pos[40] = 0.281171
|
||||
corr_pos[41] = 0.272735
|
||||
corr_pos[42] = 0.263888
|
||||
corr_pos[43] = 0.254469
|
||||
corr_pos[44] = 0.203024
|
||||
corr_pos[45] = 0.192980
|
||||
corr_pos[46] = 0.182359
|
||||
corr_pos[47] = 0.170780
|
||||
corr_pos[48] = 0.111580
|
||||
corr_pos[49] = 0.101222
|
||||
corr_pos[50] = 0.090051
|
||||
corr_pos[51] = 0.077918
|
||||
corr_pos[52] = 0.015225
|
||||
corr_pos[53] = 0.003876
|
||||
corr_pos[54] = -0.007756
|
||||
corr_pos[55] = -0.020383
|
||||
corr_pos[56] = -0.084910
|
||||
corr_pos[57] = -0.096848
|
||||
corr_pos[58] = -0.108732
|
||||
corr_pos[59] = -0.121432
|
||||
corr_pos[60] = -0.184349
|
||||
corr_pos[61] = -0.195904
|
||||
corr_pos[62] = -0.207113
|
||||
corr_pos[63] = -0.219319
|
||||
corr_pos[64] = -0.274799
|
||||
corr_pos[65] = -0.284942
|
||||
corr_pos[66] = -0.294809
|
||||
corr_pos[67] = -0.305437
|
||||
corr_pos[68] = -0.352114
|
||||
corr_pos[69] = -0.359689
|
||||
corr_pos[70] = -0.367451
|
||||
corr_pos[71] = -0.375629
|
||||
corr_pos[72] = -0.404347
|
||||
corr_pos[73] = -0.407683
|
||||
corr_pos[74] = -0.410857
|
||||
corr_pos[75] = -0.414904
|
||||
corr_pos[76] = -0.424625
|
||||
corr_pos[77] = -0.424357
|
||||
corr_pos[78] = -0.424411
|
||||
corr_pos[79] = -0.424615
|
||||
corr_pos[80] = -0.421392
|
||||
corr_pos[81] = -0.419977
|
||||
corr_pos[82] = -0.417860
|
||||
corr_pos[83] = -0.416400
|
||||
corr_pos[84] = -0.398108
|
||||
corr_pos[85] = -0.392613
|
||||
corr_pos[86] = -0.386455
|
||||
corr_pos[87] = -0.380338
|
||||
corr_pos[88] = -0.341288
|
||||
corr_pos[89] = -0.332298
|
||||
corr_pos[90] = -0.322767
|
||||
corr_pos[91] = -0.311630
|
||||
corr_pos[92] = -0.256798
|
||||
corr_pos[93] = -0.244808
|
||||
corr_pos[94] = -0.232932
|
||||
corr_pos[95] = -0.219785
|
||||
corr_pos[96] = -0.158685
|
||||
corr_pos[97] = -0.146198
|
||||
corr_pos[98] = -0.130743
|
||||
corr_pos[99] = -0.054066
|
||||
corr_pos[100] = -0.001498
|
||||
corr_pos[101] = 0.012010
|
||||
corr_pos[102] = 0.025195
|
||||
corr_pos[103] = 0.094982
|
||||
corr_pos[104] = 0.109235
|
||||
corr_pos[105] = 0.120813
|
||||
corr_pos[106] = 0.179893
|
||||
corr_pos[107] = 0.192147
|
||||
corr_pos[108] = 0.201902
|
||||
corr_pos[109] = 0.245092
|
||||
corr_pos[110] = 0.250501
|
||||
corr_pos[111] = 0.255536
|
||||
corr_pos[112] = 0.280598
|
||||
corr_pos[113] = 0.280673
|
||||
corr_pos[114] = 0.282529
|
||||
corr_pos[115] = 0.293286
|
||||
corr_pos[116] = 0.292713
|
||||
corr_pos[117] = 0.291123
|
||||
corr_pos[118] = 0.288721
|
||||
corr_pos[119] = 0.288260
|
||||
corr_pos[120] = 0.286480
|
||||
corr_pos[121] = 0.271630
|
||||
corr_pos[122] = 0.268004
|
||||
corr_pos[123] = 0.265418
|
||||
corr_pos[124] = 0.239187
|
||||
corr_pos[125] = 0.233224
|
||||
corr_pos[126] = 0.226652
|
||||
corr_pos[127] = 0.189034
|
||||
corr_pos[128] = 0.180302
|
||||
corr_pos[129] = 0.170931
|
||||
corr_pos[130] = 0.125760
|
||||
corr_pos[131] = 0.116433
|
||||
corr_pos[132] = 0.106845
|
||||
corr_pos[133] = 0.057551
|
||||
corr_pos[134] = 0.048531
|
||||
corr_pos[135] = 0.038276
|
||||
corr_pos[136] = -0.012292
|
||||
corr_pos[137] = -0.021223
|
||||
corr_pos[138] = -0.030046
|
||||
corr_pos[139] = -0.077647
|
||||
corr_pos[140] = -0.085863
|
||||
corr_pos[141] = -0.093816
|
||||
corr_pos[142] = -0.138532
|
||||
corr_pos[143] = -0.145584
|
||||
corr_pos[144] = -0.152699
|
||||
corr_pos[145] = -0.192594
|
||||
corr_pos[146] = -0.200980
|
||||
corr_pos[147] = -0.208816
|
||||
corr_pos[148] = -0.241983
|
||||
corr_pos[149] = -0.248319
|
||||
corr_pos[150] = -0.253789
|
||||
corr_pos[151] = -0.276869
|
||||
corr_pos[152] = -0.279240
|
||||
corr_pos[153] = -0.281538
|
||||
corr_pos[154] = -0.294317
|
||||
corr_pos[155] = -0.295586
|
||||
corr_pos[156] = -0.296029
|
||||
corr_pos[157] = -0.297443
|
||||
corr_pos[158] = -0.297185
|
||||
corr_pos[159] = -0.295946
|
||||
corr_pos[160] = -0.284463
|
||||
corr_pos[161] = -0.282598
|
||||
corr_pos[162] = -0.281260
|
||||
corr_pos[163] = -0.256509
|
||||
corr_pos[164] = -0.250326
|
||||
corr_pos[165] = -0.244372
|
||||
corr_pos[166] = -0.204043
|
||||
corr_pos[167] = -0.196147
|
||||
corr_pos[168] = -0.187928
|
||||
corr_pos[169] = -0.131831
|
||||
corr_pos[170] = -0.121351
|
||||
corr_pos[171] = -0.110548
|
||||
corr_pos[172] = -0.038951
|
||||
corr_pos[173] = -0.025887
|
||||
corr_pos[174] = -0.012501
|
||||
@@ -416,7 +416,7 @@ class FlomniSampleTransferMixin:
|
||||
def laser_tracker_on(self):
|
||||
dev.rtx.controller.laser_tracker_on()
|
||||
time.sleep(0.2)
|
||||
self._laser_tracker_check_signalstrength()
|
||||
dev.rtx.controller.laser_tracker_check_signalstrength()
|
||||
|
||||
def laser_tracker_off(self):
|
||||
dev.rtx.controller.laser_tracker_off()
|
||||
@@ -424,18 +424,18 @@ class FlomniSampleTransferMixin:
|
||||
def show_signal_strength_interferometer(self):
|
||||
dev.rtx.controller.show_signal_strength_interferometer()
|
||||
|
||||
def rt_feedback_disable(self):
|
||||
def feedback_disable(self):
|
||||
self.device_manager.devices.rtx.controller.feedback_disable()
|
||||
|
||||
def rt_feedback_enable_with_reset(self):
|
||||
def feedback_enable_with_reset(self):
|
||||
self.device_manager.devices.rtx.controller.feedback_enable_with_reset()
|
||||
self.rt_feedback_status()
|
||||
self.feedback_status()
|
||||
|
||||
def rt_feedback_enable_without_reset(self):
|
||||
def feedback_enable_without_reset(self):
|
||||
self.device_manager.devices.rtx.controller.feedback_enable_without_reset()
|
||||
self.rt_feedback_status()
|
||||
self.feedback_status()
|
||||
|
||||
def rt_feedback_status(self):
|
||||
def feedback_status(self):
|
||||
feedback_status = self.device_manager.devices.rtx.controller.feedback_is_running()
|
||||
if feedback_status == True:
|
||||
print("The rt feedback is \x1b[92mrunning\x1b[0m.")
|
||||
@@ -455,7 +455,7 @@ class FlomniSampleTransferMixin:
|
||||
|
||||
umv(dev.fsamroy, 0)
|
||||
|
||||
self.rt_feedback_disable()
|
||||
self.feedback_disable()
|
||||
|
||||
self.ensure_fheater_up()
|
||||
|
||||
@@ -1051,7 +1051,7 @@ class FlomniAlignmentMixin:
|
||||
value = line.split(" ")[2]
|
||||
name = line.split(" ")[0].split("[")[0]
|
||||
if name == "corr_pos":
|
||||
corr_pos.append(float(value) / 1000)
|
||||
corr_pos.append(float(value))
|
||||
elif name == "corr_angle":
|
||||
corr_angle.append(float(value))
|
||||
print(
|
||||
@@ -1142,7 +1142,7 @@ class Flomni(
|
||||
fsamx_in = self._get_user_param_safe(dev.fsamx, "in")
|
||||
if np.isclose(fsamx_in, dev.fsamx.readback.get(), 0.5):
|
||||
print("Stopping alignment. Returning to fsamx in position.")
|
||||
self.rt_feedback_disable()
|
||||
self.feedback_disable()
|
||||
umv(dev.fsamx, fsamx_in)
|
||||
raise exc
|
||||
|
||||
@@ -1521,7 +1521,7 @@ class Flomni(
|
||||
angles = np.flip(angles)
|
||||
for angle in angles:
|
||||
self.progress["subtomo"] = subtomo_number
|
||||
self.progress["subtomo_projection"] = angles.index(angle)
|
||||
self.progress["subtomo_projection"] = np.where(angles == angle)[0][0]
|
||||
self.progress["subtomo_total_projections"] = 180 / self.tomo_angle_stepsize
|
||||
self.progress["projection"] = (subtomo_number - 1) * self.progress[
|
||||
"subtomo_total_projections"
|
||||
@@ -1701,7 +1701,7 @@ class Flomni(
|
||||
def _print_progress(self):
|
||||
print("\x1b[95mProgress report:")
|
||||
print(f"Tomo type: ....................... {self.progress['tomo_type']}")
|
||||
print(f"Projection: ...................... {self.progress['projection']}")
|
||||
print(f"Projection: ...................... {self.progress['projection']:.0f}")
|
||||
print(f"Total projections expected ....... {self.progress['total_projections']}")
|
||||
print(f"Angle: ........................... {self.progress['angle']}")
|
||||
print(f"Current subtomo: ................. {self.progress['subtomo']}")
|
||||
|
||||
@@ -100,7 +100,7 @@ class XrayEyeAlign:
|
||||
|
||||
self.flomni.laser_tracker_on()
|
||||
|
||||
self.flomni.rt_feedback_enable_with_reset()
|
||||
self.flomni.feedback_enable_with_reset()
|
||||
|
||||
# disable movement buttons
|
||||
self.movement_buttons_enabled = False
|
||||
@@ -109,7 +109,7 @@ class XrayEyeAlign:
|
||||
epics_put("XOMNYI-XEYE-SAMPLENAME:0.DESC", sample_name)
|
||||
|
||||
# this makes sure we are in a defined state
|
||||
self.flomni.rt_feedback_disable()
|
||||
self.flomni.feedback_disable()
|
||||
|
||||
epics_put("XOMNYI-XEYE-PIXELSIZE:0", self.PIXEL_CALIBRATION)
|
||||
|
||||
@@ -143,13 +143,13 @@ class XrayEyeAlign:
|
||||
self.movement_buttons_enabled = False
|
||||
epics_put("XOMNYI-XEYE-SUBMIT:0", -1) # disable submit button
|
||||
|
||||
self.flomni.rt_feedback_disable()
|
||||
self.flomni.feedback_disable()
|
||||
fsamx_in = self.flomni._get_user_param_safe("fsamx", "in")
|
||||
umv(dev.fsamx, fsamx_in)
|
||||
|
||||
self.flomni.foptics_out()
|
||||
|
||||
self.flomni.rt_feedback_disable()
|
||||
self.flomni.feedback_disable()
|
||||
umv(dev.fsamx, fsamx_in - 0.25)
|
||||
|
||||
self.update_frame()
|
||||
@@ -160,7 +160,7 @@ class XrayEyeAlign:
|
||||
|
||||
umv(dev.fsamx, fsamx_in)
|
||||
time.sleep(0.5)
|
||||
self.flomni.rt_feedback_enable_with_reset()
|
||||
self.flomni.feedback_enable_with_reset()
|
||||
|
||||
self.update_frame()
|
||||
self.send_message("Adjust sample height and submit center")
|
||||
@@ -205,11 +205,11 @@ class XrayEyeAlign:
|
||||
# allow movements, store movements to calculate center
|
||||
_xrayeyalignmvy = epics_get("XOMNYI-XEYE-MVY:0")
|
||||
if _xrayeyalignmvy != 0:
|
||||
self.flomni.rt_feedback_disable()
|
||||
self.flomni.feedback_disable()
|
||||
umvr(dev.fsamy, _xrayeyalignmvy / 1000)
|
||||
time.sleep(2)
|
||||
epics_put("XOMNYI-XEYE-MVY:0", 0)
|
||||
self.flomni.rt_feedback_enable_with_reset()
|
||||
self.flomni.feedback_enable_with_reset()
|
||||
self.update_frame()
|
||||
time.sleep(0.2)
|
||||
|
||||
|
||||
1
csaxs_bec/bec_ipython_client/plugins/omny/__init__.py
Normal file
1
csaxs_bec/bec_ipython_client/plugins/omny/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .omny import OMNY
|
||||
@@ -0,0 +1,715 @@
|
||||
corr_elements = 357
|
||||
corr_angle[0] = 0.097400
|
||||
corr_angle[1] = 0.603500
|
||||
corr_angle[2] = 1.134200
|
||||
corr_angle[3] = 1.625000
|
||||
corr_angle[4] = 2.162200
|
||||
corr_angle[5] = 2.700100
|
||||
corr_angle[6] = 3.191600
|
||||
corr_angle[7] = 3.714300
|
||||
corr_angle[8] = 4.223200
|
||||
corr_angle[9] = 4.730900
|
||||
corr_angle[10] = 5.253300
|
||||
corr_angle[11] = 5.743300
|
||||
corr_angle[12] = 6.279200
|
||||
corr_angle[13] = 6.782900
|
||||
corr_angle[14] = 7.301200
|
||||
corr_angle[15] = 7.808100
|
||||
corr_angle[16] = 8.325300
|
||||
corr_angle[17] = 8.859400
|
||||
corr_angle[18] = 9.359400
|
||||
corr_angle[19] = 9.887900
|
||||
corr_angle[20] = 10.395400
|
||||
corr_angle[21] = 10.930700
|
||||
corr_angle[22] = 11.415400
|
||||
corr_angle[23] = 11.928900
|
||||
corr_angle[24] = 12.456900
|
||||
corr_angle[25] = 12.955900
|
||||
corr_angle[26] = 13.479000
|
||||
corr_angle[27] = 13.982700
|
||||
corr_angle[28] = 14.500900
|
||||
corr_angle[29] = 15.016200
|
||||
corr_angle[30] = 15.528000
|
||||
corr_angle[31] = 16.053800
|
||||
corr_angle[32] = 16.562800
|
||||
corr_angle[33] = 17.076600
|
||||
corr_angle[34] = 17.592400
|
||||
corr_angle[35] = 18.094100
|
||||
corr_angle[36] = 18.623800
|
||||
corr_angle[37] = 19.118400
|
||||
corr_angle[38] = 19.655000
|
||||
corr_angle[39] = 20.143900
|
||||
corr_angle[40] = 20.672200
|
||||
corr_angle[41] = 21.171500
|
||||
corr_angle[42] = 21.696300
|
||||
corr_angle[43] = 22.215900
|
||||
corr_angle[44] = 22.728900
|
||||
corr_angle[45] = 23.233100
|
||||
corr_angle[46] = 23.760000
|
||||
corr_angle[47] = 24.267700
|
||||
corr_angle[48] = 24.792100
|
||||
corr_angle[49] = 25.287900
|
||||
corr_angle[50] = 25.824600
|
||||
corr_angle[51] = 26.321900
|
||||
corr_angle[52] = 26.842600
|
||||
corr_angle[53] = 27.337200
|
||||
corr_angle[54] = 27.873500
|
||||
corr_angle[55] = 28.373900
|
||||
corr_angle[56] = 28.895900
|
||||
corr_angle[57] = 29.404900
|
||||
corr_angle[58] = 29.926700
|
||||
corr_angle[59] = 30.439300
|
||||
corr_angle[60] = 30.963700
|
||||
corr_angle[61] = 31.460000
|
||||
corr_angle[62] = 31.989500
|
||||
corr_angle[63] = 32.489700
|
||||
corr_angle[64] = 33.026400
|
||||
corr_angle[65] = 33.516500
|
||||
corr_angle[66] = 34.044000
|
||||
corr_angle[67] = 34.547700
|
||||
corr_angle[68] = 35.070800
|
||||
corr_angle[69] = 35.578400
|
||||
corr_angle[70] = 36.103200
|
||||
corr_angle[71] = 36.603200
|
||||
corr_angle[72] = 37.128200
|
||||
corr_angle[73] = 37.645200
|
||||
corr_angle[74] = 38.164300
|
||||
corr_angle[75] = 38.662100
|
||||
corr_angle[76] = 39.191000
|
||||
corr_angle[77] = 39.693400
|
||||
corr_angle[78] = 40.222500
|
||||
corr_angle[79] = 40.719500
|
||||
corr_angle[80] = 41.247000
|
||||
corr_angle[81] = 41.755700
|
||||
corr_angle[82] = 42.271300
|
||||
corr_angle[83] = 42.767600
|
||||
corr_angle[84] = 43.302200
|
||||
corr_angle[85] = 43.818700
|
||||
corr_angle[86] = 44.330000
|
||||
corr_angle[87] = 44.835300
|
||||
corr_angle[88] = 45.359800
|
||||
corr_angle[89] = 45.867200
|
||||
corr_angle[90] = 46.396200
|
||||
corr_angle[91] = 46.895000
|
||||
corr_angle[92] = 47.411500
|
||||
corr_angle[93] = 47.915200
|
||||
corr_angle[94] = 48.436600
|
||||
corr_angle[95] = 48.946100
|
||||
corr_angle[96] = 49.472300
|
||||
corr_angle[97] = 49.979800
|
||||
corr_angle[98] = 50.503300
|
||||
corr_angle[99] = 51.008700
|
||||
corr_angle[100] = 51.535000
|
||||
corr_angle[101] = 52.036700
|
||||
corr_angle[102] = 52.563700
|
||||
corr_angle[103] = 53.063600
|
||||
corr_angle[104] = 53.591700
|
||||
corr_angle[105] = 54.091200
|
||||
corr_angle[106] = 54.618900
|
||||
corr_angle[107] = 55.116900
|
||||
corr_angle[108] = 55.636100
|
||||
corr_angle[109] = 56.143100
|
||||
corr_angle[110] = 56.672200
|
||||
corr_angle[111] = 57.172900
|
||||
corr_angle[112] = 57.704600
|
||||
corr_angle[113] = 58.204800
|
||||
corr_angle[114] = 58.728600
|
||||
corr_angle[115] = 59.239500
|
||||
corr_angle[116] = 59.768400
|
||||
corr_angle[117] = 60.268300
|
||||
corr_angle[118] = 60.788800
|
||||
corr_angle[119] = 61.289300
|
||||
corr_angle[120] = 61.813300
|
||||
corr_angle[121] = 62.310800
|
||||
corr_angle[122] = 62.836700
|
||||
corr_angle[123] = 63.353700
|
||||
corr_angle[124] = 63.866600
|
||||
corr_angle[125] = 64.377200
|
||||
corr_angle[126] = 64.906600
|
||||
corr_angle[127] = 65.414000
|
||||
corr_angle[128] = 65.937100
|
||||
corr_angle[129] = 66.429400
|
||||
corr_angle[130] = 66.970000
|
||||
corr_angle[131] = 67.459200
|
||||
corr_angle[132] = 67.996400
|
||||
corr_angle[133] = 68.499800
|
||||
corr_angle[134] = 69.014500
|
||||
corr_angle[135] = 69.509500
|
||||
corr_angle[136] = 70.044000
|
||||
corr_angle[137] = 70.543200
|
||||
corr_angle[138] = 71.079400
|
||||
corr_angle[139] = 71.579300
|
||||
corr_angle[140] = 72.103500
|
||||
corr_angle[141] = 72.607000
|
||||
corr_angle[142] = 73.137100
|
||||
corr_angle[143] = 73.633000
|
||||
corr_angle[144] = 74.164500
|
||||
corr_angle[145] = 74.660200
|
||||
corr_angle[146] = 75.180600
|
||||
corr_angle[147] = 75.674200
|
||||
corr_angle[148] = 76.215400
|
||||
corr_angle[149] = 76.718900
|
||||
corr_angle[150] = 77.242300
|
||||
corr_angle[151] = 77.752000
|
||||
corr_angle[152] = 78.279300
|
||||
corr_angle[153] = 78.780500
|
||||
corr_angle[154] = 79.314900
|
||||
corr_angle[155] = 79.424500
|
||||
corr_angle[156] = 79.807000
|
||||
corr_angle[157] = 80.336500
|
||||
corr_angle[158] = 80.338700
|
||||
corr_angle[159] = 80.835300
|
||||
corr_angle[160] = 81.367100
|
||||
corr_angle[161] = 81.376100
|
||||
corr_angle[162] = 81.859000
|
||||
corr_angle[163] = 82.382300
|
||||
corr_angle[164] = 82.384000
|
||||
corr_angle[165] = 82.881100
|
||||
corr_angle[166] = 83.415000
|
||||
corr_angle[167] = 83.421600
|
||||
corr_angle[168] = 83.917100
|
||||
corr_angle[169] = 84.439200
|
||||
corr_angle[170] = 84.439300
|
||||
corr_angle[171] = 84.947400
|
||||
corr_angle[172] = 85.472700
|
||||
corr_angle[173] = 85.987700
|
||||
corr_angle[174] = 86.512400
|
||||
corr_angle[175] = 87.009500
|
||||
corr_angle[176] = 87.536500
|
||||
corr_angle[177] = 88.035600
|
||||
corr_angle[178] = 88.560100
|
||||
corr_angle[179] = 89.057600
|
||||
corr_angle[180] = 89.583200
|
||||
corr_angle[181] = 90.090500
|
||||
corr_angle[182] = 90.614600
|
||||
corr_angle[183] = 91.119900
|
||||
corr_angle[184] = 91.638300
|
||||
corr_angle[185] = 92.153300
|
||||
corr_angle[186] = 92.681600
|
||||
corr_angle[187] = 93.181800
|
||||
corr_angle[188] = 93.710900
|
||||
corr_angle[189] = 94.206100
|
||||
corr_angle[190] = 94.732700
|
||||
corr_angle[191] = 95.226600
|
||||
corr_angle[192] = 95.766400
|
||||
corr_angle[193] = 96.259000
|
||||
corr_angle[194] = 96.783400
|
||||
corr_angle[195] = 97.283100
|
||||
corr_angle[196] = 97.811500
|
||||
corr_angle[197] = 98.323600
|
||||
corr_angle[198] = 98.839400
|
||||
corr_angle[199] = 99.350700
|
||||
corr_angle[200] = 99.880200
|
||||
corr_angle[201] = 100.378100
|
||||
corr_angle[202] = 100.913300
|
||||
corr_angle[203] = 101.405000
|
||||
corr_angle[204] = 101.935700
|
||||
corr_angle[205] = 102.426300
|
||||
corr_angle[206] = 102.957300
|
||||
corr_angle[207] = 103.456500
|
||||
corr_angle[208] = 103.985100
|
||||
corr_angle[209] = 104.490300
|
||||
corr_angle[210] = 105.015100
|
||||
corr_angle[211] = 105.518400
|
||||
corr_angle[212] = 106.047400
|
||||
corr_angle[213] = 106.551100
|
||||
corr_angle[214] = 107.077100
|
||||
corr_angle[215] = 107.575800
|
||||
corr_angle[216] = 108.115200
|
||||
corr_angle[217] = 108.598800
|
||||
corr_angle[218] = 109.129100
|
||||
corr_angle[219] = 109.626200
|
||||
corr_angle[220] = 110.158400
|
||||
corr_angle[221] = 110.661700
|
||||
corr_angle[222] = 111.188600
|
||||
corr_angle[223] = 111.695000
|
||||
corr_angle[224] = 112.216400
|
||||
corr_angle[225] = 112.720000
|
||||
corr_angle[226] = 113.246200
|
||||
corr_angle[227] = 113.757100
|
||||
corr_angle[228] = 114.281900
|
||||
corr_angle[229] = 114.780000
|
||||
corr_angle[230] = 115.306800
|
||||
corr_angle[231] = 115.799800
|
||||
corr_angle[232] = 116.328400
|
||||
corr_angle[233] = 116.829900
|
||||
corr_angle[234] = 117.355400
|
||||
corr_angle[235] = 117.858200
|
||||
corr_angle[236] = 118.380600
|
||||
corr_angle[237] = 118.893200
|
||||
corr_angle[238] = 119.418200
|
||||
corr_angle[239] = 119.921000
|
||||
corr_angle[240] = 120.446100
|
||||
corr_angle[241] = 120.953800
|
||||
corr_angle[242] = 121.480300
|
||||
corr_angle[243] = 121.977600
|
||||
corr_angle[244] = 122.507100
|
||||
corr_angle[245] = 122.996200
|
||||
corr_angle[246] = 123.529600
|
||||
corr_angle[247] = 124.032200
|
||||
corr_angle[248] = 124.554800
|
||||
corr_angle[249] = 125.057800
|
||||
corr_angle[250] = 125.580800
|
||||
corr_angle[251] = 126.090000
|
||||
corr_angle[252] = 126.611600
|
||||
corr_angle[253] = 127.127000
|
||||
corr_angle[254] = 127.650900
|
||||
corr_angle[255] = 128.153700
|
||||
corr_angle[256] = 128.677200
|
||||
corr_angle[257] = 129.170800
|
||||
corr_angle[258] = 129.703000
|
||||
corr_angle[259] = 130.194900
|
||||
corr_angle[260] = 130.733600
|
||||
corr_angle[261] = 131.228300
|
||||
corr_angle[262] = 131.756900
|
||||
corr_angle[263] = 132.263200
|
||||
corr_angle[264] = 132.784700
|
||||
corr_angle[265] = 133.296300
|
||||
corr_angle[266] = 133.818800
|
||||
corr_angle[267] = 134.318900
|
||||
corr_angle[268] = 134.846800
|
||||
corr_angle[269] = 135.344800
|
||||
corr_angle[270] = 135.876000
|
||||
corr_angle[271] = 136.372600
|
||||
corr_angle[272] = 136.902700
|
||||
corr_angle[273] = 137.397800
|
||||
corr_angle[274] = 137.926800
|
||||
corr_angle[275] = 138.428000
|
||||
corr_angle[276] = 138.953700
|
||||
corr_angle[277] = 139.461300
|
||||
corr_angle[278] = 139.987800
|
||||
corr_angle[279] = 140.492600
|
||||
corr_angle[280] = 141.017900
|
||||
corr_angle[281] = 141.524000
|
||||
corr_angle[282] = 142.061200
|
||||
corr_angle[283] = 142.545400
|
||||
corr_angle[284] = 143.069900
|
||||
corr_angle[285] = 143.568700
|
||||
corr_angle[286] = 144.102200
|
||||
corr_angle[287] = 144.596200
|
||||
corr_angle[288] = 145.126400
|
||||
corr_angle[289] = 145.626900
|
||||
corr_angle[290] = 146.158700
|
||||
corr_angle[291] = 146.662200
|
||||
corr_angle[292] = 147.184700
|
||||
corr_angle[293] = 147.692300
|
||||
corr_angle[294] = 148.225900
|
||||
corr_angle[295] = 148.716000
|
||||
corr_angle[296] = 149.243400
|
||||
corr_angle[297] = 149.739400
|
||||
corr_angle[298] = 150.272400
|
||||
corr_angle[299] = 150.762100
|
||||
corr_angle[300] = 151.301800
|
||||
corr_angle[301] = 151.796300
|
||||
corr_angle[302] = 152.333500
|
||||
corr_angle[303] = 152.833100
|
||||
corr_angle[304] = 153.363600
|
||||
corr_angle[305] = 153.862900
|
||||
corr_angle[306] = 154.396100
|
||||
corr_angle[307] = 154.895400
|
||||
corr_angle[308] = 155.417700
|
||||
corr_angle[309] = 155.916900
|
||||
corr_angle[310] = 156.451600
|
||||
corr_angle[311] = 156.942700
|
||||
corr_angle[312] = 157.467600
|
||||
corr_angle[313] = 157.970900
|
||||
corr_angle[314] = 158.501100
|
||||
corr_angle[315] = 158.999600
|
||||
corr_angle[316] = 159.526100
|
||||
corr_angle[317] = 160.028100
|
||||
corr_angle[318] = 160.555800
|
||||
corr_angle[319] = 161.067700
|
||||
corr_angle[320] = 161.597300
|
||||
corr_angle[321] = 162.091300
|
||||
corr_angle[322] = 162.616600
|
||||
corr_angle[323] = 163.107400
|
||||
corr_angle[324] = 163.639100
|
||||
corr_angle[325] = 164.139700
|
||||
corr_angle[326] = 164.672500
|
||||
corr_angle[327] = 165.176800
|
||||
corr_angle[328] = 165.698600
|
||||
corr_angle[329] = 166.205400
|
||||
corr_angle[330] = 166.727300
|
||||
corr_angle[331] = 167.238600
|
||||
corr_angle[332] = 167.771500
|
||||
corr_angle[333] = 168.265000
|
||||
corr_angle[334] = 168.794800
|
||||
corr_angle[335] = 169.293800
|
||||
corr_angle[336] = 169.813300
|
||||
corr_angle[337] = 170.320200
|
||||
corr_angle[338] = 170.845100
|
||||
corr_angle[339] = 171.344000
|
||||
corr_angle[340] = 171.866200
|
||||
corr_angle[341] = 172.375300
|
||||
corr_angle[342] = 172.899400
|
||||
corr_angle[343] = 173.404400
|
||||
corr_angle[344] = 173.928500
|
||||
corr_angle[345] = 174.430800
|
||||
corr_angle[346] = 174.827800
|
||||
corr_angle[347] = 175.460400
|
||||
corr_angle[348] = 175.993100
|
||||
corr_angle[349] = 176.492100
|
||||
corr_angle[350] = 177.014200
|
||||
corr_angle[351] = 177.512100
|
||||
corr_angle[352] = 178.044200
|
||||
corr_angle[353] = 178.643800
|
||||
corr_angle[354] = 179.067200
|
||||
corr_angle[355] = 179.571600
|
||||
corr_angle[356] = 180.093700
|
||||
corr_pos[0] = -0.814519
|
||||
corr_pos[1] = -0.810675
|
||||
corr_pos[2] = -0.806338
|
||||
corr_pos[3] = -0.802047
|
||||
corr_pos[4] = -0.797044
|
||||
corr_pos[5] = -0.791712
|
||||
corr_pos[6] = -0.786558
|
||||
corr_pos[7] = -0.780781
|
||||
corr_pos[8] = -0.774864
|
||||
corr_pos[9] = -0.768674
|
||||
corr_pos[10] = -0.762005
|
||||
corr_pos[11] = -0.755474
|
||||
corr_pos[12] = -0.748024
|
||||
corr_pos[13] = -0.740730
|
||||
corr_pos[14] = -0.732929
|
||||
corr_pos[15] = -0.725011
|
||||
corr_pos[16] = -0.716637
|
||||
corr_pos[17] = -0.707676
|
||||
corr_pos[18] = -0.698861
|
||||
corr_pos[19] = -0.686245
|
||||
corr_pos[20] = -0.676596
|
||||
corr_pos[21] = -0.663643
|
||||
corr_pos[22] = -0.655664
|
||||
corr_pos[23] = -0.642249
|
||||
corr_pos[24] = -0.633516
|
||||
corr_pos[25] = -0.621402
|
||||
corr_pos[26] = -0.612846
|
||||
corr_pos[27] = -0.599978
|
||||
corr_pos[28] = -0.592135
|
||||
corr_pos[29] = -0.577949
|
||||
corr_pos[30] = -0.570695
|
||||
corr_pos[31] = -0.555560
|
||||
corr_pos[32] = -0.549015
|
||||
corr_pos[33] = -0.532162
|
||||
corr_pos[34] = -0.524479
|
||||
corr_pos[35] = -0.507848
|
||||
corr_pos[36] = -0.500301
|
||||
corr_pos[37] = -0.484361
|
||||
corr_pos[38] = -0.477365
|
||||
corr_pos[39] = -0.461751
|
||||
corr_pos[40] = -0.455963
|
||||
corr_pos[41] = -0.439438
|
||||
corr_pos[42] = -0.436103
|
||||
corr_pos[43] = -0.420587
|
||||
corr_pos[44] = -0.417361
|
||||
corr_pos[45] = -0.402270
|
||||
corr_pos[46] = -0.400459
|
||||
corr_pos[47] = -0.384745
|
||||
corr_pos[48] = -0.385280
|
||||
corr_pos[49] = -0.371217
|
||||
corr_pos[50] = -0.373612
|
||||
corr_pos[51] = -0.361838
|
||||
corr_pos[52] = -0.365771
|
||||
corr_pos[53] = -0.354641
|
||||
corr_pos[54] = -0.360647
|
||||
corr_pos[55] = -0.351446
|
||||
corr_pos[56] = -0.359147
|
||||
corr_pos[57] = -0.348696
|
||||
corr_pos[58] = -0.359348
|
||||
corr_pos[59] = -0.349962
|
||||
corr_pos[60] = -0.360782
|
||||
corr_pos[61] = -0.355040
|
||||
corr_pos[62] = -0.367441
|
||||
corr_pos[63] = -0.360511
|
||||
corr_pos[64] = -0.372617
|
||||
corr_pos[65] = -0.366120
|
||||
corr_pos[66] = -0.377568
|
||||
corr_pos[67] = -0.370362
|
||||
corr_pos[68] = -0.380918
|
||||
corr_pos[69] = -0.372553
|
||||
corr_pos[70] = -0.381460
|
||||
corr_pos[71] = -0.371828
|
||||
corr_pos[72] = -0.379428
|
||||
corr_pos[73] = -0.368465
|
||||
corr_pos[74] = -0.374297
|
||||
corr_pos[75] = -0.360340
|
||||
corr_pos[76] = -0.365429
|
||||
corr_pos[77] = -0.349168
|
||||
corr_pos[78] = -0.351544
|
||||
corr_pos[79] = -0.332514
|
||||
corr_pos[80] = -0.333505
|
||||
corr_pos[81] = -0.313316
|
||||
corr_pos[82] = -0.313014
|
||||
corr_pos[83] = -0.291107
|
||||
corr_pos[84] = -0.287807
|
||||
corr_pos[85] = -0.264483
|
||||
corr_pos[86] = -0.260891
|
||||
corr_pos[87] = -0.236646
|
||||
corr_pos[88] = -0.231362
|
||||
corr_pos[89] = -0.205833
|
||||
corr_pos[90] = -0.200663
|
||||
corr_pos[91] = -0.174834
|
||||
corr_pos[92] = -0.171662
|
||||
corr_pos[93] = -0.146200
|
||||
corr_pos[94] = -0.142890
|
||||
corr_pos[95] = -0.118389
|
||||
corr_pos[96] = -0.112895
|
||||
corr_pos[97] = -0.088879
|
||||
corr_pos[98] = -0.084146
|
||||
corr_pos[99] = -0.061625
|
||||
corr_pos[100] = -0.054742
|
||||
corr_pos[101] = -0.032741
|
||||
corr_pos[102] = -0.026725
|
||||
corr_pos[103] = -0.004827
|
||||
corr_pos[104] = 0.002678
|
||||
corr_pos[105] = 0.024418
|
||||
corr_pos[106] = 0.031105
|
||||
corr_pos[107] = 0.052334
|
||||
corr_pos[108] = 0.060072
|
||||
corr_pos[109] = 0.081470
|
||||
corr_pos[110] = 0.088550
|
||||
corr_pos[111] = 0.109685
|
||||
corr_pos[112] = 0.116675
|
||||
corr_pos[113] = 0.136298
|
||||
corr_pos[114] = 0.142152
|
||||
corr_pos[115] = 0.161986
|
||||
corr_pos[116] = 0.166996
|
||||
corr_pos[117] = 0.186466
|
||||
corr_pos[118] = 0.190874
|
||||
corr_pos[119] = 0.208729
|
||||
corr_pos[120] = 0.212714
|
||||
corr_pos[121] = 0.229992
|
||||
corr_pos[122] = 0.232685
|
||||
corr_pos[123] = 0.248748
|
||||
corr_pos[124] = 0.249688
|
||||
corr_pos[125] = 0.264768
|
||||
corr_pos[126] = 0.264432
|
||||
corr_pos[127] = 0.278801
|
||||
corr_pos[128] = 0.278424
|
||||
corr_pos[129] = 0.289656
|
||||
corr_pos[130] = 0.289612
|
||||
corr_pos[131] = 0.299899
|
||||
corr_pos[132] = 0.299782
|
||||
corr_pos[133] = 0.307473
|
||||
corr_pos[134] = 0.309489
|
||||
corr_pos[135] = 0.315999
|
||||
corr_pos[136] = 0.317413
|
||||
corr_pos[137] = 0.325060
|
||||
corr_pos[138] = 0.325931
|
||||
corr_pos[139] = 0.332544
|
||||
corr_pos[140] = 0.332095
|
||||
corr_pos[141] = 0.338738
|
||||
corr_pos[142] = 0.337864
|
||||
corr_pos[143] = 0.343909
|
||||
corr_pos[144] = 0.342973
|
||||
corr_pos[145] = 0.348449
|
||||
corr_pos[146] = 0.346060
|
||||
corr_pos[147] = 0.351949
|
||||
corr_pos[148] = 0.349873
|
||||
corr_pos[149] = 0.354969
|
||||
corr_pos[150] = 0.352375
|
||||
corr_pos[151] = 0.358388
|
||||
corr_pos[152] = 0.354736
|
||||
corr_pos[153] = 0.359539
|
||||
corr_pos[154] = 0.354369
|
||||
corr_pos[155] = 0.358549
|
||||
corr_pos[156] = 0.353217
|
||||
corr_pos[157] = 0.355795
|
||||
corr_pos[158] = 0.351299
|
||||
corr_pos[159] = 0.352922
|
||||
corr_pos[160] = 0.346933
|
||||
corr_pos[161] = 0.350201
|
||||
corr_pos[162] = 0.343937
|
||||
corr_pos[163] = 0.349304
|
||||
corr_pos[164] = 0.344857
|
||||
corr_pos[165] = 0.347471
|
||||
corr_pos[166] = 0.335793
|
||||
corr_pos[167] = 0.341109
|
||||
corr_pos[168] = 0.330453
|
||||
corr_pos[169] = 0.326576
|
||||
corr_pos[170] = 0.320299
|
||||
corr_pos[171] = 0.315348
|
||||
corr_pos[172] = 0.300744
|
||||
corr_pos[173] = 0.294366
|
||||
corr_pos[174] = 0.280438
|
||||
corr_pos[175] = 0.273618
|
||||
corr_pos[176] = 0.259335
|
||||
corr_pos[177] = 0.251464
|
||||
corr_pos[178] = 0.237247
|
||||
corr_pos[179] = 0.229092
|
||||
corr_pos[180] = 0.214882
|
||||
corr_pos[181] = 0.207363
|
||||
corr_pos[182] = 0.193246
|
||||
corr_pos[183] = 0.185868
|
||||
corr_pos[184] = 0.173256
|
||||
corr_pos[185] = 0.165378
|
||||
corr_pos[186] = 0.153853
|
||||
corr_pos[187] = 0.147031
|
||||
corr_pos[188] = 0.135535
|
||||
corr_pos[189] = 0.129435
|
||||
corr_pos[190] = 0.119843
|
||||
corr_pos[191] = 0.115062
|
||||
corr_pos[192] = 0.107115
|
||||
corr_pos[193] = 0.105182
|
||||
corr_pos[194] = 0.099647
|
||||
corr_pos[195] = 0.098162
|
||||
corr_pos[196] = 0.094628
|
||||
corr_pos[197] = 0.095257
|
||||
corr_pos[198] = 0.084490
|
||||
corr_pos[199] = 0.085389
|
||||
corr_pos[200] = 0.074302
|
||||
corr_pos[201] = 0.075265
|
||||
corr_pos[202] = 0.063623
|
||||
corr_pos[203] = 0.064487
|
||||
corr_pos[204] = 0.052841
|
||||
corr_pos[205] = 0.053559
|
||||
corr_pos[206] = 0.042022
|
||||
corr_pos[207] = 0.043321
|
||||
corr_pos[208] = 0.032826
|
||||
corr_pos[209] = 0.032277
|
||||
corr_pos[210] = 0.022169
|
||||
corr_pos[211] = 0.020740
|
||||
corr_pos[212] = 0.010378
|
||||
corr_pos[213] = 0.008668
|
||||
corr_pos[214] = -0.001930
|
||||
corr_pos[215] = -0.003882
|
||||
corr_pos[216] = -0.014694
|
||||
corr_pos[217] = -0.016629
|
||||
corr_pos[218] = -0.026380
|
||||
corr_pos[219] = -0.028069
|
||||
corr_pos[220] = -0.037125
|
||||
corr_pos[221] = -0.039429
|
||||
corr_pos[222] = -0.047773
|
||||
corr_pos[223] = -0.049261
|
||||
corr_pos[224] = -0.056872
|
||||
corr_pos[225] = -0.058756
|
||||
corr_pos[226] = -0.065279
|
||||
corr_pos[227] = -0.066859
|
||||
corr_pos[228] = -0.073027
|
||||
corr_pos[229] = -0.074253
|
||||
corr_pos[230] = -0.079062
|
||||
corr_pos[231] = -0.079800
|
||||
corr_pos[232] = -0.084350
|
||||
corr_pos[233] = -0.084805
|
||||
corr_pos[234] = -0.088621
|
||||
corr_pos[235] = -0.088437
|
||||
corr_pos[236] = -0.092166
|
||||
corr_pos[237] = -0.090769
|
||||
corr_pos[238] = -0.093820
|
||||
corr_pos[239] = -0.091724
|
||||
corr_pos[240] = -0.094152
|
||||
corr_pos[241] = -0.090866
|
||||
corr_pos[242] = -0.092575
|
||||
corr_pos[243] = -0.089239
|
||||
corr_pos[244] = -0.089666
|
||||
corr_pos[245] = -0.086864
|
||||
corr_pos[246] = -0.087183
|
||||
corr_pos[247] = -0.083974
|
||||
corr_pos[248] = -0.083709
|
||||
corr_pos[249] = -0.079848
|
||||
corr_pos[250] = -0.078494
|
||||
corr_pos[251] = -0.072782
|
||||
corr_pos[252] = -0.069638
|
||||
corr_pos[253] = -0.063004
|
||||
corr_pos[254] = -0.058599
|
||||
corr_pos[255] = -0.051737
|
||||
corr_pos[256] = -0.046451
|
||||
corr_pos[257] = -0.040238
|
||||
corr_pos[258] = -0.034071
|
||||
corr_pos[259] = -0.028578
|
||||
corr_pos[260] = -0.021481
|
||||
corr_pos[261] = -0.016346
|
||||
corr_pos[262] = -0.009475
|
||||
corr_pos[263] = -0.004141
|
||||
corr_pos[264] = 0.002780
|
||||
corr_pos[265] = 0.008455
|
||||
corr_pos[266] = 0.015507
|
||||
corr_pos[267] = 0.021672
|
||||
corr_pos[268] = 0.029704
|
||||
corr_pos[269] = 0.036662
|
||||
corr_pos[270] = 0.044794
|
||||
corr_pos[271] = 0.052031
|
||||
corr_pos[272] = 0.061478
|
||||
corr_pos[273] = 0.069150
|
||||
corr_pos[274] = 0.078715
|
||||
corr_pos[275] = 0.087785
|
||||
corr_pos[276] = 0.098593
|
||||
corr_pos[277] = 0.107863
|
||||
corr_pos[278] = 0.118256
|
||||
corr_pos[279] = 0.127631
|
||||
corr_pos[280] = 0.139011
|
||||
corr_pos[281] = 0.150077
|
||||
corr_pos[282] = 0.162154
|
||||
corr_pos[283] = 0.173758
|
||||
corr_pos[284] = 0.186998
|
||||
corr_pos[285] = 0.200111
|
||||
corr_pos[286] = 0.214116
|
||||
corr_pos[287] = 0.227291
|
||||
corr_pos[288] = 0.240662
|
||||
corr_pos[289] = 0.252955
|
||||
corr_pos[290] = 0.265359
|
||||
corr_pos[291] = 0.275995
|
||||
corr_pos[292] = 0.287613
|
||||
corr_pos[293] = 0.295789
|
||||
corr_pos[294] = 0.306424
|
||||
corr_pos[295] = 0.313027
|
||||
corr_pos[296] = 0.322181
|
||||
corr_pos[297] = 0.329313
|
||||
corr_pos[298] = 0.338191
|
||||
corr_pos[299] = 0.345243
|
||||
corr_pos[300] = 0.353316
|
||||
corr_pos[301] = 0.360491
|
||||
corr_pos[302] = 0.367891
|
||||
corr_pos[303] = 0.375027
|
||||
corr_pos[304] = 0.380865
|
||||
corr_pos[305] = 0.388354
|
||||
corr_pos[306] = 0.395257
|
||||
corr_pos[307] = 0.401478
|
||||
corr_pos[308] = 0.408087
|
||||
corr_pos[309] = 0.414083
|
||||
corr_pos[310] = 0.420361
|
||||
corr_pos[311] = 0.424284
|
||||
corr_pos[312] = 0.429614
|
||||
corr_pos[313] = 0.433888
|
||||
corr_pos[314] = 0.438529
|
||||
corr_pos[315] = 0.442302
|
||||
corr_pos[316] = 0.445899
|
||||
corr_pos[317] = 0.449014
|
||||
corr_pos[318] = 0.451693
|
||||
corr_pos[319] = 0.453795
|
||||
corr_pos[320] = 0.455132
|
||||
corr_pos[321] = 0.455438
|
||||
corr_pos[322] = 0.455334
|
||||
corr_pos[323] = 0.454055
|
||||
corr_pos[324] = 0.451146
|
||||
corr_pos[325] = 0.447259
|
||||
corr_pos[326] = 0.442478
|
||||
corr_pos[327] = 0.437520
|
||||
corr_pos[328] = 0.432297
|
||||
corr_pos[329] = 0.426442
|
||||
corr_pos[330] = 0.418918
|
||||
corr_pos[331] = 0.411040
|
||||
corr_pos[332] = 0.402610
|
||||
corr_pos[333] = 0.394491
|
||||
corr_pos[334] = 0.383925
|
||||
corr_pos[335] = 0.374590
|
||||
corr_pos[336] = 0.363650
|
||||
corr_pos[337] = 0.353143
|
||||
corr_pos[338] = 0.339756
|
||||
corr_pos[339] = 0.328074
|
||||
corr_pos[340] = 0.315463
|
||||
corr_pos[341] = 0.302641
|
||||
corr_pos[342] = 0.288898
|
||||
corr_pos[343] = 0.275134
|
||||
corr_pos[344] = 0.260308
|
||||
corr_pos[345] = 0.245582
|
||||
corr_pos[346] = 0.233584
|
||||
corr_pos[347] = 0.213812
|
||||
corr_pos[348] = 0.196540
|
||||
corr_pos[349] = 0.179844
|
||||
corr_pos[350] = 0.161839
|
||||
corr_pos[351] = 0.144160
|
||||
corr_pos[352] = 0.124715
|
||||
corr_pos[353] = 0.102123
|
||||
corr_pos[354] = 0.085736
|
||||
corr_pos[355] = 0.065743
|
||||
corr_pos[356] = 0.044511
|
||||
File diff suppressed because it is too large
Load Diff
171
csaxs_bec/bec_ipython_client/plugins/omny/gui_tools.py
Normal file
171
csaxs_bec/bec_ipython_client/plugins/omny/gui_tools.py
Normal file
@@ -0,0 +1,171 @@
|
||||
import builtins
|
||||
|
||||
from bec_widgets.cli.client import BECDockArea
|
||||
|
||||
# from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_get, epics_put, fshopen, fshclose
|
||||
|
||||
if builtins.__dict__.get("bec") is not None:
|
||||
bec = builtins.__dict__.get("bec")
|
||||
dev = builtins.__dict__.get("dev")
|
||||
umv = builtins.__dict__.get("umv")
|
||||
umvr = builtins.__dict__.get("umvr")
|
||||
|
||||
|
||||
class OMNYGuiToolsError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class OMNYGuiTools:
|
||||
|
||||
def __init__(self, client):
|
||||
self.gui = getattr(client, "gui", None)
|
||||
self.gui_window = self.gui.windows['main'].widget
|
||||
self.fig200 = None
|
||||
self.fig201 = None
|
||||
self.fig202 = None
|
||||
self.fig203 = None
|
||||
self.progressbar = None
|
||||
self.text_box = None
|
||||
self.idle_text_box = None
|
||||
|
||||
def omnygui_show_gui(self):
|
||||
self.gui_window.show()
|
||||
|
||||
def omnygui_stop_gui(self):
|
||||
self.gui_window.hide()
|
||||
|
||||
def _omnycam_parking(self):
|
||||
self.omnygui_show_omnycam_parking()
|
||||
|
||||
def omnygui_show_omnycam_parking(self):
|
||||
self.omnygui_show_gui()
|
||||
if self.fig200 is None:
|
||||
self._omnycam_clear()
|
||||
self.fig200 = self.gui_window.add_dock(name="omnycam200").add_widget("BECImageWidget")
|
||||
if self._omnycam_check_device_exists(dev.cam200):
|
||||
fig = self.fig200.image("cam200")
|
||||
fig.set_rotation(deg_90=3)
|
||||
self.fig200.lock_aspect_ratio(True)
|
||||
else:
|
||||
print("Cannot open cam200. Device does not exist.")
|
||||
self.fig203 = self.gui_window.add_dock(name="omnycam203").add_widget("BECImageWidget")
|
||||
if self._omnycam_check_device_exists(dev.cam203):
|
||||
fig = self.fig203.image("cam203")
|
||||
fig.set_rotation(deg_90=3)
|
||||
self.fig203.lock_aspect_ratio(True)
|
||||
else:
|
||||
print("Cannot open cam203. Device does not exist.")
|
||||
try:
|
||||
self.gui_window.remove_dock(name="default_figure")
|
||||
except:
|
||||
pass
|
||||
|
||||
def omnygui_remove_all_docks(self):
|
||||
self.gui_window.clear_all()
|
||||
self.fig200 = None
|
||||
self.fig201 = None
|
||||
self.fig202 = None
|
||||
self.fig203 = None
|
||||
self.progressbar = None
|
||||
self.text_box = None
|
||||
self.idle_text_box = None
|
||||
|
||||
def omnygui_idle(self):
|
||||
self.omnygui_show_gui()
|
||||
if self.idle_text_box is None:
|
||||
self.omnygui_remove_all_docks()
|
||||
self.idle_text_box = self.gui_window.add_dock(name="idle_text").add_widget("TextBox")
|
||||
try:
|
||||
self.gui_window.remove_dock(name="default_figure")
|
||||
except:
|
||||
pass
|
||||
text = (
|
||||
"<pre>"
|
||||
+ " ,o888888o. ,8. ,8. b. 8 `8.`8888. ,8' \n"
|
||||
+ " . 8888 `88. ,888. ,888. 888o. 8 `8.`8888. ,8' \n"
|
||||
+ ",8 8888 `8b .`8888. .`8888. Y88888o. 8 `8.`8888. ,8' \n"
|
||||
+ "88 8888 `8b ,8.`8888. ,8.`8888. .`Y888888o. 8 `8.`8888.,8' \n"
|
||||
+ "88 8888 88 ,8'8.`8888,8^8.`8888. 8o. `Y888888o. 8 `8.`88888' \n"
|
||||
+ "88 8888 88 ,8' `8.`8888' `8.`8888. 8`Y8o. `Y88888o8 `8. 8888 \n"
|
||||
+ "88 8888 ,8P ,8' `8.`88' `8.`8888. 8 `Y8o. `Y8888 `8 8888 \n"
|
||||
+ "`8 8888 ,8P ,8' `8.`' `8.`8888. 8 `Y8o. `Y8 8 8888 \n"
|
||||
+ " ` 8888 ,88' ,8' `8 `8.`8888. 8 `Y8o.` 8 8888 \n"
|
||||
+ " `8888888P' ,8' ` `8.`8888. 8 `Yo 8 8888 \n"
|
||||
+ "</pre>"
|
||||
)
|
||||
self.idle_text_box.set_html_text(text)
|
||||
|
||||
def _omnycam_clear(self):
|
||||
self.omnygui_remove_all_docks()
|
||||
|
||||
def _omnycam_check_device_exists(self, device):
|
||||
try:
|
||||
device
|
||||
except:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def _omnycam_samplestage(self):
|
||||
self.omnygui_show_omnycam_samplestage()
|
||||
|
||||
def omnygui_show_omnycam_samplestage(self):
|
||||
self.omnygui_show_gui()
|
||||
if self.fig201 is None:
|
||||
self.omnygui_remove_all_docks()
|
||||
self.fig201 = self.gui_window.add_dock(name="omnycam201").add_widget("BECImageWidget")
|
||||
if self._omnycam_check_device_exists(dev.cam201):
|
||||
fig = self.fig201.image("cam201")
|
||||
fig.set_rotation(deg_90=3)
|
||||
self.fig201.lock_aspect_ratio(True)
|
||||
else:
|
||||
print("Cannot open cam201. Device does not exist.")
|
||||
self.fig202 = self.gui_window.add_dock(name="omnycam202").add_widget("BECImageWidget")
|
||||
if self._omnycam_check_device_exists(dev.cam202):
|
||||
fig = self.fig202.image("cam202")
|
||||
fig.set_rotation(deg_90=3)
|
||||
self.fig202.lock_aspect_ratio(True)
|
||||
else:
|
||||
print("Cannot open cam202. Device does not exist.")
|
||||
try:
|
||||
self.gui_window.remove_dock(name="default_figure")
|
||||
except:
|
||||
pass
|
||||
|
||||
def omnygui_show_progress(self):
|
||||
self.omnygui_show_gui()
|
||||
if self.progressbar is None:
|
||||
self.omnygui_remove_all_docks()
|
||||
# Add a new dock with a RingProgressBar widget
|
||||
self.progressbar = self.gui_window.add_dock(name="progress").add_widget("RingProgressBar")
|
||||
# Customize the size of the progress ring
|
||||
self.progressbar.set_line_widths(20)
|
||||
# Disable automatic updates and manually set the self.progressbar value
|
||||
self.progressbar.enable_auto_updates(False)
|
||||
# Set precision for the self.progressbar display
|
||||
self.progressbar.set_precision(1) # Display self.progressbar with one decimal places
|
||||
# Setting multiple rigns with different values
|
||||
self.progressbar.set_number_of_bars(3)
|
||||
self.progressbar.rings[2].set_update("scan")
|
||||
# Set the values of the rings to 50, 75, and 25 from outer to inner ring
|
||||
# self.progressbar.set_value([50, 75])
|
||||
# Add a new dock with a TextBox widget
|
||||
self.text_box = self.gui_window.add_dock(name="progress_text").add_widget("TextBox")
|
||||
try:
|
||||
self.gui_window.remove_dock(name="default_figure")
|
||||
except:
|
||||
pass
|
||||
self._omnygui_update_progress()
|
||||
|
||||
def _omnygui_update_progress(self):
|
||||
if self.progressbar is not None:
|
||||
progress = self.progress["projection"] / self.progress["total_projections"] * 100
|
||||
subotmo_progress = (
|
||||
self.progress["subtomo_projection"]
|
||||
/ self.progress["subtomo_total_projections"]
|
||||
* 100
|
||||
)
|
||||
self.progressbar.set_value([progress, subotmo_progress])
|
||||
|
||||
text = f"Progress report:\n Tomo type: ....................... {self.progress['tomo_type']}\n Projection: ...................... {self.progress['projection']:.0f}\n Total projections expected ....... {self.progress['total_projections']}\n Angle: ........................... {self.progress['angle']}\n Current subtomo: ................. {self.progress['subtomo']}\n Current projection within subtomo: {self.progress['subtomo_projection']}\n Total projections per subtomo: ... {self.progress['subtomo_total_projections']}"
|
||||
self.text_box.set_plain_text(text)
|
||||
1532
csaxs_bec/bec_ipython_client/plugins/omny/omny.py
Normal file
1532
csaxs_bec/bec_ipython_client/plugins/omny/omny.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,246 @@
|
||||
import time
|
||||
import numpy as np
|
||||
import os
|
||||
|
||||
from rich import box
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
from typeguard import typechecked
|
||||
from bec_lib import bec_logger
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class OMNYAlignmentError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class OMNYAlignmentMixin:
|
||||
default_correction_file = "correction_omny_202204.txt"
|
||||
default_correction_file_x = "correction_omny_202204_x.txt"
|
||||
|
||||
def reset_correction(self, use_default_correction=True):
|
||||
"""
|
||||
Reset the correction to the default values.
|
||||
If use_default_correction is False, the correction will be set to empty values.
|
||||
Otherwise the default values will be loaded.
|
||||
|
||||
Args:
|
||||
use_default_correction (bool, optional): If set to true, a call reset the correction to the default values. Defaults to True.
|
||||
"""
|
||||
self.corr_pos_x = []
|
||||
self.corr_angle_x = []
|
||||
self.corr_pos_y = []
|
||||
self.corr_angle_y = []
|
||||
self.corr_pos_y_2 = []
|
||||
self.corr_angle_y_2 = []
|
||||
|
||||
if use_default_correction:
|
||||
try:
|
||||
self.read_additional_correction_x(self.default_correction_file_x)
|
||||
logger.info(f"Applying default x correction from {self.default_correction_file_x}")
|
||||
except FileNotFoundError:
|
||||
logger.warning(
|
||||
f"Could not find default correction file {self.default_correction_file_x}."
|
||||
)
|
||||
logger.warning("Not applying any correction.")
|
||||
try:
|
||||
self.read_additional_correction_y(self.default_correction_file)
|
||||
logger.info(f"Applying default y correction from {self.default_correction_file}")
|
||||
except FileNotFoundError:
|
||||
logger.warning(
|
||||
f"Could not find default correction file {self.default_correction_file}."
|
||||
)
|
||||
logger.warning("Not applying any correction.")
|
||||
|
||||
def reset_tomo_alignment_fit(self):
|
||||
self.client.delete_global_var("tomo_alignment_fit")
|
||||
|
||||
def read_alignment_offset(
|
||||
self,
|
||||
dir_path=os.path.expanduser("~/Data10/specES1/internal/"),
|
||||
setup="omny",
|
||||
use_vertical_default_values=True,
|
||||
):
|
||||
"""
|
||||
Read the alignment offset from the given directory and set the global parameter
|
||||
tomo_alignment_fit.
|
||||
|
||||
Args:
|
||||
dir_path (str, optional): The directory to read the alignment offset from. Defaults to os.path.expanduser("~/Data10/specES1/internal/").
|
||||
"""
|
||||
tomo_alignment_fit = np.zeros((2, 5))
|
||||
with open(os.path.join(dir_path, "ptychotomoalign_Ax.txt"), "r") as file:
|
||||
tomo_alignment_fit[0][0] = file.readline()
|
||||
|
||||
with open(os.path.join(dir_path, "ptychotomoalign_Bx.txt"), "r") as file:
|
||||
tomo_alignment_fit[0][1] = file.readline()
|
||||
|
||||
with open(os.path.join(dir_path, "ptychotomoalign_Cx.txt"), "r") as file:
|
||||
tomo_alignment_fit[0][2] = file.readline()
|
||||
|
||||
with open(os.path.join(dir_path, "ptychotomoalign_Ay.txt"), "r") as file:
|
||||
tomo_alignment_fit[1][0] = file.readline()
|
||||
|
||||
with open(os.path.join(dir_path, "ptychotomoalign_By.txt"), "r") as file:
|
||||
tomo_alignment_fit[1][1] = file.readline()
|
||||
|
||||
with open(os.path.join(dir_path, "ptychotomoalign_Cy.txt"), "r") as file:
|
||||
tomo_alignment_fit[1][2] = file.readline()
|
||||
|
||||
with open(os.path.join(dir_path, "ptychotomoalign_Ay3.txt"), "r") as file:
|
||||
tomo_alignment_fit[1][3] = file.readline()
|
||||
|
||||
with open(os.path.join(dir_path, "ptychotomoalign_Cy3.txt"), "r") as file:
|
||||
tomo_alignment_fit[1][4] = file.readline()
|
||||
|
||||
print("New alignment parameters loaded:")
|
||||
print(
|
||||
f"X Amplitude {tomo_alignment_fit[0][0]}, "
|
||||
f"X Phase {tomo_alignment_fit[0][1]}, "
|
||||
f"X Offset {tomo_alignment_fit[0][2]}, "
|
||||
f"Y Amplitude {tomo_alignment_fit[1][0]}, "
|
||||
f"Y Phase {tomo_alignment_fit[1][1]}, "
|
||||
f"Y Offset {tomo_alignment_fit[1][2]}, "
|
||||
f"Y 3rd Order Amplitude {tomo_alignment_fit[1][3]}, "
|
||||
f"Y 3rd Order Phase {tomo_alignment_fit[1][4]} ."
|
||||
)
|
||||
|
||||
if use_vertical_default_values:
|
||||
print(
|
||||
f"Using default values for vertical alignment for setup {setup}. Optional: use_vertical_default_values=False"
|
||||
)
|
||||
if setup == "flomni":
|
||||
tomo_alignment_fit[1][0] = 0
|
||||
tomo_alignment_fit[1][1] = 0
|
||||
tomo_alignment_fit[1][2] = 0
|
||||
tomo_alignment_fit[1][3] = 0
|
||||
tomo_alignment_fit[1][4] = 0
|
||||
elif setup == "omny":
|
||||
tomo_alignment_fit[1][0] = 2.588628
|
||||
tomo_alignment_fit[1][1] = -2.385422
|
||||
tomo_alignment_fit[1][2] = 0
|
||||
tomo_alignment_fit[1][3] = 1.010583
|
||||
tomo_alignment_fit[1][4] = -1.359157
|
||||
|
||||
print("Follwing parameters will be used:")
|
||||
print(
|
||||
f"X Amplitude {tomo_alignment_fit[0][0]}, "
|
||||
f"X Phase {tomo_alignment_fit[0][1]}, "
|
||||
f"X Offset {tomo_alignment_fit[0][2]}, "
|
||||
f"Y Amplitude {tomo_alignment_fit[1][0]}, "
|
||||
f"Y Phase {tomo_alignment_fit[1][1]}, "
|
||||
f"Y Offset {tomo_alignment_fit[1][2]}, "
|
||||
f"Y 3rd Order Amplitude {tomo_alignment_fit[1][3]}, "
|
||||
f"Y 3rd Order Phase {tomo_alignment_fit[1][4]} ."
|
||||
)
|
||||
|
||||
self.client.set_global_var("tomo_alignment_fit", tomo_alignment_fit.tolist())
|
||||
# x amp, phase, offset, y amp, phase, offset, 3rd order amp, 3rd order phase
|
||||
# 0 0 0 1 0 2 1 0 1 1 1 2 1 3 1 4
|
||||
|
||||
def get_alignment_offset(self, angle: float):
|
||||
"""
|
||||
Compute the alignment offset for the given angle.
|
||||
|
||||
Args:
|
||||
angle (float): The angle to compute the alignment offset for.
|
||||
|
||||
Returns:
|
||||
tuple: The alignment offset in x, y and z direction.
|
||||
"""
|
||||
tomo_alignment_fit = self.client.get_global_var("tomo_alignment_fit")
|
||||
if tomo_alignment_fit is None:
|
||||
print("Not applying any alignment offsets. No tomo alignment fit data available.\n")
|
||||
return (0, 0, 0)
|
||||
|
||||
# x amp, phase, offset, y amp, phase, offset
|
||||
# 0 0 0 1 0 2 1 0 1 1 1 2
|
||||
correction_x = (
|
||||
tomo_alignment_fit[0][0] * np.sin(np.radians(angle) + tomo_alignment_fit[0][1])
|
||||
+ tomo_alignment_fit[0][2]
|
||||
)
|
||||
correction_y = (
|
||||
tomo_alignment_fit[1][0] * np.sin(np.radians(angle) + tomo_alignment_fit[1][1])
|
||||
+ tomo_alignment_fit[1][2]
|
||||
+ tomo_alignment_fit[1][3] * np.sin(3 * np.radians(angle) + tomo_alignment_fit[1][4])
|
||||
)
|
||||
correction_z = tomo_alignment_fit[0][0] * np.sin(
|
||||
np.radians(angle + 90) + tomo_alignment_fit[0][1]
|
||||
)
|
||||
|
||||
print(
|
||||
f"Alignment offset x {correction_x}, y {correction_y}, z {correction_z} for angle"
|
||||
f" {angle}\n"
|
||||
)
|
||||
return (correction_x, correction_y, correction_z)
|
||||
|
||||
def _read_correction_file(self, correction_file: str):
|
||||
with open(correction_file, "r") as f:
|
||||
num_elements = f.readline()
|
||||
int_num_elements = int(num_elements.split(" ")[2])
|
||||
corr_pos = []
|
||||
corr_angle = []
|
||||
for j in range(int_num_elements * 2):
|
||||
line = f.readline()
|
||||
value = line.split(" ")[2]
|
||||
name = line.split(" ")[0].split("[")[0]
|
||||
if name == "corr_pos":
|
||||
corr_pos.append(float(value))
|
||||
elif name == "corr_angle":
|
||||
corr_angle.append(float(value))
|
||||
print(
|
||||
f"Loading default mirror correction from file {correction_file} containing {int_num_elements} elements."
|
||||
)
|
||||
# print(corr_pos)
|
||||
return corr_pos, corr_angle
|
||||
|
||||
def read_additional_correction_x(self, correction_file: str):
|
||||
self.corr_pos_x, self.corr_angle_x = self._read_correction_file(correction_file)
|
||||
|
||||
def read_additional_correction_y(self, correction_file: str):
|
||||
self.corr_pos_y, self.corr_angle_y = self._read_correction_file(correction_file)
|
||||
|
||||
def read_additional_correction_y_2(self, correction_file: str):
|
||||
self.corr_pos_y_2, self.corr_angle_y_2 = self._read_correction_file(correction_file)
|
||||
|
||||
def compute_additional_correction_x(self, angle):
|
||||
return self._compute_additional_correction(angle, iteration="x1")
|
||||
|
||||
def compute_additional_correction_y(self, angle):
|
||||
return self._compute_additional_correction(angle, iteration="y1")
|
||||
|
||||
def compute_additional_correction_y_2(self, angle):
|
||||
return self._compute_additional_correction(angle, iteration="y2")
|
||||
|
||||
def _compute_additional_correction(self, angle, iteration="y1"):
|
||||
if iteration == "x1":
|
||||
corr_pos = self.corr_pos_x
|
||||
corr_angle = self.corr_angle_x
|
||||
elif iteration == "y1":
|
||||
corr_pos = self.corr_pos_y
|
||||
corr_angle = self.corr_angle_y
|
||||
elif iteration == "y2":
|
||||
corr_pos = self.corr_pos_y_2
|
||||
corr_angle = self.corr_angle_y_2
|
||||
if not corr_pos:
|
||||
print(f"Not applying any additional correction {iteration}. No data available.\n")
|
||||
return 0
|
||||
|
||||
# find index of closest angle
|
||||
for j, _ in enumerate(corr_pos):
|
||||
newangledelta = np.fabs(corr_angle[j] - angle)
|
||||
if j == 0:
|
||||
angledelta = newangledelta
|
||||
additional_correction_shift = corr_pos[j]
|
||||
continue
|
||||
|
||||
if newangledelta < angledelta:
|
||||
additional_correction_shift = corr_pos[j]
|
||||
angledelta = newangledelta
|
||||
|
||||
if additional_correction_shift == 0 and angle > corr_angle[-1]:
|
||||
additional_correction_shift = corr_pos[-1]
|
||||
print(f"Additional correction shift {iteration}: {additional_correction_shift}")
|
||||
return additional_correction_shift
|
||||
153
csaxs_bec/bec_ipython_client/plugins/omny/omny_general_tools.py
Normal file
153
csaxs_bec/bec_ipython_client/plugins/omny/omny_general_tools.py
Normal file
@@ -0,0 +1,153 @@
|
||||
import time
|
||||
import numpy as np
|
||||
import sys
|
||||
import termios
|
||||
import tty
|
||||
import fcntl
|
||||
import os
|
||||
import builtins
|
||||
|
||||
from rich import box
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
# from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_get, epics_put, fshopen, fshclose
|
||||
|
||||
if builtins.__dict__.get("bec") is not None:
|
||||
bec = builtins.__dict__.get("bec")
|
||||
dev = builtins.__dict__.get("dev")
|
||||
umv = builtins.__dict__.get("umv")
|
||||
umvr = builtins.__dict__.get("umvr")
|
||||
|
||||
|
||||
class OMNYToolsError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class OMNYTools:
|
||||
|
||||
HEADER = "\033[95m"
|
||||
OKBLUE = "\033[94m"
|
||||
OKCYAN = "\033[96m"
|
||||
OKGREEN = "\033[92m"
|
||||
WARNING = "\033[93m"
|
||||
FAIL = "\033[91m"
|
||||
ENDC = "\033[0m"
|
||||
BOLD = "\033[1m"
|
||||
UNDERLINE = "\033[4m"
|
||||
|
||||
def __init__(self, client) -> None:
|
||||
self.client = client
|
||||
|
||||
@staticmethod
|
||||
def _get_user_param_safe(device, var):
|
||||
param = dev[device].user_parameter
|
||||
if not param or param.get(var) is None:
|
||||
raise ValueError(f"Device {device} has no user parameter definition for {var}.")
|
||||
return param.get(var)
|
||||
|
||||
def printgreen(self, string: str):
|
||||
print(self.OKGREEN + string + self.ENDC)
|
||||
|
||||
def printgreenbold(self, string: str):
|
||||
print(self.BOLD + self.OKGREEN + string + self.ENDC)
|
||||
|
||||
def yesno(self, message: str, default="none", autoconfirm=0) -> bool:
|
||||
if autoconfirm and default == "y":
|
||||
self.printgreen(message + " Automatically confirming default: yes")
|
||||
return True
|
||||
elif autoconfirm and default == "n":
|
||||
self.printgreen(message + " Automatically confirming default: no")
|
||||
return False
|
||||
if default == "y":
|
||||
message_ending = " [Y]/n? "
|
||||
elif default == "n":
|
||||
message_ending = " y/[N]? "
|
||||
else:
|
||||
message_ending = " y/n? "
|
||||
while True:
|
||||
user_input = input(self.OKBLUE + message + message_ending + self.ENDC)
|
||||
if (
|
||||
user_input == "Y" or user_input == "y" or user_input == "yes" or user_input == "Yes"
|
||||
) or (default == "y" and user_input == ""):
|
||||
return True
|
||||
if (
|
||||
user_input == "N" or user_input == "n" or user_input == "no" or user_input == "No"
|
||||
) or (default == "n" and user_input == ""):
|
||||
return False
|
||||
else:
|
||||
print("Please expicitely confirm y or n.")
|
||||
|
||||
def tweak_cursor(
|
||||
self, dev1, step1: float, dev2="none", step2: float = "0", special_command="none"
|
||||
):
|
||||
if dev1 not in dev.enabled_devices:
|
||||
print(f"Device 1 {dev} is not in enabled devices.")
|
||||
return
|
||||
if dev2 not in dev.enabled_devices and dev2 != "none":
|
||||
print(f"Device 2 {dev} is not in enabled devices.")
|
||||
return
|
||||
# Save the current terminal settings
|
||||
fd = sys.stdin.fileno()
|
||||
old_term = termios.tcgetattr(fd)
|
||||
try:
|
||||
# Set the terminal to raw mode to capture single key presses
|
||||
tty.setraw(fd)
|
||||
# Set stdin to non-blocking mode
|
||||
old_flags = fcntl.fcntl(fd, fcntl.F_GETFL)
|
||||
fcntl.fcntl(fd, fcntl.F_SETFL, old_flags | os.O_NONBLOCK)
|
||||
print("Tweak Cursor." + self.BOLD + self.OKBLUE + "Press (q) to quit!\r" + self.ENDC)
|
||||
while True:
|
||||
try:
|
||||
# Read single character input
|
||||
key = sys.stdin.read(1)
|
||||
if key == "q":
|
||||
print("\n\rExiting tweak mode\r")
|
||||
break
|
||||
elif key == "\x1b": # Escape sequences for arrow keys
|
||||
next1, next2 = sys.stdin.read(2)
|
||||
if next1 == "[":
|
||||
if next2 == "A":
|
||||
# print("up")
|
||||
if dev2 != "none":
|
||||
umvr(dev2, step2)
|
||||
if special_command != "none":
|
||||
special_command()
|
||||
elif next2 == "B":
|
||||
# print(" down")
|
||||
if dev2 != "none":
|
||||
umvr(dev2, -step2)
|
||||
if special_command != "none":
|
||||
special_command()
|
||||
elif next2 == "C":
|
||||
# print("right")
|
||||
umvr(dev1, step1)
|
||||
if special_command != "none":
|
||||
special_command()
|
||||
elif next2 == "D":
|
||||
# print("left")
|
||||
umvr(dev1, -step1)
|
||||
if special_command != "none":
|
||||
special_command()
|
||||
|
||||
elif key == "+":
|
||||
step1 = step1 * 2
|
||||
if dev2 != "none":
|
||||
step2 = step2 * 2
|
||||
print(f"\rDouble step size. New step size: {step1}, {step2}\r")
|
||||
elif key == "-":
|
||||
step1 = step1 / 2
|
||||
if dev2 != "none":
|
||||
step2 = step2 / 2
|
||||
print(f"\rHalf step size. New step size: {step1}, {step2}\r")
|
||||
except IOError:
|
||||
# No input available, keep looping
|
||||
pass
|
||||
|
||||
# Sleep for a short period to avoid high CPU usage
|
||||
time.sleep(0.02)
|
||||
|
||||
finally:
|
||||
# Restore the terminal to its original state
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old_term)
|
||||
fcntl.fcntl(fd, fcntl.F_SETFL, old_flags)
|
||||
384
csaxs_bec/bec_ipython_client/plugins/omny/omny_optics_mixin.py
Normal file
384
csaxs_bec/bec_ipython_client/plugins/omny/omny_optics_mixin.py
Normal file
@@ -0,0 +1,384 @@
|
||||
import time
|
||||
import numpy as np
|
||||
|
||||
from rich import box
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_put, fshclose
|
||||
|
||||
|
||||
class OMNYError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class OMNYOpticsMixin:
|
||||
@staticmethod
|
||||
def _get_user_param_safe(device, var):
|
||||
param = dev[device].user_parameter
|
||||
if not param or param.get(var) is None:
|
||||
raise ValueError(f"Device {device} has no user parameter definition for {var}.")
|
||||
return param.get(var)
|
||||
|
||||
def ooptics_in(self):
|
||||
self.ofzp_in()
|
||||
# ocs_in
|
||||
self.oosa_in()
|
||||
|
||||
if "rtx" in dev and dev.rtx.enabled:
|
||||
dev.rtx.controller.feedback_enable()
|
||||
|
||||
self.align.update_frame()
|
||||
|
||||
user_input = input(
|
||||
"Is the direct beam gone on the xray eye? Do you see the cone of the FZP?"
|
||||
)
|
||||
if user_input == "y":
|
||||
printf("Next oeye_out...\n")
|
||||
else:
|
||||
raise OMNYError("Failed to properly move in the Xray optics")
|
||||
|
||||
def _oeyey_mv(self, position):
|
||||
# direction dependent speeds
|
||||
if dev.oeyez.get().readback < position:
|
||||
dev.oeyez.controller.socket_put_confirmed("axspeed[7]=15000")
|
||||
else:
|
||||
dev.oeyez.controller.socket_put_confirmed("axspeed[7]=10000")
|
||||
umv(dev.oeyey, position)
|
||||
dev.oeyez.controller.socket_put_confirmed("axspeed[7]=10000")
|
||||
|
||||
def oeye_out(self):
|
||||
fshclose()
|
||||
if self.OMNYTools.yesno("Did you move in the optics?"):
|
||||
umv(dev.oeyez, -2)
|
||||
self._oeyey_mv(-60.3)
|
||||
# free camera
|
||||
epics_put("XOMNYI-XEYE-ACQ:0", 2)
|
||||
else:
|
||||
raise OMNYError("The optics were not moved in. Please do so prior to eyey_out")
|
||||
self.OMNYTools.printgreen("Oeye is out.")
|
||||
|
||||
def oeye_cam_in(self):
|
||||
if dev.oeyez.get().readback < -80:
|
||||
umv(dev.oeyez, -50)
|
||||
|
||||
if np.fabs(dev.oeyey.get().readback + 4.8) > 0.1:
|
||||
self._oeyey_mv(-4.8)
|
||||
|
||||
if np.fabs(dev.oeyez.get().readback + 2) > 0.1 or np.fabs(dev.oeyex.get().readback) > 0.1:
|
||||
umv(dev.oeyez, -2, dev.oeyex, 0)
|
||||
# if still too close in z -- safety check
|
||||
if np.fabs(dev.oeyez.get().readback + 2) > 0.1:
|
||||
raise OMNYError("The oeye is too close in z for transfer. ERROR! Aborting.")
|
||||
self.OMNYTools.printgreen("Oeye is at cam position.")
|
||||
|
||||
def _oeye_xray_is_in(self) -> bool:
|
||||
omny_oeye_xray_inx = self._get_user_param_safe("oeyex", "xray_in")
|
||||
omny_oeye_xray_iny = self._get_user_param_safe("oeyey", "xray_in")
|
||||
omny_oeye_currentx = dev.oeyex.get().readback
|
||||
omny_oeye_currenty = dev.oeyey.get().readback
|
||||
|
||||
if (
|
||||
np.fabs(omny_oeye_currentx - omny_oeye_xray_inx) < 0.1
|
||||
and np.fabs(omny_oeye_currenty - omny_oeye_xray_iny) < 0.1
|
||||
):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def oeye_xray_in(self):
|
||||
if self._oeye_xray_is_in():
|
||||
pass
|
||||
else:
|
||||
# todo
|
||||
# self._otransfer_gripper_safe_xray_in_operation()
|
||||
# if(!_oshield_is_ST_closed())
|
||||
# {
|
||||
# printf("The shield of the sample stage is not closed. Aborting.\n")
|
||||
# exit
|
||||
# }
|
||||
omny_oeye_xray_inx = self._get_user_param_safe("oeyex", "xray_in")
|
||||
omny_oeye_xray_iny = self._get_user_param_safe("oeyey", "xray_in")
|
||||
omny_oeye_xray_inz = self._get_user_param_safe("oeyez", "xray_in")
|
||||
|
||||
self._oeyey_mv(omny_oeye_xray_iny)
|
||||
omny_oeye_currenty = dev.oeyey.get().readback
|
||||
if np.fabs(omny_oeye_currenty - omny_oeye_xray_iny) > 0.1:
|
||||
raise OMNYError("The oeye did not move up.\n")
|
||||
umv(dev.oeyex, omny_oeye_xray_inx, dev.oeyez, omny_oeye_xray_inz)
|
||||
self.OMNYTools.printgreen("Oeye is at X-ray position.")
|
||||
|
||||
# some notes for the vis microscope:
|
||||
# initial position for the vis light microscope
|
||||
# do not open the shield when the microscope is at the vis mic position
|
||||
# found eoeyx -45.13, z -84.9, y 0.64
|
||||
# for a samy position of 2.8 with delta off
|
||||
# the osa position should be in z around 7.4. in x it seems better
|
||||
# around -0.6, where potentially xrays dont pass anymore
|
||||
#
|
||||
|
||||
def _oosa_check_y(self):
|
||||
omny_oosa_currenty = dev.oosay.get().readback
|
||||
if np.fabs(omny_oosa_currenty - 0.9) > 0.05:
|
||||
umv(dev.oosay, 0.9)
|
||||
omny_oosa_currenty = dev.oosay.get().readback
|
||||
if np.fabs(omny_oosa_currenty - 0.9) > 0.05:
|
||||
raise OMNYError("oosay is not around 0.9. Aborting.")
|
||||
|
||||
def _oosa_to_move_corridor(self):
|
||||
self._oosa_check_y()
|
||||
dev.oosax.limits = [-3, 3.7] # risk collision with shield
|
||||
umv(dev.oosax, -2)
|
||||
dev.oosax.read(cached=False)
|
||||
omny_oosa_currentx = dev.oosax.get().readback
|
||||
if np.fabs(omny_oosa_currentx + 2) > 0.1:
|
||||
raise OMNYError("oosax did not reach target position. Not moving in z.\n")
|
||||
|
||||
def oosa_in(self):
|
||||
self._oosa_check_y()
|
||||
dev.oshield.read(cached=False)
|
||||
omny_oshield_current = dev.oshield.get().readback
|
||||
if omny_oshield_current < 15:
|
||||
self._oshield_ST_close()
|
||||
if self.near_field == False:
|
||||
x_in_pos = self._get_user_param_safe("oosax", "far_field_in")
|
||||
y_in_pos = self._get_user_param_safe("oosay", "far_field_in")
|
||||
z_in_pos = self._get_user_param_safe("oosaz", "far_field_in")
|
||||
print("OSA movement in far-field mode.")
|
||||
dev.oosaz.read(cached=False)
|
||||
omny_oosa_currentz = dev.oosaz.get().readback
|
||||
if omny_oosa_currentz < 6.4:
|
||||
self._oosa_to_move_corridor()
|
||||
dev.oosaz.limits = [6.4, 6.6]
|
||||
umv(dev.oosaz, z_in_pos)
|
||||
umv(dev.oosax, x_in_pos)
|
||||
umv(dev.oosay, y_in_pos)
|
||||
#### For the 30 nm FZP 220 um we use this part
|
||||
# umv oosaz 6.5
|
||||
# umv oosax 3.2453
|
||||
# umv oosay 0.386015
|
||||
|
||||
if self.near_field == True:
|
||||
x_in_pos = self._get_user_param_safe("oosax", "near_field_in")
|
||||
y_in_pos = self._get_user_param_safe("oosay", "near_field_in")
|
||||
z_in_pos = self._get_user_param_safe("oosaz", "near_field_in")
|
||||
print("OSA movement in near-field mode.")
|
||||
dev.oosaz.read(cached=False)
|
||||
omny_oosa_currentz = dev.oosaz.get().readback
|
||||
if omny_oosa_currentz > 0:
|
||||
self._oosa_to_move_corridor()
|
||||
dev.oosaz.limits = [-0.4, -0.6]
|
||||
umv(dev.oosaz, z_in_pos)
|
||||
umv(dev.oosax, x_in_pos)
|
||||
omny_osamy_current = dev.osamy.get().readback
|
||||
if omny_osamy_current < 3.25:
|
||||
umv(dev.oosay, y_in_pos)
|
||||
else:
|
||||
raise OMNYError("Failed to move oosa in. osamy position is too large.")
|
||||
|
||||
self.OMNYTools.printgreen("OSA is in.")
|
||||
|
||||
# todo
|
||||
# _omny_interferometer_align_tracking
|
||||
# rt_feedback_enable
|
||||
|
||||
def oosa_out(self):
|
||||
self._oosa_check_y()
|
||||
dev.oshield.read(cached=False)
|
||||
omny_oshield_current = dev.oshield.get().readback
|
||||
if omny_oshield_current < 15:
|
||||
self._oshield_ST_close()
|
||||
omny_oosaz_current = dev.oosaz.get().readback
|
||||
if self.near_field == False:
|
||||
print("OSA movement in far-field mode.")
|
||||
if omny_oosaz_current < 6.4:
|
||||
self._oosa_to_move_corridor()
|
||||
dev.oosaz.limits = [6.4, 6.6]
|
||||
umv(dev.oosaz, 6.5)
|
||||
umv(dev.oosax, -2)
|
||||
if self.near_field == True:
|
||||
print("OSA movement in near-field mode.")
|
||||
if omny_oosaz_current > 0:
|
||||
self._oosa_to_move_corridor()
|
||||
dev.oosaz.limits = [-0.4, -0.6]
|
||||
umv(dev.oosaz, -0.45)
|
||||
umv(dev.oosax, -2)
|
||||
# todo _omny_interferometer_align_tracking
|
||||
|
||||
self.OMNYTools.printgreen("OSA is out.")
|
||||
|
||||
def oosa_move_out_of_shield(self):
|
||||
# todo: _omnycam_samplestage
|
||||
self._oosa_check_y()
|
||||
self._oosa_to_move_corridor()
|
||||
omny_osamx_current = dev.osamx.get().readback
|
||||
if np.fabs(omny_osamx_current) > 0.2:
|
||||
umv(dev.osamx, 0)
|
||||
omny_oosaz_current = dev.oosaz.get().readback
|
||||
if omny_oosaz_current > 0.1:
|
||||
dev.oosaz.limits = [-0.1, 0.1]
|
||||
umv(dev.oosaz, 0)
|
||||
|
||||
self.OMNYTools.printgreen("OSA is out of shield.")
|
||||
|
||||
def ofzp_out(self):
|
||||
if "rtx" in dev and dev.rtx.enabled:
|
||||
dev.rtx.controller.feedback_disable()
|
||||
y_out_pos = self._get_user_param_safe("ofzpy", "out")
|
||||
if np.fabs(dev.ofzpy.get().readback - y_out_pos) > 0.02:
|
||||
umv(dev.ofzpy, y_out_pos)
|
||||
self.OMNYTools.printgreen("FZP at out position")
|
||||
|
||||
def ofzp_in(self):
|
||||
if "rtx" in dev and dev.rtx.enabled:
|
||||
dev.rtx.controller.feedback_disable()
|
||||
x_in_pos = self._get_user_param_safe("ofzpx", "in")
|
||||
y_in_pos = self._get_user_param_safe("ofzpy", "in")
|
||||
if np.fabs(dev.ofzpy.get().readback - y_in_pos) > 0.02:
|
||||
umv(dev.ofzpy, y_in_pos)
|
||||
|
||||
if np.fabs(dev.ofzpx.get().readback - x_in_pos) > 0.02:
|
||||
umv(dev.ofzpx, x_in_pos)
|
||||
self.OMNYTools.printgreen("FZP at in position")
|
||||
# 220 mu FZP at ofzpz 31.8025 for eiger probe (about 2.4 mm propagation after focus)
|
||||
# umv(dev.ofzpy, 0.7944)
|
||||
# if np.fabs(dev.ofzpx.get().readback+0.4317)>0.05:
|
||||
# umv(dev.ofzpx, -0.4317)
|
||||
# note the 220 fzp also works for near field 6.2 kev by just moving back osa and fzp
|
||||
# ofzpz 24.8 leads to a 9.5 mm propagation distance.
|
||||
# With the 220 mu FZP this gives 100 nm pixel recons
|
||||
# for the oosa macro set near_field=1
|
||||
# 170 mu FZP at 6.2 kev for large beam at ofzpz 31.8025 of about 58 mu diameter
|
||||
# 120 mu FZP at ofzpz 28.1991
|
||||
|
||||
# 250 mu FZP 60 nm at 5.65 keV
|
||||
# ofzpz 29.7 for propagation distance 2.2
|
||||
# umv ofzpx -0.4457
|
||||
# umv ofzpy 0.193630
|
||||
|
||||
# 150 um fzp, 60 nm, ofzpz 33.8 at 8.9 kev for propagation of 1.7 mm after focus
|
||||
# umv ofzpx -0.756678
|
||||
# umv ofzpy 0.193515
|
||||
|
||||
# 250 um 30 nm FZP upper right
|
||||
# small abberrations, seems to give good results in weak objects
|
||||
# ofzpx -0.609240
|
||||
# umv ofzpy 0.118265
|
||||
# 250 um 30 nm FZP lower right very aberated
|
||||
# ofzpx -0.881935
|
||||
# umv ofzpy 0.537050
|
||||
|
||||
# ofzpz 28.4027
|
||||
# 5.30 mm prop at 8.9 keV, 45 nm pixel in near field
|
||||
|
||||
# ofzpz 33.103
|
||||
# 0.6 mm prop at 8.9 kev far field 7 m flight tube at foptz
|
||||
|
||||
# ofzpz 49.4 is reachable just without interferometer swap
|
||||
# which at 6.2 keV and 250 um diam, 30 nm should gives a propagation of 0.8 after focus
|
||||
# and a beam size of 6 microns diamter
|
||||
###coordinates 30 nm FZP for comparing them
|
||||
# not sure if that is really correct
|
||||
# FZP 1 - FZP 2
|
||||
# FZP 5
|
||||
# FZP 4 - FZP 1
|
||||
|
||||
# FZP
|
||||
##upper right
|
||||
# umv ofzpx -0.6154 ofzpy 0.1183
|
||||
# umv ocsx -0.6070 ocsy 0.0540
|
||||
|
||||
# lower right
|
||||
# umv ofzpx -0.8341 ofzpy 0.5683
|
||||
# umv ocsx -0.3880 ocsy -0.3960
|
||||
|
||||
# lower left
|
||||
# umv ofzpx -0.3876 ofzpy 0.7902
|
||||
# umv ocsx -0.8380 ocsy -0.6180
|
||||
|
||||
# upper left
|
||||
# umv ofzpx -0.1678 ofzpy 0.3403
|
||||
# umv ocsx -1.0550 ocsy -0.1680
|
||||
|
||||
def ofzp_info(self, mokev_val=-1, ofzpz_val=-1):
|
||||
print(f"{ofzpz_val}")
|
||||
|
||||
if mokev_val == -1:
|
||||
try:
|
||||
mokev_val = dev.mokev.readback.get()
|
||||
except:
|
||||
print(
|
||||
"Device mokev does not exist. You can specify the energy in keV as an argument instead."
|
||||
)
|
||||
return
|
||||
if ofzpz_val == -1:
|
||||
ofzpz_val = dev.ofzpz.readback.get()
|
||||
distance = 66 + 2.4 + 31.8025 - ofzpz_val
|
||||
print(
|
||||
f"\nThe sample is in a distance of \033[1m{distance:.1f} mm\033[0m from the 60 nm FZP.\n"
|
||||
)
|
||||
print(f"At the current energy of {mokev_val:.4f} keV we have following options:\n")
|
||||
|
||||
diameters = [80e-6, 100e-6, 120e-6, 150e-6, 170e-6, 200e-6, 220e-6, 250e-6]
|
||||
|
||||
console = Console()
|
||||
table = Table(title="Outermost zone width \033[1m60 nm\033[0m", box=box.SQUARE)
|
||||
table.add_column("Diameter", justify="center")
|
||||
table.add_column("Focal distance", justify="center")
|
||||
table.add_column("Current beam size", justify="center")
|
||||
|
||||
wavelength = 1.2398e-9 / mokev_val
|
||||
|
||||
for diameter in diameters:
|
||||
outermost_zonewidth = 60e-9
|
||||
focal_distance = diameter * outermost_zonewidth / wavelength * 1000
|
||||
beam_size = -diameter / (focal_distance * 1000) * (focal_distance - distance) * 1e9
|
||||
table.add_row(
|
||||
f"{diameter*1e6:.2f} microns",
|
||||
f"{focal_distance:.2f} mm",
|
||||
f"{beam_size:.2f} microns",
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
|
||||
# 30 nm with additional spacer
|
||||
distance = 53.84 + 0.6 + 33.1 - ofzpz_val
|
||||
print(
|
||||
f"\nThe sample is in a distance of \033[1m{distance:.1f} mm\033[0m from the 30 nm FZP.\n"
|
||||
)
|
||||
|
||||
diameters = [150e-6, 250e-6]
|
||||
|
||||
console = Console()
|
||||
table = Table(title="Outermost zone width \033[1m30 nm\033[0m", box=box.SQUARE)
|
||||
table.add_column("Diameter", justify="center")
|
||||
table.add_column("Focal distance", justify="center")
|
||||
table.add_column("Current beam size", justify="center")
|
||||
|
||||
wavelength = 1.2398e-9 / mokev_val
|
||||
|
||||
for diameter in diameters:
|
||||
outermost_zonewidth = 30e-9
|
||||
focal_distance = diameter * outermost_zonewidth / wavelength * 1000
|
||||
beam_size = -diameter / (focal_distance * 1000) * (focal_distance - distance) * 1e9
|
||||
table.add_row(
|
||||
f"{diameter*1e6:.2f} microns",
|
||||
f"{focal_distance:.2f} mm",
|
||||
f"{beam_size:.2f} microns",
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
|
||||
print(
|
||||
"This function can be called with explicit energy and ofzpz position.\n Example: omny.ffzp_info(mokev_val=6.2, ofzpz_val=33.2)"
|
||||
)
|
||||
|
||||
# from flomni
|
||||
# oosaz_val = dev.oosaz.readback.get()
|
||||
|
||||
# print("\nOSA Information:")
|
||||
# print(f" Current fosaz {fosaz_val:.1f}")
|
||||
# print(
|
||||
# f" The OSA will collide with a normal OMNY pin at fosaz \033[1m{(33-fosaz_val):.1f}\033[0m"
|
||||
# )
|
||||
# print(f" Remaining space: \033[1m{-fosaz_val+(33-foptz_val):.1f}\033[0m")
|
||||
241
csaxs_bec/bec_ipython_client/plugins/omny/omny_rt.py
Normal file
241
csaxs_bec/bec_ipython_client/plugins/omny/omny_rt.py
Normal file
@@ -0,0 +1,241 @@
|
||||
import time
|
||||
import numpy as np
|
||||
import sys
|
||||
import termios
|
||||
import tty
|
||||
import fcntl
|
||||
import os
|
||||
|
||||
|
||||
from rich import box
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_get, epics_put, fshopen, fshclose
|
||||
|
||||
|
||||
class OMNY_rt_clientError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class OMNY_rt_client:
|
||||
def __init__(self):
|
||||
self.mirror_channel = -1
|
||||
self.mirror_amplitutde_increase = 0
|
||||
self.mirror_parameters = {}
|
||||
for j in range(1, 9):
|
||||
self.mirror_parameters[j] = dev.rtx.controller.get_mirror_parameters(j)
|
||||
|
||||
@staticmethod
|
||||
def _get_user_param_safe(device, var):
|
||||
param = dev[device].user_parameter
|
||||
if not param or param.get(var) is None:
|
||||
raise OMNY_rt_clientError(
|
||||
f"Device {device} has no user parameter definition for {var}."
|
||||
)
|
||||
return param.get(var)
|
||||
|
||||
def _omny_interferometer_openloop_steps(self, channel, steps, amplitude):
|
||||
dev.rtx.controller._omny_interferometer_openloop_steps(channel, steps, amplitude)
|
||||
|
||||
def interferometer_tweaking(self):
|
||||
self._tweak_interferometer()
|
||||
|
||||
def _tweak_interferometer(self):
|
||||
self.mirror_channel = -1
|
||||
|
||||
# Save the current terminal settings
|
||||
fd = sys.stdin.fileno()
|
||||
old_term = termios.tcgetattr(fd)
|
||||
|
||||
print("Ready to tweak the interferometer. Press q to quit.")
|
||||
print("The arrows adjust directions.")
|
||||
print("Numbers select the mirror aligner.")
|
||||
|
||||
try:
|
||||
# Set the terminal to raw mode to capture single key presses
|
||||
tty.setraw(fd)
|
||||
# Set stdin to non-blocking mode
|
||||
old_flags = fcntl.fcntl(fd, fcntl.F_GETFL)
|
||||
fcntl.fcntl(fd, fcntl.F_SETFL, old_flags | os.O_NONBLOCK)
|
||||
opt_mirrorname = "none"
|
||||
max = 0
|
||||
while True:
|
||||
try:
|
||||
# Read single character input
|
||||
key = sys.stdin.read(1)
|
||||
|
||||
if key == "q":
|
||||
self.mirror_amplitutde_increase = 0
|
||||
self.mirror_channel = -1
|
||||
print("\n\rExiting tweak mode\r")
|
||||
break
|
||||
elif key == "\x1b": # Escape sequences for arrow keys
|
||||
next1, next2 = sys.stdin.read(2)
|
||||
|
||||
if next1 == "[":
|
||||
printit = True
|
||||
if next2 == "A":
|
||||
# print("up")
|
||||
if self.mirror_channel != -1:
|
||||
self._omny_interferometer_openloop_steps(
|
||||
4,
|
||||
-self.mirror_parameters[self.mirror_channel][
|
||||
"opt_steps2_neg"
|
||||
],
|
||||
self.mirror_parameters[self.mirror_channel][
|
||||
"opt_amplitude2_neg"
|
||||
]
|
||||
+ self.mirror_amplitutde_increase,
|
||||
)
|
||||
elif next2 == "B":
|
||||
# print(" down")
|
||||
if self.mirror_channel != -1:
|
||||
self._omny_interferometer_openloop_steps(
|
||||
4,
|
||||
self.mirror_parameters[self.mirror_channel][
|
||||
"opt_steps2_pos"
|
||||
],
|
||||
self.mirror_parameters[self.mirror_channel][
|
||||
"opt_amplitude2_pos"
|
||||
]
|
||||
+ self.mirror_amplitutde_increase,
|
||||
)
|
||||
elif next2 == "C":
|
||||
# print("right")
|
||||
if self.mirror_channel != -1:
|
||||
self._omny_interferometer_openloop_steps(
|
||||
3,
|
||||
-self.mirror_parameters[self.mirror_channel][
|
||||
"opt_steps1_neg"
|
||||
],
|
||||
self.mirror_parameters[self.mirror_channel][
|
||||
"opt_amplitude1_neg"
|
||||
]
|
||||
+ self.mirror_amplitutde_increase,
|
||||
)
|
||||
elif next2 == "D":
|
||||
# print("left")
|
||||
if self.mirror_channel != -1:
|
||||
self._omny_interferometer_openloop_steps(
|
||||
3,
|
||||
self.mirror_parameters[self.mirror_channel][
|
||||
"opt_steps1_pos"
|
||||
],
|
||||
self.mirror_parameters[self.mirror_channel][
|
||||
"opt_amplitude1_pos"
|
||||
]
|
||||
+ self.mirror_amplitutde_increase,
|
||||
)
|
||||
elif key.isdigit() and 1 <= int(key) <= 8:
|
||||
self.mirror_channel = int(key)
|
||||
opt_mirrorname = self.mirror_parameters[self.mirror_channel][
|
||||
"opt_mirrorname"
|
||||
]
|
||||
autostop = self.mirror_parameters[self.mirror_channel]["opt_signal_stop"]
|
||||
averaging_time = self.mirror_parameters[self.mirror_channel][
|
||||
"opt_averaging_time"
|
||||
]
|
||||
print(
|
||||
f"\nSelected mirror channel {self.mirror_channel}: {opt_mirrorname}. Autostop {autostop}. Signal averaging time: {averaging_time}\r"
|
||||
)
|
||||
if int(key) == 6:
|
||||
dev.rtx.controller.laser_tracker_on()
|
||||
dev.rtx.controller._omny_interferometer_switch_channel(self.mirror_channel)
|
||||
max = 0
|
||||
printit = True
|
||||
elif key == "+":
|
||||
print("\nIncreasing voltage amplitudes by 100.\r")
|
||||
self.mirror_amplitutde_increase += 100
|
||||
elif key == "-":
|
||||
print("\nDecreasing voltage amplitudes by 100.\r")
|
||||
self.mirror_amplitutde_increase -= 100
|
||||
elif key == "a":
|
||||
if self.mirror_channel != -1:
|
||||
dev.rtx.controller._omny_interferometer_optimize(
|
||||
mirror_channel=self.mirror_channel, channel=3
|
||||
)
|
||||
dev.rtx.controller._omny_interferometer_optimize(
|
||||
mirror_channel=self.mirror_channel, channel=4
|
||||
)
|
||||
dev.rtx.controller._omny_interferometer_optimize(
|
||||
mirror_channel=self.mirror_channel, channel=3
|
||||
)
|
||||
dev.rtx.controller._omny_interferometer_optimize(
|
||||
mirror_channel=self.mirror_channel, channel=4
|
||||
)
|
||||
if self.mirror_channel != -1 and printit:
|
||||
printit = False
|
||||
signal = dev.rtx.controller._omny_interferometer_get_signalsample(
|
||||
self.mirror_parameters[self.mirror_channel]["opt_signalchannel"],
|
||||
self.mirror_parameters[self.mirror_channel]["opt_averaging_time"],
|
||||
)
|
||||
if signal > max:
|
||||
max = signal
|
||||
info_str = f"Channel {self.mirror_channel}, {opt_mirrorname}, Current signal: {signal:.0f}"
|
||||
filling = " " * (50 - len(info_str))
|
||||
# Calculate the number of filled and unfilled segments
|
||||
length = 30
|
||||
percentage = signal / max
|
||||
filled_length = int(length * percentage)
|
||||
unfilled_length = length - filled_length
|
||||
bar = "#" * filled_length + "-" * unfilled_length
|
||||
print(info_str + filling + "0 " + bar + f" {max:.0f} (q)uit\r", end="")
|
||||
|
||||
except IOError:
|
||||
# No input available, keep looping
|
||||
pass
|
||||
|
||||
# Sleep for a short period to avoid high CPU usage
|
||||
time.sleep(0.02)
|
||||
|
||||
finally:
|
||||
# Restore the terminal to its original state
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old_term)
|
||||
fcntl.fcntl(fd, fcntl.F_SETFL, old_flags)
|
||||
dev.rtx.controller._omny_interferometer_switch_alloff()
|
||||
self.mirror_channel = -1
|
||||
self.mirror_amplitutde_increase = 0
|
||||
dev.rtx.controller.show_signal_strength_interferometer()
|
||||
|
||||
def show_signal_strength_interferometer(self):
|
||||
dev.rtx.controller.show_signal_strength_interferometer()
|
||||
|
||||
def omny_interferometer_align_incoupling_angle(self):
|
||||
dev.rtx.controller.omny_interferometer_align_incoupling_angle()
|
||||
|
||||
def interferometer_tweak_otrack(self):
|
||||
self.OMNYTools.tweak_cursor(
|
||||
dev.otrackz,
|
||||
0.1,
|
||||
dev.otracky,
|
||||
0.1,
|
||||
special_command=dev.rtx.controller.laser_tracker_print_intensity_for_otrack_tweaking,
|
||||
)
|
||||
|
||||
def feedback_enable_with_reset(self):
|
||||
dev.rtx.controller.feedback_enable_with_reset()
|
||||
|
||||
def feedback_disable(self):
|
||||
dev.rtx.controller.feedback_disable()
|
||||
|
||||
def feedback_status(self):
|
||||
if dev.rtx.controller.feedback_is_running():
|
||||
print("Feedback is running.")
|
||||
else:
|
||||
print("Feedback is NOT running.")
|
||||
|
||||
def laser_tracker_on(self):
|
||||
dev.rtx.controller.laser_tracker_on()
|
||||
|
||||
def laser_tracker_off(self):
|
||||
dev.rtx.controller.laser_tracker_off()
|
||||
|
||||
def laser_tracker_show_all(self):
|
||||
dev.rtx.controller.laser_tracker_show_all()
|
||||
|
||||
def omny_interferometer_align_tracking(self):
|
||||
dev.rtx.controller.omny_interferometer_align_tracking()
|
||||
|
||||
def laser_tracker_check_and_wait_for_signalstrength(self):
|
||||
dev.rtx.controller.laser_tracker_check_and_wait_for_signalstrength()
|
||||
File diff suppressed because it is too large
Load Diff
235
csaxs_bec/bec_ipython_client/plugins/omny/x_ray_eye_align.py
Normal file
235
csaxs_bec/bec_ipython_client/plugins/omny/x_ray_eye_align.py
Normal file
@@ -0,0 +1,235 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import builtins
|
||||
import os
|
||||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bec_lib import bec_logger
|
||||
|
||||
from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_get, epics_put, fshopen
|
||||
|
||||
logger = bec_logger.logger
|
||||
# import builtins to avoid linter errors
|
||||
bec = builtins.__dict__.get("bec")
|
||||
dev = builtins.__dict__.get("dev")
|
||||
umv = builtins.__dict__.get("umv")
|
||||
umvr = builtins.__dict__.get("umvr")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bec_ipython_client.plugins.omny import OMNY
|
||||
|
||||
|
||||
class XrayEyeAlign:
|
||||
# pixel calibration, multiply to get mm
|
||||
PIXEL_CALIBRATION = 0.2 / 218 # .2 with binning
|
||||
|
||||
def __init__(self, client, omny: OMNY) -> None:
|
||||
self.client = client
|
||||
self.omny = omny
|
||||
self.device_manager = client.device_manager
|
||||
self.scans = client.scans
|
||||
self.alignment_values = {}
|
||||
self.omny.reset_correction()
|
||||
self.omny.reset_tomo_alignment_fit()
|
||||
|
||||
def _reset_init_values(self):
|
||||
self.shift_xy = [0, 0]
|
||||
self._xray_fov_xy = [0, 0]
|
||||
|
||||
def save_frame(self):
|
||||
epics_put("XOMNYI-XEYE-SAVFRAME:0", 1)
|
||||
|
||||
def update_frame(self):
|
||||
epics_put("XOMNYI-XEYE-ACQDONE:0", 0)
|
||||
# start live
|
||||
epics_put("XOMNYI-XEYE-ACQ:0", 1)
|
||||
# wait for start live
|
||||
while epics_get("XOMNYI-XEYE-ACQDONE:0") == 0:
|
||||
time.sleep(0.5)
|
||||
print("waiting for live view to start...")
|
||||
fshopen()
|
||||
|
||||
epics_put("XOMNYI-XEYE-ACQDONE:0", 0)
|
||||
|
||||
while epics_get("XOMNYI-XEYE-ACQDONE:0") == 0:
|
||||
print("waiting for new frame...")
|
||||
time.sleep(0.5)
|
||||
|
||||
time.sleep(0.5)
|
||||
# stop live view
|
||||
epics_put("XOMNYI-XEYE-ACQ:0", 0)
|
||||
time.sleep(1)
|
||||
# fshclose
|
||||
print("got new frame")
|
||||
|
||||
def tomo_rotate(self, val: float):
|
||||
# pylint: disable=undefined-variable
|
||||
umv(self.device_manager.devices.osamroy, val)
|
||||
|
||||
def get_tomo_angle(self):
|
||||
return self.device_manager.devices.osamroy.readback.get()
|
||||
|
||||
def update_fov(self, k: int):
|
||||
self._xray_fov_xy[0] = max(epics_get(f"XOMNYI-XEYE-XWIDTH_X:{k}"), self._xray_fov_xy[0])
|
||||
self._xray_fov_xy[1] = max(0, self._xray_fov_xy[0])
|
||||
|
||||
@property
|
||||
def movement_buttons_enabled(self):
|
||||
return [epics_get("XOMNYI-XEYE-ENAMVX:0"), epics_get("XOMNYI-XEYE-ENAMVY:0")]
|
||||
|
||||
@movement_buttons_enabled.setter
|
||||
def movement_buttons_enabled(self, enabled: bool):
|
||||
enabled = int(enabled)
|
||||
epics_put("XOMNYI-XEYE-ENAMVX:0", enabled)
|
||||
epics_put("XOMNYI-XEYE-ENAMVY:0", enabled)
|
||||
|
||||
def send_message(self, msg: str):
|
||||
epics_put("XOMNYI-XEYE-MESSAGE:0.DESC", msg)
|
||||
|
||||
def align(self):
|
||||
# reset shift xy and fov params
|
||||
self._reset_init_values()
|
||||
|
||||
self.tomo_rotate(0)
|
||||
epics_put("XOMNYI-XEYE-ANGLE:0", 0)
|
||||
|
||||
self.omny.oeye_xray_in()
|
||||
|
||||
self.omny.feedback_enable_with_reset()
|
||||
|
||||
# disable movement buttons
|
||||
self.movement_buttons_enabled = False
|
||||
|
||||
sample_name = dev.omny_samples.get_sample_name_in_samplestage()
|
||||
epics_put("XOMNYI-XEYE-SAMPLENAME:0.DESC", sample_name)
|
||||
|
||||
# this makes sure we are in a defined state
|
||||
self.omny.feedback_disable()
|
||||
|
||||
epics_put("XOMNYI-XEYE-PIXELSIZE:0", self.PIXEL_CALIBRATION)
|
||||
|
||||
osamx_in = self.omny.OMNYTools._get_user_param_safe("osamx", "in")
|
||||
umv(dev.osamx, osamx_in - 0.35)
|
||||
|
||||
self.omny.ofzp_in()
|
||||
self.update_frame()
|
||||
|
||||
# enable submit buttons
|
||||
self.movement_buttons_enabled = False
|
||||
epics_put("XOMNYI-XEYE-SUBMIT:0", 0)
|
||||
epics_put("XOMNYI-XEYE-STEP:0", 0)
|
||||
self.send_message("Submit center value of FZP.")
|
||||
|
||||
k = 0
|
||||
while True:
|
||||
if epics_get("XOMNYI-XEYE-SUBMIT:0") == 1:
|
||||
val_x = epics_get(f"XOMNYI-XEYE-XVAL_X:{k}") / 2 * self.PIXEL_CALIBRATION # in mm
|
||||
self.alignment_values[k] = val_x
|
||||
print(f"Clicked position {k}: x {self.alignment_values[k]}")
|
||||
rtx_position = dev.rtx.readback.get() / 1000
|
||||
print(f"Current rtx position {rtx_position}")
|
||||
self.alignment_values[k] -= rtx_position
|
||||
print(f"Corrected position {k}: x {self.alignment_values[k]}")
|
||||
|
||||
if k == 0: # received center value of FZP
|
||||
self.send_message("please wait ...")
|
||||
self.movement_buttons_enabled = False
|
||||
epics_put("XOMNYI-XEYE-SUBMIT:0", -1) # disable submit button
|
||||
|
||||
self.omny.feedback_disable()
|
||||
osamx_in = self.omny.OMNYTools._get_user_param_safe("osamx", "in")
|
||||
umv(dev.osamx, osamx_in)
|
||||
|
||||
self.omny.ofzp_out()
|
||||
|
||||
self.update_frame()
|
||||
epics_put("XOMNYI-XEYE-RECBG:0", 1)
|
||||
while epics_get("XOMNYI-XEYE-RECBG:0") == 1:
|
||||
time.sleep(0.5)
|
||||
print("waiting for background frame...")
|
||||
|
||||
umv(dev.osamx, osamx_in)
|
||||
time.sleep(0.5)
|
||||
self.omny.feedback_enable_with_reset()
|
||||
|
||||
self.update_frame()
|
||||
self.send_message("Adjust sample height and submit center")
|
||||
epics_put("XOMNYI-XEYE-SUBMIT:0", 0)
|
||||
self.movement_buttons_enabled = True
|
||||
|
||||
elif 1 <= k < 5: # received sample center value at samroy 0 ... 315
|
||||
self.send_message("please wait ...")
|
||||
epics_put("XOMNYI-XEYE-SUBMIT:0", -1)
|
||||
self.movement_buttons_enabled = False
|
||||
|
||||
umv(dev.rtx, 0)
|
||||
self.tomo_rotate(k * 45)
|
||||
epics_put("XOMNYI-XEYE-ANGLE:0", self.get_tomo_angle())
|
||||
self.update_frame()
|
||||
self.send_message("Submit sample center")
|
||||
epics_put("XOMNYI-XEYE-SUBMIT:0", 0)
|
||||
epics_put("XOMNYI-XEYE-ENAMVX:0", 1)
|
||||
self.update_fov(k)
|
||||
|
||||
elif k == 5: # received sample center value at samroy 270 and done
|
||||
self.send_message("done...")
|
||||
epics_put("XOMNYI-XEYE-SUBMIT:0", -1) # disable submit button
|
||||
self.movement_buttons_enabled = False
|
||||
self.update_fov(k)
|
||||
break
|
||||
|
||||
k += 1
|
||||
epics_put("XOMNYI-XEYE-STEP:0", k)
|
||||
|
||||
_xrayeyalignmvx = epics_get("XOMNYI-XEYE-MVX:0")
|
||||
if _xrayeyalignmvx != 0:
|
||||
umvr(dev.rtx, _xrayeyalignmvx)
|
||||
print(f"Current rtx position {dev.rtx.readback.get() / 1000}")
|
||||
epics_put("XOMNYI-XEYE-MVX:0", 0)
|
||||
if k > 0:
|
||||
epics_put(f"XOMNYI-XEYE-STAGEPOSX:{k}", dev.rtx.readback.get() / 1000)
|
||||
time.sleep(3)
|
||||
self.update_frame()
|
||||
|
||||
if k < 2:
|
||||
# allow movements, store movements to calculate center
|
||||
_xrayeyalignmvy = epics_get("XOMNYI-XEYE-MVY:0")
|
||||
if _xrayeyalignmvy != 0:
|
||||
self.omny.feedback_disable()
|
||||
umvr(dev.osamy, _xrayeyalignmvy / 1000)
|
||||
time.sleep(2)
|
||||
epics_put("XOMNYI-XEYE-MVY:0", 0)
|
||||
self.omny.feedback_enable_with_reset()
|
||||
self.update_frame()
|
||||
time.sleep(0.2)
|
||||
|
||||
self.write_output()
|
||||
fovx = self._xray_fov_xy[0] * self.PIXEL_CALIBRATION * 1000 / 2
|
||||
fovy = self._xray_fov_xy[1] * self.PIXEL_CALIBRATION * 1000 / 2
|
||||
|
||||
self.tomo_rotate(0)
|
||||
|
||||
umv(dev.rtx, 0)
|
||||
|
||||
# free camera
|
||||
epics_put("XOMNYI-XEYE-ACQ:0", 2)
|
||||
|
||||
print(
|
||||
f"The largest field of view from the xrayeyealign was \nfovx = {fovx:.0f} microns, fovy"
|
||||
f" = {fovy:.0f} microns"
|
||||
)
|
||||
print("Use the matlab routine to FIT the current alignment...")
|
||||
|
||||
print("Then LOAD ALIGNMENT PARAMETERS by running omny.read_alignment_offset()\n")
|
||||
|
||||
def write_output(self):
|
||||
file = os.path.expanduser("~/Data10/specES1/internal/xrayeye_alignmentvalues")
|
||||
if not os.path.exists(file):
|
||||
os.makedirs(os.path.dirname(file), exist_ok=True)
|
||||
with open(file, "w") as alignment_values_file:
|
||||
alignment_values_file.write("angle\thorizontal\n")
|
||||
for k in range(1, 6):
|
||||
fovx_offset = self.alignment_values[0] - self.alignment_values[k]
|
||||
print(f"Writing to file new alignment: number {k}, value x {fovx_offset}")
|
||||
alignment_values_file.write(f"{(k-1)*45}\t{fovx_offset*1000}\n")
|
||||
@@ -59,10 +59,9 @@ bec._beamline_mixin._bl_info_register(SLSInfo)
|
||||
bec._beamline_mixin._bl_info_register(OperatorInfo)
|
||||
|
||||
# SETUP PROMPTS
|
||||
bec._ip.prompts.username = _session_name
|
||||
bec._ip.prompts.session_name = _session_name
|
||||
bec._ip.prompts.status = 1
|
||||
|
||||
|
||||
# REGISTER BEAMLINE CHECKS
|
||||
from bec_lib.bl_conditions import (
|
||||
FastOrbitFeedbackCondition,
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
"""
|
||||
Pre-startup script for BEC client. This script is executed before the BEC client
|
||||
is started. It can be used to add additional command line arguments.
|
||||
is started. It can be used to add additional command line arguments.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from bec_lib.service_config import ServiceConfig
|
||||
|
||||
import csaxs_bec
|
||||
|
||||
|
||||
def extend_command_line_args(parser):
|
||||
"""
|
||||
@@ -12,3 +18,15 @@ def extend_command_line_args(parser):
|
||||
parser.add_argument("--session", help="Session name", type=str, default="cSAXS")
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
# def get_config() -> ServiceConfig:
|
||||
# """
|
||||
# Create and return the ServiceConfig for the plugin repository
|
||||
# """
|
||||
# deployment_path = os.path.dirname(os.path.dirname(os.path.dirname(csaxs_bec.__file__)))
|
||||
# files = os.listdir(deployment_path)
|
||||
# if "bec_config.yaml" in files:
|
||||
# return ServiceConfig(config_path=os.path.join(deployment_path, "bec_config.yaml"))
|
||||
# else:
|
||||
# return ServiceConfig(redis={"host": "localhost", "port": 6379})
|
||||
|
||||
63
csaxs_bec/bec_widgets/auto_updates/auto_updates.py
Normal file
63
csaxs_bec/bec_widgets/auto_updates/auto_updates.py
Normal file
@@ -0,0 +1,63 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bec_widgets.widgets.containers.auto_update.auto_updates import AutoUpdates
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from bec_lib.messages import ScanStatusMessage
|
||||
|
||||
|
||||
class cSAXSUpdate(AutoUpdates):
|
||||
|
||||
#######################################################################
|
||||
################# GUI Callbacks #######################################
|
||||
#######################################################################
|
||||
|
||||
def on_start(self) -> None:
|
||||
"""
|
||||
Procedure to run when the auto updates are enabled.
|
||||
"""
|
||||
self.start_default_dock()
|
||||
|
||||
def on_stop(self) -> None:
|
||||
"""
|
||||
Procedure to run when the auto updates are disabled.
|
||||
"""
|
||||
|
||||
def on_scan_open(self, msg: ScanStatusMessage) -> None:
|
||||
"""
|
||||
Procedure to run when a scan starts.
|
||||
|
||||
Args:
|
||||
msg (ScanStatusMessage): The scan status message.
|
||||
"""
|
||||
if msg.scan_name == "line_scan" and msg.scan_report_devices:
|
||||
return self.simple_line_scan(msg)
|
||||
if msg.scan_name == "grid_scan" and msg.scan_report_devices:
|
||||
return self.simple_grid_scan(msg)
|
||||
if msg.scan_report_devices:
|
||||
return self.best_effort(msg)
|
||||
return None
|
||||
|
||||
def on_scan_closed(self, msg: ScanStatusMessage) -> None:
|
||||
"""
|
||||
Procedure to run when a scan ends.
|
||||
|
||||
Args:
|
||||
msg (ScanStatusMessage): The scan status message.
|
||||
"""
|
||||
|
||||
def on_scan_abort(self, msg: ScanStatusMessage) -> None:
|
||||
"""
|
||||
Procedure to run when a scan is aborted.
|
||||
|
||||
Args:
|
||||
msg (ScanStatusMessage): The scan status message.
|
||||
"""
|
||||
|
||||
|
||||
class cSAXSUpdateAlignment(AutoUpdates): ...
|
||||
|
||||
|
||||
class cSAXSUpdateScan(AutoUpdates): ...
|
||||
75
csaxs_bec/bec_widgets/widgets/client.py
Normal file
75
csaxs_bec/bec_widgets/widgets/client.py
Normal file
@@ -0,0 +1,75 @@
|
||||
# This file was automatically generated by generate_cli.py
|
||||
# type: ignore
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
|
||||
from bec_widgets.cli.rpc.rpc_base import RPCBase, rpc_call
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
# pylint: skip-file
|
||||
|
||||
|
||||
_Widgets = {
|
||||
"OmnyAlignment": "OmnyAlignment",
|
||||
}
|
||||
|
||||
|
||||
class OmnyAlignment(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
def enable_live_view(self):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@enable_live_view.setter
|
||||
@rpc_call
|
||||
def enable_live_view(self):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def user_message(self):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@user_message.setter
|
||||
@rpc_call
|
||||
def user_message(self):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def sample_name(self):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@sample_name.setter
|
||||
@rpc_call
|
||||
def sample_name(self):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def enable_move_buttons(self):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
|
||||
@enable_move_buttons.setter
|
||||
@rpc_call
|
||||
def enable_move_buttons(self):
|
||||
"""
|
||||
None
|
||||
"""
|
||||
140
csaxs_bec/bec_widgets/widgets/omny_alignment/omny_alignment.py
Normal file
140
csaxs_bec/bec_widgets/widgets/omny_alignment/omny_alignment.py
Normal file
@@ -0,0 +1,140 @@
|
||||
|
||||
|
||||
from typing import TypedDict
|
||||
from bec_widgets.utils.error_popups import SafeSlot
|
||||
import os
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.ui_loader import UILoader
|
||||
from qtpy.QtWidgets import QWidget, QPushButton, QLineEdit, QLabel, QVBoxLayout
|
||||
from bec_qthemes import material_icon
|
||||
from bec_lib.logger import bec_logger
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
# class OmnyAlignmentUIComponents(TypedDict):
|
||||
# moveRightButton: QPushButton
|
||||
# moveLeftButton: QPushButton
|
||||
# moveUpButton: QPushButton
|
||||
# moveDownButton: QPushButton
|
||||
# image: Image
|
||||
|
||||
|
||||
class OmnyAlignment(BECWidget, QWidget):
|
||||
USER_ACCESS = ["enable_live_view", "enable_live_view.setter", "user_message", "user_message.setter","sample_name", "sample_name.setter", "enable_move_buttons", "enable_move_buttons.setter"]
|
||||
PLUGIN = True
|
||||
ui_file = "./omny_alignment.ui"
|
||||
|
||||
def __init__(self, parent=None, **kwargs):
|
||||
super().__init__(parent=parent, **kwargs)
|
||||
|
||||
self._load_ui()
|
||||
|
||||
def _load_ui(self):
|
||||
current_path = os.path.dirname(__file__)
|
||||
self.ui = UILoader(self).loader(os.path.join(current_path, self.ui_file))
|
||||
layout = QVBoxLayout()
|
||||
layout.addWidget(self.ui)
|
||||
self.setLayout(layout)
|
||||
|
||||
icon_options = {"size": (16, 16), "convert_to_pixmap": False}
|
||||
self.ui.moveRightButton.setText("")
|
||||
self.ui.moveRightButton.setIcon(
|
||||
material_icon(icon_name="keyboard_arrow_right", **icon_options)
|
||||
)
|
||||
|
||||
self.ui.moveLeftButton.setText("")
|
||||
self.ui.moveLeftButton.setIcon(
|
||||
material_icon(icon_name="keyboard_arrow_left", **icon_options)
|
||||
)
|
||||
|
||||
self.ui.moveUpButton.setText("")
|
||||
self.ui.moveUpButton.setIcon(
|
||||
material_icon(icon_name="keyboard_arrow_up", **icon_options)
|
||||
)
|
||||
|
||||
self.ui.moveDownButton.setText("")
|
||||
self.ui.moveDownButton.setIcon(
|
||||
material_icon(icon_name="keyboard_arrow_down", **icon_options)
|
||||
)
|
||||
|
||||
self.ui.confirmButton.setText("OK")
|
||||
|
||||
|
||||
self.ui.liveViewSwitch.enabled.connect(self.on_live_view_enabled)
|
||||
|
||||
self.ui.moveUpButton.clicked.connect(self.on_move_up)
|
||||
|
||||
|
||||
@property
|
||||
def enable_live_view(self):
|
||||
return self.ui.liveViewSwitch.checked
|
||||
|
||||
@enable_live_view.setter
|
||||
def enable_live_view(self, enable:bool):
|
||||
self.ui.liveViewSwitch.checked = enable
|
||||
|
||||
|
||||
@property
|
||||
def user_message(self):
|
||||
return self.ui.messageLineEdit.text()
|
||||
|
||||
@user_message.setter
|
||||
def user_message(self, message:str):
|
||||
self.ui.messageLineEdit.setText(message)
|
||||
|
||||
@property
|
||||
def sample_name(self):
|
||||
return self.ui.sampleLineEdit.text()
|
||||
|
||||
@sample_name.setter
|
||||
def sample_name(self, message:str):
|
||||
self.ui.sampleLineEdit.setText(message)
|
||||
|
||||
|
||||
@SafeSlot(bool)
|
||||
def on_live_view_enabled(self, enabled:bool):
|
||||
from bec_widgets.widgets.plots.image.image import Image
|
||||
logger.info(f"Live view is enabled: {enabled}")
|
||||
image: Image = self.ui.image
|
||||
if enabled:
|
||||
image.image("cam200")
|
||||
return
|
||||
|
||||
image.disconnect_monitor("cam200")
|
||||
|
||||
|
||||
@property
|
||||
def enable_move_buttons(self):
|
||||
move_up:QPushButton = self.ui.moveUpButton
|
||||
move_down:QPushButton = self.ui.moveDownButton
|
||||
move_left:QPushButton = self.ui.moveLeftButton
|
||||
move_right:QPushButton = self.ui.moveRightButton
|
||||
return move_up.isEnabled() and move_down.isEnabled() and move_left.isEnabled() and move_right.isEnabled()
|
||||
|
||||
@enable_move_buttons.setter
|
||||
def enable_move_buttons(self, enabled:bool):
|
||||
move_up:QPushButton = self.ui.moveUpButton
|
||||
move_down:QPushButton = self.ui.moveDownButton
|
||||
move_left:QPushButton = self.ui.moveLeftButton
|
||||
move_right:QPushButton = self.ui.moveRightButton
|
||||
|
||||
move_up.setEnabled(enabled)
|
||||
move_down.setEnabled(enabled)
|
||||
move_left.setEnabled(enabled)
|
||||
move_right.setEnabled(enabled)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from qtpy.QtWidgets import QApplication
|
||||
import sys
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
widget = OmnyAlignment()
|
||||
|
||||
widget.show()
|
||||
sys.exit(app.exec_())
|
||||
@@ -0,0 +1 @@
|
||||
{'files': ['omny_alignment.py']}
|
||||
125
csaxs_bec/bec_widgets/widgets/omny_alignment/omny_alignment.ui
Normal file
125
csaxs_bec/bec_widgets/widgets/omny_alignment/omny_alignment.ui
Normal file
@@ -0,0 +1,125 @@
|
||||
<?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>988</width>
|
||||
<height>821</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<item row="2" column="2">
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="1" column="2">
|
||||
<widget class="QPushButton" name="moveRightButton">
|
||||
<property name="text">
|
||||
<string>PushButton</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QPushButton" name="moveLeftButton">
|
||||
<property name="text">
|
||||
<string>PushButton</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QPushButton" name="moveUpButton">
|
||||
<property name="text">
|
||||
<string>Up</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QPushButton" name="moveDownButton">
|
||||
<property name="text">
|
||||
<string>PushButton</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QPushButton" name="confirmButton">
|
||||
<property name="text">
|
||||
<string>PushButton</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="0" column="1">
|
||||
<widget class="QLineEdit" name="sampleLineEdit"/>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="messageLineEdit"/>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Sample</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Message</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="3">
|
||||
<widget class="Image" name="image">
|
||||
<property name="enable_toolbar" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="inner_axes" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="monitor" stdset="0">
|
||||
<string>cam200</string>
|
||||
</property>
|
||||
<property name="rotation" stdset="0">
|
||||
<number>3</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="3">
|
||||
<widget class="ToggleSwitch" name="liveViewSwitch"/>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Live View</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>Image</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>image</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>ToggleSwitch</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>toggle_switch</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -0,0 +1,54 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from csaxs_bec.bec_widgets.widgets.omny_alignment.omny_alignment import OmnyAlignment
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='OmnyAlignment' name='omny_alignment'>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
|
||||
class OmnyAlignmentPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
t = OmnyAlignment(parent)
|
||||
return t
|
||||
|
||||
def domXml(self):
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return ""
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(OmnyAlignment.ICON_NAME)
|
||||
|
||||
def includeFile(self):
|
||||
return "omny_alignment"
|
||||
|
||||
def initialize(self, form_editor):
|
||||
self._form_editor = form_editor
|
||||
|
||||
def isContainer(self):
|
||||
return False
|
||||
|
||||
def isInitialized(self):
|
||||
return self._form_editor is not None
|
||||
|
||||
def name(self):
|
||||
return "OmnyAlignment"
|
||||
|
||||
def toolTip(self):
|
||||
return "OmnyAlignment"
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
||||
@@ -0,0 +1,15 @@
|
||||
def main(): # pragma: no cover
|
||||
from qtpy import PYSIDE6
|
||||
|
||||
if not PYSIDE6:
|
||||
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
|
||||
return
|
||||
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||
|
||||
from csaxs_bec.bec_widgets.widgets.omny_alignment.omny_alignment_plugin import OmnyAlignmentPlugin
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(OmnyAlignmentPlugin())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
||||
@@ -1,11 +0,0 @@
|
||||
import os
|
||||
|
||||
|
||||
def setup_epics_ca():
|
||||
os.environ["EPICS_CA_AUTO_ADDR_LIST"] = "NO"
|
||||
os.environ["EPICS_CA_ADDR_LIST"] = "129.129.122.255 sls-x12sa-cagw.psi.ch:5836"
|
||||
os.environ["PYTHONIOENCODING"] = "latin1"
|
||||
|
||||
|
||||
def run():
|
||||
setup_epics_ca()
|
||||
0
csaxs_bec/deployments/__init__.py
Normal file
0
csaxs_bec/deployments/__init__.py
Normal file
0
csaxs_bec/deployments/device_server/__init__.py
Normal file
0
csaxs_bec/deployments/device_server/__init__.py
Normal file
11
csaxs_bec/deployments/device_server/startup.py
Normal file
11
csaxs_bec/deployments/device_server/startup.py
Normal file
@@ -0,0 +1,11 @@
|
||||
import os
|
||||
|
||||
|
||||
def setup_epics_ca():
|
||||
# os.environ["EPICS_CA_AUTO_ADDR_LIST"] = "NO"
|
||||
# os.environ["EPICS_CA_ADDR_LIST"] = "129.129.122.255 sls-x12sa-cagw.psi.ch:5836"
|
||||
os.environ["PYTHONIOENCODING"] = "latin1"
|
||||
|
||||
|
||||
def run():
|
||||
setup_epics_ca()
|
||||
111
csaxs_bec/device_configs/bec_device_config_sastt.yaml
Executable file → Normal file
111
csaxs_bec/device_configs/bec_device_config_sastt.yaml
Executable file → Normal file
@@ -27,20 +27,20 @@ mokev:
|
||||
onFailure: buffer
|
||||
readoutPriority: baseline
|
||||
softwareTrigger: false
|
||||
mcs:
|
||||
description: Mcs scalar card for transmission readout
|
||||
deviceClass: csaxs_bec.devices.epics.mcs_csaxs.MCScSAXS
|
||||
deviceConfig:
|
||||
prefix: 'X12SA-MCS:'
|
||||
mcs_config:
|
||||
num_lines: 1
|
||||
deviceTags:
|
||||
- cSAXS
|
||||
- mcs
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readoutPriority: monitored
|
||||
softwareTrigger: false
|
||||
# mcs:
|
||||
# description: Mcs scalar card for transmission readout
|
||||
# deviceClass: csaxs_bec.devices.epics.mcs_csaxs.MCScSAXS
|
||||
# deviceConfig:
|
||||
# prefix: 'X12SA-MCS:'
|
||||
# mcs_config:
|
||||
# num_lines: 1
|
||||
# deviceTags:
|
||||
# - cSAXS
|
||||
# - mcs
|
||||
# onFailure: buffer
|
||||
# enabled: true
|
||||
# readoutPriority: monitored
|
||||
# softwareTrigger: false
|
||||
eiger9m:
|
||||
description: Eiger9m HPC area detector 9M
|
||||
deviceClass: csaxs_bec.devices.epics.eiger9m_csaxs.Eiger9McSAXS
|
||||
@@ -53,89 +53,6 @@ eiger9m:
|
||||
enabled: true
|
||||
readoutPriority: async
|
||||
softwareTrigger: false
|
||||
ddg_detectors:
|
||||
description: DelayGenerator for detector triggering
|
||||
deviceClass: csaxs_bec.devices.epics.delay_generator_csaxs.DelayGeneratorcSAXS
|
||||
deviceConfig:
|
||||
prefix: 'delaygen:DG1:'
|
||||
ddg_config:
|
||||
delay_burst: 40.e-3
|
||||
delta_width: 0
|
||||
additional_triggers: 0
|
||||
polarity:
|
||||
- 1 # T0 -> DDG MCS
|
||||
- 0 # eiger
|
||||
- 1 # falcon
|
||||
- 1
|
||||
- 1
|
||||
amplitude: 4.5
|
||||
offset: 0
|
||||
thres_trig_level: 2.5
|
||||
set_high_on_exposure: False
|
||||
set_high_on_stage: False
|
||||
deviceTags:
|
||||
- cSAXS
|
||||
- ddg_detectors
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readoutPriority: async
|
||||
softwareTrigger: false
|
||||
ddg_mcs:
|
||||
description: DelayGenerator for mcs triggering
|
||||
deviceClass: csaxs_bec.devices.epics.delay_generator_csaxs.DelayGeneratorcSAXS
|
||||
deviceConfig:
|
||||
prefix: 'delaygen:DG2:'
|
||||
ddg_config:
|
||||
delay_burst: 0
|
||||
delta_width: 0
|
||||
additional_triggers: 1
|
||||
polarity:
|
||||
- 1
|
||||
- 0
|
||||
- 1
|
||||
- 1
|
||||
- 1
|
||||
amplitude: 4.5
|
||||
offset: 0
|
||||
thres_trig_level: 2.5
|
||||
set_high_on_exposure: False
|
||||
set_high_on_stage: False
|
||||
set_trigger_source: EXT_RISING_EDGE
|
||||
trigger_width: 3.e-3
|
||||
deviceTags:
|
||||
- cSAXS
|
||||
- ddg_mcs
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readoutPriority: async
|
||||
softwareTrigger: false
|
||||
ddg_fsh:
|
||||
description: DelayGenerator for fast shutter control
|
||||
deviceClass: csaxs_bec.devices.epics.delay_generator_csaxs.DelayGeneratorcSAXS
|
||||
deviceConfig:
|
||||
prefix: 'delaygen:DG3:'
|
||||
ddg_config:
|
||||
delay_burst: 0
|
||||
delta_width: 80.e-3
|
||||
additional_triggers: 0
|
||||
polarity:
|
||||
- 1
|
||||
- 1
|
||||
- 1
|
||||
- 1
|
||||
- 1
|
||||
amplitude: 4.5
|
||||
offset: 0
|
||||
thres_trig_level: 2.5
|
||||
set_high_on_exposure: True
|
||||
set_high_on_stage: False
|
||||
deviceTags:
|
||||
- cSAXS
|
||||
- ddg_fsh
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readoutPriority: async
|
||||
softwareTrigger: false
|
||||
falcon:
|
||||
description: Falcon detector x-ray fluoresence
|
||||
deviceClass: csaxs_bec.devices.epics.falcon_csaxs.FalconcSAXS
|
||||
|
||||
44
csaxs_bec/device_configs/endstation.yaml
Normal file
44
csaxs_bec/device_configs/endstation.yaml
Normal file
@@ -0,0 +1,44 @@
|
||||
ddg1:
|
||||
description: Main delay Generator for triggering
|
||||
deviceClass: csaxs_bec.devices.epics.delay_generator_csaxs.DDG1
|
||||
enabled: true
|
||||
deviceConfig:
|
||||
prefix: 'X12SA-CPCL-DDG1:'
|
||||
onFailure: raise
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
softwareTrigger: true
|
||||
|
||||
ddg2:
|
||||
description: Detector delay Generator for trigger burst
|
||||
deviceClass: csaxs_bec.devices.epics.delay_generator_csaxs.DDG2
|
||||
enabled: true
|
||||
deviceConfig:
|
||||
prefix: 'X12SA-CPCL-DDG2:'
|
||||
onFailure: raise
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
softwareTrigger: false
|
||||
|
||||
mcs:
|
||||
description: Mcs scalar card for transmission readout
|
||||
deviceClass: csaxs_bec.devices.epics.mcs_card.mcs_card_csaxs.MCSCardCSAXS
|
||||
deviceConfig:
|
||||
prefix: 'X12SA-MCS:'
|
||||
onFailure: raise
|
||||
enabled: true
|
||||
readoutPriority: monitored
|
||||
softwareTrigger: false
|
||||
|
||||
ids_cam:
|
||||
description: IDS camera for live image acquisition
|
||||
deviceClass: csaxs_bec.devices.ids_cameras.IDSCamera
|
||||
deviceConfig:
|
||||
camera_id: 201
|
||||
bits_per_pixel: 24
|
||||
m_n_colormode: 1
|
||||
live_mode: True
|
||||
onFailure: raise
|
||||
enabled: true
|
||||
readoutPriority: async
|
||||
softwareTrigger: True
|
||||
8
csaxs_bec/device_configs/first_light.yaml
Normal file
8
csaxs_bec/device_configs/first_light.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
optics:
|
||||
- !include ./optics_hutch.yaml
|
||||
|
||||
frontend:
|
||||
- !include ./frontend.yaml
|
||||
|
||||
endstation:
|
||||
- !include ./endstation.yaml
|
||||
215
csaxs_bec/device_configs/frontend.yaml
Normal file
215
csaxs_bec/device_configs/frontend.yaml
Normal file
@@ -0,0 +1,215 @@
|
||||
idgap:
|
||||
description: 'Motor to control the IDGap of X12SA'
|
||||
deviceClass: ophyd_devices.devices.undulator.UndulatorGap
|
||||
deviceConfig:
|
||||
prefix: 'X12SA-UIND:'
|
||||
onFailure: raise # Consider changing to buffer
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
readOnly: false # put to false if you like to move it
|
||||
softwareTrigger: false
|
||||
|
||||
xbpm1x:
|
||||
description: 'X-ray BPM1 in frontend translation x'
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
deviceConfig:
|
||||
prefix: 'X12SA-FE-XBPM1:TRX'
|
||||
onFailure: raise
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
readOnly: false
|
||||
softwareTrigger: false
|
||||
deviceTags:
|
||||
- cSAXS
|
||||
- frontend
|
||||
|
||||
xbpm1y:
|
||||
description: 'X-ray BPM1 in frontend translation y'
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
deviceConfig:
|
||||
prefix: 'X12SA-FE-XBPM1:TRY'
|
||||
onFailure: raise
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
readOnly: false
|
||||
softwareTrigger: false
|
||||
deviceTags:
|
||||
- cSAXS
|
||||
- frontend
|
||||
|
||||
sl1xr:
|
||||
description: 'slit 1 (frontend) x ring'
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
deviceConfig:
|
||||
prefix: 'X12SA-FE-SL1:TRXR'
|
||||
onFailure: raise
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
readOnly: false
|
||||
softwareTrigger: false
|
||||
deviceTags:
|
||||
- cSAXS
|
||||
- frontend
|
||||
|
||||
sl1xw:
|
||||
description: 'slit 1 (frontend) x wall'
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
deviceConfig:
|
||||
prefix: 'X12SA-FE-SL1:TRXW'
|
||||
onFailure: raise
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
readOnly: false
|
||||
softwareTrigger: false
|
||||
deviceTags:
|
||||
- cSAXS
|
||||
- frontend
|
||||
|
||||
sl1yb:
|
||||
description: 'slit 1 (frontend) y bottom'
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
deviceConfig:
|
||||
prefix: 'X12SA-FE-SL1:TRYB'
|
||||
onFailure: raise
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
readOnly: false
|
||||
softwareTrigger: false
|
||||
deviceTags:
|
||||
- cSAXS
|
||||
- frontend
|
||||
|
||||
sl1yt:
|
||||
description: 'slit 1 (frontend) y top'
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
deviceConfig:
|
||||
prefix: 'X12SA-FE-SL1:TRYT'
|
||||
onFailure: raise
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
readOnly: false
|
||||
softwareTrigger: false
|
||||
deviceTags:
|
||||
- cSAXS
|
||||
- frontend
|
||||
|
||||
sl1xc:
|
||||
description: 'slit 1 (frontend) x center'
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
deviceConfig:
|
||||
prefix: 'X12SA-FE-SL1:CENTERX'
|
||||
onFailure: raise
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
readOnly: false
|
||||
softwareTrigger: false
|
||||
deviceTags:
|
||||
- cSAXS
|
||||
- frontend
|
||||
|
||||
sl1xs:
|
||||
description: 'slit 1 (frontend) x size'
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
deviceConfig:
|
||||
prefix: 'X12SA-FE-SL1:SIZEX'
|
||||
onFailure: raise
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
readOnly: false
|
||||
softwareTrigger: false
|
||||
deviceTags:
|
||||
- cSAXS
|
||||
- frontend
|
||||
|
||||
sl1yc:
|
||||
description: 'slit 1 (frontend) y center'
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
deviceConfig:
|
||||
prefix: 'X12SA-FE-SL1:CENTERY'
|
||||
onFailure: raise
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
readOnly: false
|
||||
softwareTrigger: false
|
||||
deviceTags:
|
||||
- cSAXS
|
||||
- frontend
|
||||
|
||||
sl1ys:
|
||||
description: 'slit 1 (frontend) y size'
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
deviceConfig:
|
||||
prefix: 'X12SA-FE-SL1:SIZEY'
|
||||
onFailure: raise
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
readOnly: false
|
||||
softwareTrigger: false
|
||||
deviceTags:
|
||||
- cSAXS
|
||||
- frontend
|
||||
#####################################
|
||||
#### XBPM ###########################
|
||||
#####################################
|
||||
|
||||
# Note: The following device may not be relevant anymore
|
||||
# and can be fully replaced by the combined device "xbpm1", see below
|
||||
|
||||
xbpm1c1:
|
||||
description: 'XBPM1 (frontend) current 1'
|
||||
deviceClass: ophyd.EpicsSignalRO
|
||||
deviceConfig:
|
||||
read_pv: 'X12SA-FE-XBPM1:Current1:MeanValue_RBV'
|
||||
onFailure: raise
|
||||
enabled: true
|
||||
readoutPriority: monitored
|
||||
readOnly: true
|
||||
softwareTrigger: false
|
||||
|
||||
xbpm1c2:
|
||||
description: 'XBPM1 (frontend) current 2'
|
||||
deviceClass: ophyd.EpicsSignalRO
|
||||
deviceConfig:
|
||||
read_pv: 'X12SA-FE-XBPM1:Current2:MeanValue_RBV'
|
||||
onFailure: raise
|
||||
enabled: true
|
||||
readoutPriority: monitored
|
||||
readOnly: true
|
||||
softwareTrigger: false
|
||||
|
||||
xbpm1c3:
|
||||
description: 'XBPM1 (frontend) current 3'
|
||||
deviceClass: ophyd.EpicsSignalRO
|
||||
deviceConfig:
|
||||
read_pv: 'X12SA-FE-XBPM1:Current3:MeanValue_RBV'
|
||||
onFailure: raise
|
||||
enabled: true
|
||||
readoutPriority: monitored
|
||||
readOnly: true
|
||||
softwareTrigger: false
|
||||
|
||||
xbpm1c4:
|
||||
description: 'XBPM1 (frontend) current 4'
|
||||
deviceClass: ophyd.EpicsSignalRO
|
||||
deviceConfig:
|
||||
read_pv: 'X12SA-FE-XBPM1:Current4:MeanValue_RBV'
|
||||
onFailure: raise
|
||||
enabled: true
|
||||
readoutPriority: monitored
|
||||
readOnly: true
|
||||
softwareTrigger: false
|
||||
|
||||
############################################
|
||||
######### End of xbpm sub devices ##########
|
||||
############################################
|
||||
|
||||
xbpm1:
|
||||
description: 'XBPM1 (frontend)'
|
||||
deviceClass: csaxs_bec.devices.epics.xbpms.BPMDevice
|
||||
deviceConfig:
|
||||
prefix: 'X12SA-FE-XBPM1'
|
||||
onFailure: raise
|
||||
enabled: true
|
||||
readoutPriority: monitored
|
||||
readOnly: true
|
||||
softwareTrigger: false
|
||||
602
csaxs_bec/device_configs/omny_config.yaml
Normal file → Executable file
602
csaxs_bec/device_configs/omny_config.yaml
Normal file → Executable file
@@ -1,3 +1,107 @@
|
||||
# ############################################################
|
||||
# #################### IDS Camera ######################
|
||||
# ############################################################
|
||||
cam200:
|
||||
description: Camera200
|
||||
deviceClass: csaxs_bec.devices.ids_cameras.ids_camera.IDSCamera
|
||||
deviceConfig:
|
||||
camera_ID: 200
|
||||
bits_per_pixel: 24
|
||||
channels: 3
|
||||
m_n_colormode: 1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: async
|
||||
|
||||
cam201:
|
||||
description: Camera201
|
||||
deviceClass: csaxs_bec.devices.ids_cameras.ids_camera.IDSCamera
|
||||
deviceConfig:
|
||||
camera_ID: 201
|
||||
bits_per_pixel: 24
|
||||
channels: 3
|
||||
m_n_colormode: 1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: async
|
||||
|
||||
cam202:
|
||||
description: Camera202
|
||||
deviceClass: csaxs_bec.devices.ids_cameras.ids_camera.IDSCamera
|
||||
deviceConfig:
|
||||
camera_ID: 202
|
||||
bits_per_pixel: 24
|
||||
channels: 3
|
||||
m_n_colormode: 1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: async
|
||||
|
||||
cam203:
|
||||
description: Camera203
|
||||
deviceClass: csaxs_bec.devices.ids_cameras.ids_camera.IDSCamera
|
||||
deviceConfig:
|
||||
camera_ID: 203
|
||||
bits_per_pixel: 24
|
||||
channels: 3
|
||||
m_n_colormode: 1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: async
|
||||
|
||||
############################################################
|
||||
#################### OMNY RT motors ########################
|
||||
############################################################
|
||||
|
||||
rtx:
|
||||
description: OMNY rt
|
||||
deviceClass: csaxs_bec.devices.omny.rt.rt_omny_ophyd.RtOMNYMotor
|
||||
deviceConfig:
|
||||
axis_Id: A
|
||||
host: mpc3217.psi.ch
|
||||
port: 3333
|
||||
sign: 1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: on_request
|
||||
userParameter:
|
||||
low_signal: 8500
|
||||
min_signal: 8000
|
||||
rty:
|
||||
description: OMNY rt
|
||||
deviceClass: csaxs_bec.devices.omny.rt.rt_omny_ophyd.RtOMNYMotor
|
||||
deviceConfig:
|
||||
axis_Id: B
|
||||
host: mpc3217.psi.ch
|
||||
port: 3333
|
||||
sign: 1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: on_request
|
||||
userParameter:
|
||||
tomo_additional_offsety: 0
|
||||
rtz:
|
||||
description: OMNY rt
|
||||
deviceClass: csaxs_bec.devices.omny.rt.rt_omny_ophyd.RtOMNYMotor
|
||||
deviceConfig:
|
||||
axis_Id: C
|
||||
host: mpc3217.psi.ch
|
||||
port: 3333
|
||||
sign: 1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: on_request
|
||||
|
||||
# ############################################################
|
||||
# ##################### OMNY samples #########################
|
||||
# ############################################################
|
||||
omny_samples:
|
||||
description: OMNYSampleStorage
|
||||
deviceClass: csaxs_bec.devices.omny.omny_sample_storage.OMNYSampleStorage
|
||||
@@ -6,3 +110,501 @@ omny_samples:
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
# ############################################################
|
||||
# ##################### OMNY vacuum ##########################
|
||||
# ############################################################
|
||||
# omny_vcs:
|
||||
# description: OMNYVCS
|
||||
# deviceClass: csaxs_bec.devices.omny.omny_vcs.OMNYVCS
|
||||
# deviceConfig: {}
|
||||
# enabled: true
|
||||
# onFailure: buffer
|
||||
# readOnly: false
|
||||
# readoutPriority: baseline
|
||||
# ############################################################
|
||||
# ##################### OMNY dewar ###########################
|
||||
# ############################################################
|
||||
omny_dewar:
|
||||
description: OMNY Dewar Information
|
||||
deviceClass: csaxs_bec.devices.omny.omny_dewar.OMNYDewar
|
||||
deviceConfig: {}
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
|
||||
# ############################################################
|
||||
# ##################### OMNY temperatures ####################
|
||||
# ############################################################
|
||||
omny_temperatures:
|
||||
description: OMNY Temperatures and pressures
|
||||
deviceClass: csaxs_bec.devices.omny.omny_temperatures.OMNYTemperatures
|
||||
deviceConfig: {}
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
|
||||
############################################################
|
||||
##################### OMNY Galil motors ####################
|
||||
############################################################
|
||||
ofzpx:
|
||||
description: FZP X
|
||||
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: A
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8081
|
||||
sign: 1
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
userParameter:
|
||||
in: -0.4317
|
||||
ofzpy:
|
||||
description: FZP Y
|
||||
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: B
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8081
|
||||
sign: 1
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
userParameter:
|
||||
in: 0.7944
|
||||
out: 0.6377
|
||||
ofzpz:
|
||||
description: FZP Z
|
||||
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: C
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8081
|
||||
sign: -1
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
userParameter:
|
||||
in: 0
|
||||
otransx:
|
||||
description: Transfer X
|
||||
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: D
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8081
|
||||
sign: 1
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
userParameter:
|
||||
in: 0
|
||||
otransy:
|
||||
description: Transfer Y
|
||||
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: E
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8081
|
||||
sign: 1
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
userParameter:
|
||||
up_position: -1.2
|
||||
gripper_sensorvoltagetarget: -2.30
|
||||
otransz:
|
||||
description: Transfer Z
|
||||
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: F
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8081
|
||||
sign: 1
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
userParameter:
|
||||
in: 0
|
||||
osamx:
|
||||
description: Sample X
|
||||
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: A
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8082
|
||||
sign: 1
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
userParameter:
|
||||
in: -0.1
|
||||
osamz:
|
||||
description: Sample Z
|
||||
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: B
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8082
|
||||
sign: 1
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: false
|
||||
onFailure: buffer
|
||||
readOnly: true
|
||||
readoutPriority: baseline
|
||||
userParameter:
|
||||
in: 0
|
||||
oosay:
|
||||
description: OSA Y
|
||||
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: C
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8082
|
||||
sign: 1
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
userParameter:
|
||||
near_field_in: 0.531
|
||||
far_field_in: 0.4122
|
||||
oosax:
|
||||
description: OSA X
|
||||
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: D
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8082
|
||||
sign: -1
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
userParameter:
|
||||
near_field_in: 3.2044
|
||||
far_field_in: 3.022
|
||||
oosaz:
|
||||
description: OSA Z
|
||||
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: E
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8082
|
||||
sign: -1
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
userParameter:
|
||||
near_field_in: -0.4452
|
||||
far_field_in: 6.5
|
||||
oparkz:
|
||||
description: OSA Y
|
||||
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: F
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8082
|
||||
sign: 1
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
userParameter:
|
||||
in: 0
|
||||
oshuttleopen:
|
||||
description: Shuttle opener
|
||||
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: G
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8082
|
||||
sign: 1
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: true
|
||||
readoutPriority: baseline
|
||||
userParameter:
|
||||
in: 0
|
||||
oshuttlealign:
|
||||
description: Shuttle aligner
|
||||
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: H
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8082
|
||||
sign: 1
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: true
|
||||
readoutPriority: baseline
|
||||
userParameter:
|
||||
in: 0
|
||||
osamy:
|
||||
description: Sample Y
|
||||
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: A
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8083
|
||||
sign: 1
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
userParameter:
|
||||
in: 0
|
||||
otracky:
|
||||
description: Laser Tracker Y
|
||||
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: B
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8083
|
||||
sign: 1
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
userParameter:
|
||||
start_pos: -4.3431
|
||||
osamroy:
|
||||
description: Sample rotation
|
||||
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: C
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8083
|
||||
sign: 1
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
userParameter:
|
||||
in: 0
|
||||
otrackz:
|
||||
description: Laser Tracker Z
|
||||
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: E
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8083
|
||||
sign: -1
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
userParameter:
|
||||
start_pos: -0.6948
|
||||
oeyex:
|
||||
description: Xray eye X
|
||||
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: F
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8083
|
||||
sign: 1
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
userParameter:
|
||||
xray_in: -45.7394
|
||||
oeyez:
|
||||
description: Xray eye Z
|
||||
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: G
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8083
|
||||
sign: 1
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
userParameter:
|
||||
xray_in: -2
|
||||
oeyey:
|
||||
description: Xray eye Y
|
||||
deviceClass: csaxs_bec.devices.omny.galil.ogalil_ophyd.OMNYGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: H
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 0
|
||||
port: 8083
|
||||
sign: 1
|
||||
deviceTags:
|
||||
- omny
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
userParameter:
|
||||
xray_in: 0.0229
|
||||
|
||||
############################################################
|
||||
#################### flOMNI Smaract motors #################
|
||||
############################################################
|
||||
|
||||
ocsx:
|
||||
description: Central Stop X
|
||||
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
|
||||
deviceConfig:
|
||||
axis_Id: B
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- -2
|
||||
- 2
|
||||
port: 3334
|
||||
sign: -1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
userParameter:
|
||||
nothing: 0
|
||||
ocsy:
|
||||
description: Central Stop Y
|
||||
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
|
||||
deviceConfig:
|
||||
axis_Id: A
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- -2
|
||||
- 2
|
||||
port: 3334
|
||||
sign: 1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
userParameter:
|
||||
nothing: 0
|
||||
oshield:
|
||||
description: Thermal Shield Sample Stage
|
||||
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
|
||||
deviceConfig:
|
||||
axis_Id: C
|
||||
host: mpc3217.psi.ch
|
||||
limits:
|
||||
- -14.5
|
||||
- 15.8
|
||||
port: 3334
|
||||
sign: 1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
userParameter:
|
||||
nothing: 0
|
||||
|
||||
162
csaxs_bec/device_configs/optics_hutch.yaml
Normal file
162
csaxs_bec/device_configs/optics_hutch.yaml
Normal file
@@ -0,0 +1,162 @@
|
||||
dmmroty:
|
||||
description: 'Double Multilayer Monochromator rotation Y'
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
deviceConfig:
|
||||
prefix: 'X12SA-OP-DMM1:ROTY'
|
||||
onFailure: raise
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
readOnly: false
|
||||
softwareTrigger: false
|
||||
deviceTags:
|
||||
- cSAXS
|
||||
- optics
|
||||
|
||||
dmmx:
|
||||
description: 'Double Multilayer Monochromator, translation X'
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
deviceConfig:
|
||||
prefix: 'X12SA-OP-DMM1:TRX'
|
||||
onFailure: raise
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
readOnly: false
|
||||
softwareTrigger: false
|
||||
deviceTags:
|
||||
- cSAXS
|
||||
- optics
|
||||
|
||||
dmmy:
|
||||
description: 'Double Multilayer Monochromator, translation Y'
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
deviceConfig:
|
||||
prefix: 'X12SA-OP-DMM1:TRY'
|
||||
onFailure: raise
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
readOnly: false
|
||||
softwareTrigger: false
|
||||
deviceTags:
|
||||
- cSAXS
|
||||
- optics
|
||||
|
||||
ccmroty:
|
||||
description: 'Channel-cut Monochromator rotation Y'
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
deviceConfig:
|
||||
prefix: 'X12SA-OP-CCM1:ROTY'
|
||||
onFailure: raise
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
readOnly: false
|
||||
softwareTrigger: false
|
||||
deviceTags:
|
||||
- cSAXS
|
||||
- optics
|
||||
|
||||
ccmx:
|
||||
description: 'Channel-cut Monochromator, translation X'
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
deviceConfig:
|
||||
prefix: 'X12SA-OP-CCM1:TRX'
|
||||
onFailure: raise
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
readOnly: false
|
||||
softwareTrigger: false
|
||||
deviceTags:
|
||||
- cSAXS
|
||||
- optics
|
||||
|
||||
xbpm2x:
|
||||
description: X-ray beam position monitor 1 in OPbox
|
||||
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
|
||||
deviceConfig:
|
||||
axis_Id: A
|
||||
host: x12sa-eb-smaract-mcs-03.psi.ch
|
||||
limits:
|
||||
- -200
|
||||
- 200
|
||||
port: 5000
|
||||
sign: 1
|
||||
# precision: 3
|
||||
# tolerance: 0.005
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
|
||||
xbpm2y:
|
||||
description: X-ray beam position monitor 1 in OPbox
|
||||
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
|
||||
deviceConfig:
|
||||
axis_Id: B
|
||||
host: x12sa-eb-smaract-mcs-03.psi.ch
|
||||
limits:
|
||||
- -200
|
||||
- 200
|
||||
port: 5000
|
||||
sign: 1
|
||||
# precision: 3
|
||||
# tolerance: 0.005
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
|
||||
cu_foilx:
|
||||
description: Cu foil in OPbox
|
||||
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
|
||||
deviceConfig:
|
||||
axis_Id: C
|
||||
host: x12sa-eb-smaract-mcs-03.psi.ch
|
||||
limits:
|
||||
- -200
|
||||
- 200
|
||||
port: 5000
|
||||
sign: 1
|
||||
# precision: 3
|
||||
# tolerance: 0.005
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
|
||||
scinx:
|
||||
description: scintillator in OPbox
|
||||
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
|
||||
deviceConfig:
|
||||
axis_Id: D
|
||||
host: x12sa-eb-smaract-mcs-03.psi.ch
|
||||
limits:
|
||||
- -200
|
||||
- 200
|
||||
port: 5000
|
||||
sign: 1
|
||||
# precision: 3
|
||||
# tolerance: 0.005
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
|
||||
# dmm1_trx_readback_example: # This is the same template as for i.e. bpm4i
|
||||
# description: 'This is an example of a read-only Epics signal'
|
||||
# deviceClass: ophyd.EpicsSignalRO
|
||||
# deviceConfig:
|
||||
# read_pv: 'X12SA-OP-DMM1:TRX.RBV'
|
||||
# onFailure: raise
|
||||
# enabled: true
|
||||
# readoutPriority: monitored
|
||||
# readOnly: true
|
||||
# softwareTrigger: false
|
||||
# my_settable_signal:
|
||||
# description: 'This is an example of a settable Epics signal'
|
||||
# deviceClass: ophyd.EpicsSignal
|
||||
# deviceConfig:
|
||||
# read_pv: 'X07MA-FE-DSAPER'
|
||||
# onFailure: retry
|
||||
# enabled: true
|
||||
# readoutPriority: baseline
|
||||
# readOnly: false
|
||||
# softwareTrigger: false
|
||||
@@ -18,16 +18,22 @@
|
||||
| GirderMotorX1 | Girder X translation pseudo motor | [csaxs_bec.devices.epics.specMotors](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/epics/specMotors.py) |
|
||||
| GirderMotorY1 | Girder Y translation pseudo motor | [csaxs_bec.devices.epics.specMotors](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/epics/specMotors.py) |
|
||||
| GirderMotorYAW | Girder YAW pseudo motor | [csaxs_bec.devices.epics.specMotors](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/epics/specMotors.py) |
|
||||
| IDSCamera | | [csaxs_bec.devices.ids_cameras.ids_camera](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/ids_cameras/ids_camera.py) |
|
||||
| InsertionDevice | Python wrapper for the CSAXS insertion device control<br><br> This wrapper provides a positioner interface for the ID control.<br> is completely custom XBPM with templates directly in the<br> VME repo. Thus it needs a custom ophyd template as well...<br><br> WARN: The x and y are not updated by the IOC<br> | [csaxs_bec.devices.epics.InsertionDevice](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/epics/InsertionDevice.py) |
|
||||
| LamniGalilMotor | | [csaxs_bec.devices.omny.galil.lgalil_ophyd](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/omny/galil/lgalil_ophyd.py) |
|
||||
| MCScSAXS | MCS card for cSAXS for implementation at cSAXS beamline | [csaxs_bec.devices.epics.mcs_csaxs](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/epics/mcs_csaxs.py) |
|
||||
| NPointAxis | <br> NPointAxis class, which inherits from Device and PositionerBase. This class<br> represents an axis of an nPoint piezo stage and provides the necessary<br> functionality to move the axis and read its current position.<br> | [csaxs_bec.devices.npoint.npoint](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/npoint/npoint.py) |
|
||||
| OMNYDewar | | [csaxs_bec.devices.omny.omny_dewar](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/omny/omny_dewar.py) |
|
||||
| OMNYGalilMotor | | [csaxs_bec.devices.omny.galil.ogalil_ophyd](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/omny/galil/ogalil_ophyd.py) |
|
||||
| OMNYSampleStorage | | [csaxs_bec.devices.omny.omny_sample_storage](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/omny/omny_sample_storage.py) |
|
||||
| OMNYTemperatures | | [csaxs_bec.devices.omny.omny_temperatures](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/omny/omny_temperatures.py) |
|
||||
| OMNYVCS | | [csaxs_bec.devices.omny.omny_vcs](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/omny/omny_vcs.py) |
|
||||
| PilatuscSAXS | Pilatus_2 300k detector for CSAXS<br><br> Parent class: PSIDetectorBase<br><br> class attributes:<br> custom_prepare_cls (Eiger9MSetup) : Custom detector setup class for cSAXS,<br> inherits from CustomDetectorMixin<br> cam (SLSDetectorCam) : Detector camera<br> MIN_READOUT (float) : Minimum readout time for the detector<br><br> | [csaxs_bec.devices.epics.pilatus_csaxs](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/epics/pilatus_csaxs.py) |
|
||||
| PmDetectorRotation | Detector rotation pseudo motor<br><br> Small wrapper to convert detector pusher position to rotation angle.<br> | [csaxs_bec.devices.epics.specMotors](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/epics/specMotors.py) |
|
||||
| PmMonoBender | Monochromator bender<br><br> Small wrapper to combine the four monochromator bender motors.<br> | [csaxs_bec.devices.epics.specMotors](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/epics/specMotors.py) |
|
||||
| RtFlomniMotor | | [csaxs_bec.devices.omny.rt.rt_flomni_ophyd](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/omny/rt/rt_flomni_ophyd.py) |
|
||||
| RtLamniMotor | | [csaxs_bec.devices.omny.rt.rt_lamni_ophyd](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/omny/rt/rt_lamni_ophyd.py) |
|
||||
| RtOMNYMotor | | [csaxs_bec.devices.omny.rt.rt_omny_ophyd](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/omny/rt/rt_omny_ophyd.py) |
|
||||
| SGalilMotor | "SGalil Motors at cSAXS have a<br> DC motor (y axis - vertical) - implemented as C<br> and a step motor (x-axis horizontal) - implemented as E<br> that require different communication for control<br> | [csaxs_bec.devices.omny.galil.sgalil_ophyd](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/omny/galil/sgalil_ophyd.py) |
|
||||
| SIS38XX | SIS38XX card for access to EPICs PVs at cSAXS beamline | [csaxs_bec.devices.epics.mcs_csaxs](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/epics/mcs_csaxs.py) |
|
||||
| SLSDetectorCam | SLS Detector Camera - Pilatus<br><br> Base class to map EPICS PVs to ophyd signals.<br> | [csaxs_bec.devices.epics.pilatus_csaxs](https://gitlab.psi.ch/bec/csaxs_bec/-/blob/main/csaxs_bec/devices/epics/pilatus_csaxs.py) |
|
||||
|
||||
@@ -1,345 +0,0 @@
|
||||
from bec_lib import bec_logger
|
||||
from ophyd import Component
|
||||
from ophyd_devices.interfaces.base_classes.psi_delay_generator_base import (
|
||||
DDGCustomMixin,
|
||||
PSIDelayGeneratorBase,
|
||||
TriggerSource,
|
||||
)
|
||||
from ophyd_devices.utils import bec_utils
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class DelayGeneratorError(Exception):
|
||||
"""Exception raised for errors."""
|
||||
|
||||
|
||||
class DDGSetup(DDGCustomMixin):
|
||||
"""
|
||||
Mixin class for DelayGenerator logic at cSAXS.
|
||||
|
||||
At cSAXS, multiple DDGs were operated at the same time. There different behaviour is
|
||||
implemented in the ddg_config signals that are passed via the device config.
|
||||
"""
|
||||
|
||||
def initialize_default_parameter(self) -> None:
|
||||
"""Method to initialize default parameters."""
|
||||
for ii, channel in enumerate(self.parent.all_channels):
|
||||
self.parent.set_channels("polarity", self.parent.polarity.get()[ii], [channel])
|
||||
|
||||
self.parent.set_channels("amplitude", self.parent.amplitude.get())
|
||||
self.parent.set_channels("offset", self.parent.offset.get())
|
||||
# Setup reference
|
||||
self.parent.set_channels(
|
||||
"reference", 0, [f"channel{pair}.ch1" for pair in self.parent.all_delay_pairs]
|
||||
)
|
||||
self.parent.set_channels(
|
||||
"reference", 0, [f"channel{pair}.ch2" for pair in self.parent.all_delay_pairs]
|
||||
)
|
||||
self.parent.set_trigger(getattr(TriggerSource, self.parent.set_trigger_source.get()))
|
||||
# Set threshold level for ext. pulses
|
||||
self.parent.level.put(self.parent.thres_trig_level.get())
|
||||
|
||||
def prepare_ddg(self) -> None:
|
||||
"""
|
||||
Method to prepare scan logic of cSAXS
|
||||
|
||||
Two scantypes are supported: "step" and "fly":
|
||||
- step: Scan is performed by stepping the motor and acquiring data at each step
|
||||
- fly: Scan is performed by moving the motor with a constant velocity and acquiring data
|
||||
|
||||
Custom logic for different DDG behaviour during scans.
|
||||
|
||||
- set_high_on_exposure : If True, then TTL signal is high during
|
||||
the full exposure time of the scan (all frames).
|
||||
E.g. Keep shutter open for the full scan.
|
||||
- fixed_ttl_width : fixed_ttl_width is a list of 5 values, one for each channel.
|
||||
If the value is 0, then the width of the TTL pulse is determined,
|
||||
no matter which parameters are passed from the scaninfo for exposure time
|
||||
- set_trigger_source : Specifies the default trigger source for the DDG. For cSAXS, relevant ones
|
||||
were: SINGLE_SHOT, EXT_RISING_EDGE
|
||||
"""
|
||||
self.parent.set_trigger(getattr(TriggerSource, self.parent.set_trigger_source.get()))
|
||||
# scantype "step"
|
||||
if self.parent.scaninfo.scan_type == "step":
|
||||
# High on exposure means that the signal
|
||||
if self.parent.set_high_on_exposure.get():
|
||||
# caluculate parameters
|
||||
num_burst_cycle = 1 + self.parent.additional_triggers.get()
|
||||
|
||||
exp_time = (
|
||||
self.parent.delta_width.get()
|
||||
+ self.parent.scaninfo.frames_per_trigger
|
||||
* (self.parent.scaninfo.exp_time + self.parent.scaninfo.readout_time)
|
||||
)
|
||||
total_exposure = exp_time
|
||||
delay_burst = self.parent.delay_burst.get()
|
||||
|
||||
# Set individual channel widths, if fixed_ttl_width and trigger_width are combined, this can be a common call too
|
||||
if not self.parent.trigger_width.get():
|
||||
self.parent.set_channels("width", exp_time)
|
||||
else:
|
||||
self.parent.set_channels("width", self.parent.trigger_width.get())
|
||||
for value, channel in zip(
|
||||
self.parent.fixed_ttl_width.get(), self.parent.all_channels
|
||||
):
|
||||
logger.debug(f"Trying to set DDG {channel} to {value}")
|
||||
if value != 0:
|
||||
self.parent.set_channels("width", value, channels=[channel])
|
||||
else:
|
||||
# caluculate parameters
|
||||
exp_time = self.parent.delta_width.get() + self.parent.scaninfo.exp_time
|
||||
total_exposure = exp_time + self.parent.scaninfo.readout_time
|
||||
delay_burst = self.parent.delay_burst.get()
|
||||
num_burst_cycle = (
|
||||
self.parent.scaninfo.frames_per_trigger + self.parent.additional_triggers.get()
|
||||
)
|
||||
|
||||
# Set individual channel widths, if fixed_ttl_width and trigger_width are combined, this can be a common call too
|
||||
if not self.parent.trigger_width.get():
|
||||
self.parent.set_channels("width", exp_time)
|
||||
else:
|
||||
self.parent.set_channels("width", self.parent.trigger_width.get())
|
||||
# scantype "fly"
|
||||
elif self.parent.scaninfo.scan_type == "fly":
|
||||
if self.parent.set_high_on_exposure.get():
|
||||
# caluculate parameters
|
||||
exp_time = (
|
||||
self.parent.delta_width.get()
|
||||
+ self.parent.scaninfo.exp_time * self.parent.scaninfo.num_points
|
||||
+ self.parent.scaninfo.readout_time * (self.parent.scaninfo.num_points - 1)
|
||||
)
|
||||
total_exposure = exp_time
|
||||
delay_burst = self.parent.delay_burst.get()
|
||||
num_burst_cycle = 1 + self.parent.additional_triggers.get()
|
||||
|
||||
# Set individual channel widths, if fixed_ttl_width and trigger_width are combined, this can be a common call too
|
||||
if not self.parent.trigger_width.get():
|
||||
self.parent.set_channels("width", exp_time)
|
||||
else:
|
||||
self.parent.set_channels("width", self.parent.trigger_width.get())
|
||||
for value, channel in zip(
|
||||
self.parent.fixed_ttl_width.get(), self.parent.all_channels
|
||||
):
|
||||
logger.debug(f"Trying to set DDG {channel} to {value}")
|
||||
if value != 0:
|
||||
self.parent.set_channels("width", value, channels=[channel])
|
||||
else:
|
||||
# caluculate parameters
|
||||
exp_time = self.parent.delta_width.get() + self.parent.scaninfo.exp_time
|
||||
total_exposure = exp_time + self.parent.scaninfo.readout_time
|
||||
delay_burst = self.parent.delay_burst.get()
|
||||
num_burst_cycle = (
|
||||
self.parent.scaninfo.num_points + self.parent.additional_triggers.get()
|
||||
)
|
||||
|
||||
# Set individual channel widths, if fixed_ttl_width and trigger_width are combined, this can be a common call too
|
||||
if not self.parent.trigger_width.get():
|
||||
self.parent.set_channels("width", exp_time)
|
||||
else:
|
||||
self.parent.set_channels("width", self.parent.trigger_width.get())
|
||||
|
||||
else:
|
||||
raise Exception(f"Unknown scan type {self.parent.scaninfo.scan_type}")
|
||||
# Set common DDG parameters
|
||||
self.parent.burst_enable(num_burst_cycle, delay_burst, total_exposure, config="first")
|
||||
self.parent.set_channels("delay", 0.0)
|
||||
|
||||
def on_trigger(self) -> None:
|
||||
"""Method to be executed upon trigger"""
|
||||
if self.parent.source.read()[self.parent.source.name]["value"] == TriggerSource.SINGLE_SHOT:
|
||||
self.parent.trigger_shot.put(1)
|
||||
|
||||
def check_scan_id(self) -> None:
|
||||
"""
|
||||
Method to check if scan_id has changed.
|
||||
|
||||
If yes, then it changes parent.stopped to True, which will stop further actions.
|
||||
"""
|
||||
old_scan_id = self.parent.scaninfo.scan_id
|
||||
self.parent.scaninfo.load_scan_metadata()
|
||||
if self.parent.scaninfo.scan_id != old_scan_id:
|
||||
self.parent.stopped = True
|
||||
|
||||
def finished(self) -> None:
|
||||
"""Method checks if DDG finished acquisition"""
|
||||
|
||||
def on_pre_scan(self) -> None:
|
||||
"""
|
||||
Method called by pre_scan hook in parent class.
|
||||
|
||||
Executes trigger if premove_trigger is Trus.
|
||||
"""
|
||||
if self.parent.premove_trigger.get() is True:
|
||||
self.parent.trigger_shot.put(1)
|
||||
|
||||
|
||||
class DelayGeneratorcSAXS(PSIDelayGeneratorBase):
|
||||
"""
|
||||
DG645 delay generator at cSAXS (multiple can be in use depending on the setup)
|
||||
|
||||
Default values for setting up DDG.
|
||||
Note: checks of set calues are not (only partially) included, check manual for details on possible settings.
|
||||
https://www.thinksrs.com/downloads/pdfs/manuals/DG645m.pdf
|
||||
|
||||
- delay_burst : (float >=0) Delay between trigger and first pulse in burst mode
|
||||
- delta_width : (float >= 0) Add width to fast shutter signal to make sure its open during acquisition
|
||||
- additional_triggers : (int) add additional triggers to burst mode (mcs card needs +1 triggers per line)
|
||||
- polarity : (list of 0/1) polarity for different channels
|
||||
- amplitude : (float) amplitude voltage of TTLs
|
||||
- offset : (float) offset for ampltitude
|
||||
- thres_trig_level : (float) threshold of trigger amplitude
|
||||
|
||||
Custom signals for logic in different DDGs during scans (for custom_prepare.prepare_ddg):
|
||||
|
||||
- set_high_on_exposure : (bool): if True, then TTL signal should go high during the full acquisition time of a scan.
|
||||
# TODO trigger_width and fixed_ttl could be combined into single list.
|
||||
- fixed_ttl_width : (list of either 1 or 0), one for each channel.
|
||||
- trigger_width : (float) if fixed_ttl_width is True, then the width of the TTL pulse is set to this value.
|
||||
- set_trigger_source : (TriggerSource) specifies the default trigger source for the DDG.
|
||||
- premove_trigger : (bool) if True, then a trigger should be executed before the scan starts (to be implemented in on_pre_scan).
|
||||
- set_high_on_stage : (bool) if True, then TTL signal should go high already on stage.
|
||||
"""
|
||||
|
||||
custom_prepare_cls = DDGSetup
|
||||
|
||||
delay_burst = Component(
|
||||
bec_utils.ConfigSignal, name="delay_burst", kind="config", config_storage_name="ddg_config"
|
||||
)
|
||||
|
||||
delta_width = Component(
|
||||
bec_utils.ConfigSignal, name="delta_width", kind="config", config_storage_name="ddg_config"
|
||||
)
|
||||
|
||||
additional_triggers = Component(
|
||||
bec_utils.ConfigSignal,
|
||||
name="additional_triggers",
|
||||
kind="config",
|
||||
config_storage_name="ddg_config",
|
||||
)
|
||||
|
||||
polarity = Component(
|
||||
bec_utils.ConfigSignal, name="polarity", kind="config", config_storage_name="ddg_config"
|
||||
)
|
||||
|
||||
fixed_ttl_width = Component(
|
||||
bec_utils.ConfigSignal,
|
||||
name="fixed_ttl_width",
|
||||
kind="config",
|
||||
config_storage_name="ddg_config",
|
||||
)
|
||||
|
||||
amplitude = Component(
|
||||
bec_utils.ConfigSignal, name="amplitude", kind="config", config_storage_name="ddg_config"
|
||||
)
|
||||
|
||||
offset = Component(
|
||||
bec_utils.ConfigSignal, name="offset", kind="config", config_storage_name="ddg_config"
|
||||
)
|
||||
|
||||
thres_trig_level = Component(
|
||||
bec_utils.ConfigSignal,
|
||||
name="thres_trig_level",
|
||||
kind="config",
|
||||
config_storage_name="ddg_config",
|
||||
)
|
||||
|
||||
set_high_on_exposure = Component(
|
||||
bec_utils.ConfigSignal,
|
||||
name="set_high_on_exposure",
|
||||
kind="config",
|
||||
config_storage_name="ddg_config",
|
||||
)
|
||||
|
||||
set_high_on_stage = Component(
|
||||
bec_utils.ConfigSignal,
|
||||
name="set_high_on_stage",
|
||||
kind="config",
|
||||
config_storage_name="ddg_config",
|
||||
)
|
||||
|
||||
set_trigger_source = Component(
|
||||
bec_utils.ConfigSignal,
|
||||
name="set_trigger_source",
|
||||
kind="config",
|
||||
config_storage_name="ddg_config",
|
||||
)
|
||||
|
||||
trigger_width = Component(
|
||||
bec_utils.ConfigSignal,
|
||||
name="trigger_width",
|
||||
kind="config",
|
||||
config_storage_name="ddg_config",
|
||||
)
|
||||
premove_trigger = Component(
|
||||
bec_utils.ConfigSignal,
|
||||
name="premove_trigger",
|
||||
kind="config",
|
||||
config_storage_name="ddg_config",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
prefix="",
|
||||
*,
|
||||
name,
|
||||
kind=None,
|
||||
read_attrs=None,
|
||||
configuration_attrs=None,
|
||||
parent=None,
|
||||
device_manager=None,
|
||||
sim_mode=False,
|
||||
ddg_config=None,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
prefix (str, optional): Prefix of the device. Defaults to "".
|
||||
name (str): Name of the device.
|
||||
kind (str, optional): Kind of the device. Defaults to None.
|
||||
read_attrs (list, optional): List of attributes to read. Defaults to None.
|
||||
configuration_attrs (list, optional): List of attributes to configure. Defaults to None.
|
||||
parent (Device, optional): Parent device. Defaults to None.
|
||||
device_manager (DeviceManagerBase, optional): DeviceManagerBase object. Defaults to None.
|
||||
sim_mode (bool, optional): Simulation mode flag. Defaults to False.
|
||||
ddg_config (dict, optional): Dictionary of ddg_config signals. Defaults to None.
|
||||
|
||||
"""
|
||||
# Default values for ddg_config signals
|
||||
self.ddg_config = {
|
||||
# Setup default values
|
||||
f"{name}_delay_burst": 0,
|
||||
f"{name}_delta_width": 0,
|
||||
f"{name}_additional_triggers": 0,
|
||||
f"{name}_polarity": [1, 1, 1, 1, 1],
|
||||
f"{name}_amplitude": 4.5,
|
||||
f"{name}_offset": 0,
|
||||
f"{name}_thres_trig_level": 2.5,
|
||||
# Values for different behaviour during scans
|
||||
f"{name}_fixed_ttl_width": [0, 0, 0, 0, 0],
|
||||
f"{name}_trigger_width": None,
|
||||
f"{name}_set_high_on_exposure": False,
|
||||
f"{name}_set_high_on_stage": False,
|
||||
f"{name}_set_trigger_source": "SINGLE_SHOT",
|
||||
f"{name}_premove_trigger": False,
|
||||
}
|
||||
if ddg_config is not None:
|
||||
# pylint: disable=expression-not-assigned
|
||||
[self.ddg_config.update({f"{name}_{key}": value}) for key, value in ddg_config.items()]
|
||||
super().__init__(
|
||||
prefix=prefix,
|
||||
name=name,
|
||||
kind=kind,
|
||||
read_attrs=read_attrs,
|
||||
configuration_attrs=configuration_attrs,
|
||||
parent=parent,
|
||||
device_manager=device_manager,
|
||||
sim_mode=sim_mode,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Start delay generator in simulation mode.
|
||||
# Note: To run, access to Epics must be available.
|
||||
dgen = DelayGeneratorcSAXS("delaygen:DG1:", name="dgen", sim_mode=True)
|
||||
14
csaxs_bec/devices/epics/delay_generator_csaxs/__init__.py
Normal file
14
csaxs_bec/devices/epics/delay_generator_csaxs/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from .ddg_1 import DDG1
|
||||
from .ddg_2 import DDG2
|
||||
from .delay_generator_csaxs import (
|
||||
BURSTCONFIG,
|
||||
CHANNELREFERENCE,
|
||||
OUTPUTPOLARITY,
|
||||
STATUSBITS,
|
||||
TRIGGERINHIBIT,
|
||||
TRIGGERSOURCE,
|
||||
AllChannelNames,
|
||||
ChannelConfig,
|
||||
DelayChannelNames,
|
||||
)
|
||||
from .error_registry import ERROR_CODES
|
||||
287
csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py
Normal file
287
csaxs_bec/devices/epics/delay_generator_csaxs/ddg_1.py
Normal file
@@ -0,0 +1,287 @@
|
||||
"""
|
||||
DDG1 delay generator
|
||||
|
||||
This module implements the DDG1 delay generator logic for the CSAXS beamline.
|
||||
The attached PDF trigger_scheme_ddg1_ddg2.pdf provides a more detailed overview of
|
||||
the trigger scheme. If the logic changes in the future, it is highly recommended to
|
||||
update the PDF accordingly.
|
||||
|
||||
The DDG1 is the main trigger delay generator for the CSAXS beamline. It will
|
||||
receive either a soft trigger from BEC (depending on the scan type) or a hardware trigger
|
||||
from a beamline device (e.g. the Galil stages). It is responsible for opening the shutter
|
||||
and sending a trigger to the Delay Generator CSAXS (DDG2), which in turn will
|
||||
send the trigger to the detectors. DDG1 will not be witout burst mode, but rather in standard
|
||||
mode creating delays for the channels ab, cd, ef, gh.
|
||||
|
||||
A brief summary of the DDG1 logic:
|
||||
DELAY PAIRS:
|
||||
- DelayPair ab is connected to the EXT/EN of DDG2.
|
||||
- DelayPair cd is connected to the SHUTTER.
|
||||
- DelayPair ef is connected to an OR gate together with the detector
|
||||
PULSE train for the MCS card. The MCS card needs one extra pulse to forward points.
|
||||
|
||||
DELAY CHANNELS:
|
||||
- a = t0 + 2ms (2ms delay to allow the shutter to open)
|
||||
- b = a + 1us (short pulse)
|
||||
- c = t0
|
||||
- d = a + exp_time * burst_count + 1ms (to allow the shutter to close)
|
||||
- e = d
|
||||
- f = e + 1us (short pulse to OR gate for MCS triggering)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from ophyd import DeviceStatus
|
||||
from ophyd_devices import CompareStatus, TransitionStatus
|
||||
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
|
||||
|
||||
from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import (
|
||||
CHANNELREFERENCE,
|
||||
OUTPUTPOLARITY,
|
||||
PROC_EVENT_MODE,
|
||||
STATUSBITS,
|
||||
TRIGGERSOURCE,
|
||||
AllChannelNames,
|
||||
ChannelConfig,
|
||||
DelayGeneratorCSAXS,
|
||||
LiteralChannels,
|
||||
StatusBitsCompareStatus,
|
||||
)
|
||||
from csaxs_bec.devices.epics.mcs_card.mcs_card_csaxs import ACQUIRING, READYTOREAD
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from bec_lib.devicemanager import DeviceManagerBase, ScanInfo
|
||||
|
||||
from csaxs_bec.devices.epics.mcs_card.mcs_card_csaxs import MCSCardCSAXS
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
_DEFAULT_CHANNEL_CONFIG: ChannelConfig = {
|
||||
"amplitude": 5.0,
|
||||
"offset": 0.0,
|
||||
"polarity": OUTPUTPOLARITY.POSITIVE,
|
||||
"mode": "ttl",
|
||||
}
|
||||
|
||||
DEFAULT_IO_CONFIG: dict[AllChannelNames, ChannelConfig] = {
|
||||
"t0": _DEFAULT_CHANNEL_CONFIG,
|
||||
"ab": _DEFAULT_CHANNEL_CONFIG,
|
||||
"cd": _DEFAULT_CHANNEL_CONFIG,
|
||||
"ef": _DEFAULT_CHANNEL_CONFIG,
|
||||
"gh": _DEFAULT_CHANNEL_CONFIG,
|
||||
}
|
||||
DEFAULT_TRIGGER_SOURCE: TRIGGERSOURCE = TRIGGERSOURCE.SINGLE_SHOT
|
||||
DEFAULT_READOUT_TIMES = {"ab": 2e-4, "cd": 2e-4, "ef": 2e-4, "gh": 2e-4} # 0.2 ms 5kHz
|
||||
|
||||
DEFAULT_REFERENCES: list[tuple[LiteralChannels, CHANNELREFERENCE]] = [
|
||||
("A", CHANNELREFERENCE.T0), # T0 + 2ms delay
|
||||
("B", CHANNELREFERENCE.A),
|
||||
("C", CHANNELREFERENCE.T0), # T0
|
||||
("D", CHANNELREFERENCE.C),
|
||||
("E", CHANNELREFERENCE.D), # D One extra pulse once shutter closes for MCS
|
||||
("F", CHANNELREFERENCE.E), # E + 1mu s
|
||||
("G", CHANNELREFERENCE.T0),
|
||||
("H", CHANNELREFERENCE.G),
|
||||
]
|
||||
|
||||
|
||||
class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
|
||||
"""
|
||||
Implementation of DelayGeneratorCSAXS for master trigger delay generator at X12SA-CPCL-DDG1.
|
||||
It will be triggered by a soft trigger from BEC or a hardware trigger from a beamline device
|
||||
(e.g. the Galil stages). It is operated in standard mode, not burst mode and will trigger the
|
||||
EXT/EN of DDG2 (channel ab). It is responsible for opening the shutter (channel cd) and sending
|
||||
an extra trigger to an or gate for the MCS card (channel ef).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
prefix: str = "",
|
||||
scan_info: ScanInfo | None = None,
|
||||
device_manager: DeviceManagerBase | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Initialize the MCSCardCSAXS with the given arguments and keyword arguments.
|
||||
"""
|
||||
super().__init__(
|
||||
name=name, prefix=prefix, scan_info=scan_info, device_manager=device_manager, **kwargs
|
||||
)
|
||||
self.device_manager = device_manager
|
||||
self._poll_thread = threading.Thread(target=self._poll_event_status, daemon=True)
|
||||
self._poll_thread_run_event = threading.Event()
|
||||
self._poll_thread_poll_loop_done = threading.Event()
|
||||
self._poll_thread_kill_event = threading.Event()
|
||||
self._poll_thread.start()
|
||||
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
def on_connected(self) -> None:
|
||||
"""
|
||||
Set the default values on the device - intended to overwrite everything to a usable default state.
|
||||
Sets DEFAULT_IO_CONFIG into each channel, sets the trigger source to DEFAULT_TRIGGER_SOURCE,
|
||||
and turns off burst mode.
|
||||
"""
|
||||
self.burst_disable() # it is possible to miss setting settings if burst is enabled
|
||||
for channel, config in DEFAULT_IO_CONFIG.items():
|
||||
self.set_io_values(channel, **config)
|
||||
self.set_trigger(DEFAULT_TRIGGER_SOURCE)
|
||||
self.set_references_for_channels(DEFAULT_REFERENCES)
|
||||
# Set proc status to passively update with 5Hz (0.2s)
|
||||
self.state.proc_status_mode.put(PROC_EVENT_MODE.EVENT)
|
||||
|
||||
def on_stage(self) -> None:
|
||||
"""
|
||||
Stage logic for the DDG1 device, being th main trigger delay generator for CSAXS.
|
||||
For standard scans, it will be triggered by a soft trigger from BEC.
|
||||
It also has a hardware trigger feeded into the EXT/EN for fly-scanning, i.e. Galil stages.
|
||||
|
||||
This DDG is always not in burst mode.
|
||||
"""
|
||||
exp_time = self.scan_info.msg.scan_parameters["exp_time"]
|
||||
self.burst_enable(1, 0, exp_time)
|
||||
exp_time = self.scan_info.msg.scan_parameters["exp_time"]
|
||||
frames_per_trigger = self.scan_info.msg.scan_parameters["frames_per_trigger"]
|
||||
# Trigger DDG2
|
||||
# a = t0 + 2ms, b = a + 1us
|
||||
# a has reference to t0, b has reference to a
|
||||
self.set_delay_pairs(channel="ab", delay=2e-3, width=1e-6)
|
||||
# Trigger shutter
|
||||
shutter_width = 2e-3 + exp_time * frames_per_trigger + 1e-3
|
||||
# d = c/t0 + 2ms + exp_time * burst_count + 1ms
|
||||
# c has reference to t0, d has reference to c
|
||||
self.set_delay_pairs(channel="cd", delay=0, width=shutter_width)
|
||||
# Trigger extra pulse for MCS OR gate
|
||||
# f = e + 1us
|
||||
# e has refernce to d, f has reference to e
|
||||
self.set_delay_pairs(channel="ef", delay=0, width=1e-6)
|
||||
|
||||
def _prepare_mcs_on_trigger(self, mcs: MCSCardCSAXS) -> None:
|
||||
"""Prepare the MCS card for the next trigger.
|
||||
This method holds the logic to ensure that the MCS card is ready to read.
|
||||
It's logic is coupled to the MCS card implementation and the DDG1 trigger logic.
|
||||
"""
|
||||
status_ready_read = CompareStatus(mcs.ready_to_read, READYTOREAD.DONE)
|
||||
mcs.stop_all.put(1)
|
||||
status_acquiring = TransitionStatus(mcs.acquiring, [ACQUIRING.DONE, ACQUIRING.ACQUIRING])
|
||||
self.cancel_on_stop(status_ready_read)
|
||||
self.cancel_on_stop(status_acquiring)
|
||||
status_ready_read.wait(10)
|
||||
|
||||
mcs.ready_to_read.put(READYTOREAD.PROCESSING)
|
||||
mcs.erase_start.put(1)
|
||||
status_acquiring.wait(timeout=10) # Allow 10 seconds in case communication is slow
|
||||
|
||||
def _poll_event_status(self) -> None:
|
||||
"""
|
||||
Poll the event status register in a background thread. Control
|
||||
the polling with the _poll_thread_run_event and _poll_thread_kill_event.
|
||||
"""
|
||||
while not self._poll_thread_kill_event.is_set():
|
||||
self._poll_thread_run_event.wait()
|
||||
self._poll_thread_poll_loop_done.clear()
|
||||
while (
|
||||
self._poll_thread_run_event.is_set() and not self._poll_thread_kill_event.is_set()
|
||||
):
|
||||
self._poll_loop()
|
||||
|
||||
self._poll_thread_poll_loop_done.set()
|
||||
|
||||
def _poll_loop(self) -> None:
|
||||
"""
|
||||
Poll loop to update event status.
|
||||
The checks ensure that the loop exist after each operation and be stuck in sleep.
|
||||
The 20ms sleep was added to ensure that the event status is not polled too frequently,
|
||||
and to give the device time to process the previous command. This was found empirically
|
||||
to be necessary to avoid missing events.
|
||||
"""
|
||||
self.state.proc_status.put(1, use_complete=True)
|
||||
time.sleep(0.02) # 20ms delay for processing, important for not missing events
|
||||
if self._poll_thread_run_event.is_set() and not self._poll_thread_kill_event.is_set():
|
||||
return
|
||||
self.state.event_status.get(use_monitor=False)
|
||||
if self._poll_thread_run_event.is_set() and not self._poll_thread_kill_event.is_set():
|
||||
return
|
||||
time.sleep(0.02) # 20ms delay for processing, important for not missing events
|
||||
|
||||
def _start_polling(self) -> None:
|
||||
"""Start the polling loop in the background thread."""
|
||||
self._poll_thread_run_event.set()
|
||||
|
||||
def _stop_polling(self) -> None:
|
||||
"""Stop the polling loop in the background thread."""
|
||||
self._poll_thread_run_event.clear()
|
||||
|
||||
def _kill_poll_thread(self) -> None:
|
||||
"""Kill the polling thread."""
|
||||
self._poll_thread_kill_event.set()
|
||||
self._stop_polling()
|
||||
self._poll_thread.join(timeout=1)
|
||||
if self._poll_thread.is_alive():
|
||||
logger.warning("Polling thread did not stop gracefully.")
|
||||
else:
|
||||
logger.info("Polling thread stopped.")
|
||||
|
||||
def _prepare_trigger_status_event(self, timeout: float | None = None) -> DeviceStatus:
|
||||
"""Prepare the trigger status event for the DDG1, and trigger the de"""
|
||||
if timeout is None:
|
||||
# Default timeout of 5 seconds + exposure time * frames_per_trigger
|
||||
timeout = 5 + self.scan_info.msg.scan_parameters.get(
|
||||
"exp_time", 0.1
|
||||
) * self.scan_info.msg.scan_parameters.get("frames_per_trigger", 1)
|
||||
|
||||
# Callback to cancel the status if the device is stopped
|
||||
def cancel_cb(status: CompareStatus) -> None:
|
||||
"""Callback to cancel the status if the device is stopped."""
|
||||
self._stop_polling()
|
||||
|
||||
# Run false is important to ensure that the status is only checked on the next event status update
|
||||
status = StatusBitsCompareStatus(
|
||||
self.state.event_status, STATUSBITS.END_OF_BURST, timeout=timeout, run=False
|
||||
)
|
||||
status.add_callback(cancel_cb)
|
||||
self.cancel_on_stop(status)
|
||||
return status
|
||||
|
||||
def on_trigger(self) -> DeviceStatus:
|
||||
"""Note, we need to add a delay to the StatusBits callback on the event_status.
|
||||
If we don't then subsequent triggers may reach the DDG too early, and will be ignored. To
|
||||
avoid this, we've added the option to specify a delay via add_delay, default here is 50ms.
|
||||
"""
|
||||
# Stop polling, poll once manually to ensure that the register is clean
|
||||
self._stop_polling()
|
||||
self._poll_thread_poll_loop_done.wait(timeout=1)
|
||||
|
||||
# Prepare the MCS card for the next software trigger
|
||||
mcs = self.device_manager.devices.get("mcs", None)
|
||||
if mcs is None:
|
||||
logger.info("Did not find mcs card with name 'mcs' in current session")
|
||||
else:
|
||||
self._prepare_mcs_on_trigger(mcs)
|
||||
# Prepare status with callback to cancel the polling once finished
|
||||
status = self._prepare_trigger_status_event()
|
||||
# Start polling
|
||||
self._start_polling()
|
||||
# Trigger the DDG1
|
||||
self.trigger_shot.put(1, use_complete=True)
|
||||
return status
|
||||
|
||||
def on_stop(self) -> None:
|
||||
"""Stop the delay generator by setting the burst mode to 0"""
|
||||
self.stop_ddg()
|
||||
self._stop_polling()
|
||||
|
||||
def on_destroy(self) -> None:
|
||||
"""Clean up resources when the device is destroyed."""
|
||||
self._kill_poll_thread()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ddg = DDG1(name="ddg1", prefix="X12SA-CPCL-DDG1:")
|
||||
ddg.wait_for_connection(all_signals=True, timeout=30)
|
||||
ddg.summary()
|
||||
160
csaxs_bec/devices/epics/delay_generator_csaxs/ddg_2.py
Normal file
160
csaxs_bec/devices/epics/delay_generator_csaxs/ddg_2.py
Normal file
@@ -0,0 +1,160 @@
|
||||
"""
|
||||
DDG2 delay generator
|
||||
|
||||
This module implements the DDG2 delay generator logic for the CSAXS beamline.
|
||||
Please check also the code for DDG1, aswell as the attached PDF trigger_scheme_ddg1_ddg2.pdf
|
||||
|
||||
The DDG2 is responsible for creating a burst of triggers for all relevant detectors.
|
||||
It will receive a be triggered from the DDG1 through the EXT/EN channel.
|
||||
|
||||
A brief summary of the DDG2 logic:
|
||||
DELAY PAIRS:
|
||||
- EXT/EN is connected to the DDG1 delay pair ab.
|
||||
- DelayPair ab is connected to a multiplexer, multiplexing the trigger to the detectors.
|
||||
|
||||
DELAY CHANNELS:
|
||||
- a = t0
|
||||
- b = a + (exp_time - READOUT_TIMES)
|
||||
|
||||
Burst mode is enabled:
|
||||
- Burst count is set to the number of frames per trigger.
|
||||
- Burst delay is set to 0.
|
||||
- Burst period is set to the exposure time.
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from ophyd import DeviceStatus, StatusBase
|
||||
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
|
||||
|
||||
from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import (
|
||||
CHANNELREFERENCE,
|
||||
OUTPUTPOLARITY,
|
||||
STATUSBITS,
|
||||
TRIGGERSOURCE,
|
||||
AllChannelNames,
|
||||
ChannelConfig,
|
||||
DelayGeneratorCSAXS,
|
||||
LiteralChannels,
|
||||
)
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
_DEFAULT_CHANNEL_CONFIG: ChannelConfig = {
|
||||
"amplitude": 5.0,
|
||||
"offset": 0.0,
|
||||
"polarity": OUTPUTPOLARITY.POSITIVE,
|
||||
"mode": "ttl",
|
||||
}
|
||||
|
||||
DEFAULT_IO_CONFIG: dict[AllChannelNames, ChannelConfig] = {
|
||||
"t0": _DEFAULT_CHANNEL_CONFIG,
|
||||
"ab": _DEFAULT_CHANNEL_CONFIG,
|
||||
"cd": _DEFAULT_CHANNEL_CONFIG,
|
||||
"ef": _DEFAULT_CHANNEL_CONFIG,
|
||||
"gh": _DEFAULT_CHANNEL_CONFIG,
|
||||
}
|
||||
DEFAULT_TRIGGER_SOURCE: TRIGGERSOURCE = TRIGGERSOURCE.EXT_RISING_EDGE
|
||||
DEFAULT_READOUT_TIMES = {"ab": 2e-4, "cd": 2e-4, "ef": 2e-4, "gh": 2e-4} # 0.2 ms 5kHz
|
||||
|
||||
DEFAULT_REFERENCES: list[tuple[LiteralChannels, CHANNELREFERENCE]] = [
|
||||
("A", CHANNELREFERENCE.T0),
|
||||
("B", CHANNELREFERENCE.A),
|
||||
("C", CHANNELREFERENCE.T0),
|
||||
("D", CHANNELREFERENCE.C),
|
||||
("E", CHANNELREFERENCE.T0),
|
||||
("F", CHANNELREFERENCE.E),
|
||||
("G", CHANNELREFERENCE.T0),
|
||||
("H", CHANNELREFERENCE.G),
|
||||
]
|
||||
|
||||
|
||||
class DDG2(PSIDeviceBase, DelayGeneratorCSAXS):
|
||||
"""
|
||||
Implementation of DelayGeneratorCSAXS for the CSAXS master trigger delay generator at X12SA-CPCL-DDG2.
|
||||
This device is responsible for creating triggers in burst mode and is connected to a multiplexer that
|
||||
distributes the trigger to the detectors. The DDG2 is triggered by the DDG1 through the EXT/EN channel.
|
||||
"""
|
||||
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
def on_connected(self) -> None:
|
||||
"""
|
||||
Set the default values on the device - intended to overwrite everything to a usable default state.
|
||||
Sets DEFAULT_IO_CONFIG into each channel, sets the trigger source to DEFAULT_TRIGGER_SOURCE.
|
||||
"""
|
||||
self.burst_disable() # it is possible to miss setting settings if burst is enabled
|
||||
for channel, config in DEFAULT_IO_CONFIG.items():
|
||||
self.set_io_values(channel, **config)
|
||||
self.set_trigger(DEFAULT_TRIGGER_SOURCE)
|
||||
self.set_references_for_channels(DEFAULT_REFERENCES)
|
||||
|
||||
def on_stage(self) -> DeviceStatus | StatusBase | None:
|
||||
"""
|
||||
Stage logic for the DDG1 device, being th main trigger delay generator for CSAXS.
|
||||
For standard scans, it will be triggered by a soft trigger from BEC.
|
||||
It also has a hardware trigger feeded into the EXT/EN for fly-scanning, i.e. Galil stages.
|
||||
|
||||
This DDG is always not in burst mode.
|
||||
"""
|
||||
exp_time = self.scan_info.msg.scan_parameters["exp_time"]
|
||||
frames_per_trigger = self.scan_info.msg.scan_parameters["frames_per_trigger"]
|
||||
# a = t0
|
||||
# a has reference to t0, b has reference to a
|
||||
if any(exp_time <= rt for rt in DEFAULT_READOUT_TIMES.values()):
|
||||
raise ValueError(
|
||||
f"Exposure time {exp_time} is too short for the readout times {DEFAULT_READOUT_TIMES}"
|
||||
)
|
||||
burst_pulse_width = exp_time - DEFAULT_READOUT_TIMES["ab"]
|
||||
self.set_delay_pairs(channel="ab", delay=0, width=burst_pulse_width)
|
||||
self.burst_enable(count=frames_per_trigger, delay=0, period=exp_time)
|
||||
|
||||
def on_pre_scan(self):
|
||||
"""
|
||||
The delay generator occasionally needs a bit extra time to process all
|
||||
commands from stage. Therefore, we introduce here a short sleep
|
||||
"""
|
||||
# Delay Generator occasionaly needs a bit extra time to process all commands, sleep 50ms
|
||||
time.sleep(0.05)
|
||||
|
||||
def on_trigger(self) -> DeviceStatus | StatusBase | None:
|
||||
"""
|
||||
DDG2 will not receive a trigger from BEC, but will be triggered by the DDG1 through the EXT/EN channel.
|
||||
"""
|
||||
|
||||
def wait_for_status(
|
||||
self, status: DeviceStatus, bit_event: STATUSBITS, timeout: float = 5
|
||||
) -> None:
|
||||
"""Wait for a event status bit to be set.
|
||||
|
||||
Args:
|
||||
status (StatusBase): The status object to update.
|
||||
bit_event (STATUSBITS): The event status bit to wait for.
|
||||
timeout (float): Maximum time to wait for the event status bit to be set.
|
||||
"""
|
||||
current_time = time.time()
|
||||
while not status.done:
|
||||
self.state.proc_status.put(1, use_complete=True)
|
||||
event_status = self.state.event_status.get()
|
||||
if (STATUSBITS(event_status) & bit_event) == bit_event:
|
||||
status.set_finished()
|
||||
if time.time() - current_time > timeout:
|
||||
status.set_exception(
|
||||
TimeoutError(
|
||||
f"Timeout waiting for status of device {self.name} for event_status {bit_event}"
|
||||
)
|
||||
)
|
||||
break
|
||||
time.sleep(0.1)
|
||||
time.sleep(0.05) # Give time for the IOC to be ready again
|
||||
return status
|
||||
|
||||
def on_stop(self) -> None:
|
||||
"""Stop the delay generator by setting the burst mode to 0"""
|
||||
self.stop_ddg()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ddg = DDG2(name="ddg2", prefix="X12SA-CPCL-DDG2:")
|
||||
ddg.wait_for_connection(all_signals=True, timeout=30)
|
||||
ddg.summary()
|
||||
@@ -0,0 +1,760 @@
|
||||
"""
|
||||
Delay generator implementation for CSAXS.
|
||||
|
||||
Detailed information can be found in the manual:
|
||||
https://www.thinksrs.com/downloads/pdfs/manuals/DG645m.pdf
|
||||
"""
|
||||
|
||||
import enum
|
||||
import time
|
||||
from typing import Literal, TypedDict
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, EpicsSignal, EpicsSignalRO, Kind, Signal
|
||||
from ophyd_devices import StatusBase, SubscriptionStatus
|
||||
from typeguard import typechecked
|
||||
|
||||
from csaxs_bec.devices.epics.delay_generator_csaxs.error_registry import ERROR_CODES
|
||||
|
||||
logger = bec_logger.logger
|
||||
DelayChannelNames = Literal["ab", "cd", "ef", "gh"]
|
||||
AllChannelNames = Literal["t0", "ab", "cd", "ef", "gh"]
|
||||
LiteralChannels = Literal["A", "B", "C", "D", "E", "F", "G", "H"]
|
||||
|
||||
|
||||
class CHANNELREFERENCE(enum.Enum):
|
||||
T0 = 0
|
||||
A = 1
|
||||
B = 2
|
||||
C = 3
|
||||
D = 4
|
||||
E = 5
|
||||
F = 6
|
||||
G = 7
|
||||
H = 8
|
||||
|
||||
|
||||
class BURSTCONFIG(enum.Enum):
|
||||
"""Enum option for burst_config signal of the delay generator.
|
||||
|
||||
ALL_CYCLES: T0 triggere for all cycles.
|
||||
FIRST_CYCLE: T0 only triggered for the first cycle.
|
||||
"""
|
||||
|
||||
ALL_CYCLES = 0
|
||||
FIRST_CYCLE = 1
|
||||
|
||||
|
||||
class TRIGGERSOURCE(enum.Enum):
|
||||
"""Enum options for the trigger_source signal of the delay generator."""
|
||||
|
||||
INTERNAL = 0
|
||||
EXT_RISING_EDGE = 1
|
||||
EXT_FALLING_EDGE = 2
|
||||
SS_EXT_RISING_EDGE = 3
|
||||
SS_EXT_FALLING_EDGE = 4
|
||||
SINGLE_SHOT = 5
|
||||
LINE = 6
|
||||
|
||||
|
||||
class TRIGGERINHIBIT(enum.Enum):
|
||||
"""Enum options for the trigger_inhibit signal of the delay generator."""
|
||||
|
||||
OFF = 0
|
||||
TRIGGERS = 1
|
||||
AB = 2
|
||||
AB_CD = 3
|
||||
AB_CD_EF = 4
|
||||
AB_CD_EF_GH = 5
|
||||
|
||||
|
||||
class OUTPUTPOLARITY(enum.Enum):
|
||||
"""Enum options for the polarity signal of the static pair."""
|
||||
|
||||
NEGATIVE = 0
|
||||
POSITIVE = 1
|
||||
|
||||
|
||||
class PROC_EVENT_MODE(int, enum.Enum):
|
||||
"""Read mode for MCS channels."""
|
||||
|
||||
PASSIVE = 0
|
||||
EVENT = 1
|
||||
IO_INTR = 2
|
||||
FREQ_0_1HZ = 3
|
||||
FREQ_0_2HZ = 4
|
||||
FREQ_0_5HZ = 5
|
||||
FREQ_1HZ = 6
|
||||
FREQ_2HZ = 7
|
||||
FREQ_5HZ = 8
|
||||
FREQ_10HZ = 9
|
||||
FREQ_100HZ = 10
|
||||
|
||||
|
||||
class STATUSBITS(enum.IntFlag):
|
||||
"""Bit flags for the status signal of the delay generator."""
|
||||
|
||||
NONE = 0 << 0 # No status bits set.
|
||||
TRIG = 1 << 0 # Got a trigger.
|
||||
RATE = 1 << 1 # Got a trigger while a delay or burst was in progress.
|
||||
END_OF_DELAY = 1 << 2 # A delay cycle has completed.
|
||||
END_OF_BURST = 1 << 3 # A burst cycle has completed.
|
||||
INHIBIT = 1 << 4 # A trigger or output delay cycle was inhibited.
|
||||
ABORT_DELAY = 1 << 5 # A delay cycle was aborted early.
|
||||
PLL_UNLOCK = 1 << 6 # The 100 MHz PLL came unlocked.
|
||||
RB_UNLOCK = 1 << 7 # The installed Rb oscillator is unlocked.
|
||||
|
||||
def describe(self) -> dict:
|
||||
"""Return a description of the status bits."""
|
||||
descriptions = {
|
||||
STATUSBITS.NONE: "No status bits set.",
|
||||
STATUSBITS.TRIG: "Got a trigger.",
|
||||
STATUSBITS.RATE: "Got a trigger while a delay or burst was in progress.",
|
||||
STATUSBITS.END_OF_DELAY: "A delay cycle has completed.",
|
||||
STATUSBITS.END_OF_BURST: "A burst cycle has completed.",
|
||||
STATUSBITS.INHIBIT: "A trigger or output delay cycle was inhibited.",
|
||||
STATUSBITS.ABORT_DELAY: "A delay cycle was aborted early.",
|
||||
STATUSBITS.PLL_UNLOCK: "The 100 MHz PLL came unlocked.",
|
||||
STATUSBITS.RB_UNLOCK: "The installed Rb oscillator is unlocked.",
|
||||
}
|
||||
return {flag.name: descriptions[flag] for flag in STATUSBITS if flag in self}
|
||||
|
||||
|
||||
class StatusBitsCompareStatus(SubscriptionStatus):
|
||||
"""Compare status for STATUSBITS comparison."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
signal: EpicsSignalRO,
|
||||
value: STATUSBITS,
|
||||
raise_states: list[STATUSBITS] | None = None,
|
||||
*args,
|
||||
event_type=None,
|
||||
timeout: float | None = None,
|
||||
add_delay: float | None = None,
|
||||
settle_time: float = 0,
|
||||
run: bool = True,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize the compare status with a signal."""
|
||||
self._signal = signal
|
||||
self._value = value
|
||||
self._add_delay = add_delay or 0
|
||||
self._raise_states = raise_states or []
|
||||
super().__init__(
|
||||
device=signal,
|
||||
callback=self._compare_callback,
|
||||
timeout=timeout,
|
||||
settle_time=settle_time,
|
||||
event_type=event_type,
|
||||
run=run,
|
||||
)
|
||||
|
||||
def _compare_callback(self, value, **kwargs) -> bool:
|
||||
"""Callback for subscription status"""
|
||||
obj = kwargs.get("obj", None)
|
||||
if obj is None:
|
||||
name = "no object received"
|
||||
else:
|
||||
name = obj.name
|
||||
if any((STATUSBITS(value) & state) == state for state in self._raise_states):
|
||||
self.set_exception(
|
||||
ValueError(
|
||||
f"Status bits {STATUSBITS(value).describe()} raised an exception: {self._raise_states}"
|
||||
)
|
||||
)
|
||||
return False
|
||||
if self._add_delay != 0:
|
||||
time.sleep(self._add_delay)
|
||||
|
||||
return (STATUSBITS(value) & self._value) == self._value
|
||||
|
||||
|
||||
class ChannelConfig(TypedDict):
|
||||
amplitude: float | None
|
||||
offset: float | None
|
||||
polarity: OUTPUTPOLARITY | Literal[0, 1] | None
|
||||
mode: Literal["ttl", "nim"] | None
|
||||
|
||||
|
||||
class StaticPair(Device):
|
||||
"""
|
||||
Class to represent a static pair (T0, aswell as all AB, CB, EF, GH channels).
|
||||
It allows setting the logic levels, but the timing is fixed.
|
||||
The signal is high after receiving the trigger until the end of the holdoff period.
|
||||
"""
|
||||
|
||||
ttl_mode = Cpt(
|
||||
EpicsSignal,
|
||||
"OutputModeTtlSS.PROC",
|
||||
kind=Kind.omitted,
|
||||
auto_monitor=True,
|
||||
doc="Set the output mode to TTL",
|
||||
)
|
||||
nim_mode = Cpt(
|
||||
EpicsSignal,
|
||||
"OutputModeNimSS.PROC",
|
||||
kind=Kind.omitted,
|
||||
auto_monitor=True,
|
||||
doc="Set the output mode to NIM",
|
||||
)
|
||||
polarity = Cpt(
|
||||
EpicsSignal,
|
||||
"OutputPolarityBI",
|
||||
write_pv="OutputPolarityBO",
|
||||
name="polarity",
|
||||
kind=Kind.omitted,
|
||||
auto_monitor=True,
|
||||
doc="Control the polarity of the output signal. POS 1 or NEG 0",
|
||||
)
|
||||
|
||||
amplitude = Cpt(
|
||||
EpicsSignal,
|
||||
"OutputAmpAI",
|
||||
write_pv="OutputAmpAO",
|
||||
name="amplitude",
|
||||
kind=Kind.omitted,
|
||||
auto_monitor=True,
|
||||
doc="Amplitude of the output signal in volts.",
|
||||
)
|
||||
|
||||
offset = Cpt(
|
||||
EpicsSignal,
|
||||
"OutputOffsetAI",
|
||||
write_pv="OutputOffsetAO",
|
||||
name="offset",
|
||||
kind=Kind.omitted,
|
||||
auto_monitor=True,
|
||||
doc="Offset of the output signal in volts.",
|
||||
)
|
||||
|
||||
|
||||
class Channel(Device):
|
||||
"""
|
||||
Represents a single channel A, B, C, ... of the delay generator.
|
||||
"""
|
||||
|
||||
setpoint = Cpt(
|
||||
EpicsSignal,
|
||||
write_pv="DelayAO",
|
||||
read_pv="DelayAI",
|
||||
put_complete=True,
|
||||
auto_monitor=True,
|
||||
kind=Kind.omitted,
|
||||
doc="Setpoint value for the delay of the channel",
|
||||
)
|
||||
reference = Cpt(
|
||||
EpicsSignal,
|
||||
"ReferenceMO",
|
||||
put_complete=True,
|
||||
kind=Kind.omitted,
|
||||
auto_monitor=True,
|
||||
doc="Reference channel T0,A,B,.. for the delay of the setpoint",
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Initialize the channel with a setpoint and reference signal.
|
||||
"""
|
||||
# The read PV in EpicsSignal does not receive the prefix.. so we need to add it manually.
|
||||
self.__class__.__dict__["setpoint"].kwargs["read_pv"] = args[0] + "DelayAI"
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class WidthSignal(Signal):
|
||||
"""A signal that represents the width of a channel."""
|
||||
|
||||
def get(self, **kwargs) -> float:
|
||||
"""
|
||||
Get the width of the channel.
|
||||
|
||||
Returns:
|
||||
float: The width of the channel in seconds.
|
||||
"""
|
||||
parent: _DelayPairBase = self._parent # type: ignore
|
||||
return parent.ch2.setpoint.get() - parent.ch1.setpoint.get() # type: ignore
|
||||
|
||||
def check_value(self, value: float) -> float:
|
||||
"""Check if the value is larger equal to 0"""
|
||||
if value >= 0:
|
||||
return value
|
||||
else:
|
||||
raise ValueError(f"Width must be larger ot equal 0, got {value} seconds.")
|
||||
|
||||
def put(self, value: float, **kwargs):
|
||||
"""
|
||||
Set the width of the channel.
|
||||
|
||||
Args:
|
||||
value (float): The width to set in seconds.
|
||||
"""
|
||||
self.check_value(value)
|
||||
parent: _DelayPairBase = self._parent # type: ignore
|
||||
ch1_setpoint: float = parent.ch1.setpoint.get() # type: ignore
|
||||
parent.ch2.setpoint.put(ch1_setpoint + value, **kwargs)
|
||||
|
||||
def set(self, value: float, **kwargs):
|
||||
"""
|
||||
Set the width of the channel.
|
||||
|
||||
Args:
|
||||
value (float): The width to set in seconds.
|
||||
"""
|
||||
status = StatusBase()
|
||||
self.put(value, **kwargs)
|
||||
status.set_finished()
|
||||
return status
|
||||
|
||||
|
||||
class DelaySignal(Signal):
|
||||
"""A signal that represents the delay of a channel."""
|
||||
|
||||
def get(self, **kwargs):
|
||||
"""
|
||||
Get the delay of the channel.
|
||||
|
||||
Returns:
|
||||
float: The delay of the channel in seconds.
|
||||
"""
|
||||
parent: _DelayPairBase = self._parent # type: ignore
|
||||
return parent.ch1.setpoint.get()
|
||||
|
||||
def put(self, value: float, **kwargs):
|
||||
"""
|
||||
Set the delay of the channel.
|
||||
|
||||
Args:
|
||||
value (float): The delay to set in seconds.
|
||||
"""
|
||||
parent: _DelayPairBase = self._parent # type: ignore
|
||||
parent.ch1.setpoint.put(value, **kwargs)
|
||||
parent.ch2.setpoint.put(value + parent.width.get(), **kwargs)
|
||||
|
||||
def set(self, value: float, **kwargs):
|
||||
"""
|
||||
Set the width of the channel.
|
||||
|
||||
Args:
|
||||
value (float): The width to set in seconds.
|
||||
"""
|
||||
status = StatusBase()
|
||||
self.put(value, **kwargs)
|
||||
status.set_finished()
|
||||
return status
|
||||
|
||||
|
||||
class _DelayPairBase(Device):
|
||||
"""Base class for delay pairs. Children have to implement ch1,ch2 for
|
||||
the respective delay channels. The class attributes have to be called
|
||||
ch1, ch2 for width and delay signals to work."""
|
||||
|
||||
ch1: Cpt[Channel]
|
||||
ch2: Cpt[Channel]
|
||||
io: Cpt[StaticPair]
|
||||
width = Cpt(
|
||||
WidthSignal, name="width", kind=Kind.config, doc="Width of TTL pulse for delay pair"
|
||||
)
|
||||
delay = Cpt(
|
||||
DelaySignal, name="delay", kind=Kind.config, doc="Delay of TTL pulse for delay pair"
|
||||
)
|
||||
|
||||
|
||||
class DelayPairAB(_DelayPairBase):
|
||||
|
||||
ch1 = Cpt(Channel, "A", name="A", kind=Kind.omitted, doc="Channel A")
|
||||
ch2 = Cpt(Channel, "B", name="B", kind=Kind.omitted, doc="Channel B")
|
||||
io = Cpt(StaticPair, "AB", name="io", kind=Kind.omitted, doc="IO for delay pair AB")
|
||||
|
||||
|
||||
class DelayPairCD(_DelayPairBase):
|
||||
|
||||
ch1 = Cpt(Channel, "C", name="C", kind=Kind.omitted, doc="Channel C")
|
||||
ch2 = Cpt(Channel, "D", name="D", kind=Kind.omitted, doc="Channel D")
|
||||
io = Cpt(StaticPair, "CD", name="io", kind=Kind.omitted, doc="IO for delay pair CD")
|
||||
|
||||
|
||||
class DelayPairEF(_DelayPairBase):
|
||||
|
||||
ch1 = Cpt(Channel, "E", name="E", kind=Kind.omitted, doc="Channel E")
|
||||
ch2 = Cpt(Channel, "F", name="F", kind=Kind.omitted, doc="Channel F")
|
||||
io = Cpt(StaticPair, "EF", name="io", kind=Kind.omitted, doc="IO for delay pair EF")
|
||||
|
||||
|
||||
class DelayPairGH(_DelayPairBase):
|
||||
|
||||
ch1 = Cpt(Channel, "G", name="G", kind=Kind.omitted, doc="Channel G")
|
||||
ch2 = Cpt(Channel, "H", name="H", kind=Kind.omitted, doc="Channel H")
|
||||
io = Cpt(StaticPair, "GH", name="io", kind=Kind.omitted, doc="IO for delay pair GH")
|
||||
|
||||
|
||||
class DelayGeneratorEventStatus(Device):
|
||||
"""Subdevice to represent the event state of the delay generator."""
|
||||
|
||||
event_status = Cpt(
|
||||
EpicsSignalRO,
|
||||
"EventStatusLI",
|
||||
name="event_status",
|
||||
kind=Kind.omitted,
|
||||
doc="Event status register for the delay generator",
|
||||
)
|
||||
proc_status = Cpt(
|
||||
EpicsSignal,
|
||||
"EventStatusLI.PROC",
|
||||
name="proc_status",
|
||||
kind=Kind.omitted,
|
||||
doc="Poll and flush the latest event status register entry from the HW to the event_status signal",
|
||||
)
|
||||
|
||||
proc_status_mode = Cpt(
|
||||
EpicsSignal,
|
||||
"EventStatusLI.SCAN",
|
||||
kind=Kind.omitted,
|
||||
doc="Readout mode for transferring data from status buffer to the event_status signal.",
|
||||
)
|
||||
|
||||
|
||||
class DelayGeneratorCSAXS(Device):
|
||||
"""
|
||||
Delay Generator Stanford Research DG645. This implements an interface for the DG645 delay generator.
|
||||
|
||||
Detailed information can be found in the manual:
|
||||
https://www.thinksrs.com/downloads/pdfs/manuals/DG645m.pdf
|
||||
|
||||
The DG645 has 8 channels, each with a delay and pulse width. The channels are implemented as DelayPair objects (AB etc.).
|
||||
|
||||
Each pair has a TTL pulse width, delay and a reference signal to which they are being triggered.
|
||||
In addition, the io layer allows setting amplitude, offset and polarity for each pair.
|
||||
"""
|
||||
|
||||
# USER_ACCESS = [
|
||||
# "set_channel_reference",
|
||||
# "set_references_for_channels",
|
||||
# "set_io_values",
|
||||
# "set_trigger",
|
||||
# ]
|
||||
|
||||
_pv_timeout: float = 5 # Default timeout for PV operations in seconds
|
||||
|
||||
# Front Panel
|
||||
t0 = Cpt(StaticPair, "T0", name="t0", doc="T0 static pair")
|
||||
ab = Cpt(DelayPairAB, "", name="ab", doc="Delay pair AB")
|
||||
cd = Cpt(DelayPairCD, "", name="cd", doc="Delay pair CD")
|
||||
ef = Cpt(DelayPairEF, "", name="ef", doc="Delay pair EF")
|
||||
gh = Cpt(DelayPairGH, "", name="gh", doc="Delay pair GH")
|
||||
state = Cpt(DelayGeneratorEventStatus, "", name="state", doc="Subdevice for event status")
|
||||
status_msg = Cpt(
|
||||
EpicsSignalRO,
|
||||
"StatusSI",
|
||||
name="status_msg",
|
||||
kind=Kind.omitted,
|
||||
auto_monitor=True,
|
||||
doc="Status message from the delay generator",
|
||||
)
|
||||
status_msg_clear = Cpt(
|
||||
EpicsSignal,
|
||||
"StatusClearBO",
|
||||
name="status_msg_clear",
|
||||
kind=Kind.omitted,
|
||||
doc="Clear the status message",
|
||||
)
|
||||
|
||||
trigger_holdoff = Cpt(
|
||||
EpicsSignal,
|
||||
"TriggerHoldoffAI",
|
||||
write_pv="TriggerHoldoffAO",
|
||||
name="trigger_holdoff",
|
||||
kind=Kind.config,
|
||||
)
|
||||
trigger_inhibit = Cpt(
|
||||
EpicsSignal,
|
||||
"TriggerInhibitMI",
|
||||
write_pv="TriggerInhibitMO",
|
||||
name="trigger_inhibit",
|
||||
kind=Kind.omitted,
|
||||
)
|
||||
trigger_source = Cpt(
|
||||
EpicsSignal,
|
||||
"TriggerSourceMI",
|
||||
write_pv="TriggerSourceMO",
|
||||
name="trigger_source",
|
||||
kind=Kind.omitted,
|
||||
doc="Trigger Source for the DDG, options in TRIGGERSOURCE",
|
||||
)
|
||||
trigger_level = Cpt(
|
||||
EpicsSignal,
|
||||
"TriggerLevelAI",
|
||||
write_pv="TriggerLevelAO",
|
||||
name="trigger_level",
|
||||
kind=Kind.omitted,
|
||||
)
|
||||
trigger_rate = Cpt(
|
||||
EpicsSignal,
|
||||
"TriggerRateAI",
|
||||
write_pv="TriggerRateAO",
|
||||
name="trigger_rate",
|
||||
kind=Kind.omitted,
|
||||
)
|
||||
trigger_shot = Cpt(
|
||||
EpicsSignal,
|
||||
"TriggerDelayBO",
|
||||
name="trigger_shot",
|
||||
kind=Kind.omitted,
|
||||
doc="Software trigger, needs to be in correct mode to work",
|
||||
)
|
||||
burst_mode = Cpt(
|
||||
EpicsSignal,
|
||||
"BurstModeBI",
|
||||
write_pv="BurstModeBO",
|
||||
name="burst_mode",
|
||||
kind=Kind.omitted,
|
||||
auto_monitor=True,
|
||||
doc="Enable or disable burst mode. 1 = enabled, 0 = disabled.",
|
||||
)
|
||||
burst_config = Cpt(
|
||||
EpicsSignal,
|
||||
"BurstConfigBI",
|
||||
write_pv="BurstConfigBO",
|
||||
name="burst_config",
|
||||
kind=Kind.omitted,
|
||||
doc="Configuration of T0 during burst. Can be ALL_CYCLES (0) or FIRST_CYCLE (1) .",
|
||||
)
|
||||
burst_count = Cpt(
|
||||
EpicsSignal,
|
||||
"BurstCountLI",
|
||||
write_pv="BurstCountLO",
|
||||
name="burst_count",
|
||||
kind=Kind.omitted,
|
||||
doc="Number of bursts to trigger in burst mode. Must be >0.",
|
||||
)
|
||||
burst_delay = Cpt(
|
||||
EpicsSignal,
|
||||
"BurstDelayAI",
|
||||
write_pv="BurstDelayAO",
|
||||
name="burst_delay",
|
||||
kind=Kind.omitted,
|
||||
doc="Delay before bursts start in seconds. Must be >=0.",
|
||||
)
|
||||
burst_period = Cpt(
|
||||
EpicsSignal,
|
||||
"BurstPeriodAI",
|
||||
write_pv="BurstPeriodAO",
|
||||
name="burst_period",
|
||||
kind=Kind.omitted,
|
||||
doc="Period of the bursts in seconds. Must be >0.",
|
||||
)
|
||||
|
||||
def proc_event_status(self) -> None:
|
||||
"""The reading must be manually triggered to update the event status."""
|
||||
self.state.proc_status.put(1)
|
||||
|
||||
def wait_for_event_status(
|
||||
self, value: STATUSBITS, timeout: float | None = None
|
||||
) -> StatusBitsCompareStatus:
|
||||
"""
|
||||
Wait for a specific event status.
|
||||
|
||||
Args:
|
||||
value (STATUSBITS): The status bits to wait for.
|
||||
timeout (float): The maximum time to wait in seconds.
|
||||
"""
|
||||
return StatusBitsCompareStatus(
|
||||
signal=self.state.event_status, value=value, timeout=timeout, run=True
|
||||
)
|
||||
|
||||
def set_trigger(self, source: TRIGGERSOURCE | int) -> None:
|
||||
"""
|
||||
Set the trigger source.
|
||||
|
||||
Args:
|
||||
source (TriggerSource | int): The trigger source
|
||||
INTERNAL = 0
|
||||
EXT_RISING_EDGE = 1
|
||||
EXT_FALLING_EDGE = 2
|
||||
SS_EXT_RISING_EDGE = 3
|
||||
SS_EXT_FALLING_EDGE = 4
|
||||
SINGLE_SHOT = 5
|
||||
LINE = 6
|
||||
"""
|
||||
if isinstance(source, TRIGGERSOURCE):
|
||||
self.trigger_source.set(source.value).wait(self._pv_timeout)
|
||||
else:
|
||||
self.trigger_source.set(int(source)).wait(self._pv_timeout)
|
||||
|
||||
@typechecked
|
||||
def burst_enable(
|
||||
self,
|
||||
count: int,
|
||||
delay: float,
|
||||
period: float,
|
||||
config: Literal["all", "first"] | BURSTCONFIG = "first",
|
||||
) -> None:
|
||||
"""Enable burst mode with valid parameters.
|
||||
|
||||
Args:
|
||||
count (int): Number of bursts >0
|
||||
delay (float): Delay before bursts start in seconds >=0
|
||||
period (float): Period of the bursts in seconds >0
|
||||
config (str): Configuration of T0 duiring burst.
|
||||
In addition, to simplify triggering of other instruments synchronously with the burst,
|
||||
the T0 output may be configured to fire on the first delay cycle of the burst,
|
||||
rather than for all delay cycles as is normally the case. BURSTCONFIG
|
||||
"""
|
||||
|
||||
# Check inputs first
|
||||
if count <= 0:
|
||||
raise ValueError(f"Count must be >0, provided: {count}")
|
||||
if delay < 0:
|
||||
raise ValueError(f"Delay must be >=0, provided: {delay}")
|
||||
if period <= 0:
|
||||
raise ValueError(f"Period must be >0, provided: {period}")
|
||||
|
||||
self.burst_mode.set(1).wait(timeout=self._pv_timeout)
|
||||
self.burst_count.set(count).wait(timeout=self._pv_timeout)
|
||||
self.burst_delay.set(delay).wait(timeout=self._pv_timeout)
|
||||
self.burst_period.set(period).wait(timeout=self._pv_timeout)
|
||||
|
||||
if config == "all":
|
||||
self.burst_config.set(BURSTCONFIG.ALL_CYCLES.value).wait(timeout=self._pv_timeout)
|
||||
elif config == "first":
|
||||
self.burst_config.set(BURSTCONFIG.FIRST_CYCLE.value).wait(timeout=self._pv_timeout)
|
||||
|
||||
def burst_disable(self) -> None:
|
||||
"""Disable burst mode"""
|
||||
self.burst_mode.set(0).wait(timeout=self._pv_timeout)
|
||||
|
||||
@typechecked
|
||||
def set_io_values(
|
||||
self,
|
||||
channel: AllChannelNames | list[AllChannelNames],
|
||||
amplitude: float | None = None,
|
||||
offset: float | None = None,
|
||||
polarity: OUTPUTPOLARITY | Literal[0, 1] | None = None,
|
||||
mode: Literal["ttl", "nim"] | None = None,
|
||||
) -> None:
|
||||
"""Set the IO values for the static pair.
|
||||
|
||||
Args:
|
||||
channel (str | list[str]): Channel(s) to set the IO values for.
|
||||
Can be "t0", "ab", "cd", "ef", "gh" or a list of these.
|
||||
If a list is provided, the same values will be set for all channels.
|
||||
amplitude (float): Amplitude of the output signal in volts.
|
||||
offset (float): Offset of the output signal in volts.
|
||||
polarity (OUTPUTPOLARITY | int): Polarity of the output signal.
|
||||
ttl_mode (bool): If True, set the output to TTL mode.
|
||||
nim_mode (bool): If True, set the output to NIM mode.
|
||||
If both ttl_mode and nim_mode are set to True,
|
||||
a ValueError is raised.
|
||||
"""
|
||||
if isinstance(channel, str):
|
||||
channel = [channel]
|
||||
for ch in channel:
|
||||
if ch == "t0":
|
||||
io_channel = self.t0
|
||||
else:
|
||||
io_channel = getattr(getattr(self, ch), "io")
|
||||
if amplitude is not None:
|
||||
io_channel.amplitude.set(amplitude).wait(timeout=self._pv_timeout)
|
||||
if offset is not None:
|
||||
io_channel.offset.set(offset).wait(timeout=self._pv_timeout)
|
||||
if polarity is not None:
|
||||
if isinstance(polarity, OUTPUTPOLARITY):
|
||||
io_channel.polarity.set(polarity.value).wait(timeout=self._pv_timeout)
|
||||
else:
|
||||
io_channel.polarity.set(int(polarity)).wait(timeout=self._pv_timeout)
|
||||
if mode == "ttl":
|
||||
io_channel.ttl_mode.set(1).wait(timeout=self._pv_timeout)
|
||||
if mode == "nim":
|
||||
io_channel.nim_mode.set(1).wait(timeout=self._pv_timeout)
|
||||
|
||||
def set_delay_pairs(
|
||||
self,
|
||||
channel: DelayChannelNames | list[DelayChannelNames],
|
||||
delay: float | list[float] | None = None,
|
||||
width: float | list[float] | None = None,
|
||||
) -> None:
|
||||
"""Set the delay and width for a specific channel pair.
|
||||
|
||||
Args:
|
||||
channel (str): Channel pair to set the delay and width for.
|
||||
Can be "ab", "cd", "ef", "gh".
|
||||
delay (float): Delay in seconds to set for the channel pair.
|
||||
width (float): Width in seconds to set for the channel pair.
|
||||
"""
|
||||
if isinstance(channel, str):
|
||||
channel = [channel]
|
||||
if isinstance(delay, (float, int)):
|
||||
delay = [float(delay)] * len(channel)
|
||||
if isinstance(width, (float, int)):
|
||||
width = [float(width)] * len(channel)
|
||||
if delay is not None:
|
||||
if len(delay) != len(channel):
|
||||
raise ValueError(
|
||||
f"Length of delay {len(delay)} must match length of channel {len(channel)}."
|
||||
)
|
||||
for ii, ch in enumerate(channel):
|
||||
delay_channel = getattr(self, ch)
|
||||
delay_channel.delay.put(delay[ii])
|
||||
if width is not None:
|
||||
if len(width) != len(channel):
|
||||
raise ValueError(
|
||||
f"Length of width {len(width)} must match length of channel {len(channel)}."
|
||||
)
|
||||
for ii, ch in enumerate(channel):
|
||||
delay_channel = getattr(self, ch)
|
||||
delay_channel.width.put(width[ii])
|
||||
|
||||
def _get_literal_channel(self, channel: LiteralChannels) -> Channel:
|
||||
return {
|
||||
"A": self.ab.ch1,
|
||||
"B": self.ab.ch2,
|
||||
"C": self.cd.ch1,
|
||||
"D": self.cd.ch2,
|
||||
"E": self.ef.ch1,
|
||||
"F": self.ef.ch2,
|
||||
"G": self.gh.ch1,
|
||||
"H": self.gh.ch2,
|
||||
}[channel]
|
||||
|
||||
def set_channel_reference(self, channel: LiteralChannels, reference_channel: CHANNELREFERENCE):
|
||||
"""Set the reference channel for a specific channel.
|
||||
|
||||
Args:
|
||||
channel (LiteralChannels): The channel to set the reference for.
|
||||
reference_channel (CHANNELREFERENCE): The reference channel to set.
|
||||
"""
|
||||
self._get_literal_channel(channel).reference.put(reference_channel.value)
|
||||
|
||||
def set_references_for_channels(
|
||||
self, channels_and_refs: list[tuple[LiteralChannels, CHANNELREFERENCE]]
|
||||
):
|
||||
"""Set the reference channels for multiple channels.
|
||||
|
||||
Args:
|
||||
channels_and_refs (list[tuple[LiteralChannels, CHANNELREFERENCE]]): A list of
|
||||
tuples where each tuple contains a channel and its corresponding reference channel.
|
||||
"""
|
||||
for ch, ref in channels_and_refs:
|
||||
self.set_channel_reference(ch, ref)
|
||||
|
||||
def stop_ddg(self) -> None:
|
||||
"""Stop the delay generator by setting the burst mode to 0"""
|
||||
self.burst_mode.put(0)
|
||||
|
||||
def reset_error(self) -> None:
|
||||
"""Reset the error status message of the delay generator."""
|
||||
self.status_msg_clear.put(1)
|
||||
|
||||
def get_error_msg(self) -> str:
|
||||
"""Get the error message from the delay generator."""
|
||||
msg = self.status_msg.get()
|
||||
if msg in ERROR_CODES:
|
||||
return ERROR_CODES[msg]
|
||||
else:
|
||||
return f"Unknown error code: {msg}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ddg = DelayGeneratorCSAXS(name="ddg", prefix="X12SA-CPCL-DDG1:")
|
||||
ddg.wait_for_connection(all_signals=True, timeout=30)
|
||||
ddg.summary()
|
||||
@@ -0,0 +1,73 @@
|
||||
ERROR_CODES: dict[str, str] = {
|
||||
"STATUS OK": "No more errors left in the queue.", # renamed apparently from the IOC for 'No Error' to 'STATUS OK'
|
||||
"Illegal Value": "A parameter was out of range.",
|
||||
"Illegal Mode": "The action is illegal in the current mode.",
|
||||
"Illegal Delay": "The requested delay is out of range.",
|
||||
"Illegal Link": "The requested delay linkage is illegal.",
|
||||
"Recall Failed": "Recall of instrument settings failed; settings were invalid.",
|
||||
"Not Allowed": "Action not allowed: instrument is locked by another interface.",
|
||||
"Failed Self Test": "The DG645 self test failed.",
|
||||
"Failed Auto Calibration": "The DG645 auto calibration failed.",
|
||||
"Lost Data": "Output buffer overflow or data lost due to communication error.",
|
||||
"No Listener": "No GPIB listeners; pending output discarded.",
|
||||
"Failed ROM Check": "ROM checksum failed; firmware likely corrupted.",
|
||||
"Failed Offset T0 Test": "Self test of offset functionality for T0 failed.",
|
||||
"Failed Offset AB Test": "Self test of offset functionality for AB failed.",
|
||||
"Failed Offset CD Test": "Self test of offset functionality for CD failed.",
|
||||
"Failed Offset EF Test": "Self test of offset functionality for EF failed.",
|
||||
"Failed Offset GH Test": "Self test of offset functionality for GH failed.",
|
||||
"Failed Amplitude T0 Test": "Self test of amplitude functionality for T0 failed.",
|
||||
"Failed Amplitude AB Test": "Self test of amplitude functionality for AB failed.",
|
||||
"Failed Amplitude CD Test": "Self test of amplitude functionality for CD failed.",
|
||||
"Failed Amplitude EF Test": "Self test of amplitude functionality for EF failed.",
|
||||
"Failed Amplitude GH Test": "Self test of amplitude functionality for GH failed.",
|
||||
"Failed FPGA Communications Test": "Self test of FPGA communications failed.",
|
||||
"Failed GPIB Communications Test": "Self test of GPIB communications failed.",
|
||||
"Failed DDS Communications Test": "Self test of DDS communications failed.",
|
||||
"Failed Serial EEPROM Communications Test": "Self test of serial EEPROM failed.",
|
||||
"Failed Temperature Sensor Communications Test": "Temp sensor communication failed.",
|
||||
"Failed PLL Communications Test": "PLL communication self test failed.",
|
||||
"Failed DAC 0 Communications Test": "Self test of DAC 0 failed.",
|
||||
"Failed DAC 1 Communications Test": "Self test of DAC 1 failed.",
|
||||
"Failed DAC 2 Communications Test": "Self test of DAC 2 failed.",
|
||||
"Failed Sample and Hold Operations Test": "Sample and hold self test failed.",
|
||||
"Failed Vjitter Operations Test": "Vjitter operation self test failed.",
|
||||
"Failed Channel T0 Analog Delay Test": "Analog delay test for T0 failed.",
|
||||
"Failed Channel T1 Analog Delay Test": "Analog delay test for T1 failed.",
|
||||
"Failed Channel A Analog Delay Test": "Analog delay test for A failed.",
|
||||
"Failed Channel B Analog Delay Test": "Analog delay test for B failed.",
|
||||
"Failed Channel C Analog Delay Test": "Analog delay test for C failed.",
|
||||
"Failed Channel D Analog Delay Test": "Analog delay test for D failed.",
|
||||
"Failed Channel E Analog Delay Test": "Analog delay test for E failed.",
|
||||
"Failed Channel F Analog Delay Test": "Analog delay test for F failed.",
|
||||
"Failed Channel G Analog Delay Test": "Analog delay test for G failed.",
|
||||
"Failed Channel H Analog Delay Test": "Analog delay test for H failed.",
|
||||
"Failed Sample and Hold Calibration": "Auto calibration of sample and hold failed.",
|
||||
"Failed T0 Calibration": "Auto calibration of channel T0 failed.",
|
||||
"Failed T1 Calibration": "Auto calibration of channel T1 failed.",
|
||||
"Failed A Calibration": "Auto calibration of channel A failed.",
|
||||
"Failed B Calibration": "Auto calibration of channel B failed.",
|
||||
"Failed C Calibration": "Auto calibration of channel C failed.",
|
||||
"Failed D Calibration": "Auto calibration of channel D failed.",
|
||||
"Failed E Calibration": "Auto calibration of channel E failed.",
|
||||
"Failed F Calibration": "Auto calibration of channel F failed.",
|
||||
"Failed G Calibration": "Auto calibration of channel G failed.",
|
||||
"Failed H Calibration": "Auto calibration of channel H failed.",
|
||||
"Failed Vjitter Calibration": "Auto calibration of Vjitter failed.",
|
||||
"Illegal Command": "The command syntax used was illegal.",
|
||||
"Undefined Command": "The specified command does not exist.",
|
||||
"Illegal Query": "The specified command does not permit queries.",
|
||||
"Illegal Set": "The specified command can only be queried.",
|
||||
"Null Parameter": "The parser detected an empty parameter.",
|
||||
"Extra Parameters": "Too many parameters were provided.",
|
||||
"Missing Parameters": "Some required parameters are missing.",
|
||||
"Parameter Overflow": "Buffer overflow while parsing parameters.",
|
||||
"Invalid Floating Point Number": "Expected a float but couldn't parse it.",
|
||||
"Invalid Integer": "Expected an integer but couldn't parse it.",
|
||||
"Integer Overflow": "Parsed integer is too large.",
|
||||
"Invalid Hexadecimal": "Failed to parse expected hexadecimal input.",
|
||||
"Syntax Error": "The parser detected a syntax error.",
|
||||
"Communication Error": "Framing or parity error detected.",
|
||||
"Over run": "Input buffer overflowed.",
|
||||
"Too Many Errors": "Error buffer is full; some errors dropped.",
|
||||
}
|
||||
Binary file not shown.
1
csaxs_bec/devices/epics/mcs_card/__init__.py
Normal file
1
csaxs_bec/devices/epics/mcs_card/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .mcs_card import MCSCard
|
||||
341
csaxs_bec/devices/epics/mcs_card/mcs_card.py
Normal file
341
csaxs_bec/devices/epics/mcs_card/mcs_card.py
Normal file
@@ -0,0 +1,341 @@
|
||||
"""
|
||||
EPICS SIS38XX Multichannel Scaler (MCS) Interface
|
||||
|
||||
This module provides an interface to the SIS3801/SIS3820 multichannel scaler (MCS) cards via EPICS.
|
||||
It focuses on the implementation for the SIS3820 model, as input/output modes differ between SIS3801
|
||||
and SIS3820. It supports both MCS and scaler record operations, enabling configuration and control of
|
||||
acquisition parameters such as dwell time, channel advance mode, and input/output settings.
|
||||
The module facilitates data acquisition by managing FIFO buffers and simulating conventional
|
||||
MCS behavior through memory buffers.
|
||||
|
||||
At cSAXS, the SIS3820 model is used, which supports 32 channels.
|
||||
|
||||
References:
|
||||
- EPICS SIS3801 and SIS3820 Drivers: https://millenia.cars.aps.anl.gov/software/epics/mcaStruck.html
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, DynamicDeviceComponent, EpicsSignal, EpicsSignalRO, Kind
|
||||
|
||||
|
||||
class CHANNELADVANCE(int, enum.Enum):
|
||||
"""Channel advance pixel mode for MCS card."""
|
||||
|
||||
INTERNAL = 0
|
||||
EXTERNAL = 1
|
||||
|
||||
|
||||
class ACQUIRING(int, enum.Enum):
|
||||
"""Acquisition status for MCS card."""
|
||||
|
||||
DONE = 0
|
||||
ACQUIRING = 1
|
||||
|
||||
|
||||
class READMODE(int, enum.Enum):
|
||||
"""Read mode for MCS channels."""
|
||||
|
||||
PASSIVE = 0
|
||||
EVENT = 1
|
||||
IO_INTR = 2
|
||||
FREQ_0_1HZ = 3
|
||||
FREQ_0_2HZ = 4
|
||||
FREQ_0_5HZ = 5
|
||||
FREQ_1HZ = 6
|
||||
FREQ_2HZ = 7
|
||||
FREQ_5HZ = 8
|
||||
FREQ_10HZ = 9
|
||||
FREQ_100HZ = 10
|
||||
|
||||
|
||||
class CHANNEL1SOURCE(int, enum.Enum):
|
||||
"""Source for first counter pulses."""
|
||||
|
||||
INTERNAL_CLOCK = 0
|
||||
EXTERNAL = 1
|
||||
|
||||
|
||||
class POLARITY(int, enum.Enum):
|
||||
"""Polarity of input_polarity/output_polarity for MCS card."""
|
||||
|
||||
NORMAL = 0
|
||||
INVERTED = 1
|
||||
|
||||
|
||||
class ACQUIREMODE(int, enum.Enum):
|
||||
"""Acquire mode for the card. Allowed modes are Scaler and MCS."""
|
||||
|
||||
MCS = 0
|
||||
SCALER = 1
|
||||
|
||||
|
||||
class MODELS(int, enum.Enum):
|
||||
|
||||
SIS3801 = 0
|
||||
SIS3820 = 1
|
||||
|
||||
|
||||
class INPUTMODE(int, enum.Enum):
|
||||
"""SIS3820 input mode definitions, in total there are 8 modes (0-7).
|
||||
|
||||
Each mode defines the function of external inputs 1-4.
|
||||
Note: SIS3820 has extended input modes compared to SIS3801.
|
||||
Please check the EPICS documentation for details on the specific input modes supported by SIS3801.
|
||||
"""
|
||||
|
||||
MODE_0 = 0
|
||||
MODE_1 = 1
|
||||
MODE_2 = 2
|
||||
MODE_3 = 3
|
||||
MODE_4 = 4
|
||||
MODE_5 = 5
|
||||
MODE_6 = 6
|
||||
MODE_7 = 7
|
||||
|
||||
def describe(self) -> str:
|
||||
"""Return a description of the input mode."""
|
||||
descriptions = {
|
||||
self.MODE_0: "Inputs 1-4: No function (default idle mode)",
|
||||
self.MODE_1: "Inputs 1-4: Next pulse, User bit 1, User bit 2, Inhibit next pulse",
|
||||
self.MODE_2: "Inputs 1-4: Next pulse, User bit 1, Inhibit counting, Inhibit next pulse",
|
||||
self.MODE_3: "Inputs 1-4: Next pulse, User bit 1, User bit 2, Inhibit counting",
|
||||
self.MODE_4: "Inputs 1-4: Inhibit counting channels 1-8, 9-16, 17-24, 25-32",
|
||||
self.MODE_5: "Inputs 1-4: Next pulse, HISCAL_START, No function, No function",
|
||||
self.MODE_6: "Inputs 1-4: Next pulse, Inhibit counting, Clear counters, User bit 1",
|
||||
self.MODE_7: "Inputs 1-4: Encoder A, Encoder B, Encoder I, Inhibit counting",
|
||||
}
|
||||
return descriptions.get(self, "Unknown input mode")
|
||||
|
||||
|
||||
class OUTPUTMODE(int, enum.Enum):
|
||||
"""SIS3820 output mode definitions, in total there are 4 modes (0-3).
|
||||
|
||||
Each mode configures output signals 5-8.
|
||||
Note: SIS3820 supports 4 output modes (0-3), SIS3801 supports only Mode 0 with differen functionality.
|
||||
Please check the EPICS documentation for details on the specific output modes supported by SIS3801.
|
||||
"""
|
||||
|
||||
MODE_0 = 0
|
||||
MODE_1 = 1
|
||||
MODE_2 = 2
|
||||
MODE_3 = 3
|
||||
|
||||
def describe(self) -> str:
|
||||
"""Return a description of the output mode."""
|
||||
descriptions = {
|
||||
self.MODE_0: "Outputs 5-8: LNE/CIP, SDRAM empty, SDRAM threshold, User LED",
|
||||
self.MODE_1: "Outputs 5-8: LNE/CIP, Enabled, 50 MHz, User LED",
|
||||
self.MODE_2: "Outputs 5-8: LNE/CIP, 10 MHz (20ns), 10 MHz (20ns), User LED",
|
||||
self.MODE_3: "Outputs 5-8: LNE/CIP, 10 MHz (20ns), MUX OUT channel, User LED (requires firmware ≥ 0x10A)",
|
||||
}
|
||||
return descriptions.get(self, "Unknown output mode")
|
||||
|
||||
|
||||
def _create_mca_channels(num_channels: int) -> dict[str, tuple]:
|
||||
"""
|
||||
Create a dictionary of MCA channel definitions for the DynamicDeviceComponent.
|
||||
Starts from channel 1 to num_channels.
|
||||
|
||||
Args:
|
||||
num_channels (int): The number of MCA channels to create.
|
||||
"""
|
||||
mcs_channels = {}
|
||||
for i in range(1, num_channels + 1):
|
||||
mcs_channels[f"mca{i}"] = (
|
||||
EpicsSignalRO,
|
||||
f"mca{i}.VAL",
|
||||
{"kind": Kind.omitted, "auto_monitor": True, "doc": f"MCA channel {i}."},
|
||||
)
|
||||
return mcs_channels
|
||||
|
||||
|
||||
class MCSCard(Device):
|
||||
"""
|
||||
Ophyd implementation for the interface to the SIS3801/SIS3820 multichannel scaler (MCS) cards via EPICS.
|
||||
|
||||
This class provides signals to expose EPICS PVs of the MCS card. More details can be found in the
|
||||
documentation of the EPICS drivers for SIS3801 and SIS3820.
|
||||
|
||||
References:
|
||||
- EPICS SIS3801 and SIS3820 Drivers: https://millenia.cars.aps.anl.gov/software/epics/mcaStruck.html
|
||||
"""
|
||||
|
||||
snl_connected = Cpt(
|
||||
EpicsSignalRO,
|
||||
"SNL_Connected",
|
||||
kind=Kind.omitted,
|
||||
doc="Indicates whether the SNL program has connected to all PVs.",
|
||||
)
|
||||
erase_all = Cpt(
|
||||
EpicsSignal,
|
||||
"EraseAll",
|
||||
kind=Kind.omitted,
|
||||
doc="Erases all mca or waveform records, setting elapsed times and counts in all channels to 0.",
|
||||
)
|
||||
erase_start = Cpt(
|
||||
EpicsSignal,
|
||||
"EraseStart",
|
||||
kind=Kind.omitted,
|
||||
doc="Erases all mca or waveform records and starts acquisition.",
|
||||
)
|
||||
start_all = Cpt(
|
||||
EpicsSignal,
|
||||
"StartAll",
|
||||
kind=Kind.omitted,
|
||||
doc="Starts or resumes acquisition without erasing first.",
|
||||
)
|
||||
acquiring = Cpt(
|
||||
EpicsSignalRO,
|
||||
"Acquiring",
|
||||
kind=Kind.omitted,
|
||||
doc="Acquiring (=1) when acquisition is in progress and Done (=0) when acquisition is complete.",
|
||||
)
|
||||
stop_all = Cpt(EpicsSignal, "StopAll", kind=Kind.omitted, doc="Stops acquisition.")
|
||||
preset_real = Cpt(
|
||||
EpicsSignal,
|
||||
"PresetReal",
|
||||
kind=Kind.omitted,
|
||||
doc="Preset real time. If non-zero then acquisition will stop when this time is reached.",
|
||||
)
|
||||
elapsed_real = Cpt(
|
||||
EpicsSignalRO,
|
||||
"ElapsedReal",
|
||||
kind=Kind.omitted,
|
||||
doc="Elapsed time since acquisition started.",
|
||||
)
|
||||
read_all = Cpt(
|
||||
EpicsSignal,
|
||||
"DoReadAll.VAL",
|
||||
kind=Kind.omitted,
|
||||
doc="Forces a read of all mca or waveform records from the hardware. This record can be set to periodically process to update the records during acquisition. Note that even if this record has SCAN=Passive the mca or waveform records will always process once when acquisition completes.",
|
||||
)
|
||||
read_mode = Cpt(
|
||||
EpicsSignal,
|
||||
"ReadAll.SCAN",
|
||||
kind=Kind.omitted,
|
||||
doc="Readout mode for transferring data from FIFO buffer to mca EPICS scalars.",
|
||||
)
|
||||
num_use_all = Cpt(
|
||||
EpicsSignal,
|
||||
"NuseAll",
|
||||
kind=Kind.omitted,
|
||||
doc="The number of channels to use for the mca or waveform records. Acquisition will automatically stop when the number of channel advances reaches this value.",
|
||||
)
|
||||
dwell = Cpt(
|
||||
EpicsSignal,
|
||||
"Dwell",
|
||||
kind=Kind.omitted,
|
||||
doc="The dwell time per channel when using internal channel advance mode.",
|
||||
)
|
||||
channel_advance = Cpt(
|
||||
EpicsSignal,
|
||||
"ChannelAdvance",
|
||||
kind=Kind.omitted,
|
||||
doc="The channel advance mode. Choices are 'Internal' (count for a preset time per channel) or 'External' (advance on external hardware channel advance signal).",
|
||||
)
|
||||
count_on_start = Cpt(
|
||||
EpicsSignal,
|
||||
"CountOnStart",
|
||||
kind=Kind.omitted,
|
||||
doc="Flag controlling whether the module begins counting immediately when acquisition starts. This record only applies in External channel advance mode. If No (=0) then counting does not start in channel 0 until receipt of the first external channel advance pulse. If Yes (=1) then counting in channel 0 starts immediately when acquisition starts, without waiting for the first external channel advance pulse.",
|
||||
)
|
||||
software_channel_advance = Cpt(
|
||||
EpicsSignal,
|
||||
"SoftwareChannelAdvance",
|
||||
kind=Kind.omitted,
|
||||
doc="Processing this record causes a channel advance to occur immediately, without waiting for the current dwell time to be reached or the next external channel advance pulse to arrive.",
|
||||
)
|
||||
channel1_source = Cpt(
|
||||
EpicsSignal,
|
||||
"Channel1Source",
|
||||
kind=Kind.omitted,
|
||||
doc="Controls the source of pulses into the first counter. The choices are 'Int. clock' which selects the internal clock, and 'External' which selects the external pulse input to counter 1.",
|
||||
)
|
||||
prescale = Cpt(
|
||||
EpicsSignal,
|
||||
"Prescale",
|
||||
kind=Kind.omitted,
|
||||
doc="The prescale factor for external channel advance pulses. If the prescale factor is N then N external channel advance pulses must be received before a channel advance will occur.",
|
||||
)
|
||||
enable_client_wait = Cpt(
|
||||
EpicsSignal,
|
||||
"EnableClientWait",
|
||||
kind=Kind.omitted,
|
||||
doc="Flag to force acquisition to wait until a client clears the ClientWait busy record before proceeding to the next acquisition. This can be useful with the scan record.",
|
||||
)
|
||||
client_wait = Cpt(
|
||||
EpicsSignal,
|
||||
"ClientWait",
|
||||
kind=Kind.omitted,
|
||||
doc="Flag that will be set to 1 when acquisition completes, and which a client must set back to 0 to allow acquisition to proceed. This only has an effect if EnableClientWait is 1.",
|
||||
)
|
||||
acquire_mode = Cpt(
|
||||
EpicsSignal,
|
||||
"AcquireMode",
|
||||
kind=Kind.omitted,
|
||||
doc="The current acquisition mode (MCS=0 or Scaler=1). This record is used to turn off the scaler record Autocount in MCS mode.",
|
||||
)
|
||||
mux_output = Cpt(
|
||||
EpicsSignal,
|
||||
"MUXOutput",
|
||||
kind=Kind.omitted,
|
||||
doc="Value of 0-32 used to select which input signal is routed to output signal 7 on the SIS3820 in output mode 3.",
|
||||
)
|
||||
user_led = Cpt(
|
||||
EpicsSignal,
|
||||
"UserLED",
|
||||
kind=Kind.omitted,
|
||||
doc="Toggles the user LED and also output signal 8 on the SIS3820.",
|
||||
)
|
||||
input_mode = Cpt(
|
||||
EpicsSignal,
|
||||
"InputMode",
|
||||
kind=Kind.omitted,
|
||||
doc="The input mode. Supported input modes vary for SIS3801 and SIS3820.",
|
||||
)
|
||||
input_polarity = Cpt(
|
||||
EpicsSignal,
|
||||
"InputPolarity",
|
||||
kind=Kind.omitted,
|
||||
doc="The polarity of the input control signals on the SIS3820. Choices are Normal and Inverted.",
|
||||
)
|
||||
output_mode = Cpt(
|
||||
EpicsSignal,
|
||||
"OutputMode",
|
||||
kind=Kind.omitted,
|
||||
doc="The output mode. Supported output modes vary for SIS3801 and SIS3820.",
|
||||
)
|
||||
output_polarity = Cpt(
|
||||
EpicsSignal,
|
||||
"OutputPolarity",
|
||||
kind=Kind.omitted,
|
||||
doc="The polarity of the output control signals on the SIS3820. Choices are Normal and Inverted.",
|
||||
)
|
||||
model = Cpt(
|
||||
EpicsSignalRO,
|
||||
"Model",
|
||||
kind=Kind.omitted,
|
||||
doc="The scaler model. Values are 'SIS3801' and 'SIS3820'.",
|
||||
)
|
||||
firmware = Cpt(EpicsSignalRO, "Firmware", kind=Kind.omitted, doc="The firmware version.")
|
||||
max_channels = Cpt(
|
||||
EpicsSignalRO, "MaxChannels", kind=Kind.omitted, doc="The maximum number of channels."
|
||||
)
|
||||
|
||||
# Relevant counters
|
||||
current_channel = Cpt(
|
||||
EpicsSignalRO,
|
||||
"CurrentChannel",
|
||||
kind=Kind.omitted,
|
||||
auto_monitor=True,
|
||||
doc="The current channel number, i.e. the number of channel advances that have occurred minus 1.",
|
||||
)
|
||||
counters = DynamicDeviceComponent(
|
||||
_create_mca_channels(32),
|
||||
kind=Kind.omitted,
|
||||
doc="Sub-device with the mca counters 1-32 for SIS3820.",
|
||||
)
|
||||
284
csaxs_bec/devices/epics/mcs_card/mcs_card_csaxs.py
Normal file
284
csaxs_bec/devices/epics/mcs_card/mcs_card_csaxs.py
Normal file
@@ -0,0 +1,284 @@
|
||||
"""Module for the MCSCard CSAXS implementation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
from threading import RLock
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
from bec_lib.logger import bec_logger
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, EpicsSignalRO, Kind, Signal
|
||||
from ophyd_devices import CompareStatus, ProgressSignal, TransitionStatus
|
||||
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
|
||||
|
||||
from csaxs_bec.devices.epics.mcs_card.mcs_card import (
|
||||
ACQUIREMODE,
|
||||
ACQUIRING,
|
||||
CHANNEL1SOURCE,
|
||||
CHANNELADVANCE,
|
||||
INPUTMODE,
|
||||
OUTPUTMODE,
|
||||
POLARITY,
|
||||
READMODE,
|
||||
MCSCard,
|
||||
)
|
||||
from csaxs_bec.devices.epics.xbpms import DiffXYSignal, SumSignal
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from bec_lib.devicemanager import DeviceManagerBase, ScanInfo
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class READYTOREAD(int, enum.Enum):
|
||||
|
||||
PROCESSING = 0
|
||||
DONE = 1
|
||||
|
||||
|
||||
class BPMDevice(Device):
|
||||
"""Class for BPM device of the MCSCard."""
|
||||
|
||||
current1 = Cpt(Signal, kind=Kind.normal, doc="Normalized current 1")
|
||||
current2 = Cpt(Signal, kind=Kind.normal, doc="Normalized current 2")
|
||||
current3 = Cpt(Signal, kind=Kind.normal, doc="Normalized current 3")
|
||||
current4 = Cpt(Signal, kind=Kind.normal, doc="Normalized current 4")
|
||||
count_time = Cpt(Signal, kind=Kind.normal, doc="Count time for bpm signal counts")
|
||||
sum = Cpt(SumSignal, kind="hinted", doc="Sum of all currents")
|
||||
x = Cpt(
|
||||
DiffXYSignal,
|
||||
sum1=["current1", "current2"],
|
||||
sum2=["current3", "current4"],
|
||||
doc="X difference signal",
|
||||
)
|
||||
y = Cpt(
|
||||
DiffXYSignal,
|
||||
sum1=["current1", "current3"],
|
||||
sum2=["current2", "current4"],
|
||||
doc="Y difference signal",
|
||||
)
|
||||
diag = Cpt(
|
||||
DiffXYSignal,
|
||||
sum1=["current1", "current4"],
|
||||
sum2=["current2", "current3"],
|
||||
doc="Diagonal difference signal",
|
||||
)
|
||||
|
||||
|
||||
class MCSRaw(Device):
|
||||
"""Class for BPM device of the MCSCard with normalized currents."""
|
||||
|
||||
mca1 = Cpt(Signal, kind=Kind.normal, doc="Raw counts on mca1 channel")
|
||||
mca2 = Cpt(Signal, kind=Kind.normal, doc="Raw counts on mca2 channel")
|
||||
mca3 = Cpt(Signal, kind=Kind.normal, doc="Raw counts on mca3 channel")
|
||||
mca4 = Cpt(Signal, kind=Kind.normal, doc="Raw counts on mca4 channel")
|
||||
mca5 = Cpt(Signal, kind=Kind.normal, doc="Raw counts on mca5 channel")
|
||||
|
||||
|
||||
class MCSCardCSAXS(PSIDeviceBase, MCSCard):
|
||||
"""
|
||||
Implementation of the MCSCard SIS3820 for CSAXS, prefix 'X12SA-MCS:'.
|
||||
The basic functionality is inherited from the MCSCard class.
|
||||
"""
|
||||
|
||||
ready_to_read = Cpt(
|
||||
Signal,
|
||||
kind=Kind.omitted,
|
||||
doc="Signal that indicates if mcs card is ready to be read from after triggers. 0 not ready, 1 ready",
|
||||
)
|
||||
progress: ProgressSignal = Cpt(ProgressSignal, name="progress")
|
||||
# Make this an async signal..
|
||||
mcs = Cpt(
|
||||
MCSRaw,
|
||||
name="mcs",
|
||||
kind=Kind.normal,
|
||||
doc="MCS device with raw current and count time readings",
|
||||
)
|
||||
bpm = Cpt(
|
||||
BPMDevice,
|
||||
name="bpm",
|
||||
kind=Kind.normal,
|
||||
doc="BPM device for MCSCard with count times and normalized currents",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
prefix: str = "",
|
||||
scan_info: ScanInfo | None = None,
|
||||
device_manager: DeviceManagerBase | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Initialize the MCSCardCSAXS with the given arguments and keyword arguments.
|
||||
"""
|
||||
super().__init__(
|
||||
name=name, prefix=prefix, scan_info=scan_info, device_manager=device_manager, **kwargs
|
||||
)
|
||||
self._mcs_clock = 1e7 # 10MHz clock -> 1e7 Hz
|
||||
self._pv_timeout = 3 # TODO remove timeout once #129 in ophyd_devices is solved
|
||||
self._rlock = RLock() # Needed to ensure thread safety for counter updates
|
||||
self.counter_mapping = { # Any mca counter that should be updated has to be added here
|
||||
f"{self.counters.name}_mca1": "current1",
|
||||
f"{self.counters.name}_mca2": "current2",
|
||||
f"{self.counters.name}_mca3": "current3",
|
||||
f"{self.counters.name}_mca4": "current4",
|
||||
f"{self.counters.name}_mca5": "count_time",
|
||||
}
|
||||
self.counter_updated = []
|
||||
|
||||
def on_connected(self):
|
||||
"""
|
||||
Called when the device is connected.
|
||||
"""
|
||||
# Make sure card is not running
|
||||
self.stop_all.put(1)
|
||||
|
||||
# TODO Check channel1_source !!
|
||||
self.channel_advance.set(CHANNELADVANCE.EXTERNAL).wait(timeout=self._pv_timeout)
|
||||
self.channel1_source.set(CHANNEL1SOURCE.EXTERNAL).wait(timeout=self._pv_timeout)
|
||||
self.prescale.set(1).wait(timeout=self._pv_timeout)
|
||||
# Set the user LED to off
|
||||
self.user_led.set(0).wait(timeout=self._pv_timeout)
|
||||
# Only channel 1-5 are connected so far, adjust if more are needed
|
||||
self.mux_output.set(5).wait(timeout=self._pv_timeout)
|
||||
# Set the input and output modes & polarities
|
||||
self.input_mode.set(INPUTMODE.MODE_3).wait(timeout=self._pv_timeout)
|
||||
self.input_polarity.set(POLARITY.NORMAL).wait(timeout=self._pv_timeout)
|
||||
self.output_mode.set(OUTPUTMODE.MODE_2).wait(timeout=self._pv_timeout)
|
||||
self.output_polarity.set(POLARITY.NORMAL).wait(timeout=self._pv_timeout)
|
||||
self.count_on_start.set(0).wait(timeout=self._pv_timeout)
|
||||
|
||||
# Set appropriate read mode
|
||||
self.read_mode.set(READMODE.PASSIVE).wait(timeout=self._pv_timeout)
|
||||
|
||||
# Set the acquire mode
|
||||
self.acquire_mode.set(ACQUIREMODE.MCS).wait(timeout=self._pv_timeout)
|
||||
|
||||
# Subscribe the progress signal
|
||||
self.current_channel.subscribe(self._progress_update, run=False)
|
||||
|
||||
# Subscribe to the mca updates
|
||||
for name in self.counter_mapping.keys():
|
||||
sig: EpicsSignalRO = getattr(self.counters, name.split("_")[-1])
|
||||
sig.subscribe(self._on_counter_update, run=False)
|
||||
|
||||
def _on_counter_update(self, value, **kwargs) -> None:
|
||||
"""
|
||||
Callback for counter updates of the mca channels (1-32).
|
||||
|
||||
The raw data is pushed to the mcs sub-device (MCSRaw). We need to ensure that
|
||||
the MCSRaw device has all signals defined for which we want to push the values.
|
||||
|
||||
As we may receive multiple readings per point, e.g. if frames_per_trigger > 1,
|
||||
we also create a mean value for the counter signals. These are then pushed to the bpm device
|
||||
for plotting and further processing. The signal names are defined and mapped in the
|
||||
self.counter_mapping dictionary & the bpm sub-device.
|
||||
|
||||
There are multiple mca channels, each giving individual updates. We want to ensure that
|
||||
each is updated before we signal that we are ready to read. In future, these signals may
|
||||
become asynchronous, but we first need to ensure that we can properly combine monitored
|
||||
signals with async signals for plotting. Until then, we will keep this logic.
|
||||
"""
|
||||
with self._rlock:
|
||||
# Retrieve the signal object which executes this callback
|
||||
signal = kwargs.get("obj", None)
|
||||
if signal is None: # This should never happen, but just in case
|
||||
logger.info(f"Called without 'obj' in kwargs: {kwargs}")
|
||||
return
|
||||
# Get the maped signal name from the mapping dictionary
|
||||
mapped_signal_name = self.counter_mapping.get(signal.name, None)
|
||||
# If we did not map the signal name in counter_mapping, but receive an update
|
||||
# we will skip it.
|
||||
if mapped_signal_name is None:
|
||||
return
|
||||
# Push the raw values of the mca channels. The signal name has to be defined
|
||||
# in the self.mcs sub-device (MCSRaw) to be able to push the values. Otherwise
|
||||
# we will skip the update.
|
||||
mca_raw = getattr(self.mcs, signal.name.split("_")[-1], None)
|
||||
if mca_raw is None:
|
||||
return
|
||||
# In case there was more than one value received, i.e. frames_per_trigger > 1,
|
||||
# we will receive a np.array of values.
|
||||
if isinstance(value, np.ndarray):
|
||||
# We push the raw values as a list to the mca_raw signal
|
||||
# And otherwise compute the mean value for plotting of counter signals
|
||||
mca_raw.put(value.tolist())
|
||||
# compute the count_time in seconds
|
||||
if mapped_signal_name == "count_time":
|
||||
value = value / self._mcs_clock
|
||||
value = float(value.mean())
|
||||
else:
|
||||
# We received a single value, so we can directly push it
|
||||
mca_raw.put(value)
|
||||
# compute the count_time in seconds
|
||||
if mapped_signal_name == "count_time":
|
||||
value = value / self._mcs_clock
|
||||
|
||||
# Get the mapped signal from the bpm device and update it
|
||||
sig = getattr(self.bpm, mapped_signal_name)
|
||||
sig.put(value)
|
||||
self.counter_updated.append(signal.name)
|
||||
# Once all mca channels have been updated, we can signal that we are ready to read
|
||||
received_all_updates = set(self.counter_updated) == set(self.counter_mapping.keys())
|
||||
if received_all_updates:
|
||||
self.ready_to_read.put(READYTOREAD.DONE)
|
||||
# The reset of the signal is done in the on_trigger method of ddg1 for the next trigger
|
||||
self.counter_updated.clear() # Clear the list for the next update cycle
|
||||
|
||||
def _progress_update(self, value, **kwargs) -> None:
|
||||
"""Callback for progress updates from ophyd subscription on current_channel."""
|
||||
# This logic needs to be further refined as this is currently reporting the progress
|
||||
# of a single trigger from BEC within a burst scan.
|
||||
frames_per_trigger = self.scan_info.msg.scan_parameters.get("frames_per_trigger", 1)
|
||||
self.progress.put(
|
||||
value=value, max_value=frames_per_trigger, done=bool(value == frames_per_trigger)
|
||||
)
|
||||
|
||||
def on_stage(self) -> None:
|
||||
"""
|
||||
Called when the device is staged.
|
||||
"""
|
||||
self.erase_all.set(1).wait(timeout=self._pv_timeout)
|
||||
triggers = self.scan_info.msg.scan_parameters.get("frames_per_trigger", 1)
|
||||
self.preset_real.set(0).wait(timeout=self._pv_timeout)
|
||||
self.num_use_all.set(triggers).wait(timeout=self._pv_timeout)
|
||||
|
||||
def on_unstage(self) -> None:
|
||||
"""
|
||||
Called when the device is unstaged.
|
||||
"""
|
||||
self.stop_all.put(1)
|
||||
self.ready_to_read.put(READYTOREAD.DONE)
|
||||
# TODO why 0?
|
||||
self.erase_all.set(0).wait(timeout=self._pv_timeout)
|
||||
|
||||
def on_trigger(self) -> None:
|
||||
status = TransitionStatus(
|
||||
self.ready_to_read, strict=True, transitions=[READYTOREAD.PROCESSING, READYTOREAD.DONE]
|
||||
)
|
||||
self.cancel_on_stop(status)
|
||||
return status
|
||||
|
||||
def on_pre_scan(self) -> None:
|
||||
"""
|
||||
Called before the scan starts.
|
||||
"""
|
||||
|
||||
def on_complete(self) -> CompareStatus:
|
||||
"""On scan completion."""
|
||||
# Check if we should get a signal based on updates from the MCA channels
|
||||
status = CompareStatus(self.acquiring, ACQUIRING.DONE)
|
||||
self.cancel_on_stop(status)
|
||||
return status
|
||||
|
||||
def on_stop(self) -> None:
|
||||
"""
|
||||
Called when the scan is stopped.
|
||||
"""
|
||||
self.stop_all.put(1)
|
||||
self.ready_to_read.put(READYTOREAD.DONE)
|
||||
# Reset the progress signal
|
||||
# self.progress.put(0, done=True)
|
||||
@@ -1,319 +0,0 @@
|
||||
import enum
|
||||
import threading
|
||||
from collections import defaultdict
|
||||
|
||||
import numpy as np
|
||||
from bec_lib import bec_logger, messages
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, EpicsSignal, EpicsSignalRO
|
||||
from ophyd_devices.interfaces.base_classes.psi_detector_base import (
|
||||
CustomDetectorMixin,
|
||||
PSIDetectorBase,
|
||||
)
|
||||
from ophyd_devices.utils import bec_utils
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class MCSError(Exception):
|
||||
"""Base class for exceptions in this module."""
|
||||
|
||||
|
||||
class MCSTimeoutError(MCSError):
|
||||
"""Raise when MCS card runs into a timeout"""
|
||||
|
||||
|
||||
class TriggerSource(int, enum.Enum):
|
||||
"""Trigger source for mcs card - see manual for more information"""
|
||||
|
||||
MODE0 = 0
|
||||
MODE1 = 1
|
||||
MODE2 = 2
|
||||
MODE3 = 3
|
||||
MODE4 = 4
|
||||
MODE5 = 5
|
||||
MODE6 = 6
|
||||
|
||||
|
||||
class ChannelAdvance(int, enum.Enum):
|
||||
"""Channel advance pixel mode for mcs card - see manual for more information"""
|
||||
|
||||
INTERNAL = 0
|
||||
EXTERNAL = 1
|
||||
|
||||
|
||||
class ReadoutMode(int, enum.Enum):
|
||||
"""Readout mode for mcs card - see manual for more information"""
|
||||
|
||||
PASSIVE = 0
|
||||
EVENT = 1
|
||||
IO_INTR = 2
|
||||
FREQ_0_1HZ = 3
|
||||
FREQ_0_2HZ = 4
|
||||
FREQ_0_5HZ = 5
|
||||
FREQ_1HZ = 6
|
||||
FREQ_2HZ = 7
|
||||
FREQ_5HZ = 8
|
||||
FREQ_10HZ = 9
|
||||
FREQ_100HZ = 10
|
||||
|
||||
|
||||
class MCSSetup(CustomDetectorMixin):
|
||||
"""Setup mixin class for the MCS card"""
|
||||
|
||||
def __init__(self, *args, parent: Device = None, **kwargs) -> None:
|
||||
super().__init__(*args, parent=parent, **kwargs)
|
||||
self._lock = threading.RLock()
|
||||
self._stream_ttl = 1800
|
||||
self.acquisition_done = False
|
||||
self.counter = 0
|
||||
self.n_points = 0
|
||||
self.mca_names = [
|
||||
signal for signal in self.parent.component_names if signal.startswith("mca")
|
||||
]
|
||||
self.mca_data = defaultdict(lambda: [])
|
||||
|
||||
def on_init(self) -> None:
|
||||
"""Init sequence for the detector"""
|
||||
self.initialize_detector()
|
||||
self.initialize_detector_backend()
|
||||
|
||||
def initialize_detector(self) -> None:
|
||||
"""Initialize detector"""
|
||||
# External trigger for pixel advance
|
||||
self.parent.channel_advance.set(ChannelAdvance.EXTERNAL)
|
||||
# Use internal clock for channel 1
|
||||
self.parent.channel1_source.set(ChannelAdvance.INTERNAL)
|
||||
self.parent.user_led.set(0)
|
||||
# Set number of channels to 5
|
||||
self.parent.mux_output.set(5)
|
||||
# Trigger Mode used for cSAXS
|
||||
self.parent.input_mode.set(TriggerSource.MODE3)
|
||||
# specify polarity of trigger signals
|
||||
self.parent.input_polarity.set(0)
|
||||
self.parent.output_polarity.set(1)
|
||||
# do not start counting on start
|
||||
self.parent.count_on_start.set(0)
|
||||
self.stop_detector()
|
||||
|
||||
def initialize_detector_backend(self) -> None:
|
||||
"""Initialize detector backend"""
|
||||
for mca in self.mca_names:
|
||||
signal = getattr(self.parent, mca)
|
||||
signal.subscribe(self._on_mca_data, run=False)
|
||||
self.parent.current_channel.subscribe(self._progress_update, run=False)
|
||||
|
||||
def _progress_update(self, value, **kwargs) -> None:
|
||||
"""Progress update on the scan"""
|
||||
num_lines = self.parent.num_lines.get()
|
||||
max_value = self.parent.scaninfo.num_points
|
||||
# self.counter seems to be a deprecated variable from a former implementation of the mcs card
|
||||
# pylint: disable=protected-access
|
||||
self.parent._run_subs(
|
||||
sub_type=self.parent.SUB_PROGRESS,
|
||||
value=self.counter * int(self.parent.scaninfo.num_points / num_lines) + value,
|
||||
max_value=max_value,
|
||||
# TODO check if that is correct with
|
||||
done=bool(max_value == value), # == self.counter),
|
||||
)
|
||||
|
||||
def _on_mca_data(self, *args, obj=None, value=None, **kwargs) -> None:
|
||||
"""Callback function for scan progress"""
|
||||
with self._lock:
|
||||
if not isinstance(value, (list, np.ndarray)):
|
||||
return
|
||||
self.mca_data[obj.attr_name] = value
|
||||
if len(self.mca_names) != len(self.mca_data):
|
||||
return
|
||||
self.acquisition_done = True
|
||||
self._send_data_to_bec()
|
||||
self.mca_data = defaultdict(lambda: [])
|
||||
|
||||
def _send_data_to_bec(self) -> None:
|
||||
"""Sends bundled data to BEC"""
|
||||
if self.parent.scaninfo.scan_msg is None:
|
||||
return
|
||||
metadata = self.parent.scaninfo.scan_msg.metadata
|
||||
metadata.update({"async_update": "append", "num_lines": self.parent.num_lines.get()})
|
||||
msg = messages.DeviceMessage(
|
||||
signals=dict(self.mca_data), metadata=self.parent.scaninfo.scan_msg.metadata
|
||||
)
|
||||
self.parent.connector.xadd(
|
||||
topic=MessageEndpoints.device_async_readback(
|
||||
scan_id=self.parent.scaninfo.scan_id, device=self.parent.name
|
||||
),
|
||||
msg={"data": msg},
|
||||
expire=self._stream_ttl,
|
||||
)
|
||||
|
||||
def on_stage(self) -> None:
|
||||
"""Stage detector"""
|
||||
self.prepare_detector()
|
||||
self.prepare_detector_backend()
|
||||
|
||||
def prepare_detector(self) -> None:
|
||||
"""Prepare detector for scan"""
|
||||
self.set_acquisition_params()
|
||||
self.parent.input_mode.set(TriggerSource.MODE3)
|
||||
|
||||
def set_acquisition_params(self) -> None:
|
||||
"""Set acquisition parameters for scan"""
|
||||
if self.parent.scaninfo.scan_type == "step":
|
||||
self.n_points = int(self.parent.scaninfo.frames_per_trigger) * int(
|
||||
self.parent.scaninfo.num_points
|
||||
)
|
||||
elif self.parent.scaninfo.scan_type == "fly":
|
||||
self.n_points = int(self.parent.scaninfo.num_points) # / int(self.num_lines.get()))
|
||||
else:
|
||||
raise MCSError(f"Scantype {self.parent.scaninfo} not implemented for MCS card")
|
||||
if self.n_points > 10000:
|
||||
raise MCSError(
|
||||
f"Requested number of points N={self.n_points} exceeds hardware limit of mcs card"
|
||||
" 10000 (N-1)"
|
||||
)
|
||||
self.parent.num_use_all.set(self.n_points)
|
||||
self.parent.preset_real.set(0)
|
||||
|
||||
def prepare_detector_backend(self) -> None:
|
||||
"""Prepare detector backend for scan"""
|
||||
self.parent.erase_all.set(1)
|
||||
self.parent.read_mode.set(ReadoutMode.EVENT)
|
||||
|
||||
def arm_acquisition(self) -> None:
|
||||
"""Arm detector for acquisition"""
|
||||
self.counter = 0
|
||||
self.parent.erase_start.set(1)
|
||||
|
||||
def on_unstage(self) -> None:
|
||||
"""Unstage detector"""
|
||||
pass
|
||||
|
||||
def on_complete(self) -> None:
|
||||
"""Complete detector"""
|
||||
self.finished(timeout=self.parent.TIMEOUT_FOR_SIGNALS)
|
||||
|
||||
def finished(self, timeout: int = 5) -> None:
|
||||
"""Check if acquisition is finished, if not successful, rais MCSTimeoutError"""
|
||||
signal_conditions = [
|
||||
(lambda: self.acquisition_done, True),
|
||||
(self.parent.acquiring.get, 0), # Considering making a enum.Int class for this state
|
||||
]
|
||||
if not self.wait_for_signals(
|
||||
signal_conditions=signal_conditions,
|
||||
timeout=timeout,
|
||||
check_stopped=True,
|
||||
all_signals=True,
|
||||
):
|
||||
total_frames = self.counter * int(
|
||||
self.parent.scaninfo.num_points / self.parent.num_lines.get()
|
||||
) + max(self.parent.current_channel.get(), 0)
|
||||
raise MCSTimeoutError(
|
||||
f"Reached timeout with mcs in state {self.parent.acquiring.get()} and"
|
||||
f" {total_frames} frames arriving at the mcs card"
|
||||
)
|
||||
|
||||
def on_stop(self) -> None:
|
||||
"""Stop detector"""
|
||||
self.stop_detector()
|
||||
self.stop_detector_backend()
|
||||
|
||||
def stop_detector(self) -> None:
|
||||
"""Stop detector"""
|
||||
self.parent.stop_all.set(1)
|
||||
|
||||
def stop_detector_backend(self) -> None:
|
||||
"""Stop acquisition of data"""
|
||||
self.acquisition_done = True
|
||||
|
||||
|
||||
class SIS38XX(Device):
|
||||
"""SIS38XX card for access to EPICs PVs at cSAXS beamline"""
|
||||
|
||||
|
||||
class MCScSAXS(PSIDetectorBase):
|
||||
"""MCS card for cSAXS for implementation at cSAXS beamline"""
|
||||
|
||||
USER_ACCESS = []
|
||||
SUB_PROGRESS = "progress"
|
||||
SUB_VALUE = "value"
|
||||
_default_sub = SUB_VALUE
|
||||
|
||||
# specify Setup class
|
||||
custom_prepare_cls = MCSSetup
|
||||
# specify minimum readout time for detector
|
||||
MIN_READOUT = 0
|
||||
TIMEOUT_FOR_SIGNALS = 5
|
||||
|
||||
# PV access to SISS38XX card
|
||||
# Acquisition
|
||||
erase_all = Cpt(EpicsSignal, "EraseAll")
|
||||
erase_start = Cpt(EpicsSignal, "EraseStart") # ,trigger_value=1
|
||||
start_all = Cpt(EpicsSignal, "StartAll")
|
||||
stop_all = Cpt(EpicsSignal, "StopAll")
|
||||
acquiring = Cpt(EpicsSignal, "Acquiring")
|
||||
preset_real = Cpt(EpicsSignal, "PresetReal")
|
||||
elapsed_real = Cpt(EpicsSignal, "ElapsedReal")
|
||||
read_mode = Cpt(EpicsSignal, "ReadAll.SCAN")
|
||||
read_all = Cpt(EpicsSignal, "DoReadAll.VAL") # ,trigger_value=1
|
||||
num_use_all = Cpt(EpicsSignal, "NuseAll")
|
||||
current_channel = Cpt(EpicsSignal, "CurrentChannel")
|
||||
dwell = Cpt(EpicsSignal, "Dwell")
|
||||
channel_advance = Cpt(EpicsSignal, "ChannelAdvance")
|
||||
count_on_start = Cpt(EpicsSignal, "CountOnStart")
|
||||
software_channel_advance = Cpt(EpicsSignal, "SoftwareChannelAdvance")
|
||||
channel1_source = Cpt(EpicsSignal, "Channel1Source")
|
||||
prescale = Cpt(EpicsSignal, "Prescale")
|
||||
enable_client_wait = Cpt(EpicsSignal, "EnableClientWait")
|
||||
client_wait = Cpt(EpicsSignal, "ClientWait")
|
||||
acquire_mode = Cpt(EpicsSignal, "AcquireMode")
|
||||
mux_output = Cpt(EpicsSignal, "MUXOutput")
|
||||
user_led = Cpt(EpicsSignal, "UserLED")
|
||||
input_mode = Cpt(EpicsSignal, "InputMode")
|
||||
input_polarity = Cpt(EpicsSignal, "InputPolarity")
|
||||
output_mode = Cpt(EpicsSignal, "OutputMode")
|
||||
output_polarity = Cpt(EpicsSignal, "OutputPolarity")
|
||||
model = Cpt(EpicsSignalRO, "Model", string=True)
|
||||
firmware = Cpt(EpicsSignalRO, "Firmware")
|
||||
max_channels = Cpt(EpicsSignalRO, "MaxChannels")
|
||||
|
||||
# PV access to MCA signals
|
||||
mca1 = Cpt(EpicsSignalRO, "mca1.VAL", auto_monitor=True)
|
||||
mca3 = Cpt(EpicsSignalRO, "mca3.VAL", auto_monitor=True)
|
||||
mca4 = Cpt(EpicsSignalRO, "mca4.VAL", auto_monitor=True)
|
||||
current_channel = Cpt(EpicsSignalRO, "CurrentChannel", auto_monitor=True)
|
||||
|
||||
# Custom signal readout from device config
|
||||
num_lines = Cpt(
|
||||
bec_utils.ConfigSignal, name="num_lines", kind="config", config_storage_name="mcs_config"
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
prefix="",
|
||||
*,
|
||||
name,
|
||||
kind=None,
|
||||
parent=None,
|
||||
device_manager=None,
|
||||
mcs_config=None,
|
||||
**kwargs,
|
||||
):
|
||||
self.mcs_config = {f"{name}_num_lines": 1}
|
||||
if mcs_config is not None:
|
||||
# pylint: disable=expression-not-assigned
|
||||
[self.mcs_config.update({f"{name}_{key}": value}) for key, value in mcs_config.items()]
|
||||
|
||||
super().__init__(
|
||||
prefix=prefix,
|
||||
name=name,
|
||||
kind=kind,
|
||||
parent=parent,
|
||||
device_manager=device_manager,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
# Automatically connect to test environmenr if directly invoked
|
||||
if __name__ == "__main__":
|
||||
mcs = MCScSAXS(name="mcs", prefix="X12SA-MCS:", sim_mode=True)
|
||||
127
csaxs_bec/devices/epics/xbpms.py
Normal file
127
csaxs_bec/devices/epics/xbpms.py
Normal file
@@ -0,0 +1,127 @@
|
||||
import time
|
||||
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, EpicsSignalRO, Signal
|
||||
|
||||
|
||||
class SumSignal(Signal):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._metadata.update(write_access=False)
|
||||
|
||||
def wait_for_connection(self, timeout=0):
|
||||
super().wait_for_connection(timeout)
|
||||
self._metadata.update(connected=True)
|
||||
|
||||
def get(self, **kwargs):
|
||||
self._metadata["timestamp"] = time.time()
|
||||
val1 = self.parent.current1.get()
|
||||
val2 = self.parent.current2.get()
|
||||
val3 = self.parent.current3.get()
|
||||
val4 = self.parent.current4.get()
|
||||
return val1 + val2 + val3 + val4
|
||||
|
||||
def describe(self):
|
||||
source = [
|
||||
self.parent.current1.describe()[self.parent.current1.name]["source"],
|
||||
self.parent.current2.describe()[self.parent.current2.name]["source"],
|
||||
self.parent.current3.describe()[self.parent.current3.name]["source"],
|
||||
self.parent.current4.describe()[self.parent.current4.name]["source"],
|
||||
]
|
||||
source = " / ".join(source)
|
||||
desc = {
|
||||
"shape": [],
|
||||
"dtype": "number",
|
||||
"source": f"PV: {source}",
|
||||
"units": "",
|
||||
"precision": (
|
||||
self.parent.current1.precision if hasattr(self.parent.current1, "precision") else 0
|
||||
),
|
||||
}
|
||||
return desc
|
||||
|
||||
|
||||
class DiffXYSignal(Signal):
|
||||
def __init__(self, sum1, sum2, *args, **kwargs):
|
||||
self.sum1 = sum1
|
||||
self.sum2 = sum2
|
||||
super().__init__(*args, **kwargs)
|
||||
self._metadata.update(write_access=False)
|
||||
|
||||
def wait_for_connection(self, timeout=0):
|
||||
super().wait_for_connection(timeout)
|
||||
self._metadata.update(connected=True)
|
||||
|
||||
def get(self, **kwargs):
|
||||
self._metadata["timestamp"] = time.time()
|
||||
summed_1 = 0
|
||||
summed_2 = 0
|
||||
for signal in self.sum1:
|
||||
summed_1 += getattr(self.parent, signal).get()
|
||||
for signal in self.sum2:
|
||||
summed_2 += getattr(self.parent, signal).get()
|
||||
|
||||
_sum = summed_1 + summed_2
|
||||
if _sum == 0:
|
||||
return 0.0
|
||||
return (summed_1 - summed_2) / _sum
|
||||
|
||||
def describe(self):
|
||||
source = [
|
||||
getattr(self.parent, signal).describe()[getattr(self.parent, signal).name]["source"]
|
||||
for signal in self.sum1 + self.sum2
|
||||
]
|
||||
source = " / ".join(source)
|
||||
desc = {
|
||||
"shape": [],
|
||||
"dtype": "number",
|
||||
"source": f"PV: {source}",
|
||||
"units": "",
|
||||
"precision": (
|
||||
self.parent.current1.precision if hasattr(self.parent.current1, "precision") else 0
|
||||
),
|
||||
}
|
||||
return desc
|
||||
|
||||
|
||||
class BPMDevice(Device):
|
||||
current1 = Cpt(
|
||||
EpicsSignalRO, ":Current1:MeanValue_RBV", kind="normal", doc="Current 1", auto_monitor=True
|
||||
)
|
||||
current2 = Cpt(
|
||||
EpicsSignalRO, ":Current2:MeanValue_RBV", kind="normal", doc="Current 2", auto_monitor=True
|
||||
)
|
||||
current3 = Cpt(
|
||||
EpicsSignalRO, ":Current3:MeanValue_RBV", kind="normal", doc="Current 3", auto_monitor=True
|
||||
)
|
||||
current4 = Cpt(
|
||||
EpicsSignalRO, ":Current4:MeanValue_RBV", kind="normal", doc="Current 4", auto_monitor=True
|
||||
)
|
||||
sum = Cpt(SumSignal, kind="hinted", doc="Sum of all currents")
|
||||
x = Cpt(
|
||||
DiffXYSignal,
|
||||
sum1=["current1", "current2"],
|
||||
sum2=["current3", "current4"],
|
||||
doc="X difference signal",
|
||||
)
|
||||
y = Cpt(
|
||||
DiffXYSignal,
|
||||
sum1=["current1", "current3"],
|
||||
sum2=["current2", "current4"],
|
||||
doc="Y difference signal",
|
||||
)
|
||||
diag = Cpt(
|
||||
DiffXYSignal,
|
||||
sum1=["current1", "current4"],
|
||||
sum2=["current2", "current3"],
|
||||
doc="Diagonal difference signal",
|
||||
)
|
||||
|
||||
def __init__(self, prefix="", *args, **kwargs):
|
||||
super().__init__(*args, prefix=prefix, **kwargs)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
dev = BPMDevice(name="bpm", prefix="X12SA-FE-XBPM1")
|
||||
dev.wait_for_connection()
|
||||
print(dev.read())
|
||||
1
csaxs_bec/devices/ids_cameras/__init__.py
Normal file
1
csaxs_bec/devices/ids_cameras/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .ids_camera_new import IDSCamera
|
||||
274
csaxs_bec/devices/ids_cameras/base_integration/camera.py
Normal file
274
csaxs_bec/devices/ids_cameras/base_integration/camera.py
Normal file
@@ -0,0 +1,274 @@
|
||||
"""
|
||||
This module provides a Camera class for handling IDS cameras using the pyueye library,
|
||||
that links to the vendors C++ SDK. Details about the camera's C++ SDK API can be found
|
||||
in the IDS Software Suite 4.96.1 documentation:
|
||||
(https://www.1stvision.com/cameras/IDS/IDS-manuals/uEye_Manual/sdk_einleitung_schnellstart.html)
|
||||
|
||||
Here, we follow a procedure to set up the camera, configure its basic parameters and
|
||||
allow automated capturing of images. The IDSCameraObject class is the low-level interface,
|
||||
and requires the pyueye library and appropriate DLL files on the system. The Camera class
|
||||
provides a high level interface which only creates the IDSCameraObject instance when the
|
||||
on_connect method is called. This allows for lazy initialization of the camera, and
|
||||
CI/CD pipelines can run without the pyueye library or the related DLLs installed on the system.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import atexit
|
||||
from typing import Literal
|
||||
|
||||
import numpy as np
|
||||
from bec_lib.logger import bec_logger
|
||||
|
||||
from csaxs_bec.devices.ids_cameras.base_integration.utils import check_error
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
try:
|
||||
from pyueye import ueye
|
||||
except ImportError as exc:
|
||||
logger.warning(f"The pyueye library is not properly installed : {exc}")
|
||||
ueye = None # type: ignore[assignment]
|
||||
|
||||
|
||||
class IDSCameraObject:
|
||||
"""Low-level base class for IDS Camera object.
|
||||
|
||||
Args:
|
||||
device_id (int): The ID of the camera device. # e.g. 201; check idscamera tool
|
||||
m_n_colormode (int): Color mode for the camera. # 1 for cSAXS color cameras
|
||||
bits_per_pixel (int): Number of bits per pixel for the camera. # 24 for color cameras, 8 for monochrome cameras
|
||||
"""
|
||||
|
||||
def __init__(self, device_id: int, m_n_colormode, bits_per_pixel):
|
||||
if ueye is None:
|
||||
raise ImportError(
|
||||
"The pyueye library is not installed or library files are missing. Please check your Python environment or library paths."
|
||||
)
|
||||
self.ueye = ueye
|
||||
self._device_id = device_id
|
||||
self.h_cam = ueye.HIDS(device_id)
|
||||
self.s_info = ueye.SENSORINFO()
|
||||
self.c_info = ueye.CAMINFO()
|
||||
self.rect_roi = ueye.IS_RECT()
|
||||
self.pc_image_mem = ueye.c_mem_p()
|
||||
self.mem_id = ueye.int()
|
||||
self.pitch = ueye.INT()
|
||||
self.m_n_colormode = ueye.INT(m_n_colormode)
|
||||
self.n_bits_per_pixel = ueye.INT(bits_per_pixel)
|
||||
self.bytes_per_pixel = int(self.n_bits_per_pixel / 8)
|
||||
|
||||
# Sequence to initialize the camera
|
||||
check_error(ueye.is_InitCamera(self.h_cam, None), "IDSCameraObject")
|
||||
check_error(ueye.is_GetSensorInfo(self.h_cam, self.s_info), "IDSCameraObject")
|
||||
check_error(ueye.is_GetCameraInfo(self.h_cam, self.c_info), "IDSCameraObject")
|
||||
check_error(ueye.is_ResetToDefault(self.h_cam), "IDSCameraObject")
|
||||
check_error(ueye.is_SetDisplayMode(self.h_cam, ueye.IS_SET_DM_DIB), "IDSCameraObject")
|
||||
|
||||
if (
|
||||
int.from_bytes(self.s_info.nColorMode.value, byteorder="big")
|
||||
== self.ueye.IS_COLORMODE_BAYER
|
||||
):
|
||||
logger.info("Bayer color mode detected.")
|
||||
# setup the color depth to the current windows setting
|
||||
self.ueye.is_GetColorDepth(
|
||||
self.h_cam, self.n_bits_per_pixel, self.m_n_colormode
|
||||
) # TODO This raises an error - maybe check the m_n_colormode value
|
||||
self.bytes_per_pixel = int(self.n_bits_per_pixel / 8)
|
||||
elif (
|
||||
int.from_bytes(self.s_info.nColorMode.value, byteorder="big")
|
||||
== self.ueye.IS_COLORMODE_CBYCRY
|
||||
):
|
||||
# for color camera models use RGB32 mode
|
||||
self.m_n_colormode = self.ueye.IS_CM_BGRA8_PACKED
|
||||
self.n_bits_per_pixel = self.ueye.INT(32)
|
||||
self.bytes_per_pixel = int(self.n_bits_per_pixel / 8)
|
||||
elif (
|
||||
int.from_bytes(self.s_info.nColorMode.value, byteorder="big")
|
||||
== self.ueye.IS_COLORMODE_MONOCHROME
|
||||
):
|
||||
# for color camera models use RGB32 mode
|
||||
self.m_n_colormode = self.ueye.IS_CM_MONO8
|
||||
self.n_bits_per_pixel = self.ueye.INT(8)
|
||||
self.bytes_per_pixel = int(self.n_bits_per_pixel / 8)
|
||||
else:
|
||||
# for monochrome camera models use Y8 mode
|
||||
self.m_n_colormode = self.ueye.IS_CM_MONO8
|
||||
self.n_bits_per_pixel = self.ueye.INT(8)
|
||||
self.bytes_per_pixel = int(self.n_bits_per_pixel / 8)
|
||||
logger.info("Monochrome camera mode detected.")
|
||||
|
||||
# Can be used to set the size and position of an "area of interest"(AOI) within an image
|
||||
check_error(
|
||||
self.ueye.is_AOI(
|
||||
self.h_cam,
|
||||
self.ueye.IS_AOI_IMAGE_GET_AOI,
|
||||
self.rect_roi,
|
||||
self.ueye.sizeof(self.rect_roi),
|
||||
),
|
||||
"IDSCameraObject",
|
||||
)
|
||||
self.width = self.rect_roi.s32Width
|
||||
self.height = self.rect_roi.s32Height
|
||||
|
||||
check_error(
|
||||
self.ueye.is_AllocImageMem(
|
||||
self.h_cam,
|
||||
self.width,
|
||||
self.height,
|
||||
self.n_bits_per_pixel,
|
||||
self.pc_image_mem,
|
||||
self.mem_id,
|
||||
),
|
||||
"IDSCameraObject",
|
||||
)
|
||||
|
||||
check_error(
|
||||
self.ueye.is_SetImageMem(self.h_cam, self.pc_image_mem, self.mem_id), "IDSCameraObject"
|
||||
)
|
||||
check_error(self.ueye.is_SetColorMode(self.h_cam, self.m_n_colormode), "IDSCameraObject")
|
||||
|
||||
check_error(
|
||||
self.ueye.is_CaptureVideo(self.h_cam, self.ueye.IS_DONT_WAIT), "IDSCameraObject"
|
||||
)
|
||||
check_error(
|
||||
self.ueye.is_InquireImageMem(
|
||||
self.h_cam,
|
||||
self.pc_image_mem,
|
||||
self.mem_id,
|
||||
self.width,
|
||||
self.height,
|
||||
self.n_bits_per_pixel,
|
||||
self.pitch,
|
||||
),
|
||||
"IDSCameraObject",
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"IDSCameraObject\n\ndevice_id={self._device_id},\ns_info={self.s_info},\nc_info={self.c_info},\nrect_roi={self.rect_roi},\npc_image_mem={self.pc_image_mem},\nmem_id={self.mem_id},\npitch={self.pitch},\nm_n_colormode={self.m_n_colormode},\nn_bits_per_pixel={self.n_bits_per_pixel},\nbytes_per_pixel={self.bytes_per_pixel}"
|
||||
|
||||
|
||||
class Camera:
|
||||
"""High level camera base class for IDS cameras.
|
||||
|
||||
Args:
|
||||
camera_id (int): The ID of the camera device.
|
||||
m_n_colormode (Literal[0, 1, 2, 3]): Color mode for the camera.
|
||||
bits_per_pixel (Literal[8, 24]): Number of bits per pixel for the camera.
|
||||
live_mode (bool): Whether to enable live mode for the camera.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
camera_id: int,
|
||||
m_n_colormode: Literal[0, 1, 2, 3] = 1,
|
||||
bits_per_pixel: int = 24,
|
||||
connect: bool = True,
|
||||
):
|
||||
self.ueye = ueye
|
||||
self.camera_id = camera_id
|
||||
self._inputs = {"m_n_colormode": m_n_colormode, "bits_per_pixel": bits_per_pixel}
|
||||
self._connected = False
|
||||
self.cam = None
|
||||
atexit.register(self.on_disconnect)
|
||||
|
||||
if connect:
|
||||
self.on_connect()
|
||||
|
||||
def set_roi(self, x: int, y: int, width: int, height: int):
|
||||
"""Set the region of interest (ROI) for the camera."""
|
||||
rect_roi = ueye.IS_RECT()
|
||||
rect_roi.s32X = x
|
||||
rect_roi.s32Y = y
|
||||
rect_roi.s32Width = width
|
||||
rect_roi.s32Height = height
|
||||
|
||||
ret = self.ueye.is_AOI(
|
||||
self.cam.h_cam, self.ueye.IS_AOI_IMAGE_SET_AOI, rect_roi, self.ueye.sizeof(rect_roi)
|
||||
)
|
||||
check_error(ret, "IDSCameraObject")
|
||||
logger.info(f"ROI set to: {rect_roi}")
|
||||
|
||||
def on_connect(self):
|
||||
"""Connect to the camera and initialize it."""
|
||||
if self._connected:
|
||||
logger.warning("Camera is already connected.")
|
||||
return
|
||||
self.cam = IDSCameraObject(self.camera_id, **self._inputs)
|
||||
self._connected = True
|
||||
|
||||
def on_disconnect(self):
|
||||
"""Disconnect from the camera."""
|
||||
try:
|
||||
if self.cam and self.cam.h_cam:
|
||||
check_error(self.ueye.is_ExitCamera(self.cam.h_cam), "IDSCameraObject")
|
||||
self._connected = False
|
||||
self.cam = None
|
||||
logger.info("Camera disconnected.")
|
||||
except Exception as e:
|
||||
logger.info(f"Error during camera disconnection: {e}")
|
||||
|
||||
@property
|
||||
def exposure_time(self) -> float:
|
||||
"""Get the exposure time of the camera."""
|
||||
exposure = ueye.c_double()
|
||||
ret = self.ueye.is_Exposure(self.cam.h_cam, ueye.IS_EXPOSURE_CMD_GET_EXPOSURE, exposure, 8)
|
||||
check_error(ret, "IDSCameraObject")
|
||||
return exposure.value
|
||||
|
||||
@exposure_time.setter
|
||||
def exposure_time(self, value: float):
|
||||
"""Set the exposure time of the camera."""
|
||||
exposure = ueye.c_double(value)
|
||||
check_error(
|
||||
self.ueye.is_Exposure(self.cam.h_cam, ueye.IS_EXPOSURE_CMD_SET_EXPOSURE, exposure, 8),
|
||||
"IDSCameraObject",
|
||||
)
|
||||
|
||||
def set_auto_gain(self, enable: bool):
|
||||
"""Enable or disable auto gain."""
|
||||
enable = ueye.c_int(1) if enable else ueye.c_int(0)
|
||||
value_to_return = ueye.c_double()
|
||||
check_error(
|
||||
self.ueye.is_SetAutoParameter(
|
||||
self.cam.h_cam, ueye.IS_SET_ENABLE_AUTO_GAIN, enable, value_to_return
|
||||
),
|
||||
"IDSCameraObject",
|
||||
)
|
||||
|
||||
def set_auto_shutter(self, enable: bool):
|
||||
"""Enable or disable auto exposure."""
|
||||
enable = ueye.c_int(1) if enable else ueye.c_int(0)
|
||||
value_to_return = ueye.c_double()
|
||||
check_error(
|
||||
self.ueye.is_SetAutoParameter(
|
||||
self.cam.h_cam, ueye.IS_SET_ENABLE_AUTO_SHUTTER, enable, value_to_return
|
||||
),
|
||||
"IDSCameraObject",
|
||||
)
|
||||
|
||||
def get_image_data(self) -> np.ndarray | None:
|
||||
"""Get the image data from the camera."""
|
||||
if not self._connected:
|
||||
logger.warning("Camera is not connected.")
|
||||
return None
|
||||
array = self.ueye.get_data(
|
||||
self.cam.pc_image_mem,
|
||||
self.cam.width,
|
||||
self.cam.height,
|
||||
self.cam.n_bits_per_pixel,
|
||||
self.cam.pitch,
|
||||
copy=False,
|
||||
)
|
||||
if array is None:
|
||||
logger.error("Failed to get image data from the camera.")
|
||||
return None
|
||||
return np.reshape(
|
||||
array, (self.cam.height.value, self.cam.width.value, self.cam.bytes_per_pixel)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Example usage
|
||||
camera = Camera(camera_id=201)
|
||||
camera.on_connect()
|
||||
282
csaxs_bec/devices/ids_cameras/base_integration/utils.py
Normal file
282
csaxs_bec/devices/ids_cameras/base_integration/utils.py
Normal file
@@ -0,0 +1,282 @@
|
||||
"""Utility functions and classes for IDS cameras using the pyueye library."""
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
try:
|
||||
from pyueye import ueye
|
||||
except ImportError as exc:
|
||||
logger.warning(f"The pyueye library is not properly installed : {exc}")
|
||||
ueye = None
|
||||
|
||||
if ueye is not None:
|
||||
error_codes = {
|
||||
ueye.IS_NO_SUCCESS: "No success",
|
||||
ueye.IS_SUCCESS: "Success",
|
||||
ueye.IS_INVALID_CAMERA_HANDLE: "Invalid camera handle",
|
||||
ueye.IS_INVALID_HANDLE: "Invalid handle",
|
||||
ueye.IS_IO_REQUEST_FAILED: "IO request failed",
|
||||
ueye.IS_CANT_OPEN_DEVICE: "Cannot open device",
|
||||
ueye.IS_CANT_CLOSE_DEVICE: "Cannot close device",
|
||||
ueye.IS_CANT_SETUP_MEMORY: "Cannot setup memory",
|
||||
ueye.IS_NO_HWND_FOR_ERROR_REPORT: "No HWND for error report",
|
||||
ueye.IS_ERROR_MESSAGE_NOT_CREATED: "Error message not created",
|
||||
ueye.IS_ERROR_STRING_NOT_FOUND: "Error string not found",
|
||||
ueye.IS_HOOK_NOT_CREATED: "Hook not created",
|
||||
ueye.IS_TIMER_NOT_CREATED: "Timer not created",
|
||||
ueye.IS_CANT_OPEN_REGISTRY: "Cannot open registry",
|
||||
ueye.IS_CANT_READ_REGISTRY: "Cannot read registry",
|
||||
ueye.IS_CANT_VALIDATE_BOARD: "Cannot validate board",
|
||||
ueye.IS_CANT_GIVE_BOARD_ACCESS: "Cannot give board access",
|
||||
ueye.IS_NO_IMAGE_MEM_ALLOCATED: "No image memory allocated",
|
||||
ueye.IS_CANT_CLEANUP_MEMORY: "Cannot clean up memory",
|
||||
ueye.IS_CANT_COMMUNICATE_WITH_DRIVER: "Cannot communicate with driver",
|
||||
ueye.IS_FUNCTION_NOT_SUPPORTED_YET: "Function not supported yet",
|
||||
ueye.IS_OPERATING_SYSTEM_NOT_SUPPORTED: "Operating system not supported",
|
||||
ueye.IS_INVALID_VIDEO_IN: "Invalid video input",
|
||||
ueye.IS_INVALID_IMG_SIZE: "Invalid image size",
|
||||
ueye.IS_INVALID_ADDRESS: "Invalid address",
|
||||
ueye.IS_INVALID_VIDEO_MODE: "Invalid video mode",
|
||||
ueye.IS_INVALID_AGC_MODE: "Invalid AGC mode",
|
||||
ueye.IS_INVALID_GAMMA_MODE: "Invalid gamma mode",
|
||||
ueye.IS_INVALID_SYNC_LEVEL: "Invalid sync level",
|
||||
ueye.IS_INVALID_CBARS_MODE: "Invalid color bars mode",
|
||||
ueye.IS_INVALID_COLOR_MODE: "Invalid color mode",
|
||||
ueye.IS_INVALID_SCALE_FACTOR: "Invalid scale factor",
|
||||
ueye.IS_INVALID_IMAGE_SIZE: "Invalid image size",
|
||||
ueye.IS_INVALID_IMAGE_POS: "Invalid image position",
|
||||
ueye.IS_INVALID_CAPTURE_MODE: "Invalid capture mode",
|
||||
ueye.IS_INVALID_RISC_PROGRAM: "Invalid RISC program",
|
||||
ueye.IS_INVALID_BRIGHTNESS: "Invalid brightness",
|
||||
ueye.IS_INVALID_CONTRAST: "Invalid contrast",
|
||||
ueye.IS_INVALID_SATURATION_U: "Invalid saturation U",
|
||||
ueye.IS_INVALID_SATURATION_V: "Invalid saturation V",
|
||||
ueye.IS_INVALID_HUE: "Invalid hue",
|
||||
ueye.IS_INVALID_HOR_FILTER_STEP: "Invalid horizontal filter step",
|
||||
ueye.IS_INVALID_VERT_FILTER_STEP: "Invalid vertical filter step",
|
||||
ueye.IS_INVALID_EEPROM_READ_ADDRESS: "Invalid EEPROM read address",
|
||||
ueye.IS_INVALID_EEPROM_WRITE_ADDRESS: "Invalid EEPROM write address",
|
||||
ueye.IS_INVALID_EEPROM_READ_LENGTH: "Invalid EEPROM read length",
|
||||
ueye.IS_INVALID_EEPROM_WRITE_LENGTH: "Invalid EEPROM write length",
|
||||
ueye.IS_INVALID_BOARD_INFO_POINTER: "Invalid board info pointer",
|
||||
ueye.IS_INVALID_DISPLAY_MODE: "Invalid display mode",
|
||||
ueye.IS_INVALID_ERR_REP_MODE: "Invalid error report mode",
|
||||
ueye.IS_INVALID_BITS_PIXEL: "Invalid bits per pixel",
|
||||
ueye.IS_INVALID_MEMORY_POINTER: "Invalid memory pointer",
|
||||
ueye.IS_FILE_WRITE_OPEN_ERROR: "File write open error",
|
||||
ueye.IS_FILE_READ_OPEN_ERROR: "File read open error",
|
||||
ueye.IS_FILE_READ_INVALID_BMP_ID: "File read invalid BMP ID",
|
||||
ueye.IS_FILE_READ_INVALID_BMP_SIZE: "File read invalid BMP size",
|
||||
ueye.IS_FILE_READ_INVALID_BIT_COUNT: "File read invalid bit count",
|
||||
ueye.IS_WRONG_KERNEL_VERSION: "Wrong kernel version",
|
||||
ueye.IS_RISC_INVALID_XLENGTH: "RISC invalid X length",
|
||||
ueye.IS_RISC_INVALID_YLENGTH: "RISC invalid Y length",
|
||||
ueye.IS_RISC_EXCEED_IMG_SIZE: "RISC exceed image size",
|
||||
ueye.IS_DD_MAIN_FAILED: "DirectDraw main surface failed",
|
||||
ueye.IS_DD_PRIMSURFACE_FAILED: "DirectDraw primary surface failed",
|
||||
ueye.IS_DD_SCRN_SIZE_NOT_SUPPORTED: "Screen size not supported",
|
||||
ueye.IS_DD_CLIPPER_FAILED: "Clipper failed",
|
||||
ueye.IS_DD_CLIPPER_HWND_FAILED: "Clipper HWND failed",
|
||||
ueye.IS_DD_CLIPPER_CONNECT_FAILED: "Clipper connect failed",
|
||||
ueye.IS_DD_BACKSURFACE_FAILED: "Backsurface failed",
|
||||
ueye.IS_DD_BACKSURFACE_IN_SYSMEM: "Backsurface in system memory",
|
||||
ueye.IS_DD_MDL_MALLOC_ERR: "Memory malloc error",
|
||||
ueye.IS_DD_MDL_SIZE_ERR: "Memory size error",
|
||||
ueye.IS_DD_CLIP_NO_CHANGE: "Clip no change",
|
||||
ueye.IS_DD_PRIMMEM_NULL: "Primary memory null",
|
||||
ueye.IS_DD_BACKMEM_NULL: "Back memory null",
|
||||
ueye.IS_DD_BACKOVLMEM_NULL: "Back overlay memory null",
|
||||
ueye.IS_DD_OVERLAYSURFACE_FAILED: "Overlay surface failed",
|
||||
ueye.IS_DD_OVERLAYSURFACE_IN_SYSMEM: "Overlay surface in system memory",
|
||||
ueye.IS_DD_OVERLAY_NOT_ALLOWED: "Overlay not allowed",
|
||||
ueye.IS_DD_OVERLAY_COLKEY_ERR: "Overlay color key error",
|
||||
ueye.IS_DD_OVERLAY_NOT_ENABLED: "Overlay not enabled",
|
||||
ueye.IS_DD_GET_DC_ERROR: "Get DC error",
|
||||
ueye.IS_DD_DDRAW_DLL_NOT_LOADED: "DirectDraw DLL not loaded",
|
||||
ueye.IS_DD_THREAD_NOT_CREATED: "DirectDraw thread not created",
|
||||
ueye.IS_DD_CANT_GET_CAPS: "Cannot get capabilities",
|
||||
ueye.IS_DD_NO_OVERLAYSURFACE: "No overlay surface",
|
||||
ueye.IS_DD_NO_OVERLAYSTRETCH: "No overlay stretch",
|
||||
ueye.IS_DD_CANT_CREATE_OVERLAYSURFACE: "Cannot create overlay surface",
|
||||
ueye.IS_DD_CANT_UPDATE_OVERLAYSURFACE: "Cannot update overlay surface",
|
||||
ueye.IS_DD_INVALID_STRETCH: "Invalid stretch",
|
||||
ueye.IS_EV_INVALID_EVENT_NUMBER: "Invalid event number",
|
||||
ueye.IS_INVALID_MODE: "Invalid mode",
|
||||
ueye.IS_CANT_FIND_HOOK: "Cannot find hook",
|
||||
ueye.IS_CANT_GET_HOOK_PROC_ADDR: "Cannot get hook procedure address",
|
||||
ueye.IS_CANT_CHAIN_HOOK_PROC: "Cannot chain hook procedure",
|
||||
ueye.IS_CANT_SETUP_WND_PROC: "Cannot setup window procedure",
|
||||
ueye.IS_HWND_NULL: "HWND is null",
|
||||
ueye.IS_INVALID_UPDATE_MODE: "Invalid update mode",
|
||||
ueye.IS_NO_ACTIVE_IMG_MEM: "No active image memory",
|
||||
ueye.IS_CANT_INIT_EVENT: "Cannot initialize event",
|
||||
ueye.IS_FUNC_NOT_AVAIL_IN_OS: "Function not available in OS",
|
||||
ueye.IS_CAMERA_NOT_CONNECTED: "Camera not connected",
|
||||
ueye.IS_SEQUENCE_LIST_EMPTY: "Sequence list empty",
|
||||
ueye.IS_CANT_ADD_TO_SEQUENCE: "Cannot add to sequence",
|
||||
ueye.IS_LOW_OF_SEQUENCE_RISC_MEM: "Low sequence RISC memory",
|
||||
ueye.IS_IMGMEM2FREE_USED_IN_SEQ: "Image memory to free used in sequence",
|
||||
ueye.IS_IMGMEM_NOT_IN_SEQUENCE_LIST: "Image memory not in sequence list",
|
||||
ueye.IS_SEQUENCE_BUF_ALREADY_LOCKED: "Sequence buffer already locked",
|
||||
ueye.IS_INVALID_DEVICE_ID: "Invalid device ID",
|
||||
ueye.IS_INVALID_BOARD_ID: "Invalid board ID",
|
||||
ueye.IS_ALL_DEVICES_BUSY: "All devices busy",
|
||||
ueye.IS_HOOK_BUSY: "Hook busy",
|
||||
ueye.IS_TIMED_OUT: "Timed out",
|
||||
ueye.IS_NULL_POINTER: "Null pointer",
|
||||
ueye.IS_WRONG_HOOK_VERSION: "Wrong hook version",
|
||||
ueye.IS_INVALID_PARAMETER: "Invalid parameter",
|
||||
ueye.IS_NOT_ALLOWED: "Not allowed",
|
||||
ueye.IS_OUT_OF_MEMORY: "Out of memory",
|
||||
ueye.IS_INVALID_WHILE_LIVE: "Invalid while live",
|
||||
ueye.IS_ACCESS_VIOLATION: "Access violation",
|
||||
ueye.IS_UNKNOWN_ROP_EFFECT: "Unknown ROP effect",
|
||||
ueye.IS_INVALID_RENDER_MODE: "Invalid render mode",
|
||||
ueye.IS_INVALID_THREAD_CONTEXT: "Invalid thread context",
|
||||
ueye.IS_NO_HARDWARE_INSTALLED: "No hardware installed",
|
||||
ueye.IS_INVALID_WATCHDOG_TIME: "Invalid watchdog time",
|
||||
ueye.IS_INVALID_WATCHDOG_MODE: "Invalid watchdog mode",
|
||||
ueye.IS_INVALID_PASSTHROUGH_IN: "Invalid passthrough input",
|
||||
ueye.IS_ERROR_SETTING_PASSTHROUGH_IN: "Error setting passthrough input",
|
||||
ueye.IS_FAILURE_ON_SETTING_WATCHDOG: "Failure setting watchdog",
|
||||
ueye.IS_NO_USB20: "No USB 2.0",
|
||||
ueye.IS_CAPTURE_RUNNING: "Capture running",
|
||||
ueye.IS_MEMORY_BOARD_ACTIVATED: "Memory board activated",
|
||||
ueye.IS_MEMORY_BOARD_DEACTIVATED: "Memory board deactivated",
|
||||
ueye.IS_NO_MEMORY_BOARD_CONNECTED: "No memory board connected",
|
||||
ueye.IS_TOO_LESS_MEMORY: "Too little memory",
|
||||
ueye.IS_IMAGE_NOT_PRESENT: "Image not present",
|
||||
ueye.IS_MEMORY_MODE_RUNNING: "Memory mode running",
|
||||
ueye.IS_MEMORYBOARD_DISABLED: "Memoryboard disabled",
|
||||
ueye.IS_TRIGGER_ACTIVATED: "Trigger activated",
|
||||
ueye.IS_WRONG_KEY: "Wrong key",
|
||||
ueye.IS_CRC_ERROR: "CRC error",
|
||||
ueye.IS_NOT_YET_RELEASED: "Not yet released",
|
||||
ueye.IS_NOT_CALIBRATED: "Not calibrated", # already present
|
||||
ueye.IS_WAITING_FOR_KERNEL: "Waiting for kernel",
|
||||
ueye.IS_NOT_SUPPORTED: "Not supported", # already present
|
||||
ueye.IS_TRIGGER_NOT_ACTIVATED: "Trigger not activated",
|
||||
ueye.IS_OPERATION_ABORTED: "Operation aborted",
|
||||
ueye.IS_BAD_STRUCTURE_SIZE: "Bad structure size",
|
||||
ueye.IS_INVALID_BUFFER_SIZE: "Invalid buffer size",
|
||||
ueye.IS_INVALID_PIXEL_CLOCK: "Invalid pixel clock",
|
||||
ueye.IS_INVALID_EXPOSURE_TIME: "Invalid exposure time",
|
||||
ueye.IS_AUTO_EXPOSURE_RUNNING: "Auto exposure running",
|
||||
ueye.IS_CANNOT_CREATE_BB_SURF: "Cannot create BB surface",
|
||||
ueye.IS_CANNOT_CREATE_BB_MIX: "Cannot create BB mix",
|
||||
ueye.IS_BB_OVLMEM_NULL: "BB overlay memory null",
|
||||
ueye.IS_CANNOT_CREATE_BB_OVL: "Cannot create BB overlay",
|
||||
ueye.IS_NOT_SUPP_IN_OVL_SURF_MODE: "Not supported in overlay surface mode",
|
||||
ueye.IS_INVALID_SURFACE: "Invalid surface",
|
||||
ueye.IS_SURFACE_LOST: "Surface lost",
|
||||
ueye.IS_RELEASE_BB_OVL_DC: "Release BB overlay DC",
|
||||
ueye.IS_BB_TIMER_NOT_CREATED: "BB timer not created",
|
||||
ueye.IS_BB_OVL_NOT_EN: "BB overlay not enabled",
|
||||
ueye.IS_ONLY_IN_BB_MODE: "Only in BB mode",
|
||||
ueye.IS_INVALID_COLOR_FORMAT: "Invalid color format",
|
||||
ueye.IS_INVALID_WB_BINNING_MODE: "Invalid WB binning mode",
|
||||
ueye.IS_INVALID_I2C_DEVICE_ADDRESS: "Invalid I²C device address",
|
||||
ueye.IS_COULD_NOT_CONVERT: "Could not convert",
|
||||
ueye.IS_TRANSFER_ERROR: "Transfer error", # already present
|
||||
ueye.IS_PARAMETER_SET_NOT_PRESENT: "Parameter set not present",
|
||||
ueye.IS_INVALID_CAMERA_TYPE: "Invalid camera type",
|
||||
ueye.IS_INVALID_HOST_IP_HIBYTE: "Invalid host IP high byte",
|
||||
ueye.IS_CM_NOT_SUPP_IN_CURR_DISPLAYMODE: "Color matrix not supported in current display mode",
|
||||
ueye.IS_NO_IR_FILTER: "No IR filter",
|
||||
ueye.IS_STARTER_FW_UPLOAD_NEEDED: "Starter firmware upload needed",
|
||||
ueye.IS_DR_LIBRARY_NOT_FOUND: "Driver library not found",
|
||||
ueye.IS_DR_DEVICE_OUT_OF_MEMORY: "Driver device out of memory",
|
||||
ueye.IS_DR_CANNOT_CREATE_SURFACE: "Driver cannot create surface",
|
||||
ueye.IS_DR_CANNOT_CREATE_VERTEX_BUFFER: "Driver cannot create vertex buffer",
|
||||
ueye.IS_DR_CANNOT_CREATE_TEXTURE: "Driver cannot create texture",
|
||||
ueye.IS_DR_CANNOT_LOCK_OVERLAY_SURFACE: "Driver cannot lock overlay surface",
|
||||
ueye.IS_DR_CANNOT_UNLOCK_OVERLAY_SURFACE: "Driver cannot unlock overlay surface",
|
||||
ueye.IS_DR_CANNOT_GET_OVERLAY_DC: "Driver cannot get overlay DC",
|
||||
ueye.IS_DR_CANNOT_RELEASE_OVERLAY_DC: "Driver cannot release overlay DC",
|
||||
ueye.IS_DR_DEVICE_CAPS_INSUFFICIENT: "Driver device capabilities insufficient",
|
||||
ueye.IS_INCOMPATIBLE_SETTING: "Incompatible setting",
|
||||
ueye.IS_DR_NOT_ALLOWED_WHILE_DC_IS_ACTIVE: "Driver not allowed while DC is active",
|
||||
ueye.IS_DEVICE_ALREADY_PAIRED: "Device already paired",
|
||||
ueye.IS_SUBNETMASK_MISMATCH: "Subnet mask mismatch",
|
||||
ueye.IS_SUBNET_MISMATCH: "Subnet mismatch",
|
||||
ueye.IS_INVALID_IP_CONFIGURATION: "Invalid IP configuration",
|
||||
ueye.IS_DEVICE_NOT_COMPATIBLE: "Device not compatible",
|
||||
ueye.IS_NETWORK_FRAME_SIZE_INCOMPATIBLE: "Network frame size incompatible",
|
||||
ueye.IS_NETWORK_CONFIGURATION_INVALID: "Network configuration invalid",
|
||||
ueye.IS_ERROR_CPU_IDLE_STATES_CONFIGURATION: "CPU idle states configuration error",
|
||||
ueye.IS_DEVICE_BUSY: "Device busy",
|
||||
ueye.IS_SENSOR_INITIALIZATION_FAILED: "Sensor initialization failed",
|
||||
ueye.IS_IMAGE_BUFFER_NOT_DWORD_ALIGNED: "Image buffer not DWORD aligned",
|
||||
ueye.IS_SEQ_BUFFER_IS_LOCKED: "Sequence buffer is locked",
|
||||
ueye.IS_FILE_PATH_DOES_NOT_EXIST: "File path does not exist",
|
||||
ueye.IS_INVALID_WINDOW_HANDLE: "Invalid window handle",
|
||||
ueye.IS_INVALID_IMAGE_PARAMETER: "Invalid image parameter",
|
||||
ueye.IS_NO_SUCH_DEVICE: "No such device",
|
||||
ueye.IS_DEVICE_IN_USE: "Device in use",
|
||||
}
|
||||
|
||||
bits_per_pixel = {
|
||||
ueye.IS_CM_SENSOR_RAW8: 8,
|
||||
ueye.IS_CM_SENSOR_RAW10: 16,
|
||||
ueye.IS_CM_SENSOR_RAW12: 16,
|
||||
ueye.IS_CM_SENSOR_RAW16: 16,
|
||||
ueye.IS_CM_MONO8: 8,
|
||||
ueye.IS_CM_RGB8_PACKED: 24,
|
||||
ueye.IS_CM_BGR8_PACKED: 24,
|
||||
ueye.IS_CM_RGBA8_PACKED: 32,
|
||||
ueye.IS_CM_BGRA8_PACKED: 32,
|
||||
ueye.IS_CM_BGR10_PACKED: 32,
|
||||
ueye.IS_CM_RGB10_PACKED: 32,
|
||||
ueye.IS_CM_BGRA12_UNPACKED: 64,
|
||||
ueye.IS_CM_BGR12_UNPACKED: 48,
|
||||
ueye.IS_CM_BGRY8_PACKED: 32,
|
||||
ueye.IS_CM_BGR565_PACKED: 16,
|
||||
ueye.IS_CM_BGR5_PACKED: 16,
|
||||
ueye.IS_CM_UYVY_PACKED: 16,
|
||||
ueye.IS_CM_UYVY_MONO_PACKED: 16,
|
||||
ueye.IS_CM_UYVY_BAYER_PACKED: 16,
|
||||
ueye.IS_CM_CBYCRY_PACKED: 16,
|
||||
}
|
||||
else:
|
||||
error_codes = {}
|
||||
bits_per_pixel = {}
|
||||
|
||||
|
||||
def get_bits_per_pixel(color_mode):
|
||||
"""
|
||||
Returns the number of bits per pixel for the given color mode.
|
||||
"""
|
||||
if color_mode not in bits_per_pixel:
|
||||
raise UEyeException(f"Unknown color mode: {color_mode}")
|
||||
return bits_per_pixel[color_mode]
|
||||
|
||||
|
||||
class UEyeException(Exception):
|
||||
"""Custom exception for uEye errors."""
|
||||
|
||||
def __init__(self, error_code, called_from: str | None = None):
|
||||
self.error_code = error_code
|
||||
self.called_from = called_from if called_from is not None else ""
|
||||
|
||||
def __str__(self):
|
||||
if self.error_code in error_codes:
|
||||
return f"Exception: {error_codes[self.error_code]} raised in {self.called_from}."
|
||||
else:
|
||||
for att, val in ueye.__dict__.items():
|
||||
if (
|
||||
att[0:2] == "IS"
|
||||
and val == self.error_code
|
||||
and ("FAILED" in att or "INVALID" in att or "ERROR" in att or "NOT" in att)
|
||||
):
|
||||
return f"Exception: {str(self.error_code)} ({att} ? <value> {val}) raised in {self.called_from}."
|
||||
return f"Exception: {str(self.error_code)} raised in {self.called_from}."
|
||||
|
||||
|
||||
def check_error(error_code, called_from: str | None = None):
|
||||
"""
|
||||
Check an error code, and raise an error if adequate.
|
||||
"""
|
||||
if error_code != ueye.IS_SUCCESS:
|
||||
called_from = called_from if called_from is not None else ""
|
||||
raise UEyeException(error_code, called_from)
|
||||
403
csaxs_bec/devices/ids_cameras/ids_camera.py
Normal file
403
csaxs_bec/devices/ids_cameras/ids_camera.py
Normal file
@@ -0,0 +1,403 @@
|
||||
import threading
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
from bec_lib.logger import bec_logger
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import DeviceStatus, Kind, Signal, StatusBase
|
||||
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
|
||||
from ophyd_devices.utils.bec_signals import PreviewSignal
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class ROISignal(Signal):
|
||||
"""
|
||||
Signal to handle the Region of Interest (ROI) for the IDS camera.
|
||||
It is a tuple of (x, y, width, height).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
name,
|
||||
roi: tuple | None = None,
|
||||
value=0,
|
||||
dtype=None,
|
||||
shape=None,
|
||||
timestamp=None,
|
||||
parent=None,
|
||||
labels=None,
|
||||
kind=Kind.hinted,
|
||||
tolerance=None,
|
||||
rtolerance=None,
|
||||
metadata=None,
|
||||
cl=None,
|
||||
attr_name="",
|
||||
):
|
||||
super().__init__(
|
||||
name=name,
|
||||
value=value,
|
||||
dtype=dtype,
|
||||
shape=shape,
|
||||
timestamp=timestamp,
|
||||
parent=parent,
|
||||
labels=labels,
|
||||
kind=kind,
|
||||
tolerance=tolerance,
|
||||
rtolerance=rtolerance,
|
||||
metadata=metadata,
|
||||
cl=cl,
|
||||
attr_name=attr_name,
|
||||
)
|
||||
self.roi = roi
|
||||
|
||||
def get(self, **kwargs):
|
||||
image = self.parent.image_data.get().data
|
||||
if not isinstance(image, np.ndarray):
|
||||
return -1 # -1 if no valid image is available
|
||||
|
||||
if self.roi is None:
|
||||
roi = (0, 0, image.shape[1], image.shape[0])
|
||||
else:
|
||||
roi = self.roi
|
||||
if len(image.shape) > 2:
|
||||
image = np.sum(image, axis=2) # Convert to grayscale if it's a color image
|
||||
return np.sum(image[roi[1] : roi[1] + roi[3], roi[0] : roi[0] + roi[2]], (0, 1))
|
||||
|
||||
|
||||
class IDSCamera(PSIDeviceBase):
|
||||
""" "
|
||||
#---------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
#Variables
|
||||
hCam = ueye.HIDS(202) #0: first available camera; 1-254: The camera with the specified camera ID
|
||||
sInfo = ueye.SENSORINFO()
|
||||
cInfo = ueye.CAMINFO()
|
||||
pcImageMemory = ueye.c_mem_p()
|
||||
MemID = ueye.int()
|
||||
rectAOI = ueye.IS_RECT()
|
||||
pitch = ueye.INT()
|
||||
nBitsPerPixel = ueye.INT(24) #24: bits per pixel for color mode; take 8 bits per pixel for monochrome
|
||||
channels = 3 #3: channels for color mode(RGB); take 1 channel for monochrome
|
||||
m_nColorMode = ueye.INT(1) # Y8/RGB16/RGB24/REG32 (1 for our color cameras)
|
||||
bytes_per_pixel = int(nBitsPerPixel / 8)
|
||||
|
||||
ids_cam
|
||||
...
|
||||
"""
|
||||
|
||||
USER_ACCESS = ["start_live_mode", "stop_live_mode", "set_roi", "width", "height"]
|
||||
|
||||
image_data = Cpt(PreviewSignal, ndim=2, kind=Kind.omitted)
|
||||
# roi_bot_left = Cpt(ROISignal, roi=(400, 525, 118, 105), kind=Kind.normal)
|
||||
# roi_bot_right = Cpt(ROISignal, roi=(518, 525, 118, 105), kind=Kind.normal)
|
||||
# roi_top_left = Cpt(ROISignal, roi=(400, 630, 118, 105), kind=Kind.normal)
|
||||
# roi_top_right = Cpt(ROISignal, roi=(518, 630, 118, 105), kind=Kind.normal)
|
||||
# roi_signal = Cpt(ROISignal, kind=Kind.normal, doc="Region of Interest signal")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
prefix="",
|
||||
*,
|
||||
name: str,
|
||||
camera_ID: int,
|
||||
bits_per_pixel: int,
|
||||
channels: int,
|
||||
m_n_colormode: int,
|
||||
kind=None,
|
||||
device_manager=None,
|
||||
**kwargs,
|
||||
):
|
||||
|
||||
super().__init__(
|
||||
prefix=prefix, name=name, kind=kind, device_manager=device_manager, **kwargs
|
||||
)
|
||||
self.camera_ID = camera_ID
|
||||
self.bits_per_pixel = bits_per_pixel
|
||||
self.bytes_per_pixel = None
|
||||
self.channels = channels
|
||||
self._m_n_colormode_input = m_n_colormode
|
||||
self.m_n_colormode = None
|
||||
self.ueye = ueye
|
||||
self.h_cam = None
|
||||
self.s_info = None
|
||||
self.data_thread = None
|
||||
self.c_info = None
|
||||
self.pc_image_memory = None
|
||||
self.mem_id = None
|
||||
self.rect_aoi = None
|
||||
self.pitch = None
|
||||
self.n_bits_per_pixel = None
|
||||
self.width = None
|
||||
self.height = None
|
||||
self.thread_event = threading.Event()
|
||||
self.data_thread = None
|
||||
self._roi: tuple | None = None # x, y, width, height
|
||||
logger.info(
|
||||
f"Deprecation warning: The IDSCamera class is deprecated. Use the new IDSCameraNew class instead."
|
||||
)
|
||||
|
||||
def set_roi(self, x: int, y: int, width: int, height: int):
|
||||
self._roi = (x, y, width, height)
|
||||
|
||||
def start_backend(self):
|
||||
if self.ueye is None:
|
||||
raise ImportError("The pyueye library is not installed.")
|
||||
self.h_cam = self.ueye.HIDS(
|
||||
self.camera_ID
|
||||
) # 0: first available camera; 1-254: The camera with the specified camera ID
|
||||
self.s_info = self.ueye.SENSORINFO()
|
||||
self.c_info = self.ueye.CAMINFO()
|
||||
self.pc_image_memory = self.ueye.c_mem_p()
|
||||
self.mem_id = self.ueye.int()
|
||||
self.rect_aoi = self.ueye.IS_RECT()
|
||||
self.pitch = self.ueye.INT()
|
||||
self.n_bits_per_pixel = self.ueye.INT(
|
||||
self.bits_per_pixel
|
||||
) # 24: bits per pixel for color mode; take 8 bits per pixel for monochrome
|
||||
self.m_n_colormode = self.ueye.INT(
|
||||
self._m_n_colormode_input
|
||||
) # Y8/RGB16/RGB24/REG32 (1 for our color cameras)
|
||||
self.bytes_per_pixel = int(self.n_bits_per_pixel / 8)
|
||||
|
||||
# Starts the driver and establishes the connection to the camera
|
||||
ret = self.ueye.is_InitCamera(self.h_cam, None)
|
||||
if ret != self.ueye.IS_SUCCESS:
|
||||
print("is_InitCamera ERROR")
|
||||
|
||||
# Reads out the data hard-coded in the non-volatile camera memory and writes it to the data structure that c_info points to
|
||||
ret = self.ueye.is_GetCameraInfo(self.h_cam, self.c_info)
|
||||
if ret != self.ueye.IS_SUCCESS:
|
||||
print("is_GetCameraInfo ERROR")
|
||||
|
||||
# You can query additional information about the sensor type used in the camera
|
||||
ret = self.ueye.is_GetSensorInfo(self.h_cam, self.s_info)
|
||||
if ret != self.ueye.IS_SUCCESS:
|
||||
print("is_GetSensorInfo ERROR")
|
||||
|
||||
ret = self.ueye.is_ResetToDefault(self.h_cam)
|
||||
if ret != self.ueye.IS_SUCCESS:
|
||||
print("is_ResetToDefault ERROR")
|
||||
|
||||
# Set display mode to DIB
|
||||
ret = self.ueye.is_SetDisplayMode(self.h_cam, self.ueye.IS_SET_DM_DIB)
|
||||
|
||||
# Set the right color mode
|
||||
if (
|
||||
int.from_bytes(self.s_info.nColorMode.value, byteorder="big")
|
||||
== self.ueye.IS_COLORMODE_BAYER
|
||||
):
|
||||
# setup the color depth to the current windows setting
|
||||
self.ueye.is_GetColorDepth(self.h_cam, self.n_bits_per_pixel, self.m_n_colormode)
|
||||
bytes_per_pixel = int(self.n_bits_per_pixel / 8)
|
||||
print("IS_COLORMODE_BAYER: ")
|
||||
print("\tm_n_colormode: \t\t", self.m_n_colormode)
|
||||
print("\tn_bits_per_pixel: \t\t", self.n_bits_per_pixel)
|
||||
print("\tbytes_per_pixel: \t\t", bytes_per_pixel)
|
||||
print()
|
||||
|
||||
elif (
|
||||
int.from_bytes(self.s_info.nColorMode.value, byteorder="big")
|
||||
== self.ueye.IS_COLORMODE_CBYCRY
|
||||
):
|
||||
# for color camera models use RGB32 mode
|
||||
m_n_colormode = self.ueye.IS_CM_BGRA8_PACKED
|
||||
n_bits_per_pixel = self.ueye.INT(32)
|
||||
bytes_per_pixel = int(self.n_bits_per_pixel / 8)
|
||||
print("IS_COLORMODE_CBYCRY: ")
|
||||
print("\tm_n_colormode: \t\t", m_n_colormode)
|
||||
print("\tn_bits_per_pixel: \t\t", n_bits_per_pixel)
|
||||
print("\tbytes_per_pixel: \t\t", bytes_per_pixel)
|
||||
print()
|
||||
|
||||
elif (
|
||||
int.from_bytes(self.s_info.nColorMode.value, byteorder="big")
|
||||
== self.ueye.IS_COLORMODE_MONOCHROME
|
||||
):
|
||||
# for color camera models use RGB32 mode
|
||||
m_n_colormode = self.ueye.IS_CM_MONO8
|
||||
n_bits_per_pixel = self.ueye.INT(8)
|
||||
bytes_per_pixel = int(n_bits_per_pixel / 8)
|
||||
print("IS_COLORMODE_MONOCHROME: ")
|
||||
print("\tm_n_colormode: \t\t", m_n_colormode)
|
||||
print("\tn_bits_per_pixel: \t\t", n_bits_per_pixel)
|
||||
print("\tbytes_per_pixel: \t\t", bytes_per_pixel)
|
||||
print()
|
||||
|
||||
else:
|
||||
# for monochrome camera models use Y8 mode
|
||||
m_n_colormode = self.ueye.IS_CM_MONO8
|
||||
n_bits_per_pixel = self.ueye.INT(8)
|
||||
bytes_per_pixel = int(n_bits_per_pixel / 8)
|
||||
print("else")
|
||||
|
||||
# Can be used to set the size and position of an "area of interest"(AOI) within an image
|
||||
ret = self.ueye.is_AOI(
|
||||
self.h_cam,
|
||||
self.ueye.IS_AOI_IMAGE_GET_AOI,
|
||||
self.rect_aoi,
|
||||
self.ueye.sizeof(self.rect_aoi),
|
||||
)
|
||||
if ret != self.ueye.IS_SUCCESS:
|
||||
print("is_AOI ERROR")
|
||||
|
||||
self.width = self.rect_aoi.s32Width
|
||||
self.height = self.rect_aoi.s32Height
|
||||
|
||||
# Prints out some information about the camera and the sensor
|
||||
print("Camera model:\t\t", self.s_info.strSensorName.decode("utf-8"))
|
||||
print("Camera serial no.:\t", self.c_info.SerNo.decode("utf-8"))
|
||||
print("Maximum image width:\t", self.width)
|
||||
print("Maximum image height:\t", self.height)
|
||||
print()
|
||||
|
||||
# ---------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
# Allocates an image memory for an image having its dimensions defined by width and height and its color depth defined by n_bits_per_pixel
|
||||
ret = self.ueye.is_AllocImageMem(
|
||||
self.h_cam,
|
||||
self.width,
|
||||
self.height,
|
||||
self.n_bits_per_pixel,
|
||||
self.pc_image_memory,
|
||||
self.mem_id,
|
||||
)
|
||||
if ret != self.ueye.IS_SUCCESS:
|
||||
print("is_AllocImageMem ERROR")
|
||||
else:
|
||||
# Makes the specified image memory the active memory
|
||||
ret = self.ueye.is_SetImageMem(self.h_cam, self.pc_image_memory, self.mem_id)
|
||||
if ret != self.ueye.IS_SUCCESS:
|
||||
print("is_SetImageMem ERROR")
|
||||
else:
|
||||
# Set the desired color mode
|
||||
ret = self.ueye.is_SetColorMode(self.h_cam, self.m_n_colormode)
|
||||
|
||||
# Activates the camera's live video mode (free run mode)
|
||||
ret = self.ueye.is_CaptureVideo(self.h_cam, self.ueye.IS_DONT_WAIT)
|
||||
if ret != self.ueye.IS_SUCCESS:
|
||||
print("is_CaptureVideo ERROR")
|
||||
|
||||
# Enables the queue mode for existing image memory sequences
|
||||
ret = self.ueye.is_InquireImageMem(
|
||||
self.h_cam,
|
||||
self.pc_image_memory,
|
||||
self.mem_id,
|
||||
self.width,
|
||||
self.height,
|
||||
self.n_bits_per_pixel,
|
||||
self.pitch,
|
||||
)
|
||||
if ret != self.ueye.IS_SUCCESS:
|
||||
print("is_InquireImageMem ERROR")
|
||||
else:
|
||||
print("Press q to leave the programm")
|
||||
# startmeasureframerate = True
|
||||
# Gain = False
|
||||
|
||||
# Start live mode of camera immediately
|
||||
self.start_live_mode()
|
||||
|
||||
def _start_data_thread(self):
|
||||
self.data_thread = threading.Thread(target=self._receive_data_from_camera, daemon=True)
|
||||
self.data_thread.start()
|
||||
|
||||
def _receive_data_from_camera(self):
|
||||
while not self.thread_event.is_set():
|
||||
if self.ueye is None:
|
||||
print("pyueye library not available.")
|
||||
return
|
||||
# In order to display the image in an OpenCV window we need to...
|
||||
# ...extract the data of our image memory
|
||||
array = self.ueye.get_data(
|
||||
self.pc_image_memory,
|
||||
self.width,
|
||||
self.height,
|
||||
self.n_bits_per_pixel,
|
||||
self.pitch,
|
||||
copy=False,
|
||||
)
|
||||
|
||||
# ...reshape it in an numpy array...
|
||||
frame = np.reshape(array, (self.height.value, self.width.value, self.bytes_per_pixel))
|
||||
self.image_data.put(frame)
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
def wait_for_connection(self, all_signals=False, timeout=10):
|
||||
if ueye is None:
|
||||
raise ImportError(
|
||||
"The pyueye library is not installed or doesn't provide the necessary c libs"
|
||||
)
|
||||
super().wait_for_connection(all_signals, timeout)
|
||||
|
||||
def start_live_mode(self):
|
||||
if self.data_thread is not None:
|
||||
self.stop_live_mode()
|
||||
self._start_data_thread()
|
||||
|
||||
def stop_live_mode(self):
|
||||
"""Stopping the camera live mode."""
|
||||
self.thread_event.set()
|
||||
if self.data_thread is not None:
|
||||
self.data_thread.join()
|
||||
self.thread_event.clear()
|
||||
self.data_thread = None
|
||||
|
||||
########################################
|
||||
# Beamline Specific Implementations #
|
||||
########################################
|
||||
|
||||
def on_init(self) -> None:
|
||||
"""
|
||||
Called when the device is initialized.
|
||||
|
||||
No signals are connected at this point. If you like to
|
||||
set default values on signals, please use on_connected instead.
|
||||
"""
|
||||
|
||||
def on_connected(self) -> None:
|
||||
"""
|
||||
Called after the device is connected and its signals are connected.
|
||||
Default values for signals should be set here.
|
||||
"""
|
||||
self.start_backend()
|
||||
self.start_live_mode()
|
||||
|
||||
def on_stage(self) -> DeviceStatus | StatusBase | None:
|
||||
"""
|
||||
Called while staging the device.
|
||||
|
||||
Information about the upcoming scan can be accessed from the scan_info (self.scan_info.msg) object.
|
||||
"""
|
||||
|
||||
def on_unstage(self) -> DeviceStatus | StatusBase | None:
|
||||
"""Called while unstaging the device."""
|
||||
|
||||
def on_pre_scan(self) -> DeviceStatus | StatusBase | None:
|
||||
"""Called right before the scan starts on all devices automatically."""
|
||||
|
||||
def on_trigger(self) -> DeviceStatus | StatusBase | None:
|
||||
"""Called when the device is triggered."""
|
||||
|
||||
def on_complete(self) -> DeviceStatus | StatusBase | None:
|
||||
"""Called to inquire if a device has completed a scans."""
|
||||
|
||||
def on_kickoff(self) -> DeviceStatus | StatusBase | None:
|
||||
"""Called to kickoff a device for a fly scan. Has to be called explicitly."""
|
||||
|
||||
def on_stop(self) -> None:
|
||||
"""Called when the device is stopped."""
|
||||
|
||||
def on_destroy(self) -> None:
|
||||
"""Called when the device is destroyed. Cleanup resources here."""
|
||||
self.stop_live_mode()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Example usage
|
||||
camera = IDSCamera(name="camera", camera_ID=201, bits_per_pixel=24, channels=3, m_n_colormode=1)
|
||||
camera.wait_for_connection()
|
||||
|
||||
camera.on_destroy()
|
||||
226
csaxs_bec/devices/ids_cameras/ids_camera_new.py
Normal file
226
csaxs_bec/devices/ids_cameras/ids_camera_new.py
Normal file
@@ -0,0 +1,226 @@
|
||||
"""IDS Camera class for cSAXS IDS cameras."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Literal, Tuple, TypedDict
|
||||
|
||||
import numpy as np
|
||||
from bec_lib import messages
|
||||
from bec_lib.logger import bec_logger
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
|
||||
from ophyd_devices.utils.bec_signals import AsyncSignal, PreviewSignal
|
||||
|
||||
from csaxs_bec.devices.ids_cameras.base_integration.camera import Camera
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bec_lib.devicemanager import ScanInfo
|
||||
from pydantic import ValidationInfo
|
||||
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class IDSCamera(PSIDeviceBase):
|
||||
"""IDS Camera class for cSAXS.
|
||||
|
||||
This class inherits from PSIDeviceBase and implements the necessary methods
|
||||
to interact with the IDS camera using the pyueye library.
|
||||
"""
|
||||
|
||||
image = Cpt(PreviewSignal, name="image", ndim=2, doc="Preview signal for the camera.")
|
||||
roi_signal = Cpt(
|
||||
AsyncSignal,
|
||||
name="roi_signal",
|
||||
ndim=0,
|
||||
max_size=1000,
|
||||
doc="Signal for the region of interest (ROI).",
|
||||
async_update={"type": "add", "max_shape": [None]},
|
||||
)
|
||||
|
||||
USER_ACCESS = ["live_mode", "mask", "set_rect_roi", "get_last_image"]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
name: str,
|
||||
camera_id: int,
|
||||
prefix: str = "",
|
||||
scan_info: ScanInfo | None = None,
|
||||
m_n_colormode: Literal[0, 1, 2, 3] = 1,
|
||||
bits_per_pixel: Literal[8, 24] = 24,
|
||||
live_mode: bool = False,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize the IDS Camera.
|
||||
|
||||
Args:
|
||||
name (str): Name of the device.
|
||||
camera_id (int): The ID of the camera device.
|
||||
prefix (str): Prefix for the device.
|
||||
scan_info (ScanInfo | None): Scan information for the device.
|
||||
m_n_colormode (Literal[0, 1, 2, 3]): Color mode for the camera.
|
||||
bits_per_pixel (Literal[8, 24]): Number of bits per pixel for the camera.
|
||||
live_mode (bool): Whether to enable live mode for the camera.
|
||||
"""
|
||||
super().__init__(name=name, prefix=prefix, scan_info=scan_info, **kwargs)
|
||||
self._live_mode_thread: threading.Thread | None = None
|
||||
self._stop_live_mode_event: threading.Event = threading.Event()
|
||||
self.cam = Camera(
|
||||
camera_id=camera_id,
|
||||
m_n_colormode=m_n_colormode,
|
||||
bits_per_pixel=bits_per_pixel,
|
||||
connect=False,
|
||||
)
|
||||
self._live_mode = False
|
||||
self._inputs = {"live_mode": live_mode}
|
||||
self._mask = np.zeros((1, 1), dtype=np.uint8)
|
||||
|
||||
############## Live Mode Methods ##############
|
||||
|
||||
@property
|
||||
def mask(self) -> np.ndarray:
|
||||
"""Return the current region of interest (ROI) for the camera."""
|
||||
return self._mask
|
||||
|
||||
@mask.setter
|
||||
def mask(self, value: np.ndarray):
|
||||
"""
|
||||
Set the region of interest (ROI) for the camera.
|
||||
|
||||
Args:
|
||||
value (np.ndarray): The mask to set as the ROI.
|
||||
"""
|
||||
if value.ndim != 2:
|
||||
raise ValueError("ROI mask must be a 2D array.")
|
||||
img_shape = (self.cam.cam.height.value, self.cam.cam.width.value)
|
||||
if value.shape[0] != img_shape[0] or value.shape[1] != img_shape[1]:
|
||||
raise ValueError(
|
||||
f"ROI mask shape {value.shape} does not match image shape {img_shape}."
|
||||
)
|
||||
self._mask = value
|
||||
|
||||
@property
|
||||
def live_mode(self) -> bool:
|
||||
"""Return whether the camera is in live mode."""
|
||||
return self._live_mode
|
||||
|
||||
@live_mode.setter
|
||||
def live_mode(self, value: bool):
|
||||
"""Set the live mode for the camera."""
|
||||
if value != self._live_mode:
|
||||
if self.cam._connected is False: # $ pylint: disable=protected-access
|
||||
self.cam.on_connect()
|
||||
self._live_mode = value
|
||||
if value:
|
||||
self._start_live()
|
||||
else:
|
||||
self._stop_live()
|
||||
|
||||
def set_rect_roi(self, x: int, y: int, width: int, height: int):
|
||||
"""Set the rectangular region of interest (ROI) for the camera."""
|
||||
if x < 0 or y < 0 or width <= 0 or height <= 0:
|
||||
raise ValueError("ROI coordinates and dimensions must be positive integers.")
|
||||
img_shape = (self.cam.cam.height.value, self.cam.cam.width.value)
|
||||
if x + width > img_shape[1] or y + height > img_shape[0]:
|
||||
raise ValueError("ROI exceeds camera dimensions.")
|
||||
mask = np.zeros(img_shape, dtype=np.uint8)
|
||||
mask[y : y + height, x : x + width] = 1
|
||||
self.mask = mask
|
||||
|
||||
def _start_live(self):
|
||||
"""Start the live mode for the camera."""
|
||||
if self._live_mode_thread is not None:
|
||||
logger.info("Live mode thread is already running.")
|
||||
return
|
||||
self._stop_live_mode_event.clear()
|
||||
self._live_mode_thread = threading.Thread(
|
||||
target=self._live_mode_loop, args=(self._stop_live_mode_event,)
|
||||
)
|
||||
self._live_mode_thread.start()
|
||||
|
||||
def _stop_live(self):
|
||||
"""Stop the live mode for the camera."""
|
||||
if self._live_mode_thread is None:
|
||||
logger.info("Live mode thread is not running.")
|
||||
return
|
||||
self._stop_live_mode_event.set()
|
||||
self._live_mode_thread.join(timeout=5)
|
||||
if self._live_mode_thread.is_alive():
|
||||
logger.warning("Live mode thread did not stop gracefully.")
|
||||
else:
|
||||
self._live_mode_thread = None
|
||||
logger.info("Live mode stopped.")
|
||||
|
||||
def _live_mode_loop(self, stop_event: threading.Event):
|
||||
"""Loop to capture images in live mode."""
|
||||
while not stop_event.is_set():
|
||||
try:
|
||||
self.process_data(self.cam.get_image_data())
|
||||
except Exception as e:
|
||||
logger.error(f"Error in live mode loop: {e}")
|
||||
break
|
||||
stop_event.wait(0.2) # 5 Hz
|
||||
|
||||
def process_data(self, image: np.ndarray | None):
|
||||
"""Process the image data before sending it to the preview signal."""
|
||||
if image is None:
|
||||
return
|
||||
self.image.put(image)
|
||||
|
||||
def get_last_image(self) -> np.ndarray:
|
||||
"""Get the last captured image from the camera."""
|
||||
image = self.image.get()
|
||||
if image:
|
||||
return image.data
|
||||
|
||||
############## User Interface Methods ##############
|
||||
|
||||
def on_connected(self):
|
||||
"""Connect to the camera."""
|
||||
self.cam.on_connect()
|
||||
self.live_mode = self._inputs.get("live_mode", False)
|
||||
self.set_rect_roi(0, 0, self.cam.cam.width.value, self.cam.cam.height.value)
|
||||
|
||||
def on_destroy(self):
|
||||
"""Clean up resources when the device is destroyed."""
|
||||
self.cam.on_disconnect()
|
||||
super().on_destroy()
|
||||
|
||||
def on_trigger(self):
|
||||
"""Handle the trigger event."""
|
||||
if not self.live_mode:
|
||||
return
|
||||
image = self.image.get()
|
||||
if image is not None:
|
||||
image: messages.DevicePreviewMessage
|
||||
if self.mask.shape[0:2] != image.data.shape[0:2]:
|
||||
logger.info(
|
||||
f"ROI shape does not match image shape, skipping ROI application for device {self.name}."
|
||||
)
|
||||
return
|
||||
|
||||
if len(image.data.shape) == 3:
|
||||
# If the image has multiple channels, apply the mask to each channel
|
||||
data = image.data * self.mask[:, :, np.newaxis] # Apply mask to the image data
|
||||
n_channels = 3
|
||||
else:
|
||||
data = image.data * self.mask
|
||||
n_channels = 1
|
||||
self.roi_signal.put(
|
||||
{
|
||||
self.roi_signal.name: {
|
||||
"value": np.sum(data)
|
||||
/ (np.sum(self.mask) * n_channels), # TODO could be optimized
|
||||
"timestamp": time.time(),
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Example usage of the IDSCamera class
|
||||
camera = IDSCamera(name="TestCamera", camera_id=201, live_mode=False)
|
||||
print(f"Camera {camera.name} initialized with ID {camera.cam.camera_id}.")
|
||||
153
csaxs_bec/devices/jungfraujoch/jungfrau_joch_client.py
Normal file
153
csaxs_bec/devices/jungfraujoch/jungfrau_joch_client.py
Normal file
@@ -0,0 +1,153 @@
|
||||
import enum
|
||||
import math
|
||||
|
||||
import jfjoch_client
|
||||
from bec_lib.logger import bec_logger
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class JungfrauJochClientError(Exception):
|
||||
"""Base class for exceptions in this module."""
|
||||
|
||||
|
||||
class DetectorState(enum.StrEnum):
|
||||
"""Detector states for Jungfrau Joch detector
|
||||
['Inactive', 'Idle', 'Busy', 'Measuring', 'Pedestal', 'Error']
|
||||
"""
|
||||
|
||||
INACTIVE = "Inactive"
|
||||
IDLE = "Idle"
|
||||
BUSY = "Busy"
|
||||
MEASURING = "Measuring"
|
||||
PEDESTAL = "Pedestal"
|
||||
ERROR = "Error"
|
||||
|
||||
|
||||
class ResponseWaitDone(enum.IntEnum):
|
||||
"""Response state for Jungfrau Joch detector wait till done"""
|
||||
|
||||
DETECTOR_IDLE = 200
|
||||
TIMEOUT_PARAM_OUT_OF_RANGE = 400
|
||||
JUNGFRAU_ERROR = 500
|
||||
DETECTOR_INACTIVE = 502
|
||||
TIMEOUT_REACHED = 504
|
||||
|
||||
|
||||
class JungfrauJochClient:
|
||||
"""Thin wrapper around the Jungfrau Joch API client"""
|
||||
|
||||
def __init__(self, host: str = "http://sls-jfjoch-001:8080") -> None:
|
||||
self._initialised = False
|
||||
configuration = jfjoch_client.Configuration(host=host)
|
||||
api_client = jfjoch_client.ApiClient(configuration)
|
||||
self.api = jfjoch_client.DefaultApi(api_client)
|
||||
|
||||
@property
|
||||
def initialised(self) -> bool:
|
||||
"""Check if jfj is connected and ready to receive commands"""
|
||||
return self._initialised
|
||||
|
||||
@initialised.setter
|
||||
def initialised(self, value: bool) -> None:
|
||||
"""Set the connected status"""
|
||||
self._initialised = value
|
||||
|
||||
def get_jungfrau_joch_status(self) -> DetectorState:
|
||||
"""Get the status of JungfrauJoch"""
|
||||
return self.api.status_get().state
|
||||
|
||||
def connect_and_initialise(self, timeout: int = 5) -> None:
|
||||
"""Check if JungfrauJoch is connected and ready to receive commands"""
|
||||
status = self.api.status_get().state
|
||||
if status != DetectorState.IDLE:
|
||||
self.api.initialize_post()
|
||||
self.wait_till_done(timeout)
|
||||
self.initialised = True
|
||||
|
||||
def set_detector_settings(self, settings: dict | jfjoch_client.DatasetSettings) -> None:
|
||||
"""Set the detector settings. JungfrauJoch must be in IDLE, Error or Inactive state.
|
||||
Note, the full settings have to be provided, otherwise the settings will be overwritten with default values.
|
||||
|
||||
Args:
|
||||
settings (dict): dictionary of settings
|
||||
"""
|
||||
state = self.api.status_get().state
|
||||
if state not in [DetectorState.IDLE, DetectorState.ERROR, DetectorState.INACTIVE]:
|
||||
raise JungfrauJochClientError(
|
||||
f"Detector must be in IDLE, ERROR or INACTIVE state to set settings. Current state: {state}"
|
||||
)
|
||||
|
||||
if isinstance(settings, dict):
|
||||
settings = jfjoch_client.DatasetSettings(**settings)
|
||||
self.api.config_detector_put(settings)
|
||||
|
||||
def set_mesaurement_settings(self, settings: dict | jfjoch_client.DatasetSettings) -> None:
|
||||
"""Set the measurement settings. JungfrauJoch must be in IDLE state.
|
||||
The method call is blocking and JungfrauJoch will be ready to measure after the call resolves.
|
||||
|
||||
Please check the DataSettings class for the available settings.
|
||||
The minimum required settings are:
|
||||
beam_x_pxl: StrictFloat | StrictInt,
|
||||
beam_y_pxl: StrictFloat | StrictInt,
|
||||
detector_distance_mm: float | int,
|
||||
incident_energy_keV: float | int,
|
||||
|
||||
Args:
|
||||
settings (dict): dictionary of settings
|
||||
"""
|
||||
state = self.api.status_get().state
|
||||
if state != DetectorState.IDLE:
|
||||
raise JungfrauJochClientError(
|
||||
f"Detector must be in IDLE state to set settings. Current state: {state}"
|
||||
)
|
||||
|
||||
if isinstance(settings, dict):
|
||||
settings = jfjoch_client.DatasetSettings(**settings)
|
||||
try:
|
||||
res = self.api.start_post_with_http_info(dataset_settings=settings)
|
||||
if res.status_code != 200:
|
||||
logger.error(
|
||||
f"Error while setting measurement settings {settings}, response: {res}"
|
||||
)
|
||||
raise JungfrauJochClientError(
|
||||
f"Error while setting measurement settings {settings}, response: {res}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error while setting measurement settings {settings}. Exception raised {e}"
|
||||
)
|
||||
raise JungfrauJochClientError(
|
||||
f"Error while setting measurement settings {settings}. Exception raised {e}"
|
||||
) from e
|
||||
|
||||
def wait_till_done(self, timeout: int = 5) -> None:
|
||||
"""Wait until JungfrauJoch is done.
|
||||
|
||||
Args:
|
||||
timeout (int): timeout in seconds
|
||||
"""
|
||||
success = False
|
||||
try:
|
||||
response = self.api.wait_till_done_post_with_http_info(math.ceil(timeout / 2))
|
||||
if response.status_code != ResponseWaitDone.DETECTOR_IDLE:
|
||||
logger.info(
|
||||
f"Waitin for JungfrauJoch to be done, status: {ResponseWaitDone(response.status_code)}; response msg {response}"
|
||||
)
|
||||
response = self.api.wait_till_done_post_with_http_info(math.floor(timeout / 2))
|
||||
if response.status_code == ResponseWaitDone.DETECTOR_IDLE:
|
||||
success = True
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error(f"Error while waiting for JungfrauJoch to initialise: {e}")
|
||||
raise JungfrauJochClientError(
|
||||
f"Error while waiting for JungfrauJoch to initialise: {e}"
|
||||
) from e
|
||||
else:
|
||||
if success is False:
|
||||
logger.error(
|
||||
f"Failed to initialise JungfrauJoch with status: {response.status_code}; response msg {response}"
|
||||
)
|
||||
raise JungfrauJochClientError(
|
||||
f"Failed to initialise JungfrauJoch with status: {response.status_code}; response msg {response}"
|
||||
)
|
||||
0
csaxs_bec/devices/omny/__init__.py
Normal file
0
csaxs_bec/devices/omny/__init__.py
Normal file
@@ -141,7 +141,7 @@ class FlomniGalilAxesReferenced(GalilAxesReferenced):
|
||||
|
||||
|
||||
class FlomniGalilMotor(Device, PositionerBase):
|
||||
USER_ACCESS = ["controller"]
|
||||
USER_ACCESS = ["controller", "drive_axis_to_limit"]
|
||||
readback = Cpt(FlomniGalilReadbackSignal, signal_name="readback", kind="hinted")
|
||||
user_setpoint = Cpt(FlomniGalilSetpointSignal, signal_name="setpoint")
|
||||
motor_resolution = Cpt(FlomniGalilMotorResolution, signal_name="resolution", kind="config")
|
||||
@@ -337,6 +337,18 @@ class FlomniGalilMotor(Device, PositionerBase):
|
||||
def unstage(self) -> list[object]:
|
||||
return super().unstage()
|
||||
|
||||
def drive_axis_to_limit(self, direction: str) -> None:
|
||||
"""
|
||||
Drive an axis to the limit in a specified direction.
|
||||
|
||||
Args:
|
||||
direction (str): Direction in which the axis should be driven to the limit. Either 'forward' or 'reverse'.
|
||||
"""
|
||||
self.controller.drive_axis_to_limit(self.axis_Id_numeric, direction)
|
||||
#now force position read to cache
|
||||
val = self.readback.read()
|
||||
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
|
||||
|
||||
def stop(self, *, success=False):
|
||||
self.controller.stop_all_axes()
|
||||
return super().stop(success=success)
|
||||
|
||||
@@ -17,8 +17,8 @@ from csaxs_bec.devices.omny.galil.galil_ophyd import (
|
||||
GalilError,
|
||||
GalilMotorIsMoving,
|
||||
GalilMotorResolution,
|
||||
GalilReadbackSignal,
|
||||
GalilSetpointSignal,
|
||||
GalilSignalRO,
|
||||
retry_once,
|
||||
)
|
||||
|
||||
@@ -44,7 +44,7 @@ class FuprGalilController(GalilController):
|
||||
raise NotImplementedError("This function is not implemented for the FuprGalilController.")
|
||||
|
||||
|
||||
class FuprGalilReadbackSignal(GalilReadbackSignal):
|
||||
class FuprGalilReadbackSignal(GalilSignalRO):
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def _socket_get(self) -> float:
|
||||
|
||||
@@ -59,6 +59,13 @@ class GalilController(Controller):
|
||||
"all_axes_referenced",
|
||||
]
|
||||
|
||||
OKBLUE = '\033[94m'
|
||||
OKCYAN = '\033[96m'
|
||||
OKGREEN = '\033[92m'
|
||||
WARNING = '\033[93m'
|
||||
FAIL = '\033[91m'
|
||||
ENDC = '\033[0m'
|
||||
|
||||
@threadlocked
|
||||
def socket_put(self, val: str) -> None:
|
||||
self.sock.put(f"{val}\r".encode())
|
||||
@@ -98,21 +105,64 @@ class GalilController(Controller):
|
||||
return True
|
||||
|
||||
def stop_all_axes(self) -> str:
|
||||
return self.socket_put_and_receive("XQ#STOP,1")
|
||||
if not self.is_thread_active(1):
|
||||
return self.socket_put_and_receive("XQ#STOP,1")
|
||||
else:
|
||||
return ":"
|
||||
|
||||
def get_digital_input(self, channel):
|
||||
return bool(float(self.socket_put_and_receive(f"MG @IN[{channel}]").strip()))
|
||||
|
||||
def axis_is_referenced(self, axis_Id_numeric) -> bool:
|
||||
return bool(float(self.socket_put_and_receive(f"MG axisref[{axis_Id_numeric}]").strip()))
|
||||
|
||||
def folerr_status(self, axis_Id_numeric) -> bool:
|
||||
return bool(float(self.socket_put_and_receive(f"MG folaxerr[{axis_Id_numeric}]").strip()))
|
||||
|
||||
def motor_temperature(self, axis_Id_numeric) -> float:
|
||||
#this is only valid for omny. consider moving to ogalil
|
||||
voltage = float(self.socket_put_and_receive(f"MG @AN[{axis_Id_numeric+1}]").strip())
|
||||
voltage2 = float(self.socket_put_and_receive(f"MG @AN[{axis_Id_numeric+1}]").strip())
|
||||
if voltage2 < voltage:
|
||||
voltage = voltage2
|
||||
# convert from [-10,10]V to [0,300]degC
|
||||
temperature_degC = round((voltage+10.0) / 20.0 * 300.0, 1)
|
||||
|
||||
#the motors of the parking station have a different offset
|
||||
#the range is reduced, so if at the limit, we show an extreme value
|
||||
if self.sock.port == 8082:
|
||||
#controller 2
|
||||
if axis_Id_numeric == 6:
|
||||
temperature_degC = round((voltage+10.0-11.4) / 20.0 * 300.0, 1)
|
||||
if voltage > 9.9:
|
||||
temperature_degC = 300
|
||||
if axis_Id_numeric == 7:
|
||||
temperature_degC = round((voltage+.0-12) / 20.0 * 300.0, 1)
|
||||
if voltage > 9.9:
|
||||
temperature_degC = 300
|
||||
return temperature_degC
|
||||
|
||||
def all_axes_referenced(self) -> bool:
|
||||
"""
|
||||
Check if all axes are referenced.
|
||||
"""
|
||||
return bool(float(self.socket_put_and_receive("MG allaxref").strip()))
|
||||
|
||||
def _omny_get_microstep_position(self,axis_Id):
|
||||
return float(self.socket_put_and_receive(f"MG _TD{axis_Id}").strip())
|
||||
|
||||
|
||||
def _omny_get_reference_limit(self,axis_Id):
|
||||
get_axis_no = float(self.socket_put_and_receive(f"MG frmmv").strip())
|
||||
if(get_axis_no>0):
|
||||
reference_is_before = float(self.socket_put_and_receive(f"MG _FL{axis_Id}").strip())
|
||||
elif(get_axis_no<0):
|
||||
reference_is_before = float(self.socket_put_and_receive(f"MG _BL{axis_Id}").strip())
|
||||
else:
|
||||
reference_is_before = 0
|
||||
return reference_is_before
|
||||
|
||||
def drive_axis_to_limit(self, axis_Id_numeric: int, direction: str) -> None:
|
||||
def drive_axis_to_limit(self, axis_Id_numeric: int, direction: str, verbose=0) -> None:
|
||||
"""
|
||||
Drive an axis to the limit in a specified direction.
|
||||
|
||||
@@ -133,10 +183,13 @@ class GalilController(Controller):
|
||||
time.sleep(0.1)
|
||||
self.socket_put_confirmed("XQ#FES")
|
||||
time.sleep(0.1)
|
||||
axis_Id = self.axis_Id_numeric_to_alpha(axis_Id_numeric)
|
||||
while self.is_axis_moving(None, axis_Id_numeric):
|
||||
time.sleep(0.01)
|
||||
if verbose:
|
||||
self.get_device_manager().connector.send_client_info(f"Current microstep position {self._omny_get_microstep_position(axis_Id):.0f}", scope="drive axis to limit", show_asap=True)
|
||||
time.sleep(0.5)
|
||||
|
||||
axis_Id = self.axis_Id_numeric_to_alpha(axis_Id_numeric)
|
||||
# check if we actually hit the limit
|
||||
if direction == "forward":
|
||||
limit = self.get_motor_limit_switch(axis_Id)[1]
|
||||
@@ -145,8 +198,16 @@ class GalilController(Controller):
|
||||
|
||||
if not limit:
|
||||
raise GalilError(f"Failed to drive axis {axis_Id}/{axis_Id_numeric} to limit.")
|
||||
else:
|
||||
print("Limit reached.")
|
||||
|
||||
def find_reference(self, axis_Id_numeric: int) -> None:
|
||||
def get_device_manager(self):
|
||||
for axis in self._axis:
|
||||
if hasattr(axis, "device_manager") and axis.device_manager:
|
||||
return axis.device_manager
|
||||
raise BECConfigError("Could not access the device_manager")
|
||||
|
||||
def find_reference(self, axis_Id_numeric: int, verbose=0, raise_error = 1) -> None:
|
||||
"""
|
||||
Find the reference of an axis.
|
||||
|
||||
@@ -159,14 +220,23 @@ class GalilController(Controller):
|
||||
time.sleep(0.1)
|
||||
self.socket_put_confirmed("XQ#FRM")
|
||||
time.sleep(0.1)
|
||||
axis_Id = self.axis_Id_numeric_to_alpha(axis_Id_numeric)
|
||||
while self.is_axis_moving(None, axis_Id_numeric):
|
||||
time.sleep(0.1)
|
||||
if verbose:
|
||||
self.get_device_manager().connector.send_client_info(f"Current microstep position {self._omny_get_microstep_position(axis_Id):.0f} reference is before {self._omny_get_reference_limit(axis_Id)}", scope="find axis reference", show_asap=True)
|
||||
time.sleep(0.5)
|
||||
|
||||
if not self.axis_is_referenced(axis_Id_numeric):
|
||||
raise GalilError(f"Failed to find reference of axis {axis_Id_numeric}.")
|
||||
|
||||
logger.info(f"Successfully found reference of axis {axis_Id_numeric}.")
|
||||
if raise_error:
|
||||
raise GalilError(f"Failed to find reference of axis {axis_Id_numeric}.")
|
||||
else:
|
||||
print(f"Failed to find reference of axis {axis_Id_numeric}.")
|
||||
else:
|
||||
logger.info(f"Successfully found reference of axis {axis_Id_numeric}.")
|
||||
print(f"Successfully found reference of axis {axis_Id_numeric}.")
|
||||
|
||||
|
||||
def show_running_threads(self) -> None:
|
||||
t = PrettyTable()
|
||||
t.title = f"Threads on {self.sock.host}:{self.sock.port}"
|
||||
@@ -181,7 +251,7 @@ class GalilController(Controller):
|
||||
|
||||
def is_motor_on(self, axis_Id) -> bool:
|
||||
return not bool(float(self.socket_put_and_receive(f"MG _MO{axis_Id}").strip()))
|
||||
|
||||
|
||||
def get_motor_limit_switch(self, axis_Id) -> list:
|
||||
"""
|
||||
Get the status of the motor limit switches.
|
||||
@@ -199,29 +269,60 @@ class GalilController(Controller):
|
||||
def describe(self) -> None:
|
||||
t = PrettyTable()
|
||||
t.title = f"{self.__class__.__name__} on {self.sock.host}:{self.sock.port}"
|
||||
t.field_names = [
|
||||
field_names = [
|
||||
"Axis",
|
||||
"Name",
|
||||
"Connected",
|
||||
"Referenced",
|
||||
"Motor On",
|
||||
"Limits",
|
||||
"Position",
|
||||
]
|
||||
# in case of OMNY
|
||||
if self.sock.host == "mpc3217.psi.ch":
|
||||
field_names.append("Temperature")
|
||||
field_names.append("FolErr")
|
||||
t.field_names = field_names
|
||||
for ax in range(self._axes_per_controller):
|
||||
axis = self._axis[ax]
|
||||
if axis is not None:
|
||||
t.add_row(
|
||||
[
|
||||
f"{axis.axis_Id_numeric}/{axis.axis_Id}",
|
||||
axis.name,
|
||||
axis.connected,
|
||||
self.axis_is_referenced(axis.axis_Id_numeric),
|
||||
self.is_motor_on(axis.axis_Id),
|
||||
self.get_motor_limit_switch(axis.axis_Id),
|
||||
axis.readback.read().get(axis.name).get("value"),
|
||||
]
|
||||
)
|
||||
if self.sock.host == "mpc3217.psi.ch":
|
||||
#case of omny. possibly consider moving to ogalil
|
||||
motor_on = self.is_motor_on(axis.axis_Id)
|
||||
if motor_on == True:
|
||||
motor_on = self.WARNING + "ON" + self.ENDC
|
||||
else:
|
||||
motor_on = "OFF"
|
||||
|
||||
folerr_status = self.folerr_status(axis.axis_Id_numeric)
|
||||
if folerr_status == True:
|
||||
folerr_status = self.WARNING + "True" + self.ENDC
|
||||
else:
|
||||
folerr_status = "False"
|
||||
position = axis.readback.read().get(axis.name).get("value")
|
||||
position = f'{position:.3f}'
|
||||
t.add_row(
|
||||
[
|
||||
f"{axis.axis_Id_numeric}/{axis.axis_Id}",
|
||||
axis.name,
|
||||
self.axis_is_referenced(axis.axis_Id_numeric),
|
||||
motor_on,
|
||||
self.get_motor_limit_switch(axis.axis_Id),
|
||||
position,
|
||||
self.motor_temperature(axis.axis_Id_numeric),
|
||||
self.folerr_status(axis.axis_Id_numeric),
|
||||
]
|
||||
)
|
||||
else:
|
||||
t.add_row(
|
||||
[
|
||||
f"{axis.axis_Id_numeric}/{axis.axis_Id}",
|
||||
axis.name,
|
||||
self.axis_is_referenced(axis.axis_Id_numeric),
|
||||
self.is_motor_on(axis.axis_Id),
|
||||
self.get_motor_limit_switch(axis.axis_Id),
|
||||
axis.readback.read().get(axis.name).get("value"),
|
||||
]
|
||||
)
|
||||
else:
|
||||
t.add_row([None for t in t.field_names])
|
||||
print(t)
|
||||
@@ -229,6 +330,8 @@ class GalilController(Controller):
|
||||
self.show_running_threads()
|
||||
self.show_status_other()
|
||||
|
||||
|
||||
|
||||
def show_status_other(self) -> None:
|
||||
"""
|
||||
Show additional device-specific status information.
|
||||
@@ -254,7 +357,6 @@ class GalilSignalBase(SocketSignal):
|
||||
self.signal_name = signal_name
|
||||
super().__init__(**kwargs)
|
||||
self.controller = self.parent.controller
|
||||
self.sock = self.parent.controller.sock
|
||||
|
||||
|
||||
class GalilSignalRO(GalilSignalBase):
|
||||
@@ -275,7 +377,6 @@ class GalilReadbackSignal(GalilSignalRO):
|
||||
Returns:
|
||||
float: Readback value after adjusting for sign and motor resolution.
|
||||
"""
|
||||
|
||||
current_pos = float(self.controller.socket_put_and_receive(f"TD{self.parent.axis_Id}"))
|
||||
current_pos *= self.parent.sign
|
||||
step_mm = self.parent.motor_resolution.get()
|
||||
@@ -284,13 +385,6 @@ class GalilReadbackSignal(GalilSignalRO):
|
||||
def read(self):
|
||||
self._metadata["timestamp"] = time.time()
|
||||
val = super().read()
|
||||
if self.parent.axis_Id_numeric == 2:
|
||||
try:
|
||||
rt = self.parent.device_manager.devices[self.parent.rt]
|
||||
if rt.enabled:
|
||||
rt.obj.controller.set_rotation_angle(val[self.parent.name]["value"])
|
||||
except KeyError:
|
||||
logger.warning("Failed to set RT value during readback.")
|
||||
return val
|
||||
|
||||
|
||||
@@ -326,7 +420,8 @@ class GalilSetpointSignal(GalilSignalBase):
|
||||
while self.controller.is_thread_active(0):
|
||||
time.sleep(0.1)
|
||||
|
||||
if self.parent.axis_Id_numeric == 2:
|
||||
#in the case of lamni, consider moving to lgalil
|
||||
if self.parent.axis_Id_numeric == 2 and self.controller.sock.host == "mpc2680.psi.ch":
|
||||
try:
|
||||
rt = self.parent.device_manager.devices[self.parent.rt]
|
||||
if rt.enabled:
|
||||
|
||||
@@ -7,6 +7,7 @@ from ophyd import Component as Cpt
|
||||
from ophyd import Device, PositionerBase, Signal
|
||||
from ophyd.status import wait as status_wait
|
||||
from ophyd.utils import LimitError
|
||||
from ophyd_devices.utils.controller import threadlocked
|
||||
from ophyd_devices.utils.socket import SocketIO, raise_if_disconnected
|
||||
|
||||
from csaxs_bec.devices.omny.galil.galil_ophyd import (
|
||||
@@ -15,8 +16,9 @@ from csaxs_bec.devices.omny.galil.galil_ophyd import (
|
||||
GalilController,
|
||||
GalilMotorIsMoving,
|
||||
GalilMotorResolution,
|
||||
GalilReadbackSignal,
|
||||
GalilSetpointSignal,
|
||||
GalilSignalRO,
|
||||
retry_once,
|
||||
)
|
||||
|
||||
logger = bec_logger.logger
|
||||
@@ -71,10 +73,35 @@ class LamniGalilController(GalilController):
|
||||
air_off = bool(self.socket_put_and_receive("MG@OUT[13]"))
|
||||
return rt_not_blocked_by_galil and air_off
|
||||
|
||||
class LamniGalilReadbackSignal(GalilSignalRO):
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def _socket_get(self) -> float:
|
||||
"""Get command for the readback signal
|
||||
|
||||
Returns:
|
||||
float: Readback value after adjusting for sign and motor resolution.
|
||||
"""
|
||||
current_pos = float(self.controller.socket_put_and_receive(f"TD{self.parent.axis_Id}"))
|
||||
current_pos *= self.parent.sign
|
||||
step_mm = self.parent.motor_resolution.get()
|
||||
return current_pos / step_mm
|
||||
|
||||
def read(self):
|
||||
self._metadata["timestamp"] = time.time()
|
||||
val = super().read()
|
||||
if self.parent.axis_Id_numeric == 2:
|
||||
try:
|
||||
rt = self.parent.device_manager.devices[self.parent.rtx]
|
||||
if rt.enabled:
|
||||
rt.obj.controller.set_rotation_angle(val[self.parent.name]["value"])
|
||||
except KeyError:
|
||||
logger.warning("Failed to set RT value during readback.")
|
||||
return val
|
||||
|
||||
class LamniGalilMotor(Device, PositionerBase):
|
||||
USER_ACCESS = ["controller"]
|
||||
readback = Cpt(GalilReadbackSignal, signal_name="readback", kind="hinted")
|
||||
USER_ACCESS = ["controller", "drive_axis_to_limit", "find_reference"]
|
||||
readback = Cpt(LamniGalilReadbackSignal, signal_name="readback", kind="hinted")
|
||||
user_setpoint = Cpt(GalilSetpointSignal, signal_name="setpoint")
|
||||
motor_resolution = Cpt(GalilMotorResolution, signal_name="resolution", kind="config")
|
||||
motor_is_moving = Cpt(GalilMotorIsMoving, signal_name="motor_is_moving", kind="normal")
|
||||
@@ -260,6 +287,27 @@ class LamniGalilMotor(Device, PositionerBase):
|
||||
"""The engineering units (EGU) for positions"""
|
||||
return "mm"
|
||||
|
||||
def find_reference(self):
|
||||
"""
|
||||
Find the reference of the axis.
|
||||
"""
|
||||
self.controller.find_reference(self.axis_Id_numeric)
|
||||
#now force position read to cache
|
||||
val = self.readback.read()
|
||||
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
|
||||
|
||||
def drive_axis_to_limit(self, direction: str) -> None:
|
||||
"""
|
||||
Drive an axis to the limit in a specified direction.
|
||||
|
||||
Args:
|
||||
direction (str): Direction in which the axis should be driven to the limit. Either 'forward' or 'reverse'.
|
||||
"""
|
||||
self.controller.drive_axis_to_limit(self.axis_Id_numeric, direction)
|
||||
#now force position read to cache
|
||||
val = self.readback.read()
|
||||
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
|
||||
|
||||
def stop(self, *, success=False):
|
||||
self.controller.stop_all_axes()
|
||||
return super().stop(success=success)
|
||||
|
||||
515
csaxs_bec/devices/omny/galil/ogalil_ophyd.py
Normal file
515
csaxs_bec/devices/omny/galil/ogalil_ophyd.py
Normal file
@@ -0,0 +1,515 @@
|
||||
import functools
|
||||
import threading
|
||||
import time
|
||||
import urllib.request
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
import numpy as np
|
||||
from bec_lib.logger import bec_logger
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, PositionerBase, Signal
|
||||
from ophyd.status import wait as status_wait
|
||||
from ophyd.utils import LimitError
|
||||
from ophyd_devices.utils.controller import threadlocked
|
||||
from ophyd_devices.utils.socket import SocketIO, raise_if_disconnected
|
||||
|
||||
from csaxs_bec.devices.omny.galil.galil_ophyd import (
|
||||
BECConfigError,
|
||||
GalilAxesReferenced,
|
||||
GalilCommunicationError,
|
||||
GalilController,
|
||||
GalilError,
|
||||
GalilMotorIsMoving,
|
||||
GalilSetpointSignal,
|
||||
GalilSignalRO,
|
||||
)
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
def retry_once(fcn):
|
||||
"""Decorator to rerun a function in case a Galil communication error was raised. This may happen if the buffer was not empty."""
|
||||
|
||||
@functools.wraps(fcn)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
try:
|
||||
val = fcn(self, *args, **kwargs)
|
||||
except (GalilCommunicationError, GalilError):
|
||||
val = fcn(self, *args, **kwargs)
|
||||
return val
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class GalilMotorResolution(GalilSignalRO):
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def _socket_get(self):
|
||||
if self.controller.sock.port == 8083 and self.parent.axis_Id_numeric == 2:
|
||||
# rotation stage
|
||||
return 89565.8666667
|
||||
else:
|
||||
return 51200
|
||||
|
||||
|
||||
class OMNYGalilReadbackSignal(GalilSignalRO):
|
||||
|
||||
previous_rotation_angle = 0
|
||||
ignore_glitch = True
|
||||
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def _socket_get(self) -> float:
|
||||
"""Get command for the readback signal
|
||||
|
||||
Returns:
|
||||
float: Readback value after adjusting for sign and motor resolution.
|
||||
"""
|
||||
|
||||
current_pos = float(self.controller.socket_put_and_receive(f"TP{self.parent.axis_Id}"))
|
||||
current_pos *= self.parent.sign
|
||||
step_mm = self.parent.motor_resolution.get()
|
||||
#here we introduce an offset of 25 to the rotation axis
|
||||
#when setting a position this is taken into account in the controller
|
||||
#that way we just do tomography from 0 to 180 degrees
|
||||
if self.parent.axis_Id_numeric == 2 and self.controller.sock.port == 8083:
|
||||
return (current_pos / step_mm)+25
|
||||
else:
|
||||
return current_pos / step_mm
|
||||
|
||||
def read(self):
|
||||
self._metadata["timestamp"] = time.time()
|
||||
val = super().read()
|
||||
|
||||
#if reading rotation stage angle
|
||||
if self.parent.axis_Id_numeric == 2 and self.controller.sock.port == 8083:
|
||||
current_readback_value = val[self.parent.name]["value"]
|
||||
#print (f"previous rotation angle {self.previous_rotation_angle}, current readback {current_readback_value}.")
|
||||
|
||||
if np.fabs((self.previous_rotation_angle-current_readback_value)>10):
|
||||
message = f"Glitch detected in rotation stage. Previous rotation angle {self.previous_rotation_angle}, current readback {current_readback_value}."
|
||||
print(message)
|
||||
self.parent.device_manager.connector.send_client_info(message, scope="glitch detector", show_asap=True)
|
||||
|
||||
val = super().read()
|
||||
current_readback_value = val[self.parent.name]["value"]
|
||||
|
||||
if np.fabs((self.previous_rotation_angle-current_readback_value)>10):
|
||||
message = f"Glitch detected in rotation stage second read. Previous rotation angle {self.previous_rotation_angle}, current readback {current_readback_value}. Disabling the controller."
|
||||
print(message)
|
||||
self.parent.device_manager.connector.send_client_info(message, scope="glitch detector", show_asap=True)
|
||||
|
||||
self.parent.device_manager.devices["osamroy"].obj.controller.socket_put_confirmed("allaxref=0")
|
||||
self.parent.device_manager.devices["osamroy"].obj.enabled = False
|
||||
|
||||
return val
|
||||
|
||||
self.previous_rotation_angle = current_readback_value
|
||||
try:
|
||||
rt = self.parent.device_manager.devices["rtx"]
|
||||
if rt.enabled:
|
||||
rt.obj.controller.set_rotation_angle(val[self.parent.name]["value"]-25+54)
|
||||
except KeyError:
|
||||
logger.warning("Failed to set RT value during ogalil readback.")
|
||||
return val
|
||||
|
||||
|
||||
|
||||
class OMNYGalilController(GalilController):
|
||||
USER_ACCESS = [
|
||||
"describe",
|
||||
"show_running_threads",
|
||||
"galil_show_all",
|
||||
"socket_put_and_receive",
|
||||
"socket_put_confirmed",
|
||||
"get_motor_limit_switch",
|
||||
"is_motor_on",
|
||||
"all_axes_referenced",
|
||||
"_ogalil_switchsocket",
|
||||
"_ogalil_switchsocket_switch_all_on",
|
||||
"_ogalil_switchsocket_status",
|
||||
"_ogalil_switchsocket_are_all_on",
|
||||
"_ogalil_folerr_not_ignore",
|
||||
]
|
||||
|
||||
OKBLUE = '\033[94m'
|
||||
OKCYAN = '\033[96m'
|
||||
OKGREEN = '\033[92m'
|
||||
WARNING = '\033[93m'
|
||||
FAIL = '\033[91m'
|
||||
ENDC = '\033[0m'
|
||||
|
||||
def on(self) -> None:
|
||||
"""Open a new socket connection to the controller"""
|
||||
self._ogalil_switchsocket_switch_all_on()
|
||||
time.sleep(0.3)
|
||||
super().on()
|
||||
|
||||
def _ogalil_switchsocket(self, number: int, switch: bool):
|
||||
# number is socket number ranging from 1 to 4
|
||||
# switch is either 0 or 1
|
||||
if number not in range(1, 5):
|
||||
raise Exception("Socket number ranges from 1 to 4")
|
||||
else:
|
||||
contents = urllib.request.urlopen(
|
||||
f"http://mpc3217:8091/netio.cgi?pass=24A42C3929C5&output{number}={switch}"
|
||||
).read()
|
||||
# print(contents)
|
||||
if b"OK" in contents:
|
||||
print(f"Controller number switchsocket {number} is now {switch}")
|
||||
return 1
|
||||
else:
|
||||
print(f"Failed to switch controller number {number}")
|
||||
return 0
|
||||
|
||||
def _ogalil_switchsocket_are_all_on(self):
|
||||
if self._ogalil_switchsocket_status() == [1, 1, 1, 1]:
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
def _ogalil_tempdisabledebug(self):
|
||||
# sock_put(_ogalil_debugging_host_and_port_str[ogalil_no][],"WH\r\n") < 1) {
|
||||
# ret_str = sock_get(_ogalil_debugging_host_and_port_str[ogalil_no][],_ogalil_prompt_str)
|
||||
# printf("%s _ogalil_redirect_debug_output(%d): expected \"IHx\" received \"%s\"\n",\
|
||||
# cmd_str = sprintf("CF %s\r\n",substr(ret_str,3,1))
|
||||
# if (sock_put(_ogalil_debugging_host_and_port_str[ogalil_no][],cmd_str) < 1) {
|
||||
# cmd_str = sprintf("CW 2\r\n")
|
||||
# if (sock_put(_ogalil_debugging_host_and_port_str[ogalil_no][],cmd_str) < 1) {
|
||||
print("not yet implemented")
|
||||
return 0
|
||||
|
||||
def _ogalil_folerr_reset_and_ignore(self, axis_id_numeric: int) -> None:
|
||||
self.socket_put_confirmed(f"folaxerr[{axis_id_numeric}]=0")
|
||||
self.socket_put_confirmed("folerr=0")
|
||||
self.socket_put_confirmed("IgNoFol=1")
|
||||
self.socket_put_confirmed("XQ#STOP,1")
|
||||
|
||||
|
||||
def _ogalil_set_axis_to_pos_wo_reference_search(self, axis_id_numeric, axis_id, pos_mm, motor_resolution, motor_sign):
|
||||
|
||||
self.socket_put_confirmed("IgNoFol=1")
|
||||
|
||||
# pos_mm = pos_encoder / motor_resolution
|
||||
pos_encoder = pos_mm * motor_resolution * motor_sign
|
||||
#print(motor_resolution)
|
||||
|
||||
self.socket_put_confirmed(f"DE{axis_id}={pos_encoder:.0f}")
|
||||
self.socket_put_confirmed(f"DP{axis_id}=_TP{axis_id}*ratio[{axis_id_numeric:.0f}]")
|
||||
|
||||
self.socket_put_confirmed(f"folaxerr[{axis_id_numeric}]=0")
|
||||
self.socket_put_confirmed(f"axisref[{axis_id_numeric}]=1")
|
||||
self.socket_put_confirmed("folerr=0")
|
||||
|
||||
self._ogalil_folerr_not_ignore()
|
||||
|
||||
|
||||
def _ogalil_folerr_not_ignore(self):
|
||||
self.socket_put_confirmed("IgNoFol=0")
|
||||
|
||||
def _ogalil_switchsocket_switch_all_on(self):
|
||||
if not self._ogalil_switchsocket_are_all_on():
|
||||
for j in range(1, 5):
|
||||
self._ogalil_switchsocket(j, 1)
|
||||
time.sleep(0.4)
|
||||
|
||||
def _ogalil_switchsocket_status(self):
|
||||
contents = urllib.request.urlopen("http://mpc3217:8091/netio.xml").read()
|
||||
root = ET.fromstring(contents)
|
||||
returnvalue = []
|
||||
for j in range(0, 4):
|
||||
status = int(root[1][j][2].text)
|
||||
returnvalue.append(int(root[1][j][2].text))
|
||||
if status:
|
||||
print(f"Controller {j+1} is ON")
|
||||
else:
|
||||
print(f"Controller {j+1} is OFF")
|
||||
return returnvalue
|
||||
|
||||
def show_status_other(self):
|
||||
swver = float(self.socket_put_and_receive("MGswver"))
|
||||
allaxref = float(self.socket_put_and_receive("MGallaxref"))
|
||||
tempab = float(self.socket_put_and_receive("MGtempab"))
|
||||
timeab = float(self.socket_put_and_receive("MGtimeab"))
|
||||
IgNoFol = float(self.socket_put_and_receive("MGIgNoFol"))
|
||||
print(
|
||||
f"OMNY galil firmware {swver:2.0f}, TempAbort: {tempab:1.0f}, Allaxref: {allaxref:1.0f}, TimeAbort: {timeab:1.0f}, Ignore Folerr: {IgNoFol:1.0f}\n"
|
||||
)
|
||||
if self.sock.port == 8083:
|
||||
self._ogalil_switchsocket_status()
|
||||
|
||||
|
||||
class OMNYGalilMotor(Device, PositionerBase):
|
||||
USER_ACCESS = ["controller", "find_reference", "omny_osamx_to_scan_center", "drive_axis_to_limit", "_ogalil_folerr_reset_and_ignore", "_ogalil_set_axis_to_pos_wo_reference_search", "get_motor_limit_switch", "axis_is_referenced", "get_motor_temperature", "folerr_status"]
|
||||
readback = Cpt(OMNYGalilReadbackSignal, signal_name="readback", kind="hinted")
|
||||
user_setpoint = Cpt(GalilSetpointSignal, signal_name="setpoint")
|
||||
motor_resolution = Cpt(GalilMotorResolution, signal_name="resolution", kind="config")
|
||||
motor_is_moving = Cpt(GalilMotorIsMoving, signal_name="motor_is_moving", kind="normal")
|
||||
all_axes_referenced = Cpt(GalilAxesReferenced, signal_name="all_axes_referenced", kind="config")
|
||||
high_limit_travel = Cpt(Signal, value=0, kind="omitted")
|
||||
low_limit_travel = Cpt(Signal, value=0, kind="omitted")
|
||||
|
||||
SUB_READBACK = "readback"
|
||||
SUB_CONNECTION_CHANGE = "connection_change"
|
||||
_default_sub = SUB_READBACK
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
axis_Id,
|
||||
prefix="",
|
||||
*,
|
||||
name,
|
||||
kind=None,
|
||||
read_attrs=None,
|
||||
configuration_attrs=None,
|
||||
parent=None,
|
||||
host="mpc3217.psi.ch",
|
||||
port=8081,
|
||||
limits=None,
|
||||
sign=1,
|
||||
socket_cls=SocketIO,
|
||||
device_manager=None,
|
||||
**kwargs,
|
||||
):
|
||||
self.controller = OMNYGalilController(
|
||||
socket_cls=socket_cls, socket_host=host, socket_port=port
|
||||
)
|
||||
self.axis_Id = axis_Id
|
||||
self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric)
|
||||
self.sign = sign
|
||||
self.tolerance = kwargs.pop("tolerance", 0.5)
|
||||
self.device_mapping = kwargs.pop("device_mapping", {})
|
||||
self.device_manager = device_manager
|
||||
|
||||
if len(self.device_mapping) > 0 and self.device_manager is None:
|
||||
raise BECConfigError(
|
||||
"device_mapping has been specified but the device_manager cannot be accessed."
|
||||
)
|
||||
self.rt = self.device_mapping.get("rt")
|
||||
|
||||
super().__init__(
|
||||
prefix,
|
||||
name=name,
|
||||
kind=kind,
|
||||
read_attrs=read_attrs,
|
||||
configuration_attrs=configuration_attrs,
|
||||
parent=parent,
|
||||
**kwargs,
|
||||
)
|
||||
self.readback.name = self.name
|
||||
self.controller.subscribe(
|
||||
self._update_connection_state, event_type=self.SUB_CONNECTION_CHANGE
|
||||
)
|
||||
self._update_connection_state()
|
||||
# self.readback.subscribe(self._forward_readback, event_type=self.readback.SUB_VALUE)
|
||||
|
||||
if limits is not None:
|
||||
assert len(limits) == 2
|
||||
self.low_limit_travel.put(limits[0])
|
||||
self.high_limit_travel.put(limits[1])
|
||||
|
||||
@property
|
||||
def limits(self):
|
||||
return (self.low_limit_travel.get(), self.high_limit_travel.get())
|
||||
|
||||
@property
|
||||
def low_limit(self):
|
||||
return self.limits[0]
|
||||
|
||||
@property
|
||||
def high_limit(self):
|
||||
return self.limits[1]
|
||||
|
||||
def check_value(self, pos):
|
||||
"""Check that the position is within the soft limits"""
|
||||
low_limit, high_limit = self.limits
|
||||
|
||||
if low_limit < high_limit and not (low_limit <= pos <= high_limit):
|
||||
raise LimitError(f"position={pos} not within limits {self.limits}")
|
||||
|
||||
def _update_connection_state(self, **kwargs):
|
||||
for walk in self.walk_signals():
|
||||
walk.item._metadata["connected"] = self.controller.connected
|
||||
|
||||
def _forward_readback(self, **kwargs):
|
||||
kwargs.pop("sub_type")
|
||||
self._run_subs(sub_type="readback", **kwargs)
|
||||
|
||||
@raise_if_disconnected
|
||||
def move(self, position, wait=True, **kwargs):
|
||||
"""Move to a specified position, optionally waiting for motion to
|
||||
complete.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
position
|
||||
Position to move to
|
||||
moved_cb : callable
|
||||
Call this callback when movement has finished. This callback must
|
||||
accept one keyword argument: 'obj' which will be set to this
|
||||
positioner instance.
|
||||
timeout : float, optional
|
||||
Maximum time to wait for the motion. If None, the default timeout
|
||||
for this positioner is used.
|
||||
|
||||
Returns
|
||||
-------
|
||||
status : MoveStatus
|
||||
|
||||
Raises
|
||||
------
|
||||
TimeoutError
|
||||
When motion takes longer than `timeout`
|
||||
ValueError
|
||||
On invalid positions
|
||||
RuntimeError
|
||||
If motion fails other than timing out
|
||||
"""
|
||||
self._started_moving = False
|
||||
timeout = kwargs.pop("timeout", 100)
|
||||
status = super().move(position, timeout=timeout, **kwargs)
|
||||
self.user_setpoint.put(position, wait=False)
|
||||
|
||||
def move_and_finish():
|
||||
while self.motor_is_moving.get():
|
||||
logger.info("motor is moving")
|
||||
val = self.readback.read()
|
||||
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
|
||||
time.sleep(0.1)
|
||||
val = self.readback.read()
|
||||
success = np.isclose(val[self.name]["value"], position, atol=self.tolerance)
|
||||
|
||||
if not success:
|
||||
print(" stop")
|
||||
self._done_moving(success=success)
|
||||
logger.info("Move finished")
|
||||
|
||||
threading.Thread(target=move_and_finish, daemon=True).start()
|
||||
try:
|
||||
if wait:
|
||||
status_wait(status)
|
||||
except KeyboardInterrupt:
|
||||
self.stop()
|
||||
raise
|
||||
|
||||
return status
|
||||
|
||||
@property
|
||||
def axis_Id(self):
|
||||
return self._axis_Id_alpha
|
||||
|
||||
@axis_Id.setter
|
||||
def axis_Id(self, val):
|
||||
if isinstance(val, str):
|
||||
if len(val) != 1:
|
||||
raise ValueError("Only single-character axis_Ids are supported.")
|
||||
self._axis_Id_alpha = val
|
||||
self._axis_Id_numeric = self.controller.axis_Id_to_numeric(val)
|
||||
else:
|
||||
raise TypeError(f"Expected value of type str but received {type(val)}")
|
||||
|
||||
@property
|
||||
def axis_Id_numeric(self):
|
||||
return self._axis_Id_numeric
|
||||
|
||||
@axis_Id_numeric.setter
|
||||
def axis_Id_numeric(self, val):
|
||||
if isinstance(val, int):
|
||||
if val > 26:
|
||||
raise ValueError("Numeric value exceeds supported range.")
|
||||
self._axis_Id_alpha = self.controller.axis_Id_numeric_to_alpha(val)
|
||||
self._axis_Id_numeric = val
|
||||
else:
|
||||
raise TypeError(f"Expected value of type int but received {type(val)}")
|
||||
|
||||
@property
|
||||
def egu(self):
|
||||
"""The engineering units (EGU) for positions"""
|
||||
return "mm"
|
||||
|
||||
def _ogalil_folerr_reset_and_ignore(self):
|
||||
# pylint: disable=protected-access
|
||||
self.controller._ogalil_folerr_reset_and_ignore(self.axis_Id_numeric)
|
||||
|
||||
def _ogalil_set_axis_to_pos_wo_reference_search(self, pos_mm):
|
||||
motor_resolution = self.motor_resolution.get()
|
||||
self.controller._ogalil_set_axis_to_pos_wo_reference_search(self.axis_Id_numeric, self.axis_Id, pos_mm, motor_resolution, self.sign)
|
||||
#now force position read to cache
|
||||
val = self.readback.read()
|
||||
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
|
||||
|
||||
def find_reference(self, raise_error=1):
|
||||
"""
|
||||
Find the reference of the axis.
|
||||
"""
|
||||
verbose=1
|
||||
self.controller.find_reference(self.axis_Id_numeric, verbose, raise_error)
|
||||
#now force position read to cache
|
||||
val = self.readback.read()
|
||||
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
|
||||
|
||||
def drive_axis_to_limit(self, direction: str) -> None:
|
||||
"""
|
||||
Drive an axis to the limit in a specified direction.
|
||||
|
||||
Args:
|
||||
direction (str): Direction in which the axis should be driven to the limit. Either 'forward' or 'reverse'.
|
||||
"""
|
||||
self.controller.drive_axis_to_limit(self.axis_Id_numeric, direction, verbose=1)
|
||||
#now force position read to cache
|
||||
val = self.readback.read()
|
||||
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
|
||||
|
||||
def get_motor_limit_switch(self) -> list:
|
||||
"""
|
||||
Get status of the motor limit switches
|
||||
"""
|
||||
return self.controller.get_motor_limit_switch(self.axis_Id)
|
||||
|
||||
def get_motor_temperature(self) -> float:
|
||||
"""
|
||||
Get motor temperature
|
||||
"""
|
||||
return self.controller.motor_temperature(self.axis_Id_numeric)
|
||||
|
||||
def axis_is_referenced(self) -> bool:
|
||||
"""
|
||||
check if an axis is referenced
|
||||
"""
|
||||
return self.controller.axis_is_referenced(self.axis_Id_numeric)
|
||||
|
||||
def _get_user_param_safe(self, device, var):
|
||||
param = self.device_manager.devices[device].user_parameter
|
||||
if not param or param.get(var) is None:
|
||||
raise ValueError(f"Device {device} has no user parameter definition for {var}.")
|
||||
return param.get(var)
|
||||
|
||||
def omny_osamx_to_scan_center(self, cenx):
|
||||
if self.controller.sock.port == 8082 and self.axis_Id_numeric == 0:
|
||||
# get last setpoint
|
||||
osamx = self.device_manager.devices["osamx"]
|
||||
osamx_current_setpoint = osamx.obj.readback.get()
|
||||
omny_samx_in = self._get_user_param_safe("osamx","in")
|
||||
if np.fabs(osamx_current_setpoint-(omny_samx_in+cenx/1000)) > 0.025:
|
||||
message=f"Moving osamx to scan center. new osamx target {omny_samx_in+cenx/1000:.3f}."
|
||||
logger.info(message)
|
||||
|
||||
osamx.read_only = False
|
||||
#osamx.controller.("osamx", "controller.socket_put_confirmed('axspeed[0]=1000')")
|
||||
osamx.set(omny_samx_in+cenx/1000)
|
||||
time.sleep(0.1)
|
||||
while(osamx.motor_is_moving.get()):
|
||||
time.sleep(0.05)
|
||||
osamx.read_only = True
|
||||
time.sleep(2)
|
||||
rt = self.device_manager.devices["rtx"]
|
||||
if rt.enabled:
|
||||
rt.obj.controller.laser_tracker_on()
|
||||
rt.obj.controller.laser_tracker_check_and_wait_for_signalstrength()
|
||||
|
||||
def folerr_status(self) -> bool:
|
||||
return self.controller.folerr_status(self.axis_Id_numeric)
|
||||
|
||||
def stop(self, *, success=False):
|
||||
self.controller.stop_all_axes()
|
||||
return super().stop(success=success)
|
||||
111
csaxs_bec/devices/omny/omny_dewar.py
Normal file
111
csaxs_bec/devices/omny/omny_dewar.py
Normal file
@@ -0,0 +1,111 @@
|
||||
import time
|
||||
import datetime
|
||||
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device
|
||||
from ophyd import DynamicDeviceComponent as Dcpt
|
||||
from ophyd import EpicsSignal
|
||||
from prettytable import FRAME, PrettyTable
|
||||
import numpy as np
|
||||
|
||||
class OMNYDewarError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class OMNYDewar(Device):
|
||||
USER_ACCESS = [
|
||||
"show_all",
|
||||
"is_flow_low",
|
||||
"help",
|
||||
]
|
||||
SUB_VALUE = "value"
|
||||
_default_sub = SUB_VALUE
|
||||
|
||||
|
||||
|
||||
dewar_press_0 = Cpt(
|
||||
EpicsSignal, name="dewar_press_0", read_pv="XOMNY-TEMP-DEWAR-PRESS0:GET"
|
||||
)
|
||||
|
||||
dewar_refilling = Cpt(
|
||||
EpicsSignal, name="dewar_refilling", read_pv="XOMNY-TEMP-DEWAR-Refilling:GET"
|
||||
)
|
||||
|
||||
dewar_press_1 = Cpt(
|
||||
EpicsSignal, name="dewar_press_1", read_pv="XOMNY-TEMP-DEWAR-PRESS1:GET"
|
||||
)
|
||||
|
||||
dewar_press_2 = Cpt(
|
||||
EpicsSignal, name="dewar_press_2", read_pv="XOMNY-TEMP-DEWAR-PRESS2:GET"
|
||||
)
|
||||
|
||||
dewar_flow_0 = Cpt(
|
||||
EpicsSignal, name="dewar_flow_0", read_pv="XOMNY-TEMP-DEWAR-FLOW0:GET"
|
||||
)
|
||||
|
||||
dewar_valvepos = Cpt(
|
||||
EpicsSignal, name="dewar_valvepos", read_pv="XOMNY-TEMP-DEWAR-ValvePos:GET"
|
||||
)
|
||||
|
||||
dewar_supply_voltage = Cpt(
|
||||
EpicsSignal, name="dewar_supply_voltage", read_pv="XOMNY-TEMP-DEWAR-SupplyVoltage:GET"
|
||||
)
|
||||
|
||||
dewar_uptime_h = Cpt(
|
||||
EpicsSignal, name="dewar_uptime_h", read_pv="XOMNY-TEMP-DEWAR-UpH:GET"
|
||||
)
|
||||
dewar_uptime_m = Cpt(
|
||||
EpicsSignal, name="dewar_uptime_m", read_pv="XOMNY-TEMP-DEWAR-UpM:GET"
|
||||
)
|
||||
dewar_uptime_s = Cpt(
|
||||
EpicsSignal, name="dewar_uptime_s", read_pv="XOMNY-TEMP-DEWAR-UpS:GET"
|
||||
)
|
||||
|
||||
dewar_valve_movements_h = Cpt(
|
||||
EpicsSignal, name="dewar_valve_movements_h", read_pv="XOMNY-TEMP-DEWAR-MovH:GET"
|
||||
)
|
||||
dewar_valve_movements_m = Cpt(
|
||||
EpicsSignal, name="dewar_valve_movements_m", read_pv="XOMNY-TEMP-DEWAR-MovM:GET"
|
||||
)
|
||||
dewar_valve_movements_s = Cpt(
|
||||
EpicsSignal, name="dewar_valve_movements_s", read_pv="XOMNY-TEMP-DEWAR-MovS:GET"
|
||||
)
|
||||
|
||||
def __init__(self, prefix="", *, name, **kwargs):
|
||||
super().__init__(prefix, name=name, **kwargs)
|
||||
self.dewar_flow_0.subscribe(self._emit_value)
|
||||
|
||||
def _emit_value(self, **kwargs):
|
||||
timestamp = kwargs.pop("timestamp", time.time())
|
||||
self.wait_for_connection()
|
||||
self._run_subs(sub_type=self.SUB_VALUE, timestamp=timestamp, obj=self)
|
||||
|
||||
|
||||
def is_flow_low(self):
|
||||
if(float(self.dewar_flow_0.get())<3.8):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def show_all(self):
|
||||
red = "\x1b[91m"
|
||||
white = "\x1b[0m"
|
||||
|
||||
|
||||
print("OMNY Dewar Status")
|
||||
print(f" DewarPressure: {float(self.dewar_press_0.get()):.0f} mbar")
|
||||
print(f" Dewar Refilling: {float(self.dewar_refilling.get()):.0f}")
|
||||
print(f" LN2flow In Pressure: {float(self.dewar_press_1.get()):.0f} mbar")
|
||||
print(f" LN2flow Out Pressure: {float(self.dewar_press_2.get()):.0f} mbar")
|
||||
print(f" LN2flow In Flow Rate: {float(self.dewar_flow_0.get()):.1f} l/s")
|
||||
if self.is_flow_low():
|
||||
print(red + "This flow rate is LOW. Increase the LN2flow In Pressure" + white)
|
||||
print(f" Valve Opening (0-1): {float(self.dewar_valvepos.get()):.2f}")
|
||||
print(f" Valve Supply Voltage: {float(self.dewar_supply_voltage.get()):.2f} V")
|
||||
print(f" Uptime of System: {float(self.dewar_uptime_h.get()):2.0f}:{float(self.dewar_uptime_m.get()):2.0f}:{float(self.dewar_uptime_s.get()):2.0f}")
|
||||
print(f" Active Valve Movements: {float(self.dewar_valve_movements_h.get()):2.0f}:{float(self.dewar_valve_movements_m.get()):2.0f}:{float(self.dewar_valve_movements_s.get()):2.0f}")
|
||||
|
||||
|
||||
def help(self):
|
||||
print("Help for OMNY Dewar:")
|
||||
print("This device shows an overview of the dewar status. Use show_all()")
|
||||
@@ -22,6 +22,7 @@ class OMNYSampleStorage(Device):
|
||||
"set_sample_in_samplestage",
|
||||
"unset_sample_in_samplestage",
|
||||
"get_sample_name_in_samplestage",
|
||||
"get_sample_name_in_gripper",
|
||||
"get_sample_name",
|
||||
"is_sample_in_samplestage",
|
||||
"set_shuttle_slot",
|
||||
@@ -30,6 +31,7 @@ class OMNYSampleStorage(Device):
|
||||
"is_shuttle_slot_used",
|
||||
"search_shuttle_in_slot",
|
||||
"show_all",
|
||||
"help",
|
||||
]
|
||||
SUB_VALUE = "value"
|
||||
_default_sub = SUB_VALUE
|
||||
@@ -137,20 +139,28 @@ class OMNYSampleStorage(Device):
|
||||
elif container == "C":
|
||||
getattr(self.sample_shuttle_C_placed, f"sample{slot_nr}").set(1)
|
||||
getattr(self.sample_shuttle_C_names, f"sample{slot_nr}").set(name)
|
||||
elif container == "O":
|
||||
getattr(self.sample_placed, f"sample{slot_nr}").set(1)
|
||||
getattr(self.sample_names, f"sample{slot_nr}").set(name)
|
||||
|
||||
def unset_sample_slot(self, shuttle: str, slot_nr: int) -> bool:
|
||||
|
||||
def unset_sample_slot(self, container: str, slot_nr: int) -> bool:
|
||||
if slot_nr > 20:
|
||||
raise OMNYSampleStorageError(f"Invalid slot number {slot_nr}.")
|
||||
|
||||
if shuttle == "A":
|
||||
if container == "A":
|
||||
getattr(self.sample_shuttle_A_placed, f"sample{slot_nr}").set(0)
|
||||
getattr(self.sample_shuttle_A_names, f"sample{slot_nr}").set("-")
|
||||
if shuttle == "B":
|
||||
elif container == "B":
|
||||
getattr(self.sample_shuttle_B_placed, f"sample{slot_nr}").set(0)
|
||||
getattr(self.sample_shuttle_B_names, f"sample{slot_nr}").set("-")
|
||||
if shuttle == "C":
|
||||
elif container == "C":
|
||||
getattr(self.sample_shuttle_C_placed, f"sample{slot_nr}").set(0)
|
||||
getattr(self.sample_shuttle_C_names, f"sample{slot_nr}").set("-")
|
||||
elif container == "O":
|
||||
getattr(self.sample_placed, f"sample{slot_nr}").set(0)
|
||||
getattr(self.sample_names, f"sample{slot_nr}").set("-")
|
||||
|
||||
|
||||
def set_shuttle_slot(self, container: str, slot_nr: int) -> bool:
|
||||
if slot_nr > 6:
|
||||
@@ -275,8 +285,24 @@ class OMNYSampleStorage(Device):
|
||||
row = []
|
||||
row.extend([f"Position {i:3d}"])
|
||||
if self.is_sample_slot_used("O", i):
|
||||
row.extend(self.get_sample_name("O", i))
|
||||
name = self.get_sample_name("O", i)
|
||||
row.extend([name])
|
||||
else:
|
||||
row.extend(["free"])
|
||||
t.add_row(row)
|
||||
print(t)
|
||||
|
||||
print("Use dev.omny_samples.help() for assistance.")
|
||||
|
||||
def help(self):
|
||||
print("Help for OMNY sample storage:")
|
||||
print(" To get an overview use dev.omny_samples.show_all()")
|
||||
print(" Modify a slot:")
|
||||
print(" dev.omny_samples.unset_sample_slot('system',position)")
|
||||
print(" dev.omny_samples.set_sample_slot('system',position,'name')")
|
||||
print(" system can be A, B, C, O")
|
||||
print(" dev.omny_samples.set_sample_in_gripper('name') / unset_sample_in_gripper()")
|
||||
print(" dev.omny_samples.set_sample_in_samplestage('name'), unset_sample_in_samplestage()")
|
||||
print(" dev.omny_samples.set_shuttle_slot(container, slot_nr) / unset_shuttle_slot(slot_nr)")
|
||||
print(" dev.omny_samples.set_shuttle_slot('A',2)")
|
||||
print(" omny.otransfer_help()")
|
||||
|
||||
260
csaxs_bec/devices/omny/omny_temperatures.py
Normal file
260
csaxs_bec/devices/omny/omny_temperatures.py
Normal file
@@ -0,0 +1,260 @@
|
||||
import time
|
||||
import datetime
|
||||
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device
|
||||
from ophyd import DynamicDeviceComponent as Dcpt
|
||||
from ophyd import EpicsSignal
|
||||
from prettytable import FRAME, PrettyTable
|
||||
import numpy as np
|
||||
|
||||
class OMNYTemperaturesError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class OMNYTemperatures(Device):
|
||||
USER_ACCESS = [
|
||||
"temperature_controller_used_get_name_and_values",
|
||||
"set_setpoint",
|
||||
"show_all",
|
||||
"help",
|
||||
"_set_TEMP_default_setpoints",
|
||||
"temperature_controller_TEMP_running",
|
||||
"temperature_controller_CRYO_running",
|
||||
]
|
||||
SUB_VALUE = "value"
|
||||
_default_sub = SUB_VALUE
|
||||
|
||||
temperature = {
|
||||
f"temperature{i}": (EpicsSignal, f"XOMNY-TEMP{i}:GET", {}) for i in range(1, 49)
|
||||
}
|
||||
temperature = Dcpt(temperature)
|
||||
|
||||
temperature_setpoint = {
|
||||
f"temperature_setpoint{i}": (EpicsSignal, f"XOMNY-TEMP{i}:SET", {}) for i in range(1, 49)
|
||||
}
|
||||
temperature_setpoint = Dcpt(temperature_setpoint)
|
||||
|
||||
temperature_unit = {
|
||||
f"temperature_unit{i}": (EpicsSignal, f"XOMNY-TEMP{i}:GET.EGU", {}) for i in range(1, 49)
|
||||
}
|
||||
temperature_unit = Dcpt(temperature_unit)
|
||||
|
||||
temperature_alarmstate = {
|
||||
f"temperature_alarmstate{i}": (EpicsSignal, f"XOMNY-TEMP{i}:ALARM", {}) for i in range(1, 49)
|
||||
}
|
||||
temperature_alarmstate = Dcpt(temperature_alarmstate)
|
||||
|
||||
temperature_names = {
|
||||
f"temperature_name{i}": (EpicsSignal, f"XOMNY-TEMP{i}:GET.DESC", {"string": True})
|
||||
for i in range(1, 49)
|
||||
}
|
||||
temperature_names = Dcpt(temperature_names)
|
||||
|
||||
temperature_update_time = Cpt(
|
||||
EpicsSignal, name="temperature_update_time", read_pv="XOMNY-TEMP:UPDATED.VAL"
|
||||
)
|
||||
|
||||
cryo_temperature = {
|
||||
f"cryo_temperature{i}": (EpicsSignal, f"XOMNY-TEMP-CRYO-{chr(i+ord('A')-1)}:GET.VAL", {}) for i in range(1,5)
|
||||
}
|
||||
cryo_temperature = Dcpt(cryo_temperature)
|
||||
|
||||
cryo_temperature_setpoint = {
|
||||
f"cryo_temperature_setpoint{i}": (EpicsSignal, f"XOMNY-TEMP-CRYO-{chr(i+ord('A')-1)}:SET.VAL", {}) for i in range(1,5)
|
||||
}
|
||||
cryo_temperature_setpoint = Dcpt(cryo_temperature_setpoint)
|
||||
|
||||
cryo_temperature_name = {
|
||||
f"cryo_temperature_name{i}": (EpicsSignal, f"XOMNY-TEMP-CRYO-{chr(i+ord('A')-1)}:GET.DESC", {}) for i in range(1,5)
|
||||
}
|
||||
cryo_temperature_name = Dcpt(cryo_temperature_name)
|
||||
|
||||
cryo_temperature_update_time = Cpt(
|
||||
EpicsSignal, name="cryo_temperature_update_time", read_pv="XOMNY-TEMP-CRYO:UPDATED.VAL"
|
||||
)
|
||||
|
||||
cryo_temperature_closed_loop = Cpt(
|
||||
EpicsSignal, name="cryo_temperature_closed_loop", read_pv="XOMNY-TEMP-CRYO:CLOSEDLOOP.VAL"
|
||||
)
|
||||
|
||||
def __init__(self, prefix="", *, name, **kwargs):
|
||||
super().__init__(prefix, name=name, **kwargs)
|
||||
self.temperature.temperature1.subscribe(self._emit_value)
|
||||
|
||||
def _emit_value(self, **kwargs):
|
||||
timestamp = kwargs.pop("timestamp", time.time())
|
||||
self.wait_for_connection()
|
||||
self._run_subs(sub_type=self.SUB_VALUE, timestamp=timestamp, obj=self)
|
||||
|
||||
def set_setpoint(self, type: str, controller_nr: int, temperature: float):
|
||||
if type == "TEMP":
|
||||
getattr(self.temperature_setpoint, f"temperature_setpoint{controller_nr}").set(temperature)
|
||||
elif type == "CRYO":
|
||||
getattr(self.cryo_temperature_setpoint, f"cryo_temperature_setpoint{controller_nr}").set(temperature)
|
||||
else:
|
||||
raise OMNYTemperaturesError("invalid type")
|
||||
|
||||
|
||||
def _set_TEMP_default_setpoints(self):
|
||||
self.set_setpoint("TEMP",8,-199.9)
|
||||
time.sleep(0.1)
|
||||
self.set_setpoint("TEMP",9,23)
|
||||
time.sleep(0.1)
|
||||
self.set_setpoint("TEMP",10,73.2)
|
||||
time.sleep(0.1)
|
||||
self.set_setpoint("TEMP",16,-199.9)
|
||||
time.sleep(0.1)
|
||||
self.set_setpoint("TEMP",17,23)
|
||||
time.sleep(0.1)
|
||||
self.set_setpoint("TEMP",18,26)
|
||||
time.sleep(0.1)
|
||||
self.set_setpoint("TEMP",19,26)
|
||||
time.sleep(0.1)
|
||||
self.set_setpoint("TEMP",20,26)
|
||||
time.sleep(0.1)
|
||||
self.set_setpoint("TEMP",21,23)
|
||||
time.sleep(0.1)
|
||||
self.set_setpoint("TEMP",22,-199.9)
|
||||
time.sleep(0.1)
|
||||
self.set_setpoint("TEMP",23,-199.9)
|
||||
time.sleep(0.1)
|
||||
self.set_setpoint("TEMP",24,-199.9)
|
||||
time.sleep(0.1)
|
||||
self.set_setpoint("TEMP",25,25)
|
||||
time.sleep(0.1)
|
||||
self.set_setpoint("TEMP",27,25)
|
||||
time.sleep(0.1)
|
||||
self.set_setpoint("TEMP",28,73.2)
|
||||
time.sleep(0.1)
|
||||
self.set_setpoint("TEMP",29,73.2)
|
||||
time.sleep(0.1)
|
||||
self.set_setpoint("TEMP",30,73.2)
|
||||
time.sleep(0.1)
|
||||
self.set_setpoint("TEMP",31,73.2)
|
||||
time.sleep(0.1)
|
||||
self.set_setpoint("TEMP",35,25)
|
||||
time.sleep(0.1)
|
||||
self.set_setpoint("TEMP",36,25)
|
||||
time.sleep(0.1)
|
||||
self.set_setpoint("TEMP",37,25)
|
||||
time.sleep(0.1)
|
||||
self.set_setpoint("TEMP",38,73.2)
|
||||
time.sleep(0.1)
|
||||
self.set_setpoint("TEMP",39,73.2)
|
||||
time.sleep(0.1)
|
||||
self.set_setpoint("TEMP",41,30)
|
||||
time.sleep(0.1)
|
||||
self.set_setpoint("TEMP",42,25)
|
||||
time.sleep(0.1)
|
||||
self.set_setpoint("TEMP",44,25)
|
||||
time.sleep(0.1)
|
||||
self.set_setpoint("TEMP",45,-199.9)
|
||||
time.sleep(0.1)
|
||||
self.set_setpoint("TEMP",46,73.2)
|
||||
time.sleep(0.1)
|
||||
self.set_setpoint("TEMP",47,73.2)
|
||||
time.sleep(0.1)
|
||||
|
||||
def temperature_controller_TEMP_running(self):
|
||||
time_diff = np.fabs(float(self.temperature_update_time.get()) - time.time())
|
||||
if time_diff > 600:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def temperature_controller_CRYO_running(self):
|
||||
time_diff = np.fabs(float(self.cryo_temperature_update_time.get()) - time.time())
|
||||
if time_diff > 600:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def temperature_controller_used_get_name_and_values(self, type: str, controller_nr: int) -> bool:
|
||||
if type == "TEMP":
|
||||
controller_name = str(getattr(self.temperature_names, f"temperature_name{controller_nr}").get())
|
||||
if controller_name == '-' or controller_name == 'NOT INITIALIZED':
|
||||
return (False, "none", 0, 0, "none", False, False)
|
||||
else:
|
||||
temperature = float(getattr(self.temperature, f"temperature{controller_nr}").get())
|
||||
setpoint = float(getattr(self.temperature_setpoint, f"temperature_setpoint{controller_nr}").get())
|
||||
unit = str(getattr(self.temperature_unit, f"temperature_unit{controller_nr}").get())
|
||||
alarmstate = bool(getattr(self.temperature_alarmstate, f"temperature_alarmstate{controller_nr}").get())
|
||||
controller_running = self.temperature_controller_TEMP_running()
|
||||
return (True, controller_name, temperature, setpoint, unit, alarmstate,controller_running)
|
||||
elif type == "CRYO":
|
||||
controller_name = str(getattr(self.cryo_temperature_name, f"cryo_temperature_name{controller_nr}").get())
|
||||
temperature = float(getattr(self.cryo_temperature, f"cryo_temperature{controller_nr}").get())
|
||||
setpoint = float(getattr(self.cryo_temperature_setpoint, f"cryo_temperature_setpoint{controller_nr}").get())
|
||||
unit = 'K'
|
||||
alarmstate = False
|
||||
controller_running = self.temperature_controller_CRYO_running()
|
||||
return (True, controller_name, temperature, setpoint, unit, alarmstate,controller_running)
|
||||
else:
|
||||
raise OMNYTemperaturesError("invalid type")
|
||||
|
||||
|
||||
|
||||
def show_all(self):
|
||||
red = "\x1b[91m"
|
||||
white = "\x1b[0m"
|
||||
t = PrettyTable()
|
||||
t.clear()
|
||||
t.title = "OMNY Temperature Controllers"
|
||||
t.field_names = ["Channel","Name","Temperature","Setpoint","Unit","AlarmState"]
|
||||
for i in range (1,49):
|
||||
controller_status = self.temperature_controller_used_get_name_and_values("TEMP", i)
|
||||
if controller_status[0]:
|
||||
row = []
|
||||
row.extend([f"{i}"])
|
||||
row.extend([controller_status[1]])
|
||||
row.extend([f"{controller_status[2]:.2f}"])
|
||||
if (controller_status[3] < -199 and controller_status[4] == "degC") or (np.fabs(controller_status[3]-73.25) < 0.1 and controller_status[4] == "K"):
|
||||
row.extend([" "])
|
||||
else:
|
||||
row.extend([f"{controller_status[3]:.2f}"])
|
||||
row.extend([f"{controller_status[4]}"])
|
||||
if controller_status[5]:
|
||||
row.extend(["Alarm"])
|
||||
else:
|
||||
row.extend([" "])
|
||||
t.add_row(row)
|
||||
t.header = True
|
||||
t.vrules = FRAME
|
||||
print(t)
|
||||
if not self.temperature_controller_TEMP_running():
|
||||
print (red + "Warning: the temperature controller communication is not running" + white)
|
||||
print (f"The last update was {datetime.datetime.fromtimestamp(float(self.temperature_update_time.get())).strftime('%c')}\n")
|
||||
|
||||
|
||||
t.clear()
|
||||
t.title = "OMNY Cryo Temperature Controller"
|
||||
t.field_names = ["Channel","Name","Temperature","Setpoint","Unit"]
|
||||
for i in range (1,5):
|
||||
controller_status = self.temperature_controller_used_get_name_and_values("CRYO", i)
|
||||
if controller_status[0]:
|
||||
row = []
|
||||
row.extend([f"{i}"])
|
||||
row.extend([controller_status[1]])
|
||||
row.extend([f"{controller_status[2]:.2f}"])
|
||||
row.extend([f"{controller_status[3]:.2f}"])
|
||||
row.extend([f"{controller_status[4]}"])
|
||||
t.add_row(row)
|
||||
t.header = True
|
||||
t.vrules = FRAME
|
||||
print(t)
|
||||
|
||||
if bool(self.cryo_temperature_closed_loop.get()):
|
||||
print ("Cryo controller is running in closed loop.")
|
||||
else:
|
||||
print ("Cryo controller is running in " + red + "open" + white + " loop.")
|
||||
|
||||
if not self.temperature_controller_CRYO_running():
|
||||
print (red + "Warning: the temperature controller communication is not running" + white)
|
||||
|
||||
print (f"The last update was {datetime.datetime.fromtimestamp(float(self.cryo_temperature_update_time.get())).strftime('%c')}\n")
|
||||
|
||||
print("Use dev.omny_temperatures.help() for assistance.")
|
||||
|
||||
def help(self):
|
||||
print("Help for OMNY temperatures:")
|
||||
161
csaxs_bec/devices/omny/omny_vcs.py
Normal file
161
csaxs_bec/devices/omny/omny_vcs.py
Normal file
@@ -0,0 +1,161 @@
|
||||
import time
|
||||
import datetime
|
||||
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device
|
||||
from ophyd import DynamicDeviceComponent as Dcpt
|
||||
from ophyd import EpicsSignal
|
||||
from prettytable import FRAME, PrettyTable
|
||||
import numpy as np
|
||||
|
||||
class OMNYVCSError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class OMNYVCS(Device):
|
||||
USER_ACCESS = [
|
||||
"show_all",
|
||||
"valves_in_measurement_position",
|
||||
"help",
|
||||
]
|
||||
SUB_VALUE = "value"
|
||||
_default_sub = SUB_VALUE
|
||||
|
||||
|
||||
|
||||
XOMNY_ES1_EXPMP1_VOLTAGE = Cpt(
|
||||
EpicsSignal, name="XOMNY_ES1_EXPMP1_VOLTAGE", read_pv="XOMNY-ES1-EXPMP1:VOLTAGE"
|
||||
)
|
||||
|
||||
XOMNY_ES1_EXPMP1_PRESSURE = Cpt(
|
||||
EpicsSignal, name="XOMNY_ES1_EXPMP1_PRESSURE", read_pv="XOMNY-ES1-EXPMP1:PRESSURE"
|
||||
)
|
||||
|
||||
XOMNY_ES1_PU2MT1_VOLTAGE = Cpt(
|
||||
EpicsSignal, name="XOMNY_ES1_PU2MT1_VOLTAGE", read_pv="XOMNY-ES1-PU2MT1:VOLTAGE"
|
||||
)
|
||||
|
||||
XOMNY_ES1_PU2MT1_PRESSURE = Cpt(
|
||||
EpicsSignal, name="XOMNY_ES1_PU2MT1_PRESSURE", read_pv="XOMNY-ES1-PU2MT1:PRESSURE"
|
||||
)
|
||||
|
||||
XOMNY_ES1_PU1MF1_VOLTAGE = Cpt(
|
||||
EpicsSignal, name="XOMNY_ES1_PU1MF1_VOLTAGE", read_pv="XOMNY-ES1-PU1MF1:VOLTAGE"
|
||||
)
|
||||
|
||||
XOMNY_ES1_PU1MF1_PRESSURE = Cpt(
|
||||
EpicsSignal, name="XOMNY_ES1_PU1MF1_PRESSURE", read_pv="XOMNY-ES1-PU1MF1:PRESSURE"
|
||||
)
|
||||
|
||||
XOMNY_ES1_LL1MF1_VOLTAGE = Cpt(
|
||||
EpicsSignal, name="XOMNY_ES1_LL1MF1_VOLTAGE", read_pv="XOMNY-ES1-LL1MF1:VOLTAGE"
|
||||
)
|
||||
|
||||
XOMNY_ES1_LL1MF1_PRESSURE = Cpt(
|
||||
EpicsSignal, name="XOMNY_ES1_LL1MF1_PRESSURE", read_pv="XOMNY-ES1-LL1MF1:PRESSURE"
|
||||
)
|
||||
|
||||
XOMNY_ES1_VV1MT1_VOLTAGE = Cpt(
|
||||
EpicsSignal, name="XOMNY_ES1_VV1MT1_VOLTAGE", read_pv="XOMNY-ES1-VV1MT1:VOLTAGE"
|
||||
)
|
||||
|
||||
XOMNY_ES1_VV1MT1_PRESSURE = Cpt(
|
||||
EpicsSignal, name="XOMNY_ES1_VV1MT1_PRESSURE", read_pv="XOMNY-ES1-VV1MT1:PRESSURE"
|
||||
)
|
||||
|
||||
XOMNY_ES1_EXPVG1_POSITION = Cpt(
|
||||
EpicsSignal, name="XOMNY_ES1_EXPVG1_POSITION", read_pv="XOMNY-ES1-EXPVG1:POSITION"
|
||||
)
|
||||
|
||||
XOMNY_ES1_WI2VG1_POSITION = Cpt(
|
||||
EpicsSignal, name="XOMNY_ES1_WI2VG1_POSITION", read_pv="XOMNY-ES1-WI2VG1:POSITION"
|
||||
)
|
||||
|
||||
XOMNY_ES1_EXPVG2_POSITION = Cpt(
|
||||
EpicsSignal, name="XOMNY_ES1_EXPVG2_POSITION", read_pv="XOMNY-ES1-EXPVG2:POSITION"
|
||||
)
|
||||
|
||||
XOMNY_ES1_LL1VG1_CLOSE = Cpt(
|
||||
EpicsSignal, name="XOMNY_ES1_LL1VG1_CLOSE", read_pv="XOMNY-ES1-LL1VG1:CLOSE"
|
||||
)
|
||||
|
||||
XOMNY_ES1_LL1DK_VG_CLOSED = Cpt(
|
||||
EpicsSignal, name="XOMNY_ES1_LL1DK_VG_CLOSED", read_pv="XOMNY-ES1-LL1DK:VG_CLOSED"
|
||||
)
|
||||
|
||||
XOMNY_ES1_LL1SH_VG_CLOSED = Cpt(
|
||||
EpicsSignal, name="XOMNY_ES1_LL1SH_VG_CLOSED", read_pv="XOMNY-ES1-LL1SH:VG_CLOSED"
|
||||
)
|
||||
|
||||
XOMNY_ES1_LL1SH_MAN_POS_OK = Cpt(
|
||||
EpicsSignal, name="XOMNY_ES1_LL1SH_MAN_POS_OK", read_pv="XOMNY-ES1-LL1SH:MAN_POS_OK"
|
||||
)
|
||||
|
||||
|
||||
def __init__(self, prefix="", *, name, **kwargs):
|
||||
super().__init__(prefix, name=name, **kwargs)
|
||||
self.XOMNY_ES1_LL1SH_MAN_POS_OK.subscribe(self._emit_value)
|
||||
|
||||
def _emit_value(self, **kwargs):
|
||||
timestamp = kwargs.pop("timestamp", time.time())
|
||||
self.wait_for_connection()
|
||||
self._run_subs(sub_type=self.SUB_VALUE, timestamp=timestamp, obj=self)
|
||||
|
||||
|
||||
def show_all(self):
|
||||
red = "\x1b[91m"
|
||||
white = "\x1b[0m"
|
||||
|
||||
print("OMNY Vaccum Status")
|
||||
if float(self.XOMNY_ES1_EXPMP1_VOLTAGE.get()) < 0.5:
|
||||
print(red + " Main chamber: Sensor failure" + white)
|
||||
else:
|
||||
print(f" Main chamber: {float(self.XOMNY_ES1_EXPMP1_PRESSURE.get()):.2e} mbar")
|
||||
|
||||
if float(self.XOMNY_ES1_PU2MT1_VOLTAGE.get()) < 0.5:
|
||||
print(red + " Flight tube: Sensor failure" + white)
|
||||
else:
|
||||
print(f" Flight tube: {float(self.XOMNY_ES1_PU2MT1_PRESSURE.get()):.2e} mbar")
|
||||
|
||||
if float(self.XOMNY_ES1_PU1MF1_VOLTAGE.get()) < 0.5:
|
||||
print(red+" Beamline: Sensor failure"+white)
|
||||
else:
|
||||
print(f" Beamline: {float(self.XOMNY_ES1_PU1MF1_PRESSURE.get()):.2e} mbar")
|
||||
|
||||
if float(self.XOMNY_ES1_LL1MF1_VOLTAGE.get()) < 0.5:
|
||||
print(red+" LoadLock: Sensor failure"+white)
|
||||
else:
|
||||
print(f" LoadLock: {float(self.XOMNY_ES1_LL1MF1_PRESSURE.get()):.2e} mbar")
|
||||
|
||||
if float(self.XOMNY_ES1_VV1MT1_VOLTAGE.get()) < 0.5:
|
||||
print(red+" Pre-pump: Sensor failure"+white)
|
||||
else:
|
||||
print(f" Pre-pump: {float(self.XOMNY_ES1_VV1MT1_PRESSURE.get()):.2e} mbar")
|
||||
|
||||
print("\nValve status")
|
||||
print(f" Upstream gate valve: {str(self.XOMNY_ES1_EXPVG1_POSITION.get())}")
|
||||
#printf (" Upstream window valve: %s\n", _ovcs_upstream_window_valve)
|
||||
print(f" Downstream window bypass: {str(self.XOMNY_ES1_WI2VG1_POSITION.get())}")
|
||||
print(f" Downstream gate valve: {str(self.XOMNY_ES1_EXPVG2_POSITION.get())}")
|
||||
if self.valves_in_measurement_position():
|
||||
print("The OMNY valves are in the correct configuration to perform a measurement")
|
||||
else:
|
||||
print(red+"The valves of the OMNY vacuum system are not in the state for measurements."+white)
|
||||
|
||||
|
||||
print("\nLoad Lock Status")
|
||||
print(f" Chamber: {str(self.XOMNY_ES1_LL1VG1_CLOSE.get())}")
|
||||
print(f" Dock: {str(self.XOMNY_ES1_LL1DK_VG_CLOSED.get())}")
|
||||
print(f" Shuttle: {str(self.XOMNY_ES1_LL1SH_VG_CLOSED.get())}")
|
||||
print(f" Manipulator status: {str(self.XOMNY_ES1_LL1SH_MAN_POS_OK.get())}")
|
||||
|
||||
def valves_in_measurement_position(self):
|
||||
if str(self.XOMNY_ES1_EXPVG1_POSITION.get()) == "OPEN" and str(self.XOMNY_ES1_WI2VG1_POSITION.get()) == "CLOSED" and str(self.XOMNY_ES1_EXPVG2_POSITION.get()) == "OPEN":
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def help(self):
|
||||
print("Help for OMNY Vacuum System:")
|
||||
print("This device shows an overview of the OMNY vacuum status. Use show_all()")
|
||||
@@ -47,6 +47,7 @@ class RtFlomniController(Controller):
|
||||
"read_ssi_interferometer",
|
||||
"laser_tracker_check_signalstrength",
|
||||
"laser_tracker_check_enabled",
|
||||
"is_axis_moving",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
@@ -538,8 +539,8 @@ class RtFlomniController(Controller):
|
||||
|
||||
logger.info(
|
||||
"Flomni statistics: Average of all standard deviations: x"
|
||||
f" {self.average_stdeviations_x_st_fzp/number_of_samples_to_read}, y"
|
||||
f" {self.average_stdeviations_y_st_fzp/number_of_samples_to_read}."
|
||||
f" {self.average_stdeviations_x_st_fzp/read_counter*1000:.1f}, y"
|
||||
f" {self.average_stdeviations_y_st_fzp/read_counter*1000:.1f}"
|
||||
)
|
||||
|
||||
def publish_device_data(self, signals, point_id):
|
||||
|
||||
1233
csaxs_bec/devices/omny/rt/rt_omny_ophyd.py
Normal file
1233
csaxs_bec/devices/omny/rt/rt_omny_ophyd.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -80,6 +80,8 @@ class SmaractController(Controller):
|
||||
"describe",
|
||||
"axis_is_referenced",
|
||||
"all_axes_referenced",
|
||||
"set_closed_loop_move_speed",
|
||||
"is_axis_moving",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
|
||||
6
csaxs_bec/macros/README.md
Normal file
6
csaxs_bec/macros/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Macros
|
||||
|
||||
This directory is intended to store macros which will be loaded automatically when starting BEC.
|
||||
Macros are small functions to make repetitive tasks easier. Functions defined in python files in this directory will be accessible from the BEC console.
|
||||
Please do not put any code outside of function definitions here. If you wish for code to be automatically run when starting BEC, see the startup script at csaxs_bec/bec_ipython_client/startup/post_startup.py
|
||||
For a guide on writing macros, please see: https://bec.readthedocs.io/en/latest/user/command_line_interface.html#how-to-write-a-macro
|
||||
0
csaxs_bec/macros/__init__.py
Normal file
0
csaxs_bec/macros/__init__.py
Normal file
@@ -110,9 +110,8 @@ class LamNIMixin:
|
||||
logger.info(
|
||||
f"Compensating {[val/1000 for val in lamni_to_stage_coordinates(x_drift,y_drift)]}"
|
||||
)
|
||||
yield from self.stubs.set_and_wait(
|
||||
device=["lsamx", "lsamy"], positions=[move_x, move_y]
|
||||
)
|
||||
yield from self.stubs.set(device="lsamx", value=move_x)
|
||||
yield from self.stubs.set(device="lsamy", value=move_y)
|
||||
|
||||
time.sleep(0.01)
|
||||
rtx_current = yield from self.stubs.send_rpc_and_wait("rtx", "readback.get")
|
||||
@@ -148,9 +147,9 @@ class LamNIMixin:
|
||||
+ lamni_to_stage_coordinates(x_drift, y_drift)[1] / 1000
|
||||
+ lamni_to_stage_coordinates(x_drift2, y_drift2)[1] / 1000
|
||||
)
|
||||
yield from self.stubs.set_and_wait(
|
||||
device=["lsamx", "lsamy"], positions=[move_x, move_y]
|
||||
)
|
||||
yield from self.stubs.set(device="lsamx", value=move_x)
|
||||
yield from self.stubs.set(device="lsamy", value=move_y)
|
||||
|
||||
time.sleep(0.01)
|
||||
rtx_current = yield from self.stubs.send_rpc_and_wait("rtx", "readback.get")
|
||||
rty_current = yield from self.stubs.send_rpc_and_wait("rty", "readback.get")
|
||||
@@ -444,7 +443,7 @@ class LamNIFermatScan(ScanBase, LamNIMixin):
|
||||
}
|
||||
}
|
||||
)
|
||||
yield from self.stubs.set_and_wait(device=["lsamrot"], positions=[angle])
|
||||
yield from self.stubs.set(device="lsamrot", value=angle)
|
||||
|
||||
def scan_core(self):
|
||||
if self.scan_type == "step":
|
||||
@@ -457,7 +456,7 @@ class LamNIFermatScan(ScanBase, LamNIMixin):
|
||||
# scan ID before sending the message to the device server
|
||||
yield from self.stubs.kickoff(device="rtx")
|
||||
while True:
|
||||
yield from self.stubs.read_and_wait(group="primary", wait_group="readout_primary")
|
||||
yield from self.stubs.read(group="monitored")
|
||||
msg = self.device_manager.connector.get(MessageEndpoints.device_status("rt_scan"))
|
||||
if msg:
|
||||
status = msg
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from .flomni_fermat_scan import FlomniFermatScan
|
||||
from .jungfrau_joch_scan import JungfrauJochTestScan
|
||||
from .LamNIFermatScan import LamNIFermatScan, LamNIMoveToScanCenter
|
||||
from .omny_fermat_scan import OMNYFermatScan
|
||||
from .owis_grid import OwisGrid
|
||||
from .sgalil_grid import SgalilGrid
|
||||
|
||||
@@ -35,7 +35,7 @@ class FlomniFermatScan(SyncFlyScanBase):
|
||||
scan_name = "flomni_fermat_scan"
|
||||
scan_report_hint = "table"
|
||||
scan_type = "fly"
|
||||
required_kwargs = ["fov_size", "exp_time", "step", "angle"]
|
||||
required_kwargs = ["fovx", "fovy", "exp_time", "step", "angle"]
|
||||
arg_input = {}
|
||||
arg_bundle_size = {"bundle": len(arg_input), "min": None, "max": None}
|
||||
|
||||
@@ -59,8 +59,8 @@ class FlomniFermatScan(SyncFlyScanBase):
|
||||
Args:
|
||||
fovx(float) [um]: Fov in the piezo plane (i.e. piezo range). Max 200 um
|
||||
fovy(float) [um]: Fov in the piezo plane (i.e. piezo range). Max 100 um
|
||||
cenx(float) [mm]: center position in x.
|
||||
ceny(float) [mm]: center position in y.
|
||||
cenx(float) [um]: center position in x.
|
||||
ceny(float) [um]: center position in y.
|
||||
exp_time(float) [s]: exposure time
|
||||
step(float) [um]: stepsize
|
||||
zshift(float) [um]: shift in z
|
||||
@@ -95,6 +95,7 @@ class FlomniFermatScan(SyncFlyScanBase):
|
||||
if self.zshift < -100:
|
||||
logger.warning("The zshift is smaller than -100 um. It will be limited to -100 um.")
|
||||
self.zshift = -100
|
||||
self.flomni_rotation_status = None
|
||||
|
||||
def initialize(self):
|
||||
self.scan_motors = []
|
||||
@@ -115,13 +116,12 @@ class FlomniFermatScan(SyncFlyScanBase):
|
||||
shorten the movement time. In order to keep the last state, even if the
|
||||
server is restarted, the state is stored in a global variable in redis.
|
||||
"""
|
||||
producer = self.device_manager.producer
|
||||
msg = producer.get(MessageEndpoints.global_vars("reverse_flomni_trajectory"))
|
||||
msg = self.connector.get(MessageEndpoints.global_vars("reverse_flomni_trajectory"))
|
||||
if msg:
|
||||
val = msg.content.get("value", False)
|
||||
else:
|
||||
val = False
|
||||
producer.set(
|
||||
self.connector.set(
|
||||
MessageEndpoints.global_vars("reverse_flomni_trajectory"),
|
||||
messages.VariableMessage(value=(not val)),
|
||||
)
|
||||
@@ -150,17 +150,17 @@ class FlomniFermatScan(SyncFlyScanBase):
|
||||
yield from self.stubs.send_rpc_and_wait("rty", "set", self.positions[0][1])
|
||||
|
||||
def _prepare_setup_part2(self):
|
||||
yield from self.stubs.wait(wait_type="move", device="fsamroy", wait_group="flomni_rotation")
|
||||
yield from self.stubs.set(
|
||||
device="rtx", value=self.positions[0][0], wait_group="prepare_setup_part2"
|
||||
)
|
||||
yield from self.stubs.set(
|
||||
device="rtz", value=self.positions[0][2], wait_group="prepare_setup_part2"
|
||||
)
|
||||
if self.flomni_rotation_status:
|
||||
self.flomni_rotation_status.wait()
|
||||
|
||||
rtx_status = yield from self.stubs.set(device="rtx", value=self.positions[0][0], wait=False)
|
||||
rtz_status = yield from self.stubs.set(device="rtz", value=self.positions[0][2], wait=False)
|
||||
|
||||
yield from self.stubs.send_rpc_and_wait("rtx", "controller.laser_tracker_on")
|
||||
yield from self.stubs.wait(
|
||||
wait_type="move", device=["rtx", "rtz"], wait_group="prepare_setup_part2"
|
||||
)
|
||||
|
||||
rtx_status.wait()
|
||||
rtz_status.wait()
|
||||
|
||||
yield from self._transfer_positions_to_flomni()
|
||||
yield from self.stubs.send_rpc_and_wait(
|
||||
"rtx", "controller.move_samx_to_scan_region", self.fovx, self.cenx
|
||||
@@ -172,7 +172,7 @@ class FlomniFermatScan(SyncFlyScanBase):
|
||||
self.device_manager.connector.raise_alarm(
|
||||
severity=0,
|
||||
alarm_type="LaserTrackerSignalStrength",
|
||||
source="rtx",
|
||||
source={"device": "rtx", "reason": "low signal strength", "method": "_prepare_setup_part2"},
|
||||
metadata={},
|
||||
msg="Signal strength of the laser tracker is low, sufficient to continue. Realignment recommended!",
|
||||
)
|
||||
@@ -200,7 +200,9 @@ class FlomniFermatScan(SyncFlyScanBase):
|
||||
}
|
||||
}
|
||||
)
|
||||
yield from self.stubs.set(device="fsamroy", value=angle, wait_group="flomni_rotation")
|
||||
self.flomni_rotation_status = yield from self.stubs.set(
|
||||
device="fsamroy", value=angle, wait=False
|
||||
)
|
||||
|
||||
def _transfer_positions_to_flomni(self):
|
||||
yield from self.stubs.send_rpc_and_wait(
|
||||
@@ -276,8 +278,8 @@ class FlomniFermatScan(SyncFlyScanBase):
|
||||
# scan ID before sending the message to the device server
|
||||
yield from self.stubs.kickoff(device="rtx")
|
||||
while True:
|
||||
yield from self.stubs.read_and_wait(group="primary", wait_group="readout_primary")
|
||||
status = self.device_manager.producer.get(MessageEndpoints.device_status("rt_scan"))
|
||||
yield from self.stubs.read(group="monitored")
|
||||
status = self.connector.get(MessageEndpoints.device_status("rt_scan"))
|
||||
if status:
|
||||
status_id = status.content.get("status", 1)
|
||||
request_id = status.metadata.get("RID")
|
||||
@@ -293,23 +295,12 @@ class FlomniFermatScan(SyncFlyScanBase):
|
||||
logger.debug("reading monitors")
|
||||
# yield from self.device_rpc("rtx", "controller.kickoff")
|
||||
|
||||
def return_to_start(self):
|
||||
def move_to_start(self):
|
||||
"""return to the start position"""
|
||||
# in flomni, we need to move to the start position of the next scan
|
||||
# in flomni, we need to move to the start position of the next scan, which is the end position of the current scan
|
||||
# this method is called in finalize and overwrites the default move_to_start()
|
||||
if isinstance(self.positions, np.ndarray) and len(self.positions[-1]) == 3:
|
||||
yield from self.stubs.set(
|
||||
device="rtx", value=self.positions[-1][0], wait_group="scan_motor"
|
||||
)
|
||||
yield from self.stubs.set(
|
||||
device="rty", value=self.positions[-1][1], wait_group="scan_motor"
|
||||
)
|
||||
yield from self.stubs.set(
|
||||
device="rtz", value=self.positions[-1][2], wait_group="scan_motor"
|
||||
)
|
||||
|
||||
yield from self.stubs.wait(
|
||||
wait_type="move", device=["rtx", "rty", "rtz"], wait_group="scan_motor"
|
||||
)
|
||||
yield from self.stubs.set(device=["rtx", "rty", "rtz"], value=self.positions[-1])
|
||||
return
|
||||
|
||||
logger.warning("No positions found to return to start")
|
||||
|
||||
58
csaxs_bec/scans/jungfrau_joch_scan.py
Normal file
58
csaxs_bec/scans/jungfrau_joch_scan.py
Normal file
@@ -0,0 +1,58 @@
|
||||
""" Module with JungfrauJochTestScan class. """
|
||||
|
||||
from bec_lib import bec_logger
|
||||
from bec_server.scan_server.scans import AsyncFlyScanBase, ScanAbortion
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class JungfrauJochTestScan(AsyncFlyScanBase):
|
||||
"""Owis-based grid scan."""
|
||||
|
||||
scan_name = "jjf_test"
|
||||
# scan_report_hint = "device_progress"
|
||||
required_kwargs = ["points", "exp_time", "readout_time"]
|
||||
arg_input = {}
|
||||
arg_bundle_size = {"bundle": len(arg_input), "min": None, "max": None}
|
||||
gui_config = {
|
||||
"Acquisition Parameters": ["num_points", "cycles"],
|
||||
"Exposure Parameters": ["exp_time", "readout_time"],
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self, num_points: int, exp_time: float, readout_time: float, cycles: int = 1, **kwargs
|
||||
):
|
||||
"""
|
||||
JungfrauJoch Test scan.
|
||||
|
||||
Args:
|
||||
device (DeviceBase) : The device to be triggered, currently only for delaygenerator csaxs
|
||||
num_points (int) : Number of points per burst
|
||||
exp_time (float) : exposure time.
|
||||
readout_time (float): readout time of detector
|
||||
cycles (int) : number of cycles, default is 1
|
||||
|
||||
Example:
|
||||
scans.jjf_test(points = 100, exp_time= 1e-3, readout_time=1e-3, cycles = 2)
|
||||
"""
|
||||
if readout_time <= 0:
|
||||
raise ScanAbortion(f"Readout time must be larger than 0, provided value {readout_time}")
|
||||
super().__init__(exp_time=exp_time, readout_time=readout_time, **kwargs)
|
||||
self.device = "ddg"
|
||||
self.num_points = num_points
|
||||
self.cycles = cycles
|
||||
self.primary_readout_cycle = 0.2
|
||||
|
||||
def scan_core(self):
|
||||
logger.info(f"Starting with Scan Core")
|
||||
total_exposure = self.num_points * (self.exp_time + self.readout_time)
|
||||
for i in range(self.cycles):
|
||||
logger.info(f"Beginning cycle {i} of {self.cycles}")
|
||||
|
||||
status = yield from self.stubs.trigger(min_wait=total_exposure, wait=False)
|
||||
yield from self.stubs.read(group="monitored", point_id=self.point_id, wait=True)
|
||||
self.point_id += 1
|
||||
status.wait()
|
||||
logger.info(f"Finished cycle {i} of {self.cycles}")
|
||||
logger.info(f"Finished scan")
|
||||
self.num_pos = self.point_id
|
||||
0
csaxs_bec/scans/metadata_schema/__init__.py
Normal file
0
csaxs_bec/scans/metadata_schema/__init__.py
Normal file
12
csaxs_bec/scans/metadata_schema/metadata_schema_registry.py
Normal file
12
csaxs_bec/scans/metadata_schema/metadata_schema_registry.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# from .metadata_schema_template import ExampleSchema
|
||||
|
||||
METADATA_SCHEMA_REGISTRY = {
|
||||
# Add models which should be used to validate scan metadata here.
|
||||
# Make a model according to the template, and import it as above
|
||||
# Then associate it with a scan like so:
|
||||
# "example_scan": ExampleSchema
|
||||
}
|
||||
|
||||
# Define a default schema type which should be used as the fallback for everything:
|
||||
|
||||
DEFAULT_SCHEMA = None
|
||||
34
csaxs_bec/scans/metadata_schema/metadata_schema_template.py
Normal file
34
csaxs_bec/scans/metadata_schema/metadata_schema_template.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# # By inheriting from BasicScanMetadata you can define a schema by which metadata
|
||||
# # supplied to a scan must be validated.
|
||||
# # This schema is a Pydantic model: https://docs.pydantic.dev/latest/concepts/models/
|
||||
# # but by default it will still allow you to add any arbitrary information to it.
|
||||
# # That is to say, when you run a scan with which such a model has been associated in the
|
||||
# # metadata_schema_registry, you can supply any python dictionary with strings as keys
|
||||
# # and built-in python types (strings, integers, floats) as values, and these will be
|
||||
# # added to the experiment metadata, but it *must* contain the keys and values of the
|
||||
# # types defined in the schema class.
|
||||
# #
|
||||
# #
|
||||
# # For example, say that you would like to enforce recording information about sample
|
||||
# # pretreatment, you could define the following:
|
||||
# #
|
||||
#
|
||||
# from bec_lib.metadata_schema import BasicScanMetadata
|
||||
#
|
||||
#
|
||||
# class ExampleSchema(BasicScanMetadata):
|
||||
# treatment_description: str
|
||||
# treatment_temperature_k: int
|
||||
#
|
||||
#
|
||||
# # If this was used according to the example in metadata_schema_registry.py,
|
||||
# # then when calling the scan, the user would need to write something like:
|
||||
# >>> scans.example_scan(
|
||||
# >>> motor,
|
||||
# >>> 1,
|
||||
# >>> 2,
|
||||
# >>> 3,
|
||||
# >>> metadata={"treatment_description": "oven overnight", "treatment_temperature_k": 575},
|
||||
# >>> )
|
||||
#
|
||||
# # And the additional metadata would be saved in the HDF5 file created for the scan.
|
||||
305
csaxs_bec/scans/omny_fermat_scan.py
Normal file
305
csaxs_bec/scans/omny_fermat_scan.py
Normal file
@@ -0,0 +1,305 @@
|
||||
"""
|
||||
SCAN PLUGINS
|
||||
|
||||
All new scans should be derived from ScanBase. ScanBase provides various methods that can be customized and overriden
|
||||
but they are executed in a specific order:
|
||||
|
||||
- self.initialize # initialize the class if needed
|
||||
- self.read_scan_motors # used to retrieve the start position (and the relative position shift if needed)
|
||||
- self.prepare_positions # prepare the positions for the scan. The preparation is split into multiple sub fuctions:
|
||||
- self._calculate_positions # calculate the positions
|
||||
- self._set_positions_offset # apply the previously retrieved scan position shift (if needed)
|
||||
- self._check_limits # tests to ensure the limits won't be reached
|
||||
- self.open_scan # send an open_scan message including the scan name, the number of points and the scan motor names
|
||||
- self.stage # stage all devices for the upcoming acquisiton
|
||||
- self.run_baseline_readings # read all devices to get a baseline for the upcoming scan
|
||||
- self.scan_core # run a loop over all position
|
||||
- self._at_each_point(ind, pos) # called at each position with the current index and the target positions as arguments
|
||||
- self.finalize # clean up the scan, e.g. move back to the start position; wait everything to finish
|
||||
- self.unstage # unstage all devices that have been staged before
|
||||
- self.cleanup # send a close scan message and perform additional cleanups if needed
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
from bec_lib import bec_logger, messages
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_server.scan_server.errors import ScanAbortion
|
||||
from bec_server.scan_server.scans import SyncFlyScanBase
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class OMNYFermatScan(SyncFlyScanBase):
|
||||
scan_name = "omny_fermat_scan"
|
||||
scan_report_hint = "table"
|
||||
scan_type = "fly"
|
||||
required_kwargs = ["fovx", "fovy", "exp_time", "step", "angle"]
|
||||
arg_input = {}
|
||||
arg_bundle_size = {"bundle": len(arg_input), "min": None, "max": None}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
fovx: float,
|
||||
fovy: float,
|
||||
cenx: float,
|
||||
ceny: float,
|
||||
exp_time: float,
|
||||
step: float,
|
||||
zshift: float,
|
||||
angle: float = None,
|
||||
corridor_size: float = 3,
|
||||
parameter: dict = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
An OMNY scan following Fermat's spiral.
|
||||
|
||||
Args:
|
||||
fovx(float) [um]: Fov in the piezo plane (i.e. piezo range). Max 200 um
|
||||
fovy(float) [um]: Fov in the piezo plane (i.e. piezo range). Max 100 um
|
||||
cenx(float) [um]: center position in x.
|
||||
ceny(float) [um]: center position in y.
|
||||
exp_time(float) [s]: exposure time
|
||||
step(float) [um]: stepsize
|
||||
zshift(float) [um]: shift in z
|
||||
angle(float) [deg]: rotation angle (will rotate first)
|
||||
corridor_size(float) [um]: corridor size for the corridor optimization. Default 3 um
|
||||
|
||||
Returns:
|
||||
|
||||
Examples:
|
||||
>>> scans.omny_fermat_scan(fovx=20, fovy=25, cenx=10, ceny=0, zshift=0, angle=0, step=2, exp_time=0.01)
|
||||
"""
|
||||
|
||||
super().__init__(parameter=parameter, **kwargs)
|
||||
self.axis = []
|
||||
self.fovx = fovx
|
||||
self.fovy = fovy
|
||||
self.cenx = cenx
|
||||
self.ceny = ceny
|
||||
self.exp_time = exp_time
|
||||
self.step = step
|
||||
self.zshift = zshift
|
||||
self.angle = angle
|
||||
self.optim_trajectory = "corridor"
|
||||
self.optim_trajectory_corridor = corridor_size
|
||||
if self.fovy > 100:
|
||||
raise ScanAbortion("The FOV in y must be smaller than 100 um.")
|
||||
if self.fovx > 200:
|
||||
raise ScanAbortion("The FOV in x must be smaller than 200 um.")
|
||||
if self.zshift > 100:
|
||||
logger.warning("The zshift is larger than 100 um. It will be limited to 100 um.")
|
||||
self.zshift = 100
|
||||
if self.zshift < -100:
|
||||
logger.warning("The zshift is smaller than -100 um. It will be limited to -100 um.")
|
||||
self.zshift = -100
|
||||
self.omny_rotation_status = None
|
||||
|
||||
def initialize(self):
|
||||
self.scan_motors = []
|
||||
self.update_readout_priority()
|
||||
|
||||
def _optimize_trajectory(self):
|
||||
self.positions = self.optimize_corridor(
|
||||
self.positions, corridor_size=self.optim_trajectory_corridor
|
||||
)
|
||||
|
||||
@property
|
||||
def monitor_sync(self):
|
||||
return "rt_omny"
|
||||
|
||||
def reverse_trajectory(self):
|
||||
"""
|
||||
Reverse the trajectory. Every other scan should be reversed to
|
||||
shorten the movement time. In order to keep the last state, even if the
|
||||
server is restarted, the state is stored in a global variable in redis.
|
||||
"""
|
||||
msg = self.connector.get(MessageEndpoints.global_vars("reverse_omny_trajectory"))
|
||||
if msg:
|
||||
val = msg.content.get("value", False)
|
||||
else:
|
||||
val = False
|
||||
self.connector.set(
|
||||
MessageEndpoints.global_vars("reverse_omny_trajectory"),
|
||||
messages.VariableMessage(value=(not val)),
|
||||
)
|
||||
return val
|
||||
|
||||
def prepare_positions(self):
|
||||
self._calculate_positions()
|
||||
self._optimize_trajectory()
|
||||
flip_axes = self.reverse_trajectory()
|
||||
if flip_axes:
|
||||
self.positions = np.flipud(self.positions)
|
||||
|
||||
self.num_pos = len(self.positions)
|
||||
self._check_min_positions()
|
||||
|
||||
def _check_min_positions(self):
|
||||
if self.num_pos < 20:
|
||||
raise ScanAbortion(
|
||||
f"The number of positions must exceed 20. Currently: {self.num_pos}."
|
||||
)
|
||||
|
||||
def _prepare_setup(self):
|
||||
yield from self.stubs.send_rpc_and_wait("rtx", "controller.clear_trajectory_generator")
|
||||
yield from self.omny_rotation(self.angle)
|
||||
|
||||
yield from self.stubs.send_rpc_and_wait("rty", "set", self.positions[0][1])
|
||||
|
||||
def _prepare_setup_part2(self):
|
||||
if self.omny_rotation_status:
|
||||
self.omny_rotation_status.wait()
|
||||
|
||||
rtx_status = yield from self.stubs.set(device="rtx", value=self.positions[0][0], wait=False)
|
||||
rtz_status = yield from self.stubs.set(device="rtz", value=self.positions[0][2], wait=False)
|
||||
|
||||
yield from self.stubs.send_rpc_and_wait(
|
||||
"rtx", "controller.laser_tracker_check_and_wait_for_signalstrength"
|
||||
)
|
||||
|
||||
rtx_status.wait()
|
||||
rtz_status.wait()
|
||||
|
||||
yield from self._transfer_positions_to_omny()
|
||||
yield from self.stubs.send_rpc_and_wait("osamx", "omny_osamx_to_scan_center", self.cenx)
|
||||
|
||||
def omny_rotation(self, angle):
|
||||
# get last setpoint (cannot be based on pos get because they will deviate slightly)
|
||||
osamroy_current_setpoint = yield from self.stubs.send_rpc_and_wait(
|
||||
"osamroy", "user_setpoint.get"
|
||||
)
|
||||
if angle == osamroy_current_setpoint:
|
||||
logger.info("No rotation required")
|
||||
else:
|
||||
logger.info("Rotating to requested angle")
|
||||
yield from self.stubs.scan_report_instruction(
|
||||
{
|
||||
"readback": {
|
||||
"RID": self.metadata["RID"],
|
||||
"devices": ["osamroy"],
|
||||
"start": [osamroy_current_setpoint],
|
||||
"end": [angle],
|
||||
}
|
||||
}
|
||||
)
|
||||
self.omny_rotation_status = yield from self.stubs.set(
|
||||
device="osamroy", value=angle, wait=False
|
||||
)
|
||||
|
||||
def _transfer_positions_to_omny(self):
|
||||
yield from self.stubs.send_rpc_and_wait(
|
||||
"rtx", "controller.add_pos_to_scan", self.positions.tolist()
|
||||
)
|
||||
|
||||
def _calculate_positions(self):
|
||||
self.positions = self.get_omny_fermat_spiral_pos(
|
||||
-np.abs(self.fovx / 2),
|
||||
np.abs(self.fovx / 2),
|
||||
-np.abs(self.fovy / 2),
|
||||
np.abs(self.fovy / 2),
|
||||
step=self.step,
|
||||
spiral_type=0,
|
||||
center=False,
|
||||
)
|
||||
|
||||
def get_omny_fermat_spiral_pos(
|
||||
self, m1_start, m1_stop, m2_start, m2_stop, step=1, spiral_type=0, center=False
|
||||
):
|
||||
"""
|
||||
Calculate positions for a Fermat spiral scan.
|
||||
|
||||
Args:
|
||||
m1_start(float): start position in m1
|
||||
m1_stop(float): stop position in m1
|
||||
m2_start(float): start position in m2
|
||||
m2_stop(float): stop position in m2
|
||||
step(float): stepsize
|
||||
spiral_type(int): 0 for traditional Fermat spiral
|
||||
center(bool): whether to include the center position
|
||||
|
||||
Returns:
|
||||
positions(array): positions
|
||||
"""
|
||||
positions = []
|
||||
phi = 2 * np.pi * ((1 + np.sqrt(5)) / 2.0) + spiral_type * np.pi
|
||||
|
||||
start = int(not center)
|
||||
|
||||
length_axis1 = np.abs(m1_stop - m1_start)
|
||||
length_axis2 = np.abs(m2_stop - m2_start)
|
||||
n_max = int(length_axis1 * length_axis2 * 3.2 / step / step)
|
||||
|
||||
z_pos = self.zshift
|
||||
|
||||
for ii in range(start, n_max):
|
||||
radius = step * 0.57 * np.sqrt(ii)
|
||||
# FOV is restructed below at check pos in range
|
||||
if abs(radius * np.sin(ii * phi)) > length_axis1 / 2:
|
||||
continue
|
||||
if abs(radius * np.cos(ii * phi)) > length_axis2 / 2:
|
||||
continue
|
||||
x = radius * np.sin(ii * phi)
|
||||
y = radius * np.cos(ii * phi)
|
||||
positions.append([x + self.cenx, y + self.ceny, z_pos])
|
||||
left_lower_corner = [
|
||||
min(m1_start, m1_stop) + self.cenx,
|
||||
min(m2_start, m2_stop) + self.ceny,
|
||||
z_pos,
|
||||
]
|
||||
right_upper_corner = [
|
||||
max(m1_start, m1_stop) + self.cenx,
|
||||
max(m2_start, m2_stop) + self.ceny,
|
||||
z_pos,
|
||||
]
|
||||
positions.append(left_lower_corner)
|
||||
positions.append(right_upper_corner)
|
||||
return np.array(positions)
|
||||
|
||||
def scan_core(self):
|
||||
# use a device message to receive the scan number and
|
||||
# scan ID before sending the message to the device server
|
||||
yield from self.stubs.kickoff(device="rtx")
|
||||
while True:
|
||||
yield from self.stubs.read(group="monitored")
|
||||
status = self.connector.get(MessageEndpoints.device_status("rt_scan"))
|
||||
if status:
|
||||
status_id = status.content.get("status", 1)
|
||||
request_id = status.metadata.get("RID")
|
||||
if status_id == 0 and self.metadata.get("RID") == request_id:
|
||||
break
|
||||
if status_id == 2 and self.metadata.get("RID") == request_id:
|
||||
raise ScanAbortion(
|
||||
"An error occured during the omny readout:"
|
||||
f" {status.metadata.get('error')}"
|
||||
)
|
||||
|
||||
time.sleep(1)
|
||||
logger.debug("reading monitors")
|
||||
# yield from self.device_rpc("rtx", "controller.kickoff")
|
||||
|
||||
def move_to_start(self):
|
||||
"""return to the start position"""
|
||||
# in omny, we need to move to the start position of the next scan, which is the end position of the current scan
|
||||
# this method is called in finalize and overwrites the default move_to_start()
|
||||
if isinstance(self.positions, np.ndarray) and len(self.positions[-1]) == 3:
|
||||
yield from self.stubs.set(device=["rtx", "rty", "rtz"], value=self.positions[-1])
|
||||
return
|
||||
|
||||
logger.warning("No positions found to return to start")
|
||||
|
||||
def run(self):
|
||||
self.initialize()
|
||||
yield from self.read_scan_motors()
|
||||
self.prepare_positions()
|
||||
yield from self._prepare_setup()
|
||||
yield from self.open_scan()
|
||||
yield from self.stage()
|
||||
yield from self.run_baseline_reading()
|
||||
yield from self._prepare_setup_part2()
|
||||
yield from self.scan_core()
|
||||
yield from self.finalize()
|
||||
yield from self.unstage()
|
||||
yield from self.cleanup()
|
||||
@@ -262,10 +262,8 @@ class OwisGrid(AsyncFlyScanBase):
|
||||
yield from self.stubs.send_rpc_and_wait("samy", "acceleration.put", self.acc_time)
|
||||
|
||||
# Start motion and send triggers
|
||||
yield from self.stubs.set(
|
||||
device="samy",
|
||||
value=(self.end_y + (self.sign * self.premove_distance)),
|
||||
wait_group="flyer",
|
||||
flyer_status = yield from self.stubs.set(
|
||||
device="samy", value=(self.end_y + (self.sign * self.premove_distance)), wait=False
|
||||
)
|
||||
# Trigger fast shutter, open them right away
|
||||
yield from self.stubs.send_rpc_and_wait("ddg_fsh", "trigger")
|
||||
@@ -275,32 +273,28 @@ class OwisGrid(AsyncFlyScanBase):
|
||||
# Trigger detectors
|
||||
yield from self.stubs.send_rpc_and_wait("ddg_detectors", "trigger")
|
||||
|
||||
# Readout primary devices, this waits and could lead to additional overheads
|
||||
# if devices are slow to response. For optimizing performance, primary devices
|
||||
# Readout monitored devices, this waits and could lead to additional overheads
|
||||
# if devices are slow to response. For optimizing performance, monitored devices
|
||||
# could be read out only once at beginning and end
|
||||
yield from self.stubs.read_and_wait(
|
||||
group="primary", wait_group="readout_primary", point_id=self.point_id
|
||||
)
|
||||
yield from self.stubs.read(group="monitored", point_id=self.point_id)
|
||||
self.point_id += 1
|
||||
|
||||
# Wait for motion to finish
|
||||
yield from self.stubs.wait(device="samy", wait_group="flyer", wait_type="move")
|
||||
flyer_status.wait()
|
||||
|
||||
# Move second axis by a step
|
||||
yield from self.stubs.set(
|
||||
device="samx", value=(self.start_x - ii * self.stepping_x), wait_group="motion"
|
||||
status_x = yield from self.stubs.set(
|
||||
device="samx", value=(self.start_x - ii * self.stepping_x), wait=False
|
||||
)
|
||||
# Set acceleration and velocity to max
|
||||
yield from self.stubs.send_rpc_and_wait("samy", "velocity.put", self.high_velocity)
|
||||
yield from self.stubs.send_rpc_and_wait("samy", "acceleration.put", self.high_acc_time)
|
||||
|
||||
# Move back to start
|
||||
status_prepos = yield from self.stubs.send_rpc_and_wait(
|
||||
"samy", "move", (self.start_y - self.premove_distance)
|
||||
)
|
||||
yield from self.stubs.set(device="samy", value=(self.start_y - self.premove_distance))
|
||||
|
||||
# Wait for motion to finish
|
||||
status_prepos.wait()
|
||||
status_x.wait()
|
||||
|
||||
# Set speed and acceleration to initial values
|
||||
def finalize(self):
|
||||
|
||||
@@ -126,7 +126,7 @@ class SgalilGrid(AsyncFlyScanBase):
|
||||
"""
|
||||
|
||||
# set up the delay generators
|
||||
status_ddg_detectors_burst = yield from self.stubs.send_rpc_and_wait(
|
||||
yield from self.stubs.send_rpc_and_wait(
|
||||
"ddg_detectors",
|
||||
"burst_enable",
|
||||
count=self.interval_y,
|
||||
@@ -134,7 +134,7 @@ class SgalilGrid(AsyncFlyScanBase):
|
||||
period=(self.exp_time + self.readout_time),
|
||||
config="first",
|
||||
)
|
||||
status_ddg_mcs_burst = yield from self.stubs.send_rpc_and_wait(
|
||||
yield from self.stubs.send_rpc_and_wait(
|
||||
"ddg_mcs",
|
||||
"burst_enable",
|
||||
count=self.interval_y,
|
||||
@@ -143,9 +143,9 @@ class SgalilGrid(AsyncFlyScanBase):
|
||||
config="first",
|
||||
)
|
||||
# Disable burst mod on DDF for fsh and EN of MCS card
|
||||
status_ddg_fsh_burst = yield from self.stubs.send_rpc_and_wait("ddg_fsh", "burst_disable")
|
||||
yield from self.stubs.send_rpc_and_wait("ddg_fsh", "burst_disable")
|
||||
# Set width of FSH opening to 0
|
||||
status_ddg_fsh_ttlwidth = yield from self.stubs.send_rpc_and_wait(
|
||||
yield from self.stubs.send_rpc_and_wait(
|
||||
"ddg_fsh", "set_channels", "width", 0, channels=["channelCD"]
|
||||
)
|
||||
|
||||
@@ -166,19 +166,17 @@ class SgalilGrid(AsyncFlyScanBase):
|
||||
# status_ddg_mcs_ttlwidth = yield from self.stubs.send_rpc_and_wait(
|
||||
# "ddg_mcs", "set_channels", "width", 3e-3
|
||||
# )
|
||||
status_ddg_mcs_ttldelay = yield from self.stubs.send_rpc_and_wait(
|
||||
"ddg_mcs", "set_channels", "delay", 0
|
||||
)
|
||||
yield from self.stubs.send_rpc_and_wait("ddg_mcs", "set_channels", "delay", 0)
|
||||
|
||||
# wait for the delay generators to finish setting up
|
||||
status_ddg_detectors_source.wait()
|
||||
status_ddg_mcs_source.wait()
|
||||
trigger_ddg_fsh = yield from self.stubs.send_rpc_and_wait("ddg_fsh", "trigger")
|
||||
yield from self.stubs.send_rpc_and_wait("ddg_fsh", "trigger")
|
||||
# trigger_ddg_fsh.wait()
|
||||
# status_mcs_points_per_line.wait()
|
||||
# status_mcs_lines.wait()
|
||||
|
||||
yield from self.stubs.kickoff(
|
||||
kickoff_status = yield from self.stubs.kickoff(
|
||||
device="samx",
|
||||
parameter={
|
||||
"start_y": self.start_y,
|
||||
@@ -190,19 +188,14 @@ class SgalilGrid(AsyncFlyScanBase):
|
||||
"exp_time": self.exp_time,
|
||||
"readout_time": self.readout_time,
|
||||
},
|
||||
wait=False,
|
||||
)
|
||||
target_diid = self.DIID - 1
|
||||
while True:
|
||||
# readout the primary device and wait for the fly scan to finish
|
||||
yield from self.stubs.read_and_wait(
|
||||
group="primary", wait_group="readout_primary", point_id=self.point_id
|
||||
)
|
||||
|
||||
while not kickoff_status.done:
|
||||
# readout the monitored device and wait for the fly scan to finish
|
||||
yield from self.stubs.read(group="monitored", point_id=self.point_id)
|
||||
self.point_id += 1
|
||||
status = self.stubs.get_req_status(
|
||||
device="samx", RID=self.metadata["RID"], DIID=target_diid
|
||||
)
|
||||
if status:
|
||||
break
|
||||
|
||||
time.sleep(self.sleep_time)
|
||||
if self.scan_progress() > int(self.timeout_scan_abortion / self.sleep_time):
|
||||
logger.info("would have raised a scan abortion here")
|
||||
|
||||
0
csaxs_bec/services/__init__.py
Normal file
0
csaxs_bec/services/__init__.py
Normal file
20
docs/Makefile
Normal file
20
docs/Makefile
Normal file
@@ -0,0 +1,20 @@
|
||||
# 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
Normal file
170
docs/_static/custom.css
vendored
Normal file
File diff suppressed because one or more lines are too long
34
docs/_templates/custom-class-template.rst
vendored
Normal file
34
docs/_templates/custom-class-template.rst
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
{{ 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
Normal file
66
docs/_templates/custom-module-template.rst
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
{{ 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 %}
|
||||
12
docs/api_reference/api_reference.md
Normal file
12
docs/api_reference/api_reference.md
Normal file
@@ -0,0 +1,12 @@
|
||||
(api_reference)=
|
||||
# API Reference
|
||||
|
||||
```{eval-rst}
|
||||
.. autosummary::
|
||||
:toctree: _autosummary
|
||||
:template: custom-module-template.rst
|
||||
:recursive:
|
||||
|
||||
csaxs_bec
|
||||
|
||||
```
|
||||
1
docs/assets/biotech.svg
Normal file
1
docs/assets/biotech.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="40px" viewBox="0 -960 960 960" width="40px" fill="#5f6368"><path d="M200-120v-66.67h205.33v-96h-10q-81.66 0-138.5-56.83Q200-396.33 200-478q0-61 34.5-111t91.5-71q5.33-38 35.17-61.67Q391-745.33 430-743.33l-21.33-60 40-14.38-14-36.96L504-880l13.33 37.33 39.34-14 112 296.67-41.34 14.67 14 37.33-68 24.67L560-520.67l-41.33 15.34L494-572.67q-15 16-35.17 23.34-20.16 7.33-42.39 6Q392-544.67 371-558.5q-21-13.83-34.33-34.83-32.34 16.66-51.17 47.64T266.67-478q0 53.61 37.52 91.14 37.53 37.53 91.14 37.53h338v66.66H512v96h248V-120H200Zm352.67-454L600-591.33l-76-200L476-774l76.67 200Zm-128.79-22.67q19.79 0 33.29-13.38t13.5-33.17q0-19.78-13.39-33.28Q443.9-690 424.12-690q-19.79 0-33.29 13.38-13.5 13.39-13.5 33.17t13.39 33.28q13.38 13.5 33.16 13.5ZM552.67-574ZM424-644.67Zm1.33 0Z"/></svg>
|
||||
|
After Width: | Height: | Size: 828 B |
76
docs/assets/index_contribute.svg
Normal file
76
docs/assets/index_contribute.svg
Normal file
@@ -0,0 +1,76 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
67
docs/assets/index_user_guide.svg
Normal file
67
docs/assets/index_user_guide.svg
Normal file
@@ -0,0 +1,67 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 6.3 KiB |
1
docs/assets/receipt_long.svg
Normal file
1
docs/assets/receipt_long.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="40px" viewBox="0 -960 960 960" width="40px" fill="#5f6368"><path d="M228-80q-45.83 0-77.92-32.08Q118-144.17 118-190v-123.33h124.67V-880l59.86 60 59.87-60 59.87 60 59.86-60L542-820l60-60 60 60 60-60 60 60 60-60v690q0 45.83-32.08 77.92Q777.83-80 732-80H228Zm504-66.67q19 0 31.17-12.16Q775.33-171 775.33-190v-583.33h-466v460h379.34V-190q0 19 12.16 31.17Q713-146.67 732-146.67Zm-374-468v-66.66h240v66.66H358Zm0 129.34V-552h240v66.67H358Zm328.67-129.34q-13.67 0-23.5-9.83-9.84-9.83-9.84-23.5t9.84-23.5q9.83-9.83 23.5-9.83 13.66 0 23.5 9.83Q720-661.67 720-648t-9.83 23.5q-9.84 9.83-23.5 9.83Zm0 126q-13.67 0-23.5-9.83-9.84-9.83-9.84-23.5t9.84-23.5q9.83-9.83 23.5-9.83 13.66 0 23.5 9.83Q720-535.67 720-522t-9.83 23.5q-9.84 9.83-23.5 9.83Zm-459.34 342H622v-100H184.67V-190q0 19 12.26 31.17 12.27 12.16 30.4 12.16Zm-42.66 0v-100 100Z"/></svg>
|
||||
|
After Width: | Height: | Size: 881 B |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user