Compare commits
1 Commits
ci/update_
...
update_bl_
| Author | SHA1 | Date | |
|---|---|---|---|
| cc2e0ecaa3 |
@@ -1,3 +0,0 @@
|
||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
semantic-release changelog -D version_variable=$SCRIPT_DIR/../../semantic_release/__init__.py:__version__
|
||||
semantic-release version -D version_variable=$SCRIPT_DIR/../../semantic_release/__init__.py:__version__
|
||||
@@ -1,3 +1,2 @@
|
||||
black --line-length=100 $(git diff --cached --name-only --diff-filter=ACM -- '*.py')
|
||||
isort --line-length=100 --profile=black --multi-line=3 --trailing-comma $(git diff --cached --name-only --diff-filter=ACM -- '*.py')
|
||||
git add $(git diff --cached --name-only --diff-filter=ACM -- '*.py')
|
||||
black --line-length=100 $(git diff --cached --name-only --diff-filter=ACM)
|
||||
git add $(git diff --cached --name-only --diff-filter=ACM)
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -8,9 +8,6 @@
|
||||
**/.pytest_cache
|
||||
**/*.egg*
|
||||
|
||||
# recovery_config files
|
||||
recovery_config_*
|
||||
|
||||
# file writer data
|
||||
**.h5
|
||||
|
||||
|
||||
105
.gitlab-ci.yml
105
.gitlab-ci.yml
@@ -1,105 +0,0 @@
|
||||
# This file is a template, and might need editing before it works on your project.
|
||||
# Official language image. Look for the different tagged releases at:
|
||||
# https://hub.docker.com/r/library/python/tags/
|
||||
image: $CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX/python:3.10
|
||||
|
||||
workflow:
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "schedule"
|
||||
- if: $CI_PIPELINE_SOURCE == "web"
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
|
||||
when: never
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
|
||||
include:
|
||||
- template: Security/Secret-Detection.gitlab-ci.yml
|
||||
- project: "bec/awi_utils"
|
||||
file: "awi_utils/test_pkg_versions/.gitlab-ci-job-check-pkg.yml"
|
||||
|
||||
#commands to run in the Docker container before starting each job.
|
||||
before_script:
|
||||
- pip install -e .[dev]
|
||||
|
||||
# different stages in the pipeline
|
||||
stages:
|
||||
- Formatter
|
||||
- test # must be called test for security/secret-detection to work
|
||||
- AdditionalTests
|
||||
- Deploy
|
||||
|
||||
pylint:
|
||||
stage: Formatter
|
||||
script:
|
||||
- pip install pylint pylint-exit anybadge
|
||||
- pip install -e .[dev]
|
||||
- mkdir ./pylint
|
||||
- pylint ./csaxs_bec --output-format=text --output=./pylint/pylint.log | tee ./pylint/pylint.log || pylint-exit $?
|
||||
- PYLINT_SCORE=$(sed -n 's/^Your code has been rated at \([-0-9.]*\)\/.*/\1/p' ./pylint/pylint.log)
|
||||
- anybadge --label=Pylint --file=pylint/pylint.svg --value=$PYLINT_SCORE 2=red 4=orange 8=yellow 10=green
|
||||
- echo "Pylint score is $PYLINT_SCORE"
|
||||
artifacts:
|
||||
paths:
|
||||
- ./pylint/
|
||||
expire_in: 1 week
|
||||
|
||||
pylint-check:
|
||||
stage: Formatter
|
||||
needs: []
|
||||
allow_failure: true
|
||||
before_script:
|
||||
- pip install pylint pylint-exit anybadge
|
||||
- apt-get update
|
||||
- apt-get install -y bc
|
||||
script:
|
||||
# Identify changed Python files
|
||||
- if [ "$CI_PIPELINE_SOURCE" == "merge_request_event" ]; then
|
||||
TARGET_BRANCH_COMMIT_SHA=$(git rev-parse $CI_MERGE_REQUEST_TARGET_BRANCH_NAME);
|
||||
CHANGED_FILES=$(git diff --name-only $SOURCE_BRANCH_COMMIT_SHA $TARGET_BRANCH_COMMIT_SHA | grep '\.py$' || true);
|
||||
else
|
||||
CHANGED_FILES=$(git diff --name-only $CI_COMMIT_BEFORE_SHA $CI_COMMIT_SHA | grep '\.py$' || true);
|
||||
fi
|
||||
- if [ -z "$CHANGED_FILES" ]; then echo "No Python files changed."; exit 0; fi
|
||||
|
||||
# Run pylint only on changed files
|
||||
- mkdir ./pylint
|
||||
- pylint $CHANGED_FILES --output-format=text . | tee ./pylint/pylint_changed_files.log || pylint-exit $?
|
||||
- PYLINT_SCORE=$(sed -n 's/^Your code has been rated at \([-0-9.]*\)\/.*/\1/p' ./pylint/pylint_changed_files.log)
|
||||
- echo "Pylint score is $PYLINT_SCORE"
|
||||
|
||||
# Fail the job if the pylint score is below 9
|
||||
- if [ "$(echo "$PYLINT_SCORE < 9" | bc)" -eq 1 ]; then echo "Your pylint score is below the acceptable threshold (9)."; exit 1; fi
|
||||
artifacts:
|
||||
paths:
|
||||
- ./pylint/
|
||||
expire_in: 1 week
|
||||
|
||||
secret_detection:
|
||||
before_script:
|
||||
- ""
|
||||
|
||||
config_test:
|
||||
stage: test
|
||||
script:
|
||||
- ophyd_test --config ./csaxs_bec/device_configs/ --output ./config_tests
|
||||
artifacts:
|
||||
paths:
|
||||
- ./config_tests
|
||||
when: on_failure
|
||||
expire_in: "30 days"
|
||||
allow_failure: true
|
||||
|
||||
pytest:
|
||||
stage: test
|
||||
script:
|
||||
- pip install coverage
|
||||
- coverage run --source=./csaxs_bec -m pytest -v --junitxml=report.xml --random-order --full-trace ./tests
|
||||
- coverage report
|
||||
- coverage xml
|
||||
coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
|
||||
artifacts:
|
||||
reports:
|
||||
junit: report.xml
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: coverage.xml
|
||||
69
README.md
69
README.md
@@ -6,12 +6,15 @@ You might want to run cSAXS copy scripts before in case you want to have the for
|
||||
|
||||
## Overview
|
||||
|
||||
- Clone cSAXS BEC repository into e-account (e.g. into ~/Data10/software/.)
|
||||
- Start Epics iocs
|
||||
- Start BEC, BEC server and load/modify the device config with relevant hardware
|
||||
- BEC commands
|
||||
1. Clone cSAXS BEC repository into e-account (e.g. into ~/Data10/software/.)
|
||||
2. Install BEC
|
||||
3. Start Epics iocs
|
||||
4. Start BEC, BEC server and load/modify the device config with relevant hardware
|
||||
5. BEC commands
|
||||
6. Start BEC widgets (GUI for motor control, eiger live plot)
|
||||
7. Troubleshooting and common problems
|
||||
|
||||
## Clone cSAXS BEC repository
|
||||
## 1. Clone cSAXS BEC repository
|
||||
|
||||
Clone the current cSAXS BEC repository from GIT into the new e-account.
|
||||
Create directory
|
||||
@@ -21,9 +24,19 @@ cd ~/Data10/software
|
||||
```
|
||||
Clone repository
|
||||
```bash
|
||||
git clone https://gitlab.psi.ch/bec/csaxs_bec.git
|
||||
git clone https://gitlab.psi.ch/bec/csaxs-bec.git
|
||||
```
|
||||
## Start epics iocs
|
||||
|
||||
## 2. Install BEC
|
||||
|
||||
There is a bash sript in the followin directory.
|
||||
Go to the directory and run the script on pc15543 logged in as the e-account (BEC server):
|
||||
```bash
|
||||
ssh pc15543
|
||||
cd ~/Data10/software/csaxs-bec/bin/
|
||||
./setup_bec.sh
|
||||
```
|
||||
## 3. Start epics iocs
|
||||
|
||||
You can start up the iocs while the *./setup_bec.sh* script is running. Be aware though that the scripts requires you to interact with it.
|
||||
|
||||
@@ -81,7 +94,7 @@ iocsh -7.0.6 startup.script
|
||||
```
|
||||
Be aware -7.0.6 is referring to the current epics version and might change in future (SLS 2.0)
|
||||
|
||||
## Start BEC, BEC server and load device config
|
||||
## 4. Start BEC, BEC server and load device config
|
||||
|
||||
Step 1 needs to have finished for continuing with these steps.
|
||||
What remains now is to start the bec server. Connect to pc15543 and open a new terminal to run:
|
||||
@@ -114,7 +127,7 @@ bec.config.save_current_session('~/Data10/software/current_config.yaml')
|
||||
```
|
||||
The second command is helpful if you adjust limits of motors, which will then be stored in the config and loaded if a reload of the configuration is needed.
|
||||
|
||||
## BEC commands
|
||||
## 5. BEC commands
|
||||
|
||||
A number of commands that are useful:
|
||||
|
||||
@@ -134,3 +147,41 @@ scans.line_scan(dev.samx, -1, 1, dev.samy, -1, 1, steps=20, exp_time=0.5, readou
|
||||
scans.sgalil_grid(start_y = , end_y = , interval_y = , start_x=, end_x=, interval_x =, exp_time=0.5, readout_time=3e-3, relative=True)
|
||||
```
|
||||
|
||||
## 6. Start BEC widgets (GUI for motor control, eiger live plot)
|
||||
|
||||
To start the BEC widgets, the first step is to make the bec_widgets_venv using the start startup script.
|
||||
Follow the commands below:
|
||||
``` bash
|
||||
cd ~/Data10/software/csaxs-bec/bin
|
||||
./setup_bec_widgets.sh
|
||||
```
|
||||
Afterwards, activate the environment on either cons-01 comp-1/2
|
||||
``` bash
|
||||
cd ~/Data10/software/
|
||||
source activate bec_widgets_venv/bin/activate
|
||||
```
|
||||
Each Plot needs their own shell with activate environment
|
||||
|
||||
1. Eiger Plot
|
||||
``` bash
|
||||
cd ~/Data10/software/bec-widgets/bec_widgets/examples/eiger_plot
|
||||
python eiger_plot.py
|
||||
```
|
||||
2. Motor Controller
|
||||
``` bash
|
||||
cd ~/Data10/software/bec-widgets/bec_widgets/examples/motor_movement
|
||||
python motor_example.py --config csaxs_config.yaml
|
||||
```
|
||||
|
||||
## 7. Troubleshooting and common problems
|
||||
|
||||
Sometimes the data backend for the Eiger gets stuck or misses frames, this will result in an error
|
||||
``` python
|
||||
raise EigerTimeoutError(
|
||||
ophyd_devices.epics.devices.eiger9m_csaxs.EigerTimeoutError: Reached timeout with detector state 1, std_daq state FINISHED and received frames of 100 for the file writer)
|
||||
```
|
||||
This happens more likely after CTRL C of a scan. To recover from this more reliably, perform the an acquisition in burst mode with 100 frames, little exposure until no error message is raised after. This can be up to 3 times from former experience.
|
||||
``` bash
|
||||
scans.acquire(exp_time=0.02, frames_per_trigger=100, readout_time= 3e-3)
|
||||
```
|
||||
Afterwards, you should be good to continue with 2D gridscans.
|
||||
|
||||
1
bec_plugins/__init__.py
Normal file
1
bec_plugins/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .bec_client import *
|
||||
1
bec_plugins/bec_client/__init__.py
Normal file
1
bec_plugins/bec_client/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .plugins import *
|
||||
245
bec_plugins/bec_client/high_level_interface/spec_hli.py
Normal file
245
bec_plugins/bec_client/high_level_interface/spec_hli.py
Normal file
@@ -0,0 +1,245 @@
|
||||
from bec_lib.devicemanager import Device
|
||||
from bec_lib.scan_report import ScanReport
|
||||
|
||||
# pylint:disable=undefined-variable
|
||||
# pylint: disable=too-many-arguments
|
||||
|
||||
|
||||
def dscan(
|
||||
motor1: Device, m1_from: float, m1_to: float, steps: int, exp_time: float, **kwargs
|
||||
) -> ScanReport:
|
||||
"""Relative line scan with one device.
|
||||
|
||||
Args:
|
||||
motor1 (Device): Device that should be scanned.
|
||||
m1_from (float): Start position relative to the current position.
|
||||
m1_to (float): End position relative to the current position.
|
||||
steps (int): Number of steps.
|
||||
exp_time (float): Exposure time.
|
||||
|
||||
Returns:
|
||||
ScanReport: Status object.
|
||||
|
||||
Examples:
|
||||
>>> dscan(dev.motor1, -5, 5, 10, 0.1)
|
||||
"""
|
||||
return scans.line_scan(
|
||||
motor1, m1_from, m1_to, steps=steps, exp_time=exp_time, relative=True, **kwargs
|
||||
)
|
||||
|
||||
|
||||
def d2scan(
|
||||
motor1: Device,
|
||||
m1_from: float,
|
||||
m1_to: float,
|
||||
motor2: Device,
|
||||
m2_from: float,
|
||||
m2_to: float,
|
||||
steps: int,
|
||||
exp_time: float,
|
||||
**kwargs
|
||||
) -> ScanReport:
|
||||
"""Relative line scan with two devices.
|
||||
|
||||
Args:
|
||||
motor1 (Device): First device that should be scanned.
|
||||
m1_from (float): Start position of the first device relative to its current position.
|
||||
m1_to (float): End position of the first device relative to its current position.
|
||||
motor2 (Device): Second device that should be scanned.
|
||||
m2_from (float): Start position of the second device relative to its current position.
|
||||
m2_to (float): End position of the second device relative to its current position.
|
||||
steps (int): Number of steps.
|
||||
exp_time (float): Exposure time
|
||||
|
||||
Returns:
|
||||
ScanReport: Status object.
|
||||
|
||||
Examples:
|
||||
>>> d2scan(dev.motor1, -5, 5, dev.motor2, -8, 8, 10, 0.1)
|
||||
"""
|
||||
return scans.line_scan(
|
||||
motor1,
|
||||
m1_from,
|
||||
m1_to,
|
||||
motor2,
|
||||
m2_from,
|
||||
m2_to,
|
||||
steps=steps,
|
||||
exp_time=exp_time,
|
||||
relative=True,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
def ascan(motor1, m1_from, m1_to, steps, exp_time, **kwargs):
|
||||
"""Absolute line scan with one device.
|
||||
|
||||
Args:
|
||||
motor1 (Device): Device that should be scanned.
|
||||
m1_from (float): Start position.
|
||||
m1_to (float): End position.
|
||||
steps (int): Number of steps.
|
||||
exp_time (float): Exposure time.
|
||||
|
||||
Returns:
|
||||
ScanReport: Status object.
|
||||
|
||||
Examples:
|
||||
>>> ascan(dev.motor1, -5, 5, 10, 0.1)
|
||||
"""
|
||||
return scans.line_scan(
|
||||
motor1, m1_from, m1_to, steps=steps, exp_time=exp_time, relative=False, **kwargs
|
||||
)
|
||||
|
||||
|
||||
def a2scan(motor1, m1_from, m1_to, motor2, m2_from, m2_to, steps, exp_time, **kwargs):
|
||||
"""Absolute line scan with two devices.
|
||||
|
||||
Args:
|
||||
motor1 (Device): First device that should be scanned.
|
||||
m1_from (float): Start position of the first device.
|
||||
m1_to (float): End position of the first device.
|
||||
motor2 (Device): Second device that should be scanned.
|
||||
m2_from (float): Start position of the second device.
|
||||
m2_to (float): End position of the second device.
|
||||
steps (int): Number of steps.
|
||||
exp_time (float): Exposure time
|
||||
|
||||
Returns:
|
||||
ScanReport: Status object.
|
||||
|
||||
Examples:
|
||||
>>> a2scan(dev.motor1, -5, 5, dev.motor2, -8, 8, 10, 0.1)
|
||||
"""
|
||||
return scans.line_scan(
|
||||
motor1,
|
||||
m1_from,
|
||||
m1_to,
|
||||
motor2,
|
||||
m2_from,
|
||||
m2_to,
|
||||
steps=steps,
|
||||
exp_time=exp_time,
|
||||
relative=False,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
def dmesh(motor1, m1_from, m1_to, m1_steps, motor2, m2_from, m2_to, m2_steps, exp_time, **kwargs):
|
||||
"""Relative mesh scan (grid scan) with two devices.
|
||||
|
||||
Args:
|
||||
motor1 (Device): First device that should be scanned.
|
||||
m1_from (float): Start position of the first device relative to its current position.
|
||||
m1_to (float): End position of the first device relative to its current position.
|
||||
m1_steps (int): Number of steps for motor1.
|
||||
motor2 (Device): Second device that should be scanned.
|
||||
m2_from (float): Start position of the second device relative to its current position.
|
||||
m2_to (float): End position of the second device relative to its current position.
|
||||
m2_steps (int): Number of steps for motor2.
|
||||
exp_time (float): Exposure time
|
||||
|
||||
Returns:
|
||||
ScanReport: Status object.
|
||||
|
||||
Examples:
|
||||
>>> dmesh(dev.motor1, -5, 5, 10, dev.motor2, -8, 8, 10, 0.1)
|
||||
"""
|
||||
return scans.grid_scan(
|
||||
motor1,
|
||||
m1_from,
|
||||
m1_to,
|
||||
m1_steps,
|
||||
motor2,
|
||||
m2_from,
|
||||
m2_to,
|
||||
m2_steps,
|
||||
exp_time=exp_time,
|
||||
relative=True,
|
||||
)
|
||||
|
||||
|
||||
def amesh(motor1, m1_from, m1_to, m1_steps, motor2, m2_from, m2_to, m2_steps, exp_time, **kwargs):
|
||||
"""Absolute mesh scan (grid scan) with two devices.
|
||||
|
||||
Args:
|
||||
motor1 (Device): First device that should be scanned.
|
||||
m1_from (float): Start position of the first device.
|
||||
m1_to (float): End position of the first device.
|
||||
m1_steps (int): Number of steps for motor1.
|
||||
motor2 (Device): Second device that should be scanned.
|
||||
m2_from (float): Start position of the second device.
|
||||
m2_to (float): End position of the second device.
|
||||
m2_steps (int): Number of steps for motor2.
|
||||
exp_time (float): Exposure time
|
||||
|
||||
Returns:
|
||||
ScanReport: Status object.
|
||||
|
||||
Examples:
|
||||
>>> amesh(dev.motor1, -5, 5, 10, dev.motor2, -8, 8, 10, 0.1)
|
||||
"""
|
||||
return scans.grid_scan(
|
||||
motor1,
|
||||
m1_from,
|
||||
m1_to,
|
||||
m1_steps,
|
||||
motor2,
|
||||
m2_from,
|
||||
m2_to,
|
||||
m2_steps,
|
||||
exp_time=exp_time,
|
||||
relative=False,
|
||||
)
|
||||
|
||||
|
||||
def umv(*args) -> ScanReport:
|
||||
"""Updated absolute move (i.e. blocking) for one or more devices.
|
||||
|
||||
Returns:
|
||||
ScanReport: Status object.
|
||||
|
||||
Examples:
|
||||
>>> umv(dev.samx, 1)
|
||||
>>> umv(dev.samx, 1, dev.samy, 2)
|
||||
"""
|
||||
return scans.umv(*args, relative=False)
|
||||
|
||||
|
||||
def umvr(*args) -> ScanReport:
|
||||
"""Updated relative move (i.e. blocking) for one or more devices.
|
||||
|
||||
Returns:
|
||||
ScanReport: Status object.
|
||||
|
||||
Examples:
|
||||
>>> umvr(dev.samx, 1)
|
||||
>>> umvr(dev.samx, 1, dev.samy, 2)
|
||||
"""
|
||||
return scans.umv(*args, relative=True)
|
||||
|
||||
|
||||
def mv(*args) -> ScanReport:
|
||||
"""Absolute move for one or more devices.
|
||||
|
||||
Returns:
|
||||
ScanReport: Status object.
|
||||
|
||||
Examples:
|
||||
>>> mv(dev.samx, 1)
|
||||
>>> mv(dev.samx, 1, dev.samy, 2)
|
||||
"""
|
||||
return scans.mv(*args, relative=False)
|
||||
|
||||
|
||||
def mvr(*args) -> ScanReport:
|
||||
"""Relative move for one or more devices.
|
||||
|
||||
Returns:
|
||||
ScanReport: Status object.
|
||||
|
||||
Examples:
|
||||
>>> mvr(dev.samx, 1)
|
||||
>>> mvr(dev.samx, 1, dev.samy, 2)
|
||||
"""
|
||||
return scans.mv(*args, relative=True)
|
||||
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
2
bec_plugins/bec_client/plugins/LamNI/__init__.py
Normal file
2
bec_plugins/bec_client/plugins/LamNI/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .load_additional_correction import lamni_read_additional_correction
|
||||
from .x_ray_eye_align import LamNI, XrayEyeAlign, MagLamNI, DataDrivenLamNI
|
||||
269
bec_plugins/bec_client/plugins/LamNI/bl_show_all.mac
Normal file
269
bec_plugins/bec_client/plugins/LamNI/bl_show_all.mac
Normal file
@@ -0,0 +1,269 @@
|
||||
def bl_show_all '{
|
||||
local gap
|
||||
|
||||
printf("beamline status at %s:\n",date())
|
||||
|
||||
if (!_bl_hall_temperature_ok()) {
|
||||
bl_hall_temperature
|
||||
printf("\n")
|
||||
}
|
||||
|
||||
if (_bl_sls_status_unusual()) {
|
||||
bl_sls_status
|
||||
} else {
|
||||
bl_ring_current
|
||||
}
|
||||
bl_chk_beam _show
|
||||
printf("\n")
|
||||
|
||||
printf("U19 ID gap : ",gap)
|
||||
gap = _id_get_gap_mm()
|
||||
if (gap >= 8) {
|
||||
text_bf
|
||||
}
|
||||
printf("%.3f mm\n",gap)
|
||||
text_non_bf
|
||||
|
||||
if (!_id_loss_rate_ok()) {
|
||||
id_loss_rate
|
||||
}
|
||||
|
||||
bl_shutter_status
|
||||
|
||||
if (_bl_cvd_filter_open()) {
|
||||
text_bf
|
||||
printf("CVD diamond filter : open / out\n")
|
||||
text_non_bf
|
||||
}
|
||||
|
||||
if (!_bl_xbox_valve_es1_open()) {
|
||||
bl_xbox_valve_es1 _show
|
||||
}
|
||||
|
||||
if (_bl_ln2_non_standard()) {
|
||||
text_bf
|
||||
printf("\nNon standard liquid nitrogen cooling-warning parameters occur. Please report this to your local contact.\n")
|
||||
text_non_bf
|
||||
printf("The macro bl_ln2_warn can be used to control this e-mail warning feature.\n")
|
||||
bl_ln2_warn "show"
|
||||
printf("\n")
|
||||
}
|
||||
|
||||
printf("\n")
|
||||
bl_flight_tube_pressure
|
||||
printf("\n")
|
||||
|
||||
bl_attended _show
|
||||
|
||||
_bl_check_alarm_records(1,1)
|
||||
|
||||
printf("\n")
|
||||
bl_op_msg
|
||||
}'
|
||||
|
||||
|
||||
def _bl_hall_temperature_ok() '{
|
||||
local temp_ok
|
||||
local stat
|
||||
|
||||
temp_ok = 1
|
||||
|
||||
# EH T02 average temperature
|
||||
stat = epics_get("ILUUL-02AV:TEMP")
|
||||
if ((stat < 23.0) || (stat > 26.0)) {
|
||||
temp_ok = 0
|
||||
}
|
||||
|
||||
# EH T02 temperature at T0204 axis 16
|
||||
stat = epics_get("ILUUL-0200-EB104:TEMP")
|
||||
if ((stat < 23.0) || (stat > 26.0)) {
|
||||
temp_ok = 0
|
||||
}
|
||||
|
||||
# EH T02 temperature at T0205 axis 18
|
||||
stat = epics_get("ILUUL-0200-EB105:TEMP")
|
||||
if ((stat < 23.0) || (stat > 26.0)) {
|
||||
temp_ok = 0
|
||||
}
|
||||
|
||||
return (temp_ok)
|
||||
}'
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
def bl_hall_temperature '{
|
||||
local stat
|
||||
|
||||
stat = epics_get("ILUUL-02AV:TEMP")
|
||||
printf("hall T02 average temperature : ")
|
||||
if ((stat < 23.0) || (stat > 25.0)) {
|
||||
text_bf
|
||||
}
|
||||
printf("%.2f deg.C\n",stat)
|
||||
text_non_bf
|
||||
|
||||
stat = epics_get("ILUUL-0200-EB104:TEMP")
|
||||
printf("hall temperature at T0204 axis 16 : ")
|
||||
if ((stat < 23) || (stat > 25)) {
|
||||
text_bf
|
||||
}
|
||||
printf("%.2f deg.C\n",stat)
|
||||
text_non_bf
|
||||
|
||||
stat = epics_get("ILUUL-0200-EB105:TEMP")
|
||||
printf("hall temperature at T0205 axis 18 : ")
|
||||
if ((stat < 23) || (stat > 25)) {
|
||||
text_bf
|
||||
}
|
||||
printf("%.2f deg.C\n",stat)
|
||||
text_non_bf
|
||||
|
||||
# stat = epics_get("ILUUL-0300-EB102:TEMP")
|
||||
# printf("EH T03 temperature at T0302 axis 21: ")
|
||||
# if ((stat < 23) || (stat > 25)) {
|
||||
# text_bf
|
||||
# }
|
||||
# printf("%.2f deg.C\n",stat)
|
||||
# text_non_bf
|
||||
|
||||
}'
|
||||
|
||||
def _bl_sls_status_unusual() '{
|
||||
local unusual
|
||||
local stat
|
||||
|
||||
unusual = 0
|
||||
|
||||
stat = epics_get("X12SA-SR-VAC:SETPOINT")
|
||||
if (stat != "OK") {
|
||||
unusual = 1
|
||||
}
|
||||
|
||||
stat = epics_get("ACOAU-ACCU:OP-MODE.VAL")
|
||||
if ((stat != "Light Available") && (stat != "Light-Available")) {
|
||||
unusual = 1
|
||||
}
|
||||
|
||||
stat = epics_get("ALIRF-GUN:INJ-MODE")
|
||||
if (stat != "TOP-UP") {
|
||||
unusual = 1
|
||||
}
|
||||
|
||||
# current threshold
|
||||
stat = epics_get("ALIRF-GUN:CUR-LOWLIM")
|
||||
if (stat < 350) {
|
||||
unusual = 1
|
||||
}
|
||||
|
||||
# current deadband
|
||||
stat = epics_get("ALIRF-GUN:CUR-DBAND")
|
||||
if (stat > 2) {
|
||||
unusual = 1
|
||||
}
|
||||
|
||||
# orbit feedback mode
|
||||
stat = epics_get("ARIDI-BPM:OFB-MODE")
|
||||
if (stat != "fast") {
|
||||
unusual = 1
|
||||
}
|
||||
|
||||
# fast orbit feedback
|
||||
stat = epics_get("ARIDI-BPM:FOFBSTATUS-G")
|
||||
if (stat != "running") {
|
||||
unusual = 1
|
||||
}
|
||||
|
||||
return(unusual)
|
||||
}'
|
||||
|
||||
def bl_sls_status '{
|
||||
local stat
|
||||
|
||||
stat = epics_get("ACOAU-ACCU:OP-MODE.VAL")
|
||||
printf("SLS status : ")
|
||||
if ((stat != "Light Available") && (stat != "Light-Available")) {
|
||||
text_bf
|
||||
}
|
||||
printf("%s\n",stat)
|
||||
text_non_bf
|
||||
|
||||
stat = epics_get("ALIRF-GUN:INJ-MODE")
|
||||
printf("SLS injection mode : ")
|
||||
if (stat != "TOP-UP") {
|
||||
text_bf
|
||||
}
|
||||
printf("%s\n",stat)
|
||||
text_non_bf
|
||||
|
||||
stat = epics_get("ALIRF-GUN:CUR-LOWLIM")
|
||||
printf("SLS current threshold : ")
|
||||
if (stat < 350) {
|
||||
text_bf
|
||||
}
|
||||
printf("%7.3f\n",stat)
|
||||
text_non_bf
|
||||
|
||||
stat = epics_get("ALIRF-GUN:CUR-DBAND")
|
||||
printf("SLS current deadband : ")
|
||||
if (stat > 2) {
|
||||
text_bf
|
||||
}
|
||||
printf("%7.3f\n",stat)
|
||||
text_non_bf
|
||||
|
||||
stat = epics_get("ACORF-FILL:PAT-SELECT")
|
||||
printf("SLS filling pattern : ")
|
||||
printf("%s\n",stat)
|
||||
|
||||
bl_ring_current
|
||||
|
||||
stat = epics_get("ARIDI-PCT:TAU-HOUR")
|
||||
printf("SLS filling life time : ")
|
||||
printf("%.2f h\n",stat)
|
||||
|
||||
stat = epics_get("ARIDI-BPM:OFB-MODE")
|
||||
printf("orbit feedback mode : ")
|
||||
if (stat != "fast") {
|
||||
text_bf
|
||||
}
|
||||
printf("%s\n",stat)
|
||||
text_non_bf
|
||||
|
||||
stat = epics_get("ARIDI-BPM:FOFBSTATUS-G")
|
||||
printf("fast orbit feedback : ")
|
||||
if (stat != "running") {
|
||||
text_bf
|
||||
}
|
||||
printf("%s\n",stat)
|
||||
text_non_bf
|
||||
|
||||
}'
|
||||
|
||||
def _bl_get_ring_current() '{
|
||||
return epics_get("ARIDI-PCT:CURRENT")
|
||||
}'
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
def _bl_no_ring_current() '{
|
||||
# set an arbitrary current limit of 100mA as no-beam limit
|
||||
if (_bl_get_ring_current() < 100) {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}'
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
def bl_ring_current '{
|
||||
local curr
|
||||
|
||||
curr = _bl_get_ring_current()
|
||||
|
||||
if (curr < 300) {
|
||||
text_bf
|
||||
}
|
||||
printf("SLS ring current : %.3f mA\n",curr)
|
||||
text_non_bf
|
||||
}'
|
||||
@@ -5,7 +5,7 @@ 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
|
||||
from bec_client.plugins.cSAXS import epics_get, epics_put, fshclose
|
||||
|
||||
# import builtins to avoid linter errors
|
||||
dev = builtins.__dict__.get("dev")
|
||||
@@ -1,6 +1,7 @@
|
||||
def lamni_read_additional_correction():
|
||||
# "additional_correction_shift"
|
||||
# [0][] x , [1][] y, [2][] angle, [3][0] number of elements
|
||||
import numpy as np
|
||||
|
||||
with open("correction_lamni_um_S01405_.txt", "r") as f:
|
||||
num_elements = f.readline()
|
||||
@@ -9,13 +9,12 @@ from pathlib import Path
|
||||
|
||||
import h5py
|
||||
import numpy as np
|
||||
from bec_client.plugins.cSAXS import epics_get, epics_put, fshclose, fshopen
|
||||
from bec_lib import bec_logger
|
||||
from bec_lib.alarm_handler import AlarmBase
|
||||
from bec_lib.pdf_writer import PDFWriter
|
||||
from typeguard import typechecked
|
||||
|
||||
from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_get, epics_put, fshopen
|
||||
|
||||
from .lamni_optics_mixin import LamNIOpticsMixin
|
||||
|
||||
logger = bec_logger.logger
|
||||
@@ -189,8 +188,7 @@ class XrayEyeAlign:
|
||||
val_y = epics_get(f"XOMNYI-XEYE-YVAL_Y:{k}") * self.PIXEL_CALIBRATION # in mm
|
||||
self.alignment_values[k] = [val_x, val_y]
|
||||
print(
|
||||
f"Clicked position {k}: x {self.alignment_values[k][0]}, y"
|
||||
f" {self.alignment_values[k][1]}"
|
||||
f"Clicked position {k}: x {self.alignment_values[k][0]}, y {self.alignment_values[k][1]}"
|
||||
)
|
||||
|
||||
if k == 0: # received center value of FZP
|
||||
@@ -215,10 +213,7 @@ class XrayEyeAlign:
|
||||
elif (
|
||||
k == 1
|
||||
): # received sample center value at samroy 0 ie the final base shift values
|
||||
msg = (
|
||||
f"Base shift values from movement are x {self.shift_xy[0]}, y"
|
||||
f" {self.shift_xy[1]}"
|
||||
)
|
||||
msg = f"Base shift values from movement are x {self.shift_xy[0]}, y {self.shift_xy[1]}"
|
||||
print(msg)
|
||||
logger.info(msg)
|
||||
self.shift_xy[0] += (
|
||||
@@ -228,12 +223,13 @@ class XrayEyeAlign:
|
||||
self.alignment_values[1][1] - self.alignment_values[0][1]
|
||||
) * 1000
|
||||
print(
|
||||
"Base shift values from movement and clicked position are x"
|
||||
f" {self.shift_xy[0]}, y {self.shift_xy[1]}"
|
||||
f"Base shift values from movement and clicked position are x {self.shift_xy[0]}, y {self.shift_xy[1]}"
|
||||
)
|
||||
|
||||
self.scans.lamni_move_to_scan_center(
|
||||
self.shift_xy[0] / 1000, self.shift_xy[1] / 1000, self.get_tomo_angle()
|
||||
self.shift_xy[0] / 1000,
|
||||
self.shift_xy[1] / 1000,
|
||||
self.get_tomo_angle(),
|
||||
).wait()
|
||||
|
||||
self.send_message("please wait ...")
|
||||
@@ -242,7 +238,9 @@ class XrayEyeAlign:
|
||||
time.sleep(1)
|
||||
|
||||
self.scans.lamni_move_to_scan_center(
|
||||
self.shift_xy[0] / 1000, self.shift_xy[1] / 1000, self.get_tomo_angle()
|
||||
self.shift_xy[0] / 1000,
|
||||
self.shift_xy[1] / 1000,
|
||||
self.get_tomo_angle(),
|
||||
).wait()
|
||||
|
||||
epics_put("XOMNYI-XEYE-ANGLE:0", self.get_tomo_angle())
|
||||
@@ -259,12 +257,16 @@ class XrayEyeAlign:
|
||||
self._disable_rt_feedback()
|
||||
self.tomo_rotate((k - 1) * 45 - 45 / 2)
|
||||
self.scans.lamni_move_to_scan_center(
|
||||
self.shift_xy[0] / 1000, self.shift_xy[1] / 1000, self.get_tomo_angle()
|
||||
self.shift_xy[0] / 1000,
|
||||
self.shift_xy[1] / 1000,
|
||||
self.get_tomo_angle(),
|
||||
).wait()
|
||||
self._disable_rt_feedback()
|
||||
self.tomo_rotate((k - 1) * 45)
|
||||
self.scans.lamni_move_to_scan_center(
|
||||
self.shift_xy[0] / 1000, self.shift_xy[1] / 1000, self.get_tomo_angle()
|
||||
self.shift_xy[0] / 1000,
|
||||
self.shift_xy[1] / 1000,
|
||||
self.get_tomo_angle(),
|
||||
).wait()
|
||||
|
||||
epics_put("XOMNYI-XEYE-ANGLE:0", self.get_tomo_angle())
|
||||
@@ -291,7 +293,9 @@ class XrayEyeAlign:
|
||||
self.shift_xy[0] = self.shift_xy[0] + _xrayeyalignmvx
|
||||
self.shift_xy[1] = self.shift_xy[1] + _xrayeyalignmvy
|
||||
self.scans.lamni_move_to_scan_center(
|
||||
self.shift_xy[0] / 1000, self.shift_xy[1] / 1000, self.get_tomo_angle()
|
||||
self.shift_xy[0] / 1000,
|
||||
self.shift_xy[1] / 1000,
|
||||
self.get_tomo_angle(),
|
||||
).wait()
|
||||
print(
|
||||
f"Current center horizontal {self.shift_xy[0]} vertical {self.shift_xy[1]}"
|
||||
@@ -306,14 +310,12 @@ class XrayEyeAlign:
|
||||
fovx = self._xray_fov_xy[0] * self.PIXEL_CALIBRATION * 1000 / 2
|
||||
fovy = self._xray_fov_xy[1] * self.PIXEL_CALIBRATION * 1000 / 2
|
||||
print(
|
||||
f"The largest field of view from the xrayeyealign was \nfovx = {fovx:.0f} microns, fovy"
|
||||
f" = {fovy:.0f} microns"
|
||||
f"The largest field of view from the xrayeyealign was \nfovx = {fovx:.0f} microns, fovy = {fovy:.0f} microns"
|
||||
)
|
||||
print("Use matlab routine to fit the current alignment...")
|
||||
|
||||
print(
|
||||
"This additional shift is applied to the base shift values\n which are x"
|
||||
f" {self.shift_xy[0]}, y {self.shift_xy[1]}"
|
||||
f"This additional shift is applied to the base shift values\n which are x {self.shift_xy[0]}, y {self.shift_xy[1]}"
|
||||
)
|
||||
|
||||
self._disable_rt_feedback()
|
||||
@@ -321,8 +323,7 @@ class XrayEyeAlign:
|
||||
self.tomo_rotate(0)
|
||||
|
||||
print(
|
||||
"\n\nNEXT LOAD ALIGNMENT PARAMETERS\nby running"
|
||||
" lamni.align.read_xray_eye_correction()\n"
|
||||
"\n\nNEXT LOAD ALIGNMENT PARAMETERS\nby running lamni.align.read_xray_eye_correction()\n"
|
||||
)
|
||||
|
||||
self.client.set_global_var("tomo_fov_offset", self.shift_xy)
|
||||
@@ -331,13 +332,12 @@ class XrayEyeAlign:
|
||||
with open(
|
||||
os.path.expanduser("~/Data10/specES1/internal/xrayeye_alignmentvalues"), "w"
|
||||
) as alignment_values_file:
|
||||
alignment_values_file.write("angle\thorizontal\tvertical\n")
|
||||
alignment_values_file.write(f"angle\thorizontal\tvertical\n")
|
||||
for k in range(2, 11):
|
||||
fovx_offset = (self.alignment_values[0][0] - self.alignment_values[k][0]) * 1000
|
||||
fovy_offset = (self.alignment_values[k][1] - self.alignment_values[0][1]) * 1000
|
||||
print(
|
||||
f"Writing to file new alignment: number {k}, value x {fovx_offset}, y"
|
||||
f" {fovy_offset}"
|
||||
f"Writing to file new alignment: number {k}, value x {fovx_offset}, y {fovy_offset}"
|
||||
)
|
||||
alignment_values_file.write(f"{(k-2)*45}\t{fovx_offset}\t{fovy_offset}\n")
|
||||
|
||||
@@ -422,8 +422,7 @@ class XrayEyeAlign:
|
||||
additional_correction_shift_x = self.corr_pos_x[-1]
|
||||
additional_correction_shift_y = self.corr_pos_y[-1]
|
||||
print(
|
||||
"Additional correction shifts:"
|
||||
f" {additional_correction_shift_x} {additional_correction_shift_y}"
|
||||
f"Additional correction shifts: {additional_correction_shift_x} {additional_correction_shift_y}"
|
||||
)
|
||||
return (additional_correction_shift_x, additional_correction_shift_y)
|
||||
|
||||
@@ -477,8 +476,7 @@ class XrayEyeAlign:
|
||||
additional_correction_shift_x = self.corr_pos_x_2[-1]
|
||||
additional_correction_shift_y = self.corr_pos_y_2[-1]
|
||||
print(
|
||||
"Additional correction shifts 2:"
|
||||
f" {additional_correction_shift_x} {additional_correction_shift_y}"
|
||||
f"Additional correction shifts 2: {additional_correction_shift_x} {additional_correction_shift_y}"
|
||||
)
|
||||
return (additional_correction_shift_x, additional_correction_shift_y)
|
||||
|
||||
@@ -527,8 +525,7 @@ class LamNI(LamNIOpticsMixin):
|
||||
|
||||
def get_beamline_checks_enabled(self):
|
||||
print(
|
||||
f"Shutter: {self.check_shutter}\nFOFB: {self.check_fofb}\nLight available:"
|
||||
f" {self.check_light_available}"
|
||||
f"Shutter: {self.check_shutter}\nFOFB: {self.check_fofb}\nLight available: {self.check_light_available}"
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -769,18 +766,13 @@ class LamNI(LamNIOpticsMixin):
|
||||
# pylint: disable=undefined-variable
|
||||
self._current_scan_list.append(bec.queue.next_scan_number)
|
||||
logger.info(
|
||||
f"scans.lamni_fermat_scan(fov_size=[{self.lamni_piezo_range_x},{self.lamni_piezo_range_y}],"
|
||||
f" step={self.tomo_shellstep}, stitch_x={0}, stitch_y={0},"
|
||||
f" stitch_overlap={1},center_x={self.align.tomo_fovx_offset},"
|
||||
f" center_y={self.align.tomo_fovy_offset},"
|
||||
f" shift_x={self.manual_shift_x+correction_xeye_mu[0]-additional_correction[0]-additional_correction_2[0]},"
|
||||
f" shift_y={self.manual_shift_y+correction_xeye_mu[1]-additional_correction[1]-additional_correction_2[1]},"
|
||||
f" fov_circular={self.tomo_circfov}, angle={angle}, scan_type='fly')"
|
||||
)
|
||||
log_message = (
|
||||
f"{str(datetime.datetime.now())}: LamNI scan projection at angle {angle}, scan"
|
||||
f" number {bec.queue.next_scan_number}.\n"
|
||||
f"scans.lamni_fermat_scan(fov_size=[{self.lamni_piezo_range_x},{self.lamni_piezo_range_y}], step={self.tomo_shellstep}, stitch_x={0}, stitch_y={0}, stitch_overlap={1},"
|
||||
f"center_x={self.align.tomo_fovx_offset}, center_y={self.align.tomo_fovy_offset}, "
|
||||
f"shift_x={self.manual_shift_x+correction_xeye_mu[0]-additional_correction[0]-additional_correction_2[0]}, "
|
||||
f"shift_y={self.manual_shift_y+correction_xeye_mu[1]-additional_correction[1]-additional_correction_2[1]}, "
|
||||
f"fov_circular={self.tomo_circfov}, angle={angle}, scan_type='fly')"
|
||||
)
|
||||
log_message = f"{str(datetime.datetime.now())}: LamNI scan projection at angle {angle}, scan number {bec.queue.next_scan_number}.\n"
|
||||
self.write_to_spec_log(log_message)
|
||||
# self.write_to_scilog(log_message, ["BEC_scans", self.sample_name])
|
||||
corridor_size = self.corridor_size if self.corridor_size > 0 else None
|
||||
@@ -822,7 +814,10 @@ class LamNI(LamNIOpticsMixin):
|
||||
msgs.append("Check beam failed: Shutter is closed.")
|
||||
if self.check_light_available:
|
||||
machine_status = dev.sls_machine_status.read(cached=True)
|
||||
if machine_status["value"] not in ["Light Available", "Light-Available"]:
|
||||
if machine_status["value"] not in [
|
||||
"Light Available",
|
||||
"Light-Available",
|
||||
]:
|
||||
self._beam_is_okay = False
|
||||
msgs.append("Check beam failed: Light not available.")
|
||||
if self.check_fofb:
|
||||
@@ -863,8 +858,7 @@ class LamNI(LamNIOpticsMixin):
|
||||
try:
|
||||
msg = bec.logbook.LogbookMessage()
|
||||
msg.add_text(
|
||||
"<p><mark class='pen-red'><strong>Beamline checks failed at"
|
||||
f" {str(datetime.datetime.now())}: {''.join(self._check_msgs)}</strong></mark></p>"
|
||||
f"<p><mark class='pen-red'><strong>Beamline checks failed at {str(datetime.datetime.now())}: {''.join(self._check_msgs)}</strong></mark></p>"
|
||||
).add_tag(["BEC", "beam_check"])
|
||||
self.client.logbook.send_logbook_message(msg)
|
||||
except Exception:
|
||||
@@ -881,20 +875,25 @@ class LamNI(LamNIOpticsMixin):
|
||||
try:
|
||||
msg = bec.logbook.LogbookMessage()
|
||||
msg.add_text(
|
||||
"<p><mark class='pen-red'><strong>Operation resumed at"
|
||||
f" {str(datetime.datetime.now())}.</strong></mark></p>"
|
||||
f"<p><mark class='pen-red'><strong>Operation resumed at {str(datetime.datetime.now())}.</strong></mark></p>"
|
||||
).add_tag(["BEC", "beam_check"])
|
||||
self.client.logbook.send_logbook_message(msg)
|
||||
except Exception:
|
||||
logger.warning("Failed to send update to SciLog.")
|
||||
|
||||
def add_sample_database(
|
||||
self, samplename, date, eaccount, scan_number, setup, sample_additional_info, user
|
||||
self,
|
||||
samplename,
|
||||
date,
|
||||
eaccount,
|
||||
scan_number,
|
||||
setup,
|
||||
sample_additional_info,
|
||||
user,
|
||||
):
|
||||
"""Add a sample to the omny sample database. This also retrieves the tomo id."""
|
||||
subprocess.run(
|
||||
"wget --user=omny --password=samples -q -O /tmp/currsamplesnr.txt"
|
||||
f" 'https://omny.web.psi.ch/samples/newmeasurement.php?sample={samplename}&date={date}&eaccount={eaccount}&scannr={scan_number}&setup={setup}&additional={sample_additional_info}&user={user}'",
|
||||
f"wget --user=omny --password=samples -q -O /tmp/currsamplesnr.txt 'https://omny.web.psi.ch/samples/newmeasurement.php?sample={samplename}&date={date}&eaccount={eaccount}&scannr={scan_number}&setup={setup}&additional={sample_additional_info}&user={user}'",
|
||||
shell=True,
|
||||
)
|
||||
with open("/tmp/currsamplesnr.txt") as tomo_number_file:
|
||||
@@ -1029,16 +1028,14 @@ class LamNI(LamNIOpticsMixin):
|
||||
print(f"Counting time <ctime> = {self.tomo_countingtime} s")
|
||||
print(f"Stepsize microns <step> = {self.tomo_shellstep}")
|
||||
print(
|
||||
f"Piezo range (max 80) <microns> = {self.lamni_piezo_range_x},"
|
||||
f" {self.lamni_piezo_range_y}"
|
||||
f"Piezo range (max 80) <microns> = {self.lamni_piezo_range_x}, {self.lamni_piezo_range_y}"
|
||||
)
|
||||
print(f"Stitching number x,y = {self.lamni_stitch_x}, {self.lamni_stitch_y}")
|
||||
print(f"Stitching overlap = {self.tomo_stitch_overlap}")
|
||||
print(f"Circuilar FOV diam <microns> = {self.tomo_circfov}")
|
||||
print(f"Reconstruction queue name = {self.ptycho_reconstruct_foldername}")
|
||||
print(
|
||||
"For information, fov offset is rotating and finding the ROI, manual shift moves"
|
||||
" rotation center"
|
||||
"For information, fov offset is rotating and finding the ROI, manual shift moves rotation center"
|
||||
)
|
||||
print(f" _tomo_fovx_offset <mm> = {self.align.tomo_fovx_offset}")
|
||||
print(f" _tomo_fovy_offset <mm> = {self.align.tomo_fovy_offset}")
|
||||
@@ -1094,7 +1091,7 @@ class LamNI(LamNIOpticsMixin):
|
||||
with open(ptycho_queue_file, "w") as queue_file:
|
||||
scans = " ".join([str(scan) for scan in self._current_scan_list])
|
||||
queue_file.write(f"p.scan_number {scans}\n")
|
||||
queue_file.write("p.check_nextscan_started 1\n")
|
||||
queue_file.write(f"p.check_nextscan_started 1\n")
|
||||
|
||||
def write_pdf_report(self):
|
||||
"""create and write the pdf report with the current LamNI settings"""
|
||||
@@ -1119,25 +1116,17 @@ class LamNI(LamNIOpticsMixin):
|
||||
f"{'Dataset ID:':<{padding}}{dataset_id:>{padding}}\n",
|
||||
f"{'Sample Info:':<{padding}}{'Sample Info':>{padding}}\n",
|
||||
f"{'e-account:':<{padding}}{str(self.client.username):>{padding}}\n",
|
||||
(
|
||||
f"{'Number of projections:':<{padding}}{int(360 / self.tomo_angle_stepsize * 8):>{padding}}\n"
|
||||
),
|
||||
f"{'Number of projections:':<{padding}}{int(360 / self.tomo_angle_stepsize * 8):>{padding}}\n",
|
||||
f"{'First scan number:':<{padding}}{self.client.queue.next_scan_number:>{padding}}\n",
|
||||
(
|
||||
f"{'Last scan number approx.:':<{padding}}{self.client.queue.next_scan_number + int(360 / self.tomo_angle_stepsize * 8) + 10:>{padding}}\n"
|
||||
),
|
||||
(
|
||||
f"{'Current photon energy:':<{padding}}{dev.mokev.read(cached=True)['value']:>{padding}.4f}\n"
|
||||
),
|
||||
f"{'Last scan number approx.:':<{padding}}{self.client.queue.next_scan_number + int(360 / self.tomo_angle_stepsize * 8) + 10:>{padding}}\n",
|
||||
f"{'Current photon energy:':<{padding}}{dev.mokev.read(cached=True)['value']:>{padding}.4f}\n",
|
||||
f"{'Exposure time:':<{padding}}{self.tomo_countingtime:>{padding}.2f}\n",
|
||||
f"{'Fermat spiral step size:':<{padding}}{self.tomo_shellstep:>{padding}.2f}\n",
|
||||
f"{'Piezo range (FOV sample plane):':<{padding}}{piezo_range:>{padding}}\n",
|
||||
f"{'Restriction to circular FOV:':<{padding}}{self.tomo_circfov:>{padding}.2f}\n",
|
||||
f"{'Stitching:':<{padding}}{stitching:>{padding}}\n",
|
||||
f"{'Number of individual sub-tomograms:':<{padding}}{8:>{padding}}\n",
|
||||
(
|
||||
f"{'Angular step within sub-tomogram:':<{padding}}{self.tomo_angle_stepsize:>{padding}.2f}\n"
|
||||
),
|
||||
f"{'Angular step within sub-tomogram:':<{padding}}{self.tomo_angle_stepsize:>{padding}.2f}\n",
|
||||
]
|
||||
content = "".join(content)
|
||||
user_target = os.path.expanduser(f"~/Data10/documentation/tomo_scan_ID_{self.tomo_id}.pdf")
|
||||
@@ -1145,13 +1134,20 @@ class LamNI(LamNIOpticsMixin):
|
||||
file.write(header)
|
||||
file.write(content)
|
||||
subprocess.run(
|
||||
"xterm /work/sls/spec/local/XOMNY/bin/upload/upload_last_pon.sh &", shell=True
|
||||
"xterm /work/sls/spec/local/XOMNY/bin/upload/upload_last_pon.sh &",
|
||||
shell=True,
|
||||
)
|
||||
# status = subprocess.run(f"cp /tmp/spec-e20131-specES1.pdf {user_target}", shell=True)
|
||||
msg = bec.logbook.LogbookMessage()
|
||||
logo_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "LamNI_logo.png")
|
||||
msg.add_file(logo_path).add_text("".join(content).replace("\n", "</p><p>")).add_tag(
|
||||
["BEC", "tomo_parameters", f"dataset_id_{dataset_id}", "LamNI", self.sample_name]
|
||||
[
|
||||
"BEC",
|
||||
"tomo_parameters",
|
||||
f"dataset_id_{dataset_id}",
|
||||
"LamNI",
|
||||
self.sample_name,
|
||||
]
|
||||
)
|
||||
self.client.logbook.send_logbook_message(msg)
|
||||
|
||||
@@ -1236,12 +1232,16 @@ class DataDrivenLamNI(LamNI):
|
||||
|
||||
def sub_tomo_scan(self):
|
||||
raise NotImplementedError(
|
||||
"Cannot run sub_tomo_scan with data-driven LamNI. Please use"
|
||||
" lamni.tomo_scan(subtomo_start=<START_NUM>) instead."
|
||||
"Cannot run sub_tomo_scan with data-driven LamNI. Please use lamni.tomo_scan(subtomo_start=<START_NUM>) instead."
|
||||
)
|
||||
|
||||
def _at_each_angle(
|
||||
self, angle=None, stepsize=None, loptz_pos=None, manual_shift_x=0, manual_shift_y=0
|
||||
self,
|
||||
angle=None,
|
||||
stepsize=None,
|
||||
loptz_pos=None,
|
||||
manual_shift_x=0,
|
||||
manual_shift_y=0,
|
||||
):
|
||||
# Do something...
|
||||
# self.tomo_parameters
|
||||
3
bec_plugins/bec_client/plugins/__init__.py
Normal file
3
bec_plugins/bec_client/plugins/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .cSAXS import *
|
||||
|
||||
# from .LamNI import *
|
||||
1
bec_plugins/bec_client/plugins/cSAXS/__init__.py
Normal file
1
bec_plugins/bec_client/plugins/cSAXS/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .cSAXS_beamline import fshopen, fshclose, fshstatus, epics_get, epics_put
|
||||
@@ -1,9 +1,10 @@
|
||||
import builtins
|
||||
|
||||
from bec_ipython_client.beamline_mixin import BeamlineShowInfo
|
||||
from rich import box
|
||||
from rich.table import Table
|
||||
|
||||
from bec_client.beamline_mixin import BeamlineShowInfo
|
||||
|
||||
|
||||
class BeamlineInfo(BeamlineShowInfo):
|
||||
def show(self):
|
||||
76
bec_plugins/bec_client/startup/post_startup.py
Normal file
76
bec_plugins/bec_client/startup/post_startup.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
Post startup script for the BEC client. This script is executed after the
|
||||
IPython shell is started. It is used to load the beamline specific
|
||||
information and to setup the prompts.
|
||||
|
||||
The script is executed in the global namespace of the IPython shell. This
|
||||
means that all variables defined here are available in the shell.
|
||||
|
||||
If needed, bec command-line arguments can be parsed here. For example, to
|
||||
parse the --session argument, add the following lines to the script:
|
||||
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--session", help="Session name", type=str, default="my_default_session")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.session == "my_session":
|
||||
print("Loading my_session session")
|
||||
from bec_plugins.bec_client.plugins.my_session import *
|
||||
else:
|
||||
print("Loading default session")
|
||||
from bec_plugins.bec_client.plugins.default_session import *
|
||||
"""
|
||||
|
||||
# pylint: disable=invalid-name, unused-import, import-error, undefined-variable, unused-variable, unused-argument, no-name-in-module
|
||||
import argparse
|
||||
|
||||
from bec_lib import bec_logger
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
logger.info("Using the cSAXS startup script.")
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--session", help="Session name", type=str, default="cSAXS")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.session == "LamNI":
|
||||
print("Loading LamNI session")
|
||||
from bec_plugins.bec_client.plugins.cSAXS import *
|
||||
from bec_plugins.bec_client.plugins.LamNI import *
|
||||
|
||||
lamni = LamNI(bec)
|
||||
|
||||
elif args.session == "cSAXS":
|
||||
print("Loading cSAXS session")
|
||||
from bec_plugins.bec_client.plugins.cSAXS import *
|
||||
|
||||
|
||||
# SETUP BEAMLINE INFO
|
||||
from bec_client.plugins.SLS.sls_info import OperatorInfo, SLSInfo
|
||||
|
||||
from bec_plugins.bec_client.plugins.cSAXS.beamline_info import BeamlineInfo
|
||||
|
||||
bec._beamline_mixin._bl_info_register(BeamlineInfo)
|
||||
bec._beamline_mixin._bl_info_register(SLSInfo)
|
||||
bec._beamline_mixin._bl_info_register(OperatorInfo)
|
||||
|
||||
# SETUP PROMPTS
|
||||
bec._ip.prompts.username = args.session
|
||||
bec._ip.prompts.status = 1
|
||||
|
||||
|
||||
# REGISTER BEAMLINE CHECKS
|
||||
from bec_lib.bl_conditions import (
|
||||
FastOrbitFeedbackCondition,
|
||||
LightAvailableCondition,
|
||||
ShutterCondition,
|
||||
)
|
||||
|
||||
# _fast_orbit_feedback_condition = FastOrbitFeedbackCondition(dev.sls_fast_orbit_feedback)
|
||||
_light_available_condition = LightAvailableCondition(dev.sls_machine_status)
|
||||
_shutter_condition = ShutterCondition(dev.x12sa_es1_shutter_status)
|
||||
# bec.bl_checks.register(_fast_orbit_feedback_condition)
|
||||
bec.bl_checks.register(_light_available_condition)
|
||||
bec.bl_checks.register(_shutter_condition)
|
||||
25
bec_plugins/bec_client/startup/pre_startup.py
Normal file
25
bec_plugins/bec_client/startup/pre_startup.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
Pre-startup script for BEC client. This script is executed before the BEC client
|
||||
is started. It can be used to set up the BEC client configuration. The script is
|
||||
executed in the global namespace of the BEC client. This means that all
|
||||
variables defined here are available in the BEC client.
|
||||
|
||||
To set up the BEC client configuration, use the ServiceConfig class. For example,
|
||||
to set the configuration file path, add the following lines to the script:
|
||||
|
||||
import pathlib
|
||||
from bec_lib import ServiceConfig
|
||||
|
||||
current_path = pathlib.Path(__file__).parent.resolve()
|
||||
CONFIG_PATH = f"{current_path}/<path_to_my_config_file.yaml>"
|
||||
|
||||
config = ServiceConfig(CONFIG_PATH)
|
||||
|
||||
If this startup script defined a ServiceConfig object, the BEC client will use
|
||||
it to configure itself. Otherwise, the BEC client will use the default config.
|
||||
"""
|
||||
|
||||
# example:
|
||||
# current_path = pathlib.Path(__file__).parent.resolve()
|
||||
# CONFIG_PATH = f"{current_path}/../../../bec_config.yaml"
|
||||
# config = ServiceConfig(CONFIG_PATH)
|
||||
2742
bec_plugins/device_configs/bec_device_config_sastt.yaml
Executable file
2742
bec_plugins/device_configs/bec_device_config_sastt.yaml
Executable file
File diff suppressed because it is too large
Load Diff
3027
bec_plugins/device_configs/config_session_start_e20632.yaml
Normal file
3027
bec_plugins/device_configs/config_session_start_e20632.yaml
Normal file
File diff suppressed because it is too large
Load Diff
3056
bec_plugins/device_configs/e21125_lamni_config.yaml
Normal file
3056
bec_plugins/device_configs/e21125_lamni_config.yaml
Normal file
File diff suppressed because it is too large
Load Diff
152
bec_plugins/device_configs/lamni_config.py
Normal file
152
bec_plugins/device_configs/lamni_config.py
Normal file
@@ -0,0 +1,152 @@
|
||||
import yaml
|
||||
|
||||
# TODO: fix imports, those classes are located in .../bec/scibec/init_scibec/configs/config.py
|
||||
# (see also lamni_config.py in bec repository)
|
||||
from .config import DemoConfig, X12SAConfig
|
||||
|
||||
|
||||
class LamNIConfig(DemoConfig, X12SAConfig):
|
||||
def run(self):
|
||||
# self.write_galil_motors()
|
||||
# self.write_rt_motors()
|
||||
# self.write_smaract_motors()
|
||||
# self.write_eiger1p5m()
|
||||
self.write_x12sa_status()
|
||||
self.write_sls_status()
|
||||
self.load_csaxs_config()
|
||||
# self.write_sim_user_motors()
|
||||
# self.write_sim_beamline_motors()
|
||||
# self.write_sim_beamline_monitors()
|
||||
|
||||
def write_galil_motors(self):
|
||||
lamni_galil_motors = [
|
||||
("lsamx", "A", -1, 0.5, {"center": 8.768000}),
|
||||
("lsamy", "B", 1, 0.5, {"center": 10.041000}),
|
||||
("lsamrot", "C", 1, 0.5, {}),
|
||||
("loptz", "D", -1, 0.5, {}),
|
||||
("loptx", "E", 1, 0.5, {"in": -0.8380, "out": -0.699}),
|
||||
("lopty", "F", 1, 0.5, {"in": 3.3540, "out": 3.53}),
|
||||
("leyex", "G", -1, 0.001, {"in": 14.117000}),
|
||||
("leyey", "H", -1, 0.001, {"in": 48.069000, "out": 0.5}),
|
||||
]
|
||||
out = {}
|
||||
for m in lamni_galil_motors:
|
||||
out[m[0]] = dict(
|
||||
{
|
||||
"status": {"enabled": True, "enabled_set": True},
|
||||
"deviceClass": "GalilMotor",
|
||||
"deviceConfig": {
|
||||
"axis_Id": m[1],
|
||||
"name": m[0],
|
||||
"labels": m[0],
|
||||
"host": "mpc2680.psi.ch",
|
||||
"port": 8081,
|
||||
"sign": m[2],
|
||||
"limits": [0, 0],
|
||||
"tolerance": m[3],
|
||||
"device_access": True,
|
||||
"device_mapping": {"rt": "rtx"},
|
||||
},
|
||||
"acquisitionConfig": {
|
||||
"schedule": "sync",
|
||||
"acquisitionGroup": "motor",
|
||||
"readoutPriority": "baseline",
|
||||
},
|
||||
"deviceTags": ["lamni"],
|
||||
}
|
||||
)
|
||||
if m[4]:
|
||||
out[m[0]]["userParameter"] = m[4]
|
||||
self.write_section(out, "LamNI Galil motors")
|
||||
|
||||
def write_rt_motors(self):
|
||||
lamni_rt_motors = [
|
||||
("rtx", "A", 1),
|
||||
("rty", "B", 1),
|
||||
]
|
||||
out = dict()
|
||||
for m in lamni_rt_motors:
|
||||
out[m[0]] = dict(
|
||||
{
|
||||
"status": {"enabled": True, "enabled_set": True},
|
||||
"deviceClass": "RtLamniMotor",
|
||||
"deviceConfig": {
|
||||
"axis_Id": m[1],
|
||||
"name": m[0],
|
||||
"labels": m[0],
|
||||
"host": "mpc2680.psi.ch",
|
||||
"port": 3333,
|
||||
"limits": [0, 0],
|
||||
"sign": m[2],
|
||||
"device_access": True,
|
||||
},
|
||||
"acquisitionConfig": {
|
||||
"schedule": "sync",
|
||||
"acquisitionGroup": "motor",
|
||||
"readoutPriority": "baseline",
|
||||
},
|
||||
"deviceTags": ["lamni"],
|
||||
}
|
||||
)
|
||||
self.write_section(out, "LamNI RT")
|
||||
|
||||
def write_smaract_motors(self):
|
||||
lamni_smaract_motors = [
|
||||
("losax", "A", -1, {"in": -0.848000}),
|
||||
("losay", "B", -1, {"in": 0.135000, "out": 3.8}),
|
||||
("losaz", "C", 1, {"in": -1, "out": -3}),
|
||||
("lcsx", "D", -1, {}),
|
||||
("lcsy", "E", -1, {}),
|
||||
]
|
||||
out = dict()
|
||||
for m in lamni_smaract_motors:
|
||||
out[m[0]] = dict(
|
||||
{
|
||||
"status": {"enabled": True, "enabled_set": True},
|
||||
"deviceClass": "SmaractMotor",
|
||||
"deviceConfig": {
|
||||
"axis_Id": m[1],
|
||||
"name": m[0],
|
||||
"labels": m[0],
|
||||
"host": "mpc2680.psi.ch",
|
||||
"port": 8085,
|
||||
"limits": [0, 0],
|
||||
"sign": m[2],
|
||||
"tolerance": 0.05,
|
||||
},
|
||||
"acquisitionConfig": {
|
||||
"schedule": "sync",
|
||||
"acquisitionGroup": "motor",
|
||||
"readoutPriority": "baseline",
|
||||
},
|
||||
"deviceTags": ["lamni"],
|
||||
}
|
||||
)
|
||||
if m[3]:
|
||||
out[m[0]]["userParameter"] = m[3]
|
||||
self.write_section(out, "LamNI SmarAct motors")
|
||||
|
||||
def write_eiger1p5m(self):
|
||||
out = {
|
||||
"eiger1p5m": {
|
||||
"description": "Eiger 1.5M in vacuum detector, in-house developed, PSI",
|
||||
"status": {"enabled": True, "enabled_set": True},
|
||||
"deviceClass": "Eiger1p5MDetector",
|
||||
"deviceConfig": {"device_access": True, "name": "eiger1p5m"},
|
||||
"acquisitionConfig": {
|
||||
"schedule": "sync",
|
||||
"acquisitionGroup": "detector",
|
||||
"readoutPriority": "monitored",
|
||||
},
|
||||
"deviceTags": ["detector"],
|
||||
}
|
||||
}
|
||||
self.write_section(out, "LamNI Eiger 1.5M in vacuum")
|
||||
|
||||
def load_csaxs_config(self):
|
||||
CONFIG_PATH = "./init_scibec/configs/test_config_cSAXS.yaml"
|
||||
content = {}
|
||||
with open(CONFIG_PATH, "r") as csaxs_config_file:
|
||||
content = yaml.safe_load(csaxs_config_file.read())
|
||||
|
||||
self.write_section(content, "Default cSAXS config")
|
||||
2326
bec_plugins/device_configs/test_config_cSAXS.yaml
Normal file
2326
bec_plugins/device_configs/test_config_cSAXS.yaml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -23,9 +23,9 @@ but they are executed in a specific order:
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
from bec_lib import MessageEndpoints, bec_logger
|
||||
from bec_server.scan_server.errors import ScanAbortion
|
||||
from bec_server.scan_server.scans import RequestBase, ScanArgType, ScanBase
|
||||
from bec_lib import MessageEndpoints, bec_logger, messages
|
||||
from scan_server.errors import ScanAbortion
|
||||
from scan_server.scans import RequestBase, ScanArgType, ScanBase
|
||||
|
||||
MOVEMENT_SCALE_X = np.sin(np.radians(15)) * np.cos(np.radians(30))
|
||||
MOVEMENT_SCALE_Y = np.cos(np.radians(15))
|
||||
@@ -96,8 +96,8 @@ class LamNIMixin:
|
||||
coarse_move_req_x = np.abs(lsamx_current - move_x)
|
||||
coarse_move_req_y = np.abs(lsamy_current - move_y)
|
||||
|
||||
self.device_manager.devices.lsamx.read_only = False
|
||||
self.device_manager.devices.lsamy.read_only = False
|
||||
self.device_manager.devices.lsamx.enabled_set = True
|
||||
self.device_manager.devices.lsamy.enabled_set = True
|
||||
|
||||
if (
|
||||
np.abs(y_drift) > 150
|
||||
@@ -126,14 +126,12 @@ class LamNIMixin:
|
||||
x_drift2 = x_center_expect * 1000 - rtx_current
|
||||
y_drift2 = y_center_expect * 1000 - rty_current
|
||||
logger.info(
|
||||
f"Uncompensated drift of setup after first iteration is x={x_drift2:.3f},"
|
||||
f" y={y_drift2:.3f}"
|
||||
f"Uncompensated drift of setup after first iteration is x={x_drift2:.3f}, y={y_drift2:.3f}"
|
||||
)
|
||||
|
||||
if np.abs(x_drift2) > 5 or np.abs(y_drift2) > 5:
|
||||
logger.info(
|
||||
"Compensating second iteration"
|
||||
f" {[val/1000 for val in lamni_to_stage_coordinates(x_drift2,y_drift2)]}"
|
||||
f"Compensating second iteration {[val/1000 for val in lamni_to_stage_coordinates(x_drift2,y_drift2)]}"
|
||||
)
|
||||
move_x = (
|
||||
x_stage
|
||||
@@ -155,20 +153,18 @@ class LamNIMixin:
|
||||
rty_current = yield from self.stubs.send_rpc_and_wait("rty", "readback.get")
|
||||
|
||||
logger.info(
|
||||
f"New scan center interferometer after second iteration {rtx_current:.3f},"
|
||||
f" {rty_current:.3f} microns"
|
||||
f"New scan center interferometer after second iteration {rtx_current:.3f}, {rty_current:.3f} microns"
|
||||
)
|
||||
x_drift2 = x_center_expect * 1000 - rtx_current
|
||||
y_drift2 = y_center_expect * 1000 - rty_current
|
||||
logger.info(
|
||||
f"Uncompensated drift of setup after second iteration is x={x_drift2:.3f},"
|
||||
f" y={y_drift2:.3f}"
|
||||
f"Uncompensated drift of setup after second iteration is x={x_drift2:.3f}, y={y_drift2:.3f}"
|
||||
)
|
||||
else:
|
||||
logger.info("No second iteration required")
|
||||
|
||||
self.device_manager.devices.lsamx.read_only = True
|
||||
self.device_manager.devices.lsamy.read_only = True
|
||||
self.device_manager.devices.lsamx.enabled_set = False
|
||||
self.device_manager.devices.lsamy.enabled_set = False
|
||||
|
||||
yield from self.stubs.send_rpc_and_wait("rtx", "controller.feedback_enable_without_reset")
|
||||
|
||||
@@ -178,12 +174,8 @@ class LamNIMoveToScanCenter(RequestBase, LamNIMixin):
|
||||
scan_report_hint = None
|
||||
scan_type = "step"
|
||||
required_kwargs = []
|
||||
arg_input = {
|
||||
"shift_x": ScanArgType.FLOAT,
|
||||
"shift_y": ScanArgType.FLOAT,
|
||||
"angle": ScanArgType.FLOAT,
|
||||
}
|
||||
arg_bundle_size = {"bundle": len(arg_input), "min": 1, "max": 1}
|
||||
arg_input = [ScanArgType.FLOAT, ScanArgType.FLOAT, ScanArgType.FLOAT]
|
||||
arg_bundle_size = None
|
||||
|
||||
def __init__(self, *args, parameter=None, **kwargs):
|
||||
"""
|
||||
@@ -207,10 +199,10 @@ class LamNIFermatScan(ScanBase, LamNIMixin):
|
||||
scan_report_hint = "table"
|
||||
scan_type = "step"
|
||||
required_kwargs = ["fov_size", "exp_time", "step", "angle"]
|
||||
arg_input = {}
|
||||
arg_bundle_size = {"bundle": len(arg_input), "min": None, "max": None}
|
||||
arg_input = []
|
||||
arg_bundle_size = None
|
||||
|
||||
def __init__(self, *args, parameter: dict = None, **kwargs):
|
||||
def __init__(self, *args, parameter=None, **kwargs):
|
||||
"""
|
||||
A LamNI scan following Fermat's spiral.
|
||||
|
||||
@@ -365,8 +357,7 @@ class LamNIFermatScan(ScanBase, LamNIMixin):
|
||||
self.stitch_x, self.stitch_y, self.angle
|
||||
)
|
||||
logger.info(
|
||||
f"Total shift [mm] {_shfitx+x_stitch_shift/1000+self.shift_x},"
|
||||
f" {_shfity+y_stitch_shift/1000+self.shift_y}"
|
||||
f"Total shift [mm] {_shfitx+x_stitch_shift/1000+self.shift_x}, {_shfity+y_stitch_shift/1000+self.shift_y}"
|
||||
)
|
||||
return (
|
||||
_shfitx + x_stitch_shift / 1000 + self.shift_x,
|
||||
@@ -457,17 +448,16 @@ class LamNIFermatScan(ScanBase, LamNIMixin):
|
||||
yield from self.stubs.kickoff(device="rtx")
|
||||
while True:
|
||||
yield from self.stubs.read_and_wait(group="primary", wait_group="readout_primary")
|
||||
msg = self.device_manager.connector.get(MessageEndpoints.device_status("rt_scan"))
|
||||
msg = self.device_manager.producer.get(MessageEndpoints.device_status("rt_scan"))
|
||||
if msg:
|
||||
status = msg
|
||||
status = messages.DeviceStatusMessage.loads(msg)
|
||||
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 LamNI readout:"
|
||||
f" {status.metadata.get('error')}"
|
||||
f"An error occured during the LamNI readout: {status.metadata.get('error')}"
|
||||
)
|
||||
|
||||
time.sleep(1)
|
||||
@@ -26,7 +26,7 @@ but they are executed in a specific order:
|
||||
# import numpy as np
|
||||
|
||||
# from bec_lib import MessageEndpoints, bec_logger, messages
|
||||
# from bec_server.scan_server.errors import ScanAbortion
|
||||
# from bec_server.scan_server.scans import FlyScanBase, RequestBase, ScanArgType, ScanBase
|
||||
# from scan_server.errors import ScanAbortion
|
||||
# from scan_server.scans import FlyScanBase, RequestBase, ScanArgType, ScanBase
|
||||
|
||||
# logger = bec_logger.logger
|
||||
182
bec_plugins/utils/csaxs_post_archive.py
Executable file
182
bec_plugins/utils/csaxs_post_archive.py
Executable file
@@ -0,0 +1,182 @@
|
||||
import os
|
||||
import json
|
||||
import subprocess
|
||||
import requests
|
||||
|
||||
from bec_lib.file_utils import FileWriterMixin
|
||||
#from bec_lib.bec_service import SERVICE_CONFIG
|
||||
|
||||
|
||||
class csaxs_archiver:
|
||||
"""Class to archive data from a beamtime.
|
||||
To run this script from a shell, go to discovery.psi.ch and copy your token.
|
||||
Complement the information in user_input below in the if __name__ == __main__ part of the script.
|
||||
Afterwards, get a Keberos token (kinit) for yourself in the shell.
|
||||
Activate the bec_venv by doing "source bec_venv/bin/activate" and then run this code via python $filename.
|
||||
As a last step, adjust the dictionary below in if __name__ == '__main__' with your token as well as information about the experiment
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
start_scan: int,
|
||||
stop_scan: int,
|
||||
base_path: str,
|
||||
log_path: str,
|
||||
eacc: str,
|
||||
pi: str,
|
||||
pi_email: str,
|
||||
token: str,
|
||||
type: str = "raw",
|
||||
overwrite: bool = False,
|
||||
online: bool = True,
|
||||
):
|
||||
self.start_scan = start_scan
|
||||
self.stop_scan = stop_scan
|
||||
self.log_path = os.path.expanduser(log_path)
|
||||
self.eacc = eacc
|
||||
self.pi = pi
|
||||
self.pi_email = pi_email
|
||||
self.token = token
|
||||
self.type = type
|
||||
self.overwrite = overwrite
|
||||
self.online = online
|
||||
|
||||
#from bec_lib.bec_service import SERVICE_CONFIG
|
||||
#SERVICE_CONFIG.config["service_config"]["file_writer"]
|
||||
self._load_datacatalogue_module()
|
||||
self._create_directory(base_path)
|
||||
self._disable_mail_confirmation()
|
||||
|
||||
self.service_cfg = {'base_path' : f'{self.base_path}'}
|
||||
|
||||
self.file_writer = FileWriterMixin(self.service_cfg)
|
||||
|
||||
def _disable_mail_confirmation(self):
|
||||
# Define the URL and payload
|
||||
url = "https://dacat.psi.ch/api/v3/Policies/updatewhere"
|
||||
payload = {
|
||||
"ownerGroupList": f'p{self.eacc[1:]}',
|
||||
"data": '{"archiveEmailNotification": false, "accessGroups": ["slscsaxs"]}'
|
||||
}
|
||||
|
||||
# Define headers
|
||||
headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
|
||||
# Add the access_token to the URL
|
||||
url += "?access_token=" + self.token
|
||||
|
||||
# Make a POST request
|
||||
print(url, payload, headers)
|
||||
response = requests.post(url, data=payload, headers=headers)
|
||||
|
||||
# Check the response
|
||||
if response.status_code == 200:
|
||||
print("Request was successful.")
|
||||
print(response.json())
|
||||
else:
|
||||
print("Request failed with status code:", response.status_code)
|
||||
print(response.text)
|
||||
|
||||
def _create_directory(self, base_path: str) -> None:
|
||||
if self.online:
|
||||
self.base_path = os.path.expanduser("~/Data10")
|
||||
else:
|
||||
self.base_path = base_path
|
||||
|
||||
if not os.path.exists(self.log_path):
|
||||
os.makedirs(self.log_path)
|
||||
|
||||
def _load_datacatalogue_module(self):
|
||||
command = 'module add datacatalog/1.1.9'
|
||||
os.popen(command)
|
||||
# result = subprocess.run(
|
||||
# command,
|
||||
# shell=False,
|
||||
# stdout=subprocess.PIPE,
|
||||
# stderr=subprocess.PIPE,
|
||||
# universal_newlines=True,
|
||||
# )
|
||||
# if result.returncode == 0:
|
||||
# print(f"Command {command} was succesful")
|
||||
# else:
|
||||
# print(f"Failed to run command {command} with return message {result.returncode}")
|
||||
|
||||
def prep_metadata(self, scannr: int) -> dict:
|
||||
user_metadata = {}
|
||||
user_metadata.update(
|
||||
{
|
||||
"principalInvestigator": self.pi_email,
|
||||
"owner": self.pi,
|
||||
"ownerEmail": self.pi_email,
|
||||
"sourceFolder": self.base_path,
|
||||
"creationLocation": "/PSI/SLS/CSAXS",
|
||||
"type": "raw",
|
||||
"ownerGroup": f"p{self.eacc.strip('e')}",
|
||||
"datasetName": f"S{scannr:05d}",
|
||||
}
|
||||
)
|
||||
return user_metadata
|
||||
|
||||
def _write_ingestion_log(self, scannr: int) -> None:
|
||||
...
|
||||
|
||||
def run_for_all_scans(self):
|
||||
for scan in range(self.start_scan, self.stop_scan + 1):
|
||||
print(f"Start ingestion for scan {scan}")
|
||||
fname = os.path.join(os.path.expanduser(self.log_path), f"ingestion_log_S{scan:05d}")
|
||||
self.datafile_name = f"{fname}.txt"
|
||||
if os.path.isfile(self.datafile_name) and not self.overwrite == True:
|
||||
print(
|
||||
f"Skipping scan {scan}, already ingested due to logs, moving on to next scan {scan+1}"
|
||||
)
|
||||
continue
|
||||
|
||||
user_metadata = self.prep_metadata(scan)
|
||||
|
||||
# Write metadata file in json file
|
||||
self.metadata_file = f"{fname}.json"
|
||||
with open(self.metadata_file, "w") as file:
|
||||
json.dump(user_metadata, file)
|
||||
|
||||
# Compile datapath based on structure a cSAXS
|
||||
datadir_path = os.path.join('data', self.file_writer.get_scan_directory(scan, 1000, 5))
|
||||
print(f"Archiving directory {datadir_path}")
|
||||
if not os.path.isdir(os.path.join(self.base_path, datadir_path)):
|
||||
print(f"Did not find directory {datadir_path}, skipping scan {scan}")
|
||||
continue
|
||||
|
||||
# Create datafile path for archiving
|
||||
with open(self.datafile_name, "w") as file:
|
||||
file.write(datadir_path)
|
||||
|
||||
print(f"Starting ingestion for S#{scan}")
|
||||
command = f'datasetIngestor -allowexistingsource -ingest -autoarchive -noninteractive -token {self.token} {self.metadata_file} {self.datafile_name}'
|
||||
rtr = os.popen(command)
|
||||
|
||||
#with open(os.path.join(fname,'_log.txt'), "w") as file:
|
||||
# print(f'Writing reponse to file')
|
||||
# file.write(rtr.read())
|
||||
# result = subprocess.run(command, shell=False, stdout = subprocess.PIPE, stderr = subprocess.PIPE, universal_newlines=True)
|
||||
# if result.returncode == 0:
|
||||
# print(f"Command {command} was succesful")
|
||||
# else:
|
||||
# print(f"Failed to run command {command} with return message {result.returncode}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Generate dictionary with user input.
|
||||
user_input = {
|
||||
"base_path": "~/Data10",
|
||||
"eacc": "e20638",
|
||||
"pi": "Emma Sparr",
|
||||
"pi_email": "emma.sparr@fkem1.lu.se",
|
||||
'log_path' : '~/Data10/documentation/ingestion_logs/',
|
||||
'token' : 'YK8gkmQmEVxVxjiA57D6tVmpBVs7T235nWEuBT0behN9BPM2BdWARWPPgEsQVrPe',
|
||||
'start_scan' : 1,
|
||||
'stop_scan' : 450,
|
||||
}
|
||||
archiver = csaxs_archiver(**user_input)
|
||||
archiver.run_for_all_scans()
|
||||
88
bec_plugins/utils/saxs_params.py
Executable file
88
bec_plugins/utils/saxs_params.py
Executable file
@@ -0,0 +1,88 @@
|
||||
import csv
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
class ScanItem:
|
||||
def __init__(self, offset_xy: Callable) -> None:
|
||||
self.start_entry = None
|
||||
self.end_entry = None
|
||||
self.offset_xy = offset_xy
|
||||
|
||||
def to_scan_params(self) -> dict:
|
||||
scan_params = {
|
||||
"start_x": float(self.start_entry["X"]) + self.offset_xy()[0],
|
||||
"start_y": float(self.start_entry["Y"]) + self.offset_xy()[1],
|
||||
"end_x": float(self.end_entry["X"]) + self.offset_xy()[0],
|
||||
"end_y": float(self.end_entry["Y"]) + self.offset_xy()[1],
|
||||
"interval_x": int(
|
||||
np.round(
|
||||
np.abs(float(self.start_entry["X"]) - float(self.end_entry["X"]))
|
||||
/ (float(self.start_entry["step_x [mu]"]) * 1e-3)
|
||||
)
|
||||
),
|
||||
"interval_y": int(
|
||||
np.round(
|
||||
np.abs(float(self.start_entry["Y"]) - float(self.end_entry["Y"]))
|
||||
/ (float(self.start_entry["step_y [mu]"]) * 1e-3)
|
||||
)
|
||||
),
|
||||
"exp_time": float(self.start_entry["exp_time [s]"]),
|
||||
"readout_time": 3e-3,
|
||||
"md": {"sample_name": self.start_entry["sample name"]},
|
||||
}
|
||||
if scan_params["interval_x"] < 1 or scan_params["interval_x"] < 1:
|
||||
raise ValueError("Bugger off...")
|
||||
return scan_params
|
||||
|
||||
|
||||
class SAXSParams:
|
||||
def __init__(self, offset: Callable):
|
||||
self.offset_xy = offset
|
||||
self.data = defaultdict(lambda: ScanItem(offset))
|
||||
|
||||
def load_from_csv(self, file_path: str) -> None:
|
||||
"""
|
||||
Load the acquisition parameter from a csv file.
|
||||
"""
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
raise FileNotFoundError(
|
||||
f"The specified CSV file could not be found. Please check that the given path is correct: {file_path}."
|
||||
)
|
||||
|
||||
data_transposed = defaultdict(lambda: [])
|
||||
with open(os.path.expanduser(file_path), "r") as file:
|
||||
csv_reader = csv.DictReader(file)
|
||||
for row in csv_reader:
|
||||
for key, val in row.items():
|
||||
data_transposed[key].append(val)
|
||||
if int(row["start"]):
|
||||
self.data[row["sample name"]].start_entry = row
|
||||
else:
|
||||
self.data[row["sample name"]].end_entry = row
|
||||
self._check_params(dict(data_transposed))
|
||||
|
||||
def _check_params(self, data_transposed: dict) -> dict:
|
||||
sample_names = set(data_transposed["sample name"])
|
||||
if len(data_transposed["start"]) != len(sample_names) * 2:
|
||||
raise ValueError(
|
||||
f"The given params file does not provide N*2 start/stop positions. Found {len(sample_names)} samples and {len(data_transposed['start'])} start/stop positions."
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from pprint import pprint
|
||||
|
||||
INPUT_FILE = "/sls/X12SA/data/e21206/Data10/software/test_script.csv"
|
||||
|
||||
def my_offset():
|
||||
return [0, 0]
|
||||
|
||||
params = SAXSParams(my_offset)
|
||||
params.load_from_csv(INPUT_FILE)
|
||||
for key in params.data:
|
||||
pprint(params.data[key].to_scan_params())
|
||||
@@ -1,7 +1,3 @@
|
||||
""" Script used for parsing scan parameters from a CSV file as created by the motormap GUI.
|
||||
This needs to be reviewed and tested during the deployment phase."""
|
||||
|
||||
# pylint: skip-file
|
||||
import csv
|
||||
import os
|
||||
from collections import defaultdict
|
||||
13
bec_plugins/utils/service_config.py
Executable file
13
bec_plugins/utils/service_config.py
Executable file
@@ -0,0 +1,13 @@
|
||||
import yaml
|
||||
|
||||
CONFIG_PATH = "/sls/X12SA/data/gac-x12saop/bec/config/bec_service_config.yaml"
|
||||
|
||||
|
||||
def load_service_config() -> dict:
|
||||
"""Load the service configuration from the YAML file."""
|
||||
with open(CONFIG_PATH, "r", encoding="utf-8") as stream:
|
||||
try:
|
||||
config = yaml.safe_load(stream)
|
||||
except yaml.YAMLError as exc:
|
||||
print(exc)
|
||||
return config
|
||||
5
bin/open_tunnel.sh
Executable file
5
bin/open_tunnel.sh
Executable file
@@ -0,0 +1,5 @@
|
||||
|
||||
for i in `seq 1 8`
|
||||
do
|
||||
ssh -N -R 6379:localhost:6379 x12sa-cn-$i &
|
||||
done
|
||||
48
bin/setup_bec.sh
Executable file
48
bin/setup_bec.sh
Executable file
@@ -0,0 +1,48 @@
|
||||
|
||||
if [ "pc15543.psi.ch" != "$(hostname)" ]; then
|
||||
echo "Please run this script on pc15543"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
module use unstable
|
||||
module add psi-python311/2024.02
|
||||
echo module add tmux/3.2 >> ~/.bashrc
|
||||
echo module add redis/7.0.12 >> ~/.bashrc
|
||||
|
||||
source ~/.bashrc
|
||||
|
||||
cd ~/Data10
|
||||
mkdir -p software/
|
||||
mkdir -p ~/bec/scripts
|
||||
cd software
|
||||
|
||||
git clone https://gitlab.psi.ch/bec/bec.git
|
||||
git clone https://gitlab.psi.ch/bec/ophyd_devices.git
|
||||
git clone https://gitlab.psi.ch/bec/bec-widgets.git
|
||||
|
||||
python -m venv ./bec_venv
|
||||
source ./bec_venv/bin/activate
|
||||
|
||||
cd bec
|
||||
git checkout sastt-online-changes
|
||||
pip install -e ./bec_server[dev]
|
||||
|
||||
cd ../csaxs-bec
|
||||
pip install -e .[dev]
|
||||
|
||||
#redis-server --protected-mode no &
|
||||
|
||||
read -p "Do you want to set the current BEC account to $(whoami)? (yes/no) " yn
|
||||
|
||||
val=$(whoami)
|
||||
|
||||
case $yn in
|
||||
yes ) echo ok, setting account to $val;
|
||||
redis-cli SET internal/account:val $val;;
|
||||
no ) echo ;;
|
||||
* ) echo invalid response;
|
||||
exit 1;;
|
||||
esac
|
||||
|
||||
|
||||
$(pwd)/open_tunnel.sh
|
||||
17
bin/setup_bec_widgets.sh
Executable file
17
bin/setup_bec_widgets.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
module use unstable
|
||||
module add psi-python311/2024.02
|
||||
|
||||
cd ~/Data10/software
|
||||
python -m venv ./bec_widgets_venv
|
||||
source ./bec_widgets_venv/bin/activate
|
||||
pip install --upgrade pip
|
||||
cd ~/Data10/software/bec/bec_lib
|
||||
pip install -e .
|
||||
|
||||
cd ~/Data10/software/csaxs-bec
|
||||
pip install -e .
|
||||
|
||||
cd ~/Data10/software/bec-widgets
|
||||
pip install -e .
|
||||
|
||||
echo "For the moment widgets only run on beamline consoles comp1/2 and cons1"
|
||||
@@ -1,2 +0,0 @@
|
||||
from .load_additional_correction import lamni_read_additional_correction
|
||||
from .x_ray_eye_align import DataDrivenLamNI, LamNI, MagLamNI, XrayEyeAlign
|
||||
@@ -1,2 +0,0 @@
|
||||
from .cSAXS_beamline import epics_get, epics_put, fshclose, fshopen, fshstatus
|
||||
from .csaxs_bl_checks import cSAXSBeamlineChecks
|
||||
@@ -1,122 +0,0 @@
|
||||
import builtins
|
||||
import datetime
|
||||
import threading
|
||||
import time
|
||||
|
||||
from bec_lib import bec_logger
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
if builtins.__dict__.get("bec"):
|
||||
bec = builtins.__dict__.get("bec")
|
||||
|
||||
|
||||
class cSAXSBeamlineChecks:
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.check_shutter = True
|
||||
self.check_light_available = True
|
||||
self.check_fofb = True
|
||||
self._check_msgs = []
|
||||
self._beam_is_okay = True
|
||||
self._stop_beam_check_event = None
|
||||
self.beam_check_thread = None
|
||||
|
||||
def get_beamline_checks_enabled(self):
|
||||
print(
|
||||
f"Shutter: {self.check_shutter}\nFOFB: {self.check_fofb}\nLight available:"
|
||||
f" {self.check_light_available}"
|
||||
)
|
||||
|
||||
@property
|
||||
def beamline_checks_enabled(self):
|
||||
return {
|
||||
"shutter": self.check_shutter,
|
||||
"fofb": self.check_fofb,
|
||||
"light available": self.check_light_available,
|
||||
}
|
||||
|
||||
@beamline_checks_enabled.setter
|
||||
def beamline_checks_enabled(self, val: bool):
|
||||
self.check_shutter = val
|
||||
self.check_light_available = val
|
||||
self.check_fofb = val
|
||||
self.get_beamline_checks_enabled()
|
||||
|
||||
def _run_beamline_checks(self):
|
||||
msgs = []
|
||||
dev = builtins.__dict__.get("dev")
|
||||
try:
|
||||
if self.check_shutter:
|
||||
shutter_val = dev.x12sa_es1_shutter_status.read(cached=True)
|
||||
if shutter_val["value"].lower() != "open":
|
||||
self._beam_is_okay = False
|
||||
msgs.append("Check beam failed: Shutter is closed.")
|
||||
if self.check_light_available:
|
||||
machine_status = dev.sls_machine_status.read(cached=True)
|
||||
if machine_status["value"] not in ["Light Available", "Light-Available"]:
|
||||
self._beam_is_okay = False
|
||||
msgs.append("Check beam failed: Light not available.")
|
||||
if self.check_fofb:
|
||||
fast_orbit_feedback = dev.sls_fast_orbit_feedback.read(cached=True)
|
||||
if fast_orbit_feedback["value"] != "running":
|
||||
self._beam_is_okay = False
|
||||
msgs.append("Check beam failed: Fast orbit feedback is not running.")
|
||||
except Exception:
|
||||
logger.warning("Failed to check beam.")
|
||||
return msgs
|
||||
|
||||
def _check_beam(self):
|
||||
while not self._stop_beam_check_event.is_set():
|
||||
self._check_msgs = self._run_beamline_checks()
|
||||
|
||||
if not self._beam_is_okay:
|
||||
self._stop_beam_check_event.set()
|
||||
time.sleep(1)
|
||||
|
||||
def _start_beam_check(self):
|
||||
self._beam_is_okay = True
|
||||
self._stop_beam_check_event = threading.Event()
|
||||
|
||||
self.beam_check_thread = threading.Thread(target=self._check_beam, daemon=True)
|
||||
self.beam_check_thread.start()
|
||||
|
||||
def _was_beam_okay(self):
|
||||
self._stop_beam_check_event.set()
|
||||
self.beam_check_thread.join()
|
||||
return self._beam_is_okay
|
||||
|
||||
def _print_beamline_checks(self):
|
||||
for msg in self._check_msgs:
|
||||
logger.warning(msg)
|
||||
|
||||
def _wait_for_beamline_checks(self):
|
||||
self._print_beamline_checks()
|
||||
try:
|
||||
msg = bec.logbook.LogbookMessage()
|
||||
msg.add_text(
|
||||
"<p><mark class='pen-red'><strong>Beamline checks failed at"
|
||||
f" {str(datetime.datetime.now())}: {''.join(self._check_msgs)}</strong></mark></p>"
|
||||
).add_tag(["BEC", "beam_check"])
|
||||
bec.logbook.send_logbook_message(msg)
|
||||
except Exception:
|
||||
logger.warning("Failed to send update to SciLog.")
|
||||
|
||||
while True:
|
||||
self._beam_is_okay = True
|
||||
self._check_msgs = self._run_beamline_checks()
|
||||
if self._beam_is_okay:
|
||||
break
|
||||
self._print_beamline_checks()
|
||||
time.sleep(1)
|
||||
|
||||
try:
|
||||
msg = bec.logbook.LogbookMessage()
|
||||
msg.add_text(
|
||||
"<p><mark class='pen-red'><strong>Operation resumed at"
|
||||
f" {str(datetime.datetime.now())}.</strong></mark></p>"
|
||||
).add_tag(["BEC", "beam_check"])
|
||||
bec.logbook.send_logbook_message(msg)
|
||||
except Exception:
|
||||
logger.warning("Failed to send update to SciLog.")
|
||||
@@ -1 +0,0 @@
|
||||
from .flomni import Flomni
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,177 +0,0 @@
|
||||
import time
|
||||
|
||||
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 FlomniOpticsMixin:
|
||||
@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 feye_out(self):
|
||||
fshclose()
|
||||
self.foptics_in()
|
||||
feyex_out = self._get_user_param_safe("feyex", "out")
|
||||
umv(dev.feyex, feyex_out)
|
||||
|
||||
epics_put("XOMNYI-XEYE-ACQ:0", 2)
|
||||
# move rotation stage to zero to avoid problems with wires
|
||||
umv(dev.fsamroy, 0)
|
||||
# umv(dev.fttrx1, 9.2)
|
||||
|
||||
def feye_in(self):
|
||||
bec.queue.next_dataset_number += 1
|
||||
# umv(dev.fttrx1, -17)
|
||||
|
||||
feyex_in = self._get_user_param_safe("feyex", "in")
|
||||
feyey_in = self._get_user_param_safe("feyey", "in")
|
||||
umv(dev.feyex, feyex_in, dev.feyey, feyey_in)
|
||||
self.align.update_frame()
|
||||
|
||||
def _ffzp_in(self):
|
||||
foptx_in = self._get_user_param_safe("foptx", "in")
|
||||
fopty_in = self._get_user_param_safe("fopty", "in")
|
||||
umv(dev.foptx, foptx_in)
|
||||
umv(
|
||||
dev.fopty, fopty_in
|
||||
) # for 7.2567 keV and 150 mu, 60 nm fzp, loptz 83.6000 for propagation 1.4 mm
|
||||
|
||||
def ffzp_in(self):
|
||||
"""
|
||||
move in the flomni zone plate.
|
||||
This will disable rt feedback, move the FZP and re-enabled the feedback.
|
||||
"""
|
||||
if "rtx" in dev and dev.rtx.enabled:
|
||||
dev.rtx.controller.feedback_disable()
|
||||
|
||||
self._ffzp_in()
|
||||
|
||||
if "rtx" in dev and dev.rtx.enabled:
|
||||
dev.rtx.controller.feedback_enable_with_reset()
|
||||
|
||||
def foptics_in(self):
|
||||
"""
|
||||
Move in the flomni optics, including the FZP and the OSA.
|
||||
"""
|
||||
self.ffzp_in()
|
||||
self.fosa_in()
|
||||
|
||||
def foptics_out(self):
|
||||
"""Move out the flomni optics"""
|
||||
if "rtx" in dev and dev.rtx.enabled:
|
||||
dev.rtx.controller.feedback_disable()
|
||||
|
||||
self.fosa_out()
|
||||
fopty_out = self._get_user_param_safe("fopty", "out")
|
||||
umv(dev.fopty, fopty_out)
|
||||
|
||||
if "rtx" in dev and dev.rtx.enabled:
|
||||
time.sleep(1)
|
||||
dev.rtx.controller.feedback_enable_with_reset()
|
||||
|
||||
def fosa_in(self):
|
||||
# 6.2 keV, 170 um FZP
|
||||
# umv(dev.losax, -1.4450000, dev.losay, -0.1800)
|
||||
# umv(dev.losaz, -1)
|
||||
# 6.7, 170
|
||||
# umv(dev.losax, -1.4850, dev.losay, -0.1930)
|
||||
# umv(dev.losaz, 1.0000)
|
||||
# 7.2, 150
|
||||
fosax_in = self._get_user_param_safe("fosax", "in")
|
||||
fosay_in = self._get_user_param_safe("fosay", "in")
|
||||
fosaz_in = self._get_user_param_safe("fosaz", "in")
|
||||
dev.fosax.limits = [fosax_in - 0.1, fosax_in + 0.1]
|
||||
dev.fosay.limits = [fosay_in - 0.1, fosay_in + 0.1]
|
||||
dev.fosaz.limits = [fosaz_in - 0.1, fosaz_in + 0.1]
|
||||
umv(dev.fosax, fosax_in, dev.fosay, fosay_in)
|
||||
umv(dev.fosaz, fosaz_in)
|
||||
|
||||
# 11 kev
|
||||
# umv(dev.losax, -1.161000, dev.losay, -0.196)
|
||||
# umv(dev.losaz, 1.0000)
|
||||
|
||||
def fosa_out(self):
|
||||
self.ensure_fheater_up()
|
||||
curtain_is_triggered = dev.foptz.controller.fosaz_light_curtain_is_triggered()
|
||||
if not curtain_is_triggered:
|
||||
fosaz_out = self._get_user_param_safe("fosaz", "out")
|
||||
dev.fosaz.limits = [fosaz_out - 0.1, fosaz_out + 0.1]
|
||||
umv(dev.fosaz, fosaz_out)
|
||||
fosax_out = self._get_user_param_safe("fosax", "out")
|
||||
dev.fosax.limits = [fosax_out - 0.1, fosax_out + 0.1]
|
||||
umv(dev.fosax, fosax_out)
|
||||
|
||||
def ffzp_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
|
||||
foptz_val = dev.foptz.readback.get()
|
||||
distance = -foptz_val + 43.15 + 36.7
|
||||
print(f"\nThe sample is in a distance of \033[1m{distance:.1f} mm\033[0m from the 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)
|
||||
|
||||
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)
|
||||
|
||||
fosaz_val = dev.fosaz.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")
|
||||
@@ -1,244 +0,0 @@
|
||||
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.flomni import Flomni
|
||||
|
||||
|
||||
class XrayEyeAlign:
|
||||
# pixel calibration, multiply to get mm
|
||||
PIXEL_CALIBRATION = 0.1 / 113 # .2 with binning
|
||||
|
||||
def __init__(self, client, flomni: Flomni) -> None:
|
||||
self.client = client
|
||||
self.flomni = flomni
|
||||
self.device_manager = client.device_manager
|
||||
self.scans = client.scans
|
||||
self.alignment_values = {}
|
||||
self.flomni.reset_correction()
|
||||
self.flomni.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.fsamroy, val)
|
||||
|
||||
def get_tomo_angle(self):
|
||||
return self.device_manager.devices.fsamroy.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.flomni.lights_off()
|
||||
|
||||
self.tomo_rotate(0)
|
||||
epics_put("XOMNYI-XEYE-ANGLE:0", 0)
|
||||
|
||||
self.flomni.feye_in()
|
||||
|
||||
self.flomni.laser_tracker_on()
|
||||
|
||||
self.flomni.rt_feedback_enable_with_reset()
|
||||
|
||||
# disable movement buttons
|
||||
self.movement_buttons_enabled = False
|
||||
|
||||
sample_name = self.flomni.sample_get_name(0)
|
||||
epics_put("XOMNYI-XEYE-SAMPLENAME:0.DESC", sample_name)
|
||||
|
||||
# this makes sure we are in a defined state
|
||||
self.flomni.rt_feedback_disable()
|
||||
|
||||
epics_put("XOMNYI-XEYE-PIXELSIZE:0", self.PIXEL_CALIBRATION)
|
||||
|
||||
self.flomni.fosa_out()
|
||||
|
||||
fsamx_in = self.flomni._get_user_param_safe("fsamx", "in")
|
||||
umv(dev.fsamx, fsamx_in - 0.25)
|
||||
|
||||
self.flomni.ffzp_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.flomni.rt_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()
|
||||
umv(dev.fsamx, fsamx_in - 0.25)
|
||||
|
||||
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.fsamx, fsamx_in)
|
||||
time.sleep(0.5)
|
||||
self.flomni.rt_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.flomni.rt_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.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 flomni.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")
|
||||
@@ -1,86 +0,0 @@
|
||||
"""
|
||||
Post startup script for the BEC client. This script is executed after the
|
||||
IPython shell is started. It is used to load the beamline specific
|
||||
information and to setup the prompts.
|
||||
|
||||
The script is executed in the global namespace of the IPython shell. This
|
||||
means that all variables defined here are available in the shell.
|
||||
|
||||
If needed, bec command-line arguments can be parsed here. For example, to
|
||||
parse the --session argument, add the following lines to the script:
|
||||
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--session", help="Session name", type=str, default="my_default_session")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.session == "my_session":
|
||||
print("Loading my_session session")
|
||||
from bec_plugins.bec_ipython_client.plugins.my_session import *
|
||||
else:
|
||||
print("Loading default session")
|
||||
from bec_plugins.bec_ipython_client.plugins.default_session import *
|
||||
"""
|
||||
|
||||
# pylint: disable=invalid-name, unused-import, import-error, undefined-variable, unused-variable, unused-argument, no-name-in-module
|
||||
|
||||
from bec_lib import bec_logger
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
logger.info("Using the cSAXS startup script.")
|
||||
|
||||
# pylint: disable=import-error
|
||||
_args = _main_dict["args"]
|
||||
|
||||
_session_name = "cSAXS"
|
||||
if _args.session.lower() == "lamni":
|
||||
from csaxs_bec.bec_ipython_client.plugins.cSAXS import *
|
||||
from csaxs_bec.bec_ipython_client.plugins.LamNI import *
|
||||
|
||||
_session_name = "LamNI"
|
||||
lamni = LamNI(bec)
|
||||
logger.success("LamNI session loaded.")
|
||||
|
||||
elif _args.session.lower() == "csaxs":
|
||||
print("Loading cSAXS session")
|
||||
from csaxs_bec.bec_ipython_client.plugins.cSAXS import *
|
||||
|
||||
logger.success("cSAXS session loaded.")
|
||||
|
||||
|
||||
# SETUP BEAMLINE INFO
|
||||
from bec_ipython_client.plugins.SLS.sls_info import OperatorInfo, SLSInfo
|
||||
|
||||
from csaxs_bec.bec_ipython_client.plugins.cSAXS.beamline_info import BeamlineInfo
|
||||
|
||||
bec._beamline_mixin._bl_info_register(BeamlineInfo)
|
||||
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.status = 1
|
||||
|
||||
|
||||
# REGISTER BEAMLINE CHECKS
|
||||
from bec_lib.bl_conditions import (
|
||||
FastOrbitFeedbackCondition,
|
||||
LightAvailableCondition,
|
||||
ShutterCondition,
|
||||
)
|
||||
|
||||
if "sls_machine_status" in dev:
|
||||
print("Registering light available condition for SLS machine status")
|
||||
_light_available_condition = LightAvailableCondition(dev.sls_machine_status)
|
||||
bec.bl_checks.register(_light_available_condition)
|
||||
|
||||
if "x12sa_es1_shutter_status" in dev:
|
||||
print("Registering shutter condition for X12SA ES1 shutter status")
|
||||
_shutter_condition = ShutterCondition(dev.x12sa_es1_shutter_status)
|
||||
bec.bl_checks.register(_shutter_condition)
|
||||
|
||||
# if hasattr(dev, "sls_fast_orbit_feedback"):
|
||||
# print("Registering fast orbit feedback condition for SLS fast orbit feedback")
|
||||
# _fast_orbit_feedback_condition = FastOrbitFeedbackCondition(dev.sls_fast_orbit_feedback)
|
||||
# bec.bl_checks.register(_fast_orbit_feedback_condition)
|
||||
@@ -1,14 +0,0 @@
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
def extend_command_line_args(parser):
|
||||
"""
|
||||
Extend the command line arguments of the BEC client.
|
||||
"""
|
||||
|
||||
parser.add_argument("--session", help="Session name", type=str, default="cSAXS")
|
||||
|
||||
return parser
|
||||
@@ -1,295 +0,0 @@
|
||||
# This configuration file was used for the cSAXS beamtimes in September 2023
|
||||
##################################################
|
||||
#############Config for cSAXS SAXS imaging########
|
||||
##################################################
|
||||
|
||||
bpm4i:
|
||||
description: 'XBPM 4: integrated counts'
|
||||
deviceClass: ophyd.EpicsSignalRO
|
||||
deviceConfig:
|
||||
read_pv: X12SA-OP1-SCALER.
|
||||
deviceTags:
|
||||
- monitor
|
||||
enabled: true
|
||||
readOnly: false
|
||||
onFailure: buffer
|
||||
readoutPriority: baseline
|
||||
softwareTrigger: false
|
||||
mokev:
|
||||
description: Monochromator energy in keV
|
||||
deviceClass: csaxs_bec.devices.epics.devices.specMotors.EnergyKev
|
||||
deviceConfig:
|
||||
read_pv: X12SA-OP-MO:ROX2
|
||||
deviceTags:
|
||||
- monitor
|
||||
enabled: true
|
||||
readOnly: false
|
||||
onFailure: buffer
|
||||
readoutPriority: baseline
|
||||
softwareTrigger: false
|
||||
mcs:
|
||||
description: Mcs scalar card for transmission readout
|
||||
deviceClass: csaxs_bec.devices.epics.devices.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.devices.Eiger9McSAXS
|
||||
deviceConfig:
|
||||
prefix: 'X12SA-ES-EIGER9M:'
|
||||
deviceTags:
|
||||
- cSAXS
|
||||
- eiger9m
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readoutPriority: async
|
||||
softwareTrigger: false
|
||||
ddg_detectors:
|
||||
description: DelayGenerator for detector triggering
|
||||
deviceClass: csaxs_bec.devices.epics.devices.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.devices.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.devices.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.devices.FalconcSAXS
|
||||
deviceConfig:
|
||||
prefix: 'X12SA-SITORO:'
|
||||
deviceTags:
|
||||
- cSAXS
|
||||
- falcon
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readoutPriority: async
|
||||
softwareTrigger: false
|
||||
pilatus_2:
|
||||
description: Pilatus2 HPC area detector 300k
|
||||
deviceClass: csaxs_bec.devices.epics.devices.PilatuscSAXS
|
||||
deviceConfig:
|
||||
prefix: 'X12SA-ES-PILATUS300K:'
|
||||
deviceTags:
|
||||
- cSAXS
|
||||
- pilatus_2
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readoutPriority: async
|
||||
softwareTrigger: false
|
||||
samx:
|
||||
description: SGalil motor stage
|
||||
deviceClass: csaxs_bec.devices.galil.SGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: "E"
|
||||
host: '129.129.122.26'
|
||||
port: 23
|
||||
sign: -1
|
||||
limits:
|
||||
- -181
|
||||
- -0.1
|
||||
deviceTags:
|
||||
- cSAXS
|
||||
- sgalil
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
softwareTrigger: false
|
||||
samy:
|
||||
description: SGalil motor stage
|
||||
deviceClass: csaxs_bec.devices.galil.SGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: "C"
|
||||
host: '129.129.122.26'
|
||||
port: 23
|
||||
sign: -1
|
||||
limits:
|
||||
- -66
|
||||
- -12
|
||||
deviceTags:
|
||||
- cSAXS
|
||||
- sgalil
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
softwareTrigger: false
|
||||
micfoc:
|
||||
description: Focusing motor of Microscope stage
|
||||
deviceClass: ophyd_devices.epics.devices.EpicsMotorEx
|
||||
deviceConfig:
|
||||
prefix: X12SA-ES2-ES06
|
||||
motor_resolution: 0.00125
|
||||
base_velocity: 0.25
|
||||
velocity: 2.5
|
||||
backlash_distance: 0.125
|
||||
acceleration: 0.4
|
||||
user_offset_dir: 0
|
||||
deviceTags:
|
||||
- cSAXS
|
||||
- micfoc
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
softwareTrigger: false
|
||||
owis_samx:
|
||||
description: Owis motor stage samx
|
||||
deviceClass: ophyd_devices.epics.devices.EpicsMotorEx
|
||||
deviceConfig:
|
||||
prefix: X12SA-ES2-ES01
|
||||
motor_resolution: 0.00125
|
||||
base_velocity: 0.0625
|
||||
velocity: 10
|
||||
backlash_distance: 0.125
|
||||
acceleration: 0.2
|
||||
user_offset_dir: 0
|
||||
deviceTags:
|
||||
- cSAXS
|
||||
- owis_samx
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
softwareTrigger: false
|
||||
owis_samy:
|
||||
description: Owis motor stage samx
|
||||
deviceClass: ophyd_devices.epics.devices.EpicsMotorEx
|
||||
deviceConfig:
|
||||
prefix: X12SA-ES2-ES02
|
||||
motor_resolution: 0.00125
|
||||
base_velocity: 0.0625
|
||||
velocity: 10
|
||||
backlash_distance: 0.125
|
||||
acceleration: 0.2
|
||||
user_offset_dir: 0
|
||||
deviceTags:
|
||||
- cSAXS
|
||||
- owis_samx
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
softwareTrigger: false
|
||||
rotx:
|
||||
description: Rotation stage rotx
|
||||
deviceClass: ophyd_devices.epics.devices.EpicsMotorEx
|
||||
deviceConfig:
|
||||
prefix: X12SA-ES2-ES05
|
||||
motor_resolution: 0.0025
|
||||
base_velocity: 0.5
|
||||
velocity: 7.5
|
||||
backlash_distance: 0.25
|
||||
acceleration: 0.2
|
||||
user_offset_dir: 1
|
||||
limits:
|
||||
- -0.1
|
||||
- 0.1
|
||||
deviceTags:
|
||||
- cSAXS
|
||||
- rotx
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
softwareTrigger: false
|
||||
roty:
|
||||
description: Rotation stage rotx
|
||||
deviceClass: ophyd_devices.epics.devices.EpicsMotorEx
|
||||
deviceConfig:
|
||||
prefix: X12SA-ES2-ES04
|
||||
motor_resolution: 0.0025
|
||||
base_velocity: 0.5
|
||||
velocity: 7.5
|
||||
backlash_distance: 0.25
|
||||
acceleration: 0.2
|
||||
user_offset_dir: 1
|
||||
limits:
|
||||
- -0.1
|
||||
- 0.1
|
||||
deviceTags:
|
||||
- cSAXS
|
||||
- rotx
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
softwareTrigger: false
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,344 +0,0 @@
|
||||
feyex:
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: D
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- -30
|
||||
- -1
|
||||
port: 8082
|
||||
sign: 1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
userParameter:
|
||||
in: -16.267
|
||||
out: -1
|
||||
feyey:
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: E
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- -1
|
||||
- -10
|
||||
port: 8082
|
||||
sign: 1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
userParameter:
|
||||
in: -10.467
|
||||
fheater:
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: C
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- -15
|
||||
- 0
|
||||
port: 8082
|
||||
sign: -1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
flomni_samples:
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.epics.devices.flomni_sample_storage.FlomniSampleStorage
|
||||
deviceConfig: {}
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
foptx:
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: B
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- -17
|
||||
- -12
|
||||
port: 8082
|
||||
sign: 1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: true
|
||||
readoutPriority: baseline
|
||||
userParameter:
|
||||
in: -13.761
|
||||
fopty:
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: F
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 4
|
||||
port: 8082
|
||||
sign: 1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: true
|
||||
readoutPriority: baseline
|
||||
userParameter:
|
||||
in: 0.552
|
||||
out: 0.752
|
||||
foptz:
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: A
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 27
|
||||
port: 8082
|
||||
sign: 1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
userParameter:
|
||||
in: 23
|
||||
fosax:
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
|
||||
deviceConfig:
|
||||
axis_Id: A
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- 10.2
|
||||
- 10.6
|
||||
port: 3334
|
||||
sign: -1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
userParameter:
|
||||
in: 9.124
|
||||
out: 5.3
|
||||
fosay:
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
|
||||
deviceConfig:
|
||||
axis_Id: B
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- -3.1
|
||||
- -2.9
|
||||
port: 3334
|
||||
sign: -1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
userParameter:
|
||||
in: 0.367
|
||||
fosaz:
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
|
||||
deviceConfig:
|
||||
axis_Id: C
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- -6
|
||||
- -4
|
||||
port: 3334
|
||||
sign: 1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
userParameter:
|
||||
in: 8.5
|
||||
out: 6
|
||||
fsamroy:
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.galil.fupr_ophyd.FuprGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: A
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- -5
|
||||
- 365
|
||||
port: 8084
|
||||
sign: -1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
fsamx:
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: E
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- -162
|
||||
- 0
|
||||
port: 8081
|
||||
sign: 1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: true
|
||||
readoutPriority: baseline
|
||||
userParameter:
|
||||
in: -1.1
|
||||
fsamy:
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: F
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- 2
|
||||
- 3.1
|
||||
port: 8081
|
||||
sign: 1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: true
|
||||
readoutPriority: baseline
|
||||
userParameter:
|
||||
in: 2.75
|
||||
ftracky:
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: H
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- 2.2
|
||||
- 2.8
|
||||
port: 8082
|
||||
sign: 1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
ftrackz:
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: G
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- 4.5
|
||||
- 5.5
|
||||
port: 8082
|
||||
sign: 1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
ftransx:
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: C
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 50
|
||||
port: 8081
|
||||
sign: 1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
ftransy:
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: A
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- -100
|
||||
- 0
|
||||
port: 8081
|
||||
sign: 1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
ftransz:
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: B
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 145
|
||||
port: 8081
|
||||
sign: 1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
ftray:
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: D
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- -200
|
||||
- 0
|
||||
port: 8081
|
||||
sign: -1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
rtx:
|
||||
description: flomni rt
|
||||
deviceClass: csaxs_bec.devices.rt_lamni.rt_flomni_ophyd.RtFlomniMotor
|
||||
deviceConfig:
|
||||
axis_Id: A
|
||||
host: mpc2844.psi.ch
|
||||
port: 2222
|
||||
sign: 1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: on_request
|
||||
userParameter:
|
||||
low_signal: 11000
|
||||
min_signal: 10000
|
||||
rt_pid_voltage: -0.06219
|
||||
rty:
|
||||
description: flomni rt
|
||||
deviceClass: csaxs_bec.devices.rt_lamni.rt_flomni_ophyd.RtFlomniMotor
|
||||
deviceConfig:
|
||||
axis_Id: B
|
||||
host: mpc2844.psi.ch
|
||||
port: 2222
|
||||
sign: 1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: on_request
|
||||
userParameter:
|
||||
tomo_additional_offsety: 0
|
||||
rtz:
|
||||
description: flomni rt
|
||||
deviceClass: csaxs_bec.devices.rt_lamni.rt_flomni_ophyd.RtFlomniMotor
|
||||
deviceConfig:
|
||||
axis_Id: C
|
||||
host: mpc2844.psi.ch
|
||||
port: 2222
|
||||
sign: 1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: on_request
|
||||
@@ -1,340 +0,0 @@
|
||||
fheater:
|
||||
readoutPriority: baseline
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: C
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- -15
|
||||
- 0
|
||||
port: 8082
|
||||
sign: -1
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readOnly: false
|
||||
feyex:
|
||||
readoutPriority: baseline
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: D
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- -30
|
||||
- -1
|
||||
port: 8082
|
||||
sign: 1
|
||||
onFailure: buffer
|
||||
userParameter:
|
||||
in: -16.267
|
||||
out: -1
|
||||
enabled: true
|
||||
readOnly: false
|
||||
feyey:
|
||||
readoutPriority: baseline
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: E
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- -1
|
||||
- -10
|
||||
port: 8082
|
||||
sign: 1
|
||||
onFailure: buffer
|
||||
userParameter:
|
||||
in: -10.467
|
||||
enabled: true
|
||||
readOnly: false
|
||||
foptx:
|
||||
readoutPriority: baseline
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: B
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- -17
|
||||
- -12
|
||||
port: 8082
|
||||
sign: 1
|
||||
onFailure: buffer
|
||||
userParameter:
|
||||
in: -13.761
|
||||
enabled: true
|
||||
readOnly: false
|
||||
fopty:
|
||||
readoutPriority: baseline
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: F
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 4
|
||||
port: 8082
|
||||
sign: 1
|
||||
onFailure: buffer
|
||||
userParameter:
|
||||
in: 0.552
|
||||
out: 0.752
|
||||
enabled: true
|
||||
readOnly: false
|
||||
foptz:
|
||||
readoutPriority: baseline
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: A
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 27
|
||||
port: 8082
|
||||
sign: 1
|
||||
onFailure: buffer
|
||||
userParameter:
|
||||
in: 23
|
||||
enabled: true
|
||||
readOnly: false
|
||||
fosax:
|
||||
readoutPriority: baseline
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
|
||||
deviceConfig:
|
||||
axis_Id: A
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- 10.2
|
||||
- 10.6
|
||||
port: 3334
|
||||
sign: -1
|
||||
onFailure: buffer
|
||||
userParameter:
|
||||
in: 9.124
|
||||
out: 5.3
|
||||
enabled: true
|
||||
readOnly: false
|
||||
fosay:
|
||||
readoutPriority: baseline
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
|
||||
deviceConfig:
|
||||
axis_Id: B
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- -3.1
|
||||
- -2.9
|
||||
port: 3334
|
||||
sign: -1
|
||||
onFailure: buffer
|
||||
userParameter:
|
||||
in: 0.367
|
||||
enabled: true
|
||||
readOnly: false
|
||||
fosaz:
|
||||
readoutPriority: baseline
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
|
||||
deviceConfig:
|
||||
axis_Id: C
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- -6
|
||||
- -4
|
||||
port: 3334
|
||||
sign: 1
|
||||
onFailure: buffer
|
||||
userParameter:
|
||||
in: 8.5
|
||||
out: 6
|
||||
enabled: true
|
||||
readOnly: false
|
||||
fsamroy:
|
||||
readoutPriority: baseline
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.galil.fupr_ophyd.FuprGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: A
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- -5
|
||||
- 365
|
||||
port: 8084
|
||||
sign: -1
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readOnly: false
|
||||
fsamx:
|
||||
readoutPriority: baseline
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: E
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- -162
|
||||
- 0
|
||||
port: 8081
|
||||
sign: 1
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readOnly: false
|
||||
userParameter:
|
||||
in: -1.1
|
||||
fsamy:
|
||||
readoutPriority: baseline
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: F
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- 2
|
||||
- 3.1
|
||||
port: 8081
|
||||
sign: 1
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readOnly: false
|
||||
userParameter:
|
||||
in: 2.75
|
||||
ftracky:
|
||||
readoutPriority: baseline
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: H
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- 2.2
|
||||
- 2.8
|
||||
port: 8082
|
||||
sign: 1
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readOnly: false
|
||||
ftrackz:
|
||||
readoutPriority: baseline
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: G
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- 4.5
|
||||
- 5.5
|
||||
port: 8082
|
||||
sign: 1
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readOnly: false
|
||||
ftransx:
|
||||
readoutPriority: baseline
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: C
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 50
|
||||
port: 8081
|
||||
sign: 1
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readOnly: false
|
||||
ftransy:
|
||||
readoutPriority: baseline
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: A
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- -100
|
||||
- 0
|
||||
port: 8081
|
||||
sign: 1
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readOnly: false
|
||||
ftransz:
|
||||
readoutPriority: baseline
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: B
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- 0
|
||||
- 145
|
||||
port: 8081
|
||||
sign: 1
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readOnly: false
|
||||
ftray:
|
||||
readoutPriority: baseline
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.galil.fgalil_ophyd.FlomniGalilMotor
|
||||
deviceConfig:
|
||||
axis_Id: D
|
||||
host: mpc2844.psi.ch
|
||||
limits:
|
||||
- -200
|
||||
- 0
|
||||
port: 8081
|
||||
sign: -1
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readOnly: false
|
||||
|
||||
flomni_samples:
|
||||
readoutPriority: baseline
|
||||
description: phase plate angle
|
||||
deviceClass: csaxs_bec.devices.epics.devices.FlomniSampleStorage
|
||||
deviceConfig:
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readOnly: false
|
||||
|
||||
rtx:
|
||||
readoutPriority: on_request
|
||||
description: flomni rt
|
||||
deviceClass: csaxs_bec.devices.rt_lamni.rt_flomni_ophyd.RtFlomniMotor
|
||||
deviceConfig:
|
||||
axis_Id: A
|
||||
host: mpc2844.psi.ch
|
||||
port: 2222
|
||||
sign: 1
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readOnly: false
|
||||
rty:
|
||||
readoutPriority: on_request
|
||||
description: flomni rt
|
||||
deviceClass: csaxs_bec.devices.rt_lamni.rt_flomni_ophyd.RtFlomniMotor
|
||||
deviceConfig:
|
||||
axis_Id: B
|
||||
host: mpc2844.psi.ch
|
||||
port: 2222
|
||||
sign: 1
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readOnly: false
|
||||
rtz:
|
||||
readoutPriority: on_request
|
||||
description: flomni rt
|
||||
deviceClass: csaxs_bec.devices.rt_lamni.rt_flomni_ophyd.RtFlomniMotor
|
||||
deviceConfig:
|
||||
axis_Id: C
|
||||
host: mpc2844.psi.ch
|
||||
port: 2222
|
||||
sign: 1
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readOnly: false
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,173 +0,0 @@
|
||||
import os
|
||||
import time
|
||||
|
||||
from bec_lib import MessageEndpoints, bec_logger, messages
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, DeviceStatus, EpicsSignal, EpicsSignalRO, Signal
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class _SLSDetectorConfigSignal(Signal):
|
||||
def put(self, value, *, timestamp=None, force=False):
|
||||
self._readback = value
|
||||
self.parent.sim_state[self.name] = value
|
||||
|
||||
def get(self):
|
||||
self._readback = self.parent.sim_state[self.name]
|
||||
return self.parent.sim_state[self.name]
|
||||
|
||||
|
||||
# if (_eigerinvac_is_on == 1) {
|
||||
# tic("setup eiger in vac")
|
||||
# epics_put("X12SA-ES1-DOUBLE-02",0)
|
||||
# unix(sprintf("mkdir -p /sls/X12SA/data/%s/Data10/eiger_4/S%05d-%05d/S%05d",_username,int((SCAN_N+1)/1000)*1000,int((SCAN_N+1)/1000)*1000+999,SCAN_N+1))
|
||||
|
||||
# epics_put("XOMNYI-DET-OUTDIR:0.DESC",sprintf("/sls/X12SA/data/%s/Data10/eiger_4/",_username))
|
||||
# epics_put("XOMNYI-DET-OUTDIR:1.DESC",sprintf("S%05d-%05d/S%05d",int((SCAN_N+1)/1000)*1000,int((SCAN_N+1)/1000)*1000+999,SCAN_N+1))
|
||||
# epics_put("XOMNYI-DET-CYCLES:0", _lamni_scan_numberofpts)
|
||||
# epics_put("XOMNYI-DET-EXPTIME:0", $2)
|
||||
# epics_put("XOMNYI-DET-INDEX:0", SCAN_N+1)
|
||||
|
||||
# epics_put("XOMNYI-DET-CONTROL:0.DESC", "begin")
|
||||
# if(_eigerinvac_burst==0)
|
||||
# {
|
||||
# epics_put("XOMNYI-DET-CYCLES:0", _lamni_scan_numberofpts)
|
||||
# epics_put("XOMNYI-DET-EXPTIME:0", $2)
|
||||
# metadata_set("eiger_burst", "int", 1, 1)
|
||||
# }
|
||||
# else
|
||||
# {
|
||||
# metadata_set("eiger_burst", "int", 1, (int($2/0.0085)))
|
||||
# epics_put("XOMNYI-DET-CYCLES:0", _lamni_scan_numberofpts*(int($2/0.0085)))
|
||||
# epics_put("XOMNYI-DET-EXPTIME:0", 0.005)
|
||||
# }
|
||||
# global _DC_acquisition_ID
|
||||
# _DC_acquisition_ID = SCAN_N+1
|
||||
|
||||
|
||||
class Eiger1p5MDetector(Device):
|
||||
USER_ACCESS = []
|
||||
file_path = Cpt(EpicsSignal, name="file_path", read_pv="XOMNYI-DET-OUTDIR:0.DESC")
|
||||
detector_out_scan_dir = Cpt(
|
||||
EpicsSignal, name="detector_out_scan_dir", read_pv="XOMNYI-DET-OUTDIR:1.DESC"
|
||||
)
|
||||
frames = Cpt(EpicsSignal, name="frames", read_pv="XOMNYI-DET-CYCLES:0")
|
||||
exp_time = Cpt(EpicsSignal, name="exp_time", read_pv="XOMNYI-DET-EXPTIME:0")
|
||||
index = Cpt(EpicsSignal, name="index", read_pv="XOMNYI-DET-INDEX:0")
|
||||
detector_control = Cpt(
|
||||
EpicsSignal, name="detector_control", read_pv="XOMNYI-DET-CONTROL:0.DESC"
|
||||
)
|
||||
framescaught = Cpt(EpicsSignalRO, name="framescaught", read_pv="XOMNYI-DET-CONTROL:0.VAL")
|
||||
|
||||
file_pattern = Cpt(_SLSDetectorConfigSignal, name="file_pattern", value="")
|
||||
burst = Cpt(_SLSDetectorConfigSignal, name="burst", value=1)
|
||||
save_file = Cpt(_SLSDetectorConfigSignal, name="save_file", value=False)
|
||||
|
||||
def __init__(self, *, name, kind=None, parent=None, device_manager=None, **kwargs):
|
||||
self.device_manager = device_manager
|
||||
super().__init__(name=name, parent=parent, kind=kind, **kwargs)
|
||||
self.sim_state = {
|
||||
f"{self.name}_file_path": "~/Data10/data/",
|
||||
f"{self.name}_file_pattern": f"{self.name}_{{:05d}}.h5",
|
||||
f"{self.name}_frames": 1,
|
||||
f"{self.name}_burst": 1,
|
||||
f"{self.name}_save_file": False,
|
||||
f"{self.name}_exp_time": 0,
|
||||
}
|
||||
self._stopped = False
|
||||
self.file_name = ""
|
||||
self.metadata = {}
|
||||
self.username = "e20588" # TODO get from config
|
||||
|
||||
def _get_current_scan_msg(self) -> messages.ScanStatusMessage:
|
||||
return self.device_manager.connector.get(MessageEndpoints.scan_status())
|
||||
|
||||
def _get_scan_dir(self, scan_bundle, scan_number, leading_zeros=None):
|
||||
if leading_zeros is None:
|
||||
leading_zeros = len(str(scan_bundle))
|
||||
floor_dir = scan_number // scan_bundle * scan_bundle
|
||||
return f"S{floor_dir:0{leading_zeros}d}-{floor_dir+scan_bundle-1:0{leading_zeros}d}/S{scan_number:0{leading_zeros}d}"
|
||||
|
||||
def stage(self) -> list[object]:
|
||||
scan_msg = self._get_current_scan_msg()
|
||||
self.metadata = {
|
||||
"scan_id": scan_msg.content["scan_id"],
|
||||
"RID": scan_msg.content["info"]["RID"],
|
||||
"queue_id": scan_msg.content["info"]["queue_id"],
|
||||
}
|
||||
scan_number = scan_msg.content["info"]["scan_number"]
|
||||
exp_time = scan_msg.content["info"]["exp_time"]
|
||||
|
||||
# set base path for detector output
|
||||
self.file_path.set(f"/sls/X12SA/data/{self.username}/Data10/eiger_4/")
|
||||
|
||||
# set scan directory (e.g. S00000-00999/S00020)
|
||||
scan_dir = self._get_scan_dir(scan_bundle=1000, scan_number=scan_number, leading_zeros=5)
|
||||
self.detector_out_scan_dir.set(scan_dir)
|
||||
|
||||
self.file_name = os.path.join(f"/sls/X12SA/data/{self.username}/Data10/eiger_4/", scan_dir)
|
||||
|
||||
# set the scan number
|
||||
self.index.set(scan_number)
|
||||
|
||||
# set the number of frames
|
||||
self.frames.set(scan_msg.content["info"]["num_points"])
|
||||
|
||||
# set the exposure time
|
||||
self.exp_time.set(exp_time)
|
||||
|
||||
# wait for detector control to become "ready"
|
||||
while True:
|
||||
det_ctrl = self.detector_control.get()
|
||||
if det_ctrl == "ready":
|
||||
break
|
||||
time.sleep(0.005)
|
||||
|
||||
# send the "begin" flag to start processing the above commands
|
||||
self.detector_control.set("begin")
|
||||
|
||||
# wait for detector to be "armed"
|
||||
logger.info("Waiting for detector setup")
|
||||
while True:
|
||||
det_ctrl = self.detector_control.get()
|
||||
if det_ctrl == "armed":
|
||||
break
|
||||
time.sleep(0.005)
|
||||
|
||||
self.detector_control.put("acquiring")
|
||||
|
||||
return super().stage()
|
||||
|
||||
def unstage(self) -> list[object]:
|
||||
time_waited = 0
|
||||
sleep_time = 0.2
|
||||
framesexpected = self.frames.get()
|
||||
while True:
|
||||
framescaught = self.framescaught.get()
|
||||
if self.framescaught.get() < framesexpected:
|
||||
logger.info(
|
||||
f"Waiting for frames: Transferred {framescaught} out of {framesexpected}"
|
||||
)
|
||||
time_waited += sleep_time
|
||||
time.sleep(sleep_time)
|
||||
if self._stopped:
|
||||
break
|
||||
continue
|
||||
break
|
||||
self.detector_control.put("stop")
|
||||
signals = {"config": self.read(), "data": self.file_name}
|
||||
msg = messages.DeviceMessage(signals=signals, metadata=self.metadata)
|
||||
self.device_manager.connector.set_and_publish(MessageEndpoints.device_read(self.name), msg)
|
||||
self._stopped = False
|
||||
return super().unstage()
|
||||
|
||||
def stop(self, *, success=False):
|
||||
self.detector_control.put("stop")
|
||||
super().stop(success=success)
|
||||
self._stopped = True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
eiger = Eiger1p5MDetector(name="eiger1p5m", label="eiger1p5m")
|
||||
breakpoint()
|
||||
@@ -1,8 +0,0 @@
|
||||
# Standard ophyd classes
|
||||
from ophyd import EpicsMotor, EpicsSignal, EpicsSignalRO
|
||||
from ophyd.quadem import QuadEM
|
||||
from ophyd.sim import SynAxis, SynPeriodicSignal, SynSignal
|
||||
|
||||
from .devices.delay_generator_csaxs import DelayGeneratorcSAXS
|
||||
from .devices.flomni_sample_storage import FlomniSampleStorage
|
||||
from .devices.InsertionDevice import InsertionDevice
|
||||
@@ -1,28 +0,0 @@
|
||||
from ophyd import Component, EpicsSignal, EpicsSignalRO, Kind, PVPositioner
|
||||
|
||||
|
||||
class InsertionDevice(PVPositioner):
|
||||
"""Python wrapper for the CSAXS insertion device control
|
||||
|
||||
This wrapper provides a positioner interface for the ID control.
|
||||
is completely custom XBPM with templates directly in the
|
||||
VME repo. Thus it needs a custom ophyd template as well...
|
||||
|
||||
WARN: The x and y are not updated by the IOC
|
||||
"""
|
||||
|
||||
status = Component(EpicsSignalRO, "-USER:STATUS", auto_monitor=True)
|
||||
errorSource = Component(EpicsSignalRO, "-USER:ERROR-SOURCE", auto_monitor=True)
|
||||
isOpen = Component(EpicsSignalRO, "-GAP:ISOPEN", auto_monitor=True)
|
||||
|
||||
# PVPositioner interface
|
||||
setpoint = Component(EpicsSignal, "-GAP:SET", auto_monitor=True)
|
||||
readback = Component(EpicsSignalRO, "-GAP:READ", auto_monitor=True, kind=Kind.hinted)
|
||||
done = Component(EpicsSignalRO, ":DONE", auto_monitor=True)
|
||||
stop_signal = Component(EpicsSignal, "-GAP:STOP", kind=Kind.omitted)
|
||||
|
||||
|
||||
# Automatically start simulation if directly invoked
|
||||
# (NA for important devices)
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
@@ -1,136 +0,0 @@
|
||||
import numpy as np
|
||||
from ophyd import Component, Device, EpicsSignal, EpicsSignalRO
|
||||
|
||||
|
||||
class XbpmCsaxsOp(Device):
|
||||
"""Python wrapper for custom XBPMs in the cSAXS optics hutch
|
||||
|
||||
This is completely custom XBPM with templates directly in the
|
||||
VME repo. Thus it needs a custom ophyd template as well...
|
||||
|
||||
WARN: The x and y are not updated by the IOC
|
||||
"""
|
||||
|
||||
sum = Component(EpicsSignalRO, "SUM", auto_monitor=True)
|
||||
x = Component(EpicsSignalRO, "POSH", auto_monitor=True)
|
||||
y = Component(EpicsSignalRO, "POSV", auto_monitor=True)
|
||||
s1 = Component(EpicsSignalRO, "CHAN1", auto_monitor=True)
|
||||
s2 = Component(EpicsSignalRO, "CHAN2", auto_monitor=True)
|
||||
s3 = Component(EpicsSignalRO, "CHAN3", auto_monitor=True)
|
||||
s4 = Component(EpicsSignalRO, "CHAN4", auto_monitor=True)
|
||||
|
||||
|
||||
class XbpmBase(Device):
|
||||
"""Python wrapper for X-ray Beam Position Monitors
|
||||
|
||||
XBPM's consist of a metal-coated diamond window that ejects
|
||||
photoelectrons from the incoming X-ray beam. These electons
|
||||
are collected and their current is measured. Effectively
|
||||
they act as four quadrant photodiodes and are used as BPMs
|
||||
at the undulator beamlines of SLS.
|
||||
|
||||
Note: EPICS provided signals are read only, but the user can
|
||||
change the beam position offset.
|
||||
"""
|
||||
|
||||
# Motor interface
|
||||
s1 = Component(EpicsSignalRO, "Current1", auto_monitor=True)
|
||||
s2 = Component(EpicsSignalRO, "Current2", auto_monitor=True)
|
||||
s3 = Component(EpicsSignalRO, "Current3", auto_monitor=True)
|
||||
s4 = Component(EpicsSignalRO, "Current4", auto_monitor=True)
|
||||
sum = Component(EpicsSignalRO, "SumAll", auto_monitor=True)
|
||||
asymH = Component(EpicsSignalRO, "asymH", auto_monitor=True)
|
||||
asymV = Component(EpicsSignalRO, "asymV", auto_monitor=True)
|
||||
x = Component(EpicsSignalRO, "X", auto_monitor=True)
|
||||
y = Component(EpicsSignalRO, "Y", auto_monitor=True)
|
||||
scaleH = Component(EpicsSignal, "PositionScaleX", auto_monitor=False)
|
||||
offsetH = Component(EpicsSignal, "PositionOffsetX", auto_monitor=False)
|
||||
scaleV = Component(EpicsSignal, "PositionScaleY", auto_monitor=False)
|
||||
offsetV = Component(EpicsSignal, "PositionOffsetY", auto_monitor=False)
|
||||
|
||||
|
||||
class XbpmSim(XbpmBase):
|
||||
"""Python wrapper for simulated X-ray Beam Position Monitors
|
||||
|
||||
XBPM's consist of a metal-coated diamond window that ejects
|
||||
photoelectrons from the incoming X-ray beam. These electons
|
||||
are collected and their current is measured. Effectively
|
||||
they act as four quadrant photodiodes and are used as BPMs
|
||||
at the undulator beamlines of SLS.
|
||||
|
||||
Note: EPICS provided signals are read only, but the user can
|
||||
change the beam position offset.
|
||||
|
||||
This simulation device extends the basic proxy with a script that
|
||||
fills signals with quasi-randomized values.
|
||||
"""
|
||||
|
||||
# Motor interface
|
||||
s1w = Component(EpicsSignal, "Current1:RAW.VAL", auto_monitor=False)
|
||||
s2w = Component(EpicsSignal, "Current2:RAW.VAL", auto_monitor=False)
|
||||
s3w = Component(EpicsSignal, "Current3:RAW.VAL", auto_monitor=False)
|
||||
s4w = Component(EpicsSignal, "Current4:RAW.VAL", auto_monitor=False)
|
||||
rangew = Component(EpicsSignal, "RANGEraw", auto_monitor=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._MX = 0
|
||||
self._MY = 0
|
||||
self._I0 = 255.0
|
||||
self._x = np.linspace(-5, 5, 64)
|
||||
self._y = np.linspace(-5, 5, 64)
|
||||
self._x, self._y = np.meshgrid(self._x, self._y)
|
||||
|
||||
def _simFrame(self):
|
||||
"""Generator to simulate a jumping gaussian"""
|
||||
|
||||
# define normalized 2D gaussian
|
||||
def gaus2d(x=0, y=0, mx=0, my=0, sx=1, sy=1):
|
||||
return np.exp(-((x - mx) ** 2.0 / (2.0 * sx**2.0) + (y - my) ** 2.0 / (2.0 * sy**2.0)))
|
||||
|
||||
# Generator for dynamic values
|
||||
self._MX = 0.75 * self._MX + 0.25 * (10.0 * np.random.random() - 5.0)
|
||||
self._MY = 0.75 * self._MY + 0.25 * (10.0 * np.random.random() - 5.0)
|
||||
self._I0 = 0.75 * self._I0 + 0.25 * (255.0 * np.random.random())
|
||||
|
||||
arr = self._I0 * gaus2d(self._x, self._y, self._MX, self._MY)
|
||||
return arr
|
||||
|
||||
def sim(self):
|
||||
# Get next frame
|
||||
beam = self._simFrame()
|
||||
total = np.sum(beam)
|
||||
rnge = np.floor(np.log10(total) - 0.0)
|
||||
s1 = np.sum(beam[32:64, 32:64]) / 10**rnge
|
||||
s2 = np.sum(beam[0:32, 32:64]) / 10**rnge
|
||||
s3 = np.sum(beam[32:64, 0:32]) / 10**rnge
|
||||
s4 = np.sum(beam[0:32, 0:32]) / 10**rnge
|
||||
|
||||
self.s1w.set(s1).wait()
|
||||
self.s2w.set(s2).wait()
|
||||
self.s3w.set(s3).wait()
|
||||
self.s4w.set(s4).wait()
|
||||
self.rangew.set(rnge).wait()
|
||||
# Print debug info
|
||||
print(f"Raw signals: R={rnge}\t{s1}\t{s2}\t{s3}\t{s4}")
|
||||
# plt.imshow(beam)
|
||||
# plt.show(block=False)
|
||||
# plt.pause(0.5)
|
||||
|
||||
|
||||
# Automatically start simulation if directly invoked
|
||||
if __name__ == "__main__":
|
||||
xbpm1 = XbpmSim("X01DA-FE-XBPM1:", name="xbpm1")
|
||||
xbpm2 = XbpmSim("X01DA-FE-XBPM2:", name="xbpm2")
|
||||
|
||||
xbpm1.wait_for_connection(timeout=5)
|
||||
xbpm2.wait_for_connection(timeout=5)
|
||||
|
||||
xbpm1.rangew.set(1).wait()
|
||||
xbpm2.rangew.set(1).wait()
|
||||
|
||||
while True:
|
||||
print("---")
|
||||
xbpm1.sim()
|
||||
xbpm2.sim()
|
||||
@@ -1,14 +0,0 @@
|
||||
# Standard ophyd classes
|
||||
from ophyd import EpicsMotor, EpicsSignal, EpicsSignalRO
|
||||
from ophyd.quadem import QuadEM
|
||||
from ophyd.sim import SynAxis, SynPeriodicSignal, SynSignal
|
||||
|
||||
from .delay_generator_csaxs import DelayGeneratorcSAXS
|
||||
from .eiger9m_csaxs import Eiger9McSAXS
|
||||
|
||||
# cSAXS
|
||||
from .falcon_csaxs import FalconcSAXS
|
||||
from .flomni_sample_storage import FlomniSampleStorage
|
||||
from .InsertionDevice import InsertionDevice
|
||||
from .mcs_csaxs import MCScSAXS
|
||||
from .pilatus_csaxs import PilatuscSAXS
|
||||
@@ -1,32 +0,0 @@
|
||||
""" TODO This class seems to be missing various imports and appears to have not been tested in motion yet."""
|
||||
|
||||
TABLES_DT_PUSH_DIST_MM = 890
|
||||
|
||||
|
||||
class DetectorTableTheta(PseudoPositioner):
|
||||
"""Detector table tilt motor
|
||||
|
||||
Small wrapper to adjust the detector table tilt as angle.
|
||||
The table is pushed from one side by a single vertical motor.
|
||||
|
||||
Note: Rarely used!
|
||||
"""
|
||||
|
||||
# Real axis (in degrees)
|
||||
pusher = Component(EpicsMotor, "", name="pusher")
|
||||
# Virtual axis
|
||||
theta = Component(PseudoSingle, name="theta")
|
||||
|
||||
_real = ["pusher"]
|
||||
|
||||
@pseudo_position_argument
|
||||
def forward(self, pseudo_pos):
|
||||
return self.RealPosition(
|
||||
pusher=tan(pseudo_pos.theta * 3.141592 / 180.0) * TABLES_DT_PUSH_DIST_MM
|
||||
)
|
||||
|
||||
@real_position_argument
|
||||
def inverse(self, real_pos):
|
||||
return self.PseudoPosition(
|
||||
theta=-180 * atan(real_pos.pusher / TABLES_DT_PUSH_DIST_MM) / 3.141592
|
||||
)
|
||||
@@ -1,345 +0,0 @@
|
||||
from bec_lib import bec_logger
|
||||
from ophyd import Component
|
||||
from ophyd_devices.epics.devices.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)
|
||||
@@ -1,427 +0,0 @@
|
||||
import enum
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
from bec_lib import messages, threadlocked
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.logger import bec_logger
|
||||
from ophyd import ADComponent as ADCpt
|
||||
from ophyd import Device, EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV
|
||||
from ophyd_devices.epics.devices.psi_detector_base import CustomDetectorMixin, PSIDetectorBase
|
||||
from std_daq_client import StdDaqClient
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class EigerError(Exception):
|
||||
"""Base class for exceptions in this module."""
|
||||
|
||||
|
||||
class EigerTimeoutError(EigerError):
|
||||
"""Raised when the Eiger does not respond in time."""
|
||||
|
||||
|
||||
class Eiger9MSetup(CustomDetectorMixin):
|
||||
"""Eiger setup class
|
||||
|
||||
Parent class: CustomDetectorMixin
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *args, parent: Device = None, **kwargs) -> None:
|
||||
super().__init__(*args, parent=parent, **kwargs)
|
||||
self.std_rest_server_url = (
|
||||
kwargs["file_writer_url"] if "file_writer_url" in kwargs else "http://xbl-daq-29:5000"
|
||||
)
|
||||
self.std_client = None
|
||||
self._lock = threading.RLock()
|
||||
|
||||
def initialize_default_parameter(self) -> None:
|
||||
"""Set default parameters for Eiger9M detector"""
|
||||
self.update_readout_time()
|
||||
|
||||
def update_readout_time(self) -> None:
|
||||
"""Set readout time for Eiger9M detector"""
|
||||
readout_time = (
|
||||
self.parent.scaninfo.readout_time
|
||||
if hasattr(self.parent.scaninfo, "readout_time")
|
||||
else self.parent.MIN_READOUT
|
||||
)
|
||||
self.parent.readout_time = max(readout_time, self.parent.MIN_READOUT)
|
||||
|
||||
def initialize_detector(self) -> None:
|
||||
"""Initialize detector"""
|
||||
# Stops the detector
|
||||
self.stop_detector()
|
||||
# Sets the trigger source to GATING
|
||||
self.parent.set_trigger(TriggerSource.GATING)
|
||||
|
||||
def initialize_detector_backend(self) -> None:
|
||||
"""Initialize detector backend"""
|
||||
|
||||
# Std client
|
||||
self.std_client = StdDaqClient(url_base=self.std_rest_server_url)
|
||||
|
||||
# Stop writer
|
||||
self.std_client.stop_writer()
|
||||
|
||||
# Change e-account
|
||||
eacc = self.parent.scaninfo.username
|
||||
self.update_std_cfg("writer_user_id", int(eacc.strip(" e")))
|
||||
|
||||
signal_conditions = [(lambda: self.std_client.get_status()["state"], "READY")]
|
||||
if not self.wait_for_signals(
|
||||
signal_conditions=signal_conditions, timeout=self.parent.timeout, all_signals=True
|
||||
):
|
||||
raise EigerTimeoutError(
|
||||
f"Std client not in READY state, returns: {self.std_client.get_status()}"
|
||||
)
|
||||
|
||||
def update_std_cfg(self, cfg_key: str, value: Any) -> None:
|
||||
"""
|
||||
Update std_daq config
|
||||
|
||||
Checks that the new value matches the type of the former entry.
|
||||
|
||||
Args:
|
||||
cfg_key (str) : config key of value to be updated
|
||||
value (Any) : value to be updated for the specified key
|
||||
|
||||
Raises:
|
||||
Raises EigerError if the key was not in the config before and if the new value does not match the type of the old value
|
||||
|
||||
"""
|
||||
|
||||
# Load config from client and check old value
|
||||
cfg = self.std_client.get_config()
|
||||
old_value = cfg.get(cfg_key)
|
||||
if old_value is None:
|
||||
raise EigerError(
|
||||
f"Tried to change entry for key {cfg_key} in std_config that does not exist"
|
||||
)
|
||||
if not isinstance(value, type(old_value)):
|
||||
raise EigerError(
|
||||
f"Type of new value {type(value)}:{value} does not match old value"
|
||||
f" {type(old_value)}:{old_value}"
|
||||
)
|
||||
|
||||
# Update config with new value and send back to client
|
||||
cfg.update({cfg_key: value})
|
||||
logger.debug(cfg)
|
||||
self.std_client.set_config(cfg)
|
||||
logger.debug(f"Updated std_daq config for key {cfg_key} from {old_value} to {value}")
|
||||
|
||||
def stop_detector(self) -> None:
|
||||
"""Stop the detector"""
|
||||
|
||||
# Stop detector
|
||||
self.parent.cam.acquire.put(0)
|
||||
|
||||
# Check if detector returned in idle state
|
||||
signal_conditions = [
|
||||
(
|
||||
lambda: self.parent.cam.detector_state.read()[self.parent.cam.detector_state.name][
|
||||
"value"
|
||||
],
|
||||
DetectorState.IDLE,
|
||||
)
|
||||
]
|
||||
|
||||
if not self.wait_for_signals(
|
||||
signal_conditions=signal_conditions,
|
||||
timeout=self.parent.timeout - self.parent.timeout // 2,
|
||||
check_stopped=True,
|
||||
all_signals=False,
|
||||
):
|
||||
# Retry stop detector and wait for remaining time
|
||||
self.parent.cam.acquire.put(0)
|
||||
if not self.wait_for_signals(
|
||||
signal_conditions=signal_conditions,
|
||||
timeout=self.parent.timeout - self.parent.timeout // 2,
|
||||
check_stopped=True,
|
||||
all_signals=False,
|
||||
):
|
||||
raise EigerTimeoutError(
|
||||
f"Failed to stop detector, detector state {signal_conditions[0][0]}"
|
||||
)
|
||||
|
||||
def stop_detector_backend(self) -> None:
|
||||
"""Close file writer"""
|
||||
self.std_client.stop_writer()
|
||||
|
||||
def prepare_detector(self) -> None:
|
||||
"""Prepare detector for scan"""
|
||||
self.set_detector_threshold()
|
||||
self.set_acquisition_params()
|
||||
self.parent.set_trigger(TriggerSource.GATING)
|
||||
|
||||
def set_detector_threshold(self) -> None:
|
||||
"""
|
||||
Set the detector threshold
|
||||
|
||||
The function sets the detector threshold automatically to 1/2 of the beam energy.
|
||||
"""
|
||||
|
||||
# get current beam energy from device manageer
|
||||
mokev = self.parent.device_manager.devices.mokev.obj.read()[
|
||||
self.parent.device_manager.devices.mokev.name
|
||||
]["value"]
|
||||
factor = 1
|
||||
|
||||
# Check if energies are eV or keV, assume keV as the default
|
||||
unit = getattr(self.parent.cam.threshold_energy, "units", None)
|
||||
if unit is not None and unit == "eV":
|
||||
factor = 1000
|
||||
|
||||
# set energy on detector
|
||||
setpoint = int(mokev * factor)
|
||||
energy = self.parent.cam.beam_energy.read()[self.parent.cam.beam_energy.name]["value"]
|
||||
if setpoint != energy:
|
||||
self.parent.cam.beam_energy.set(setpoint)
|
||||
|
||||
# set threshold on detector
|
||||
threshold = self.parent.cam.threshold_energy.read()[self.parent.cam.threshold_energy.name][
|
||||
"value"
|
||||
]
|
||||
if not np.isclose(setpoint / 2, threshold, rtol=0.05):
|
||||
self.parent.cam.threshold_energy.set(setpoint / 2)
|
||||
|
||||
def set_acquisition_params(self) -> None:
|
||||
"""Set acquisition parameters for the detector"""
|
||||
|
||||
# Set number of images and frames (frames is for internal burst of detector)
|
||||
self.parent.cam.num_images.put(
|
||||
int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger)
|
||||
)
|
||||
self.parent.cam.num_frames.put(1)
|
||||
|
||||
# Update the readout time of the detector
|
||||
self.update_readout_time()
|
||||
|
||||
def prepare_data_backend(self) -> None:
|
||||
"""Prepare the data backend for the scan"""
|
||||
self.parent.filepath = self.parent.filewriter.compile_full_filename(
|
||||
f"{self.parent.name}.h5"
|
||||
)
|
||||
self.filepath_exists(self.parent.filepath)
|
||||
self.stop_detector_backend()
|
||||
try:
|
||||
self.std_client.start_writer_async(
|
||||
{
|
||||
"output_file": self.parent.filepath,
|
||||
"n_images": int(
|
||||
self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger
|
||||
),
|
||||
}
|
||||
)
|
||||
except Exception as exc:
|
||||
time.sleep(5)
|
||||
if self.std_client.get_status()["state"] == "READY":
|
||||
raise EigerTimeoutError(f"Timeout of start_writer_async with {exc}") from exc
|
||||
|
||||
# Check status of std_daq
|
||||
signal_conditions = [
|
||||
(lambda: self.std_client.get_status()["acquisition"]["state"], "WAITING_IMAGES")
|
||||
]
|
||||
if not self.wait_for_signals(
|
||||
signal_conditions=signal_conditions,
|
||||
timeout=self.parent.timeout,
|
||||
check_stopped=False,
|
||||
all_signals=True,
|
||||
):
|
||||
raise EigerTimeoutError(
|
||||
"Timeout of 5s reached for std_daq start_writer_async with std_daq client status"
|
||||
f" {self.std_client.get_status()}"
|
||||
)
|
||||
|
||||
def filepath_exists(self, filepath: str) -> None:
|
||||
"""Check if filepath exists"""
|
||||
signal_conditions = [(lambda: os.path.exists(os.path.dirname(filepath)), True)]
|
||||
if not self.wait_for_signals(
|
||||
signal_conditions=signal_conditions,
|
||||
timeout=self.parent.timeout,
|
||||
check_stopped=False,
|
||||
all_signals=True,
|
||||
):
|
||||
raise EigerError(f"Timeout of 3s reached for filepath {filepath}")
|
||||
|
||||
def arm_acquisition(self) -> None:
|
||||
"""Arm Eiger detector for acquisition"""
|
||||
self.parent.cam.acquire.put(1)
|
||||
signal_conditions = [
|
||||
(
|
||||
lambda: self.parent.cam.detector_state.read()[self.parent.cam.detector_state.name][
|
||||
"value"
|
||||
],
|
||||
DetectorState.RUNNING,
|
||||
)
|
||||
]
|
||||
if not self.wait_for_signals(
|
||||
signal_conditions=signal_conditions,
|
||||
timeout=self.parent.timeout,
|
||||
check_stopped=True,
|
||||
all_signals=False,
|
||||
):
|
||||
raise EigerTimeoutError(
|
||||
f"Failed to arm the acquisition. Detector state {signal_conditions[0][0]}"
|
||||
)
|
||||
|
||||
def check_scan_id(self) -> None:
|
||||
"""Checks if scan_id has changed and stops the scan if it has"""
|
||||
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 publish_file_location(self, done: bool = False, successful: bool = None) -> None:
|
||||
"""
|
||||
Publish the filepath to REDIS.
|
||||
|
||||
We publish two events here:
|
||||
- file_event: event for the filewriter
|
||||
- public_file: event for any secondary service (e.g. radial integ code)
|
||||
|
||||
Args:
|
||||
done (bool): True if scan is finished
|
||||
successful (bool): True if scan was successful
|
||||
"""
|
||||
pipe = self.parent.connector.pipeline()
|
||||
if successful is None:
|
||||
msg = messages.FileMessage(file_path=self.parent.filepath, done=done)
|
||||
else:
|
||||
msg = messages.FileMessage(
|
||||
file_path=self.parent.filepath, done=done, successful=successful
|
||||
)
|
||||
self.parent.connector.set_and_publish(
|
||||
MessageEndpoints.public_file(self.parent.scaninfo.scan_id, self.parent.name),
|
||||
msg,
|
||||
pipe=pipe,
|
||||
)
|
||||
self.parent.connector.set_and_publish(
|
||||
MessageEndpoints.file_event(self.parent.name), msg, pipe=pipe
|
||||
)
|
||||
pipe.execute()
|
||||
|
||||
@threadlocked
|
||||
def finished(self):
|
||||
"""Check if acquisition is finished."""
|
||||
signal_conditions = [
|
||||
(
|
||||
lambda: self.parent.cam.acquire.read()[self.parent.cam.acquire.name]["value"],
|
||||
DetectorState.IDLE,
|
||||
),
|
||||
(lambda: self.std_client.get_status()["acquisition"]["state"], "FINISHED"),
|
||||
(
|
||||
lambda: self.std_client.get_status()["acquisition"]["stats"]["n_write_completed"],
|
||||
int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger),
|
||||
),
|
||||
]
|
||||
if not self.wait_for_signals(
|
||||
signal_conditions=signal_conditions,
|
||||
timeout=self.parent.timeout,
|
||||
check_stopped=True,
|
||||
all_signals=True,
|
||||
):
|
||||
raise EigerTimeoutError(
|
||||
f"Reached timeout with detector state {signal_conditions[0][0]}, std_daq state"
|
||||
f" {signal_conditions[1][0]} and received frames of {signal_conditions[2][0]} for"
|
||||
" the file writer"
|
||||
)
|
||||
self.stop_detector()
|
||||
self.stop_detector_backend()
|
||||
|
||||
|
||||
class SLSDetectorCam(Device):
|
||||
"""
|
||||
SLS Detector Camera - Eiger9M
|
||||
|
||||
Base class to map EPICS PVs to ophyd signals.
|
||||
"""
|
||||
|
||||
threshold_energy = ADCpt(EpicsSignalWithRBV, "ThresholdEnergy")
|
||||
beam_energy = ADCpt(EpicsSignalWithRBV, "BeamEnergy")
|
||||
bit_depth = ADCpt(EpicsSignalWithRBV, "BitDepth")
|
||||
num_images = ADCpt(EpicsSignalWithRBV, "NumCycles")
|
||||
num_frames = ADCpt(EpicsSignalWithRBV, "NumFrames")
|
||||
trigger_mode = ADCpt(EpicsSignalWithRBV, "TimingMode")
|
||||
trigger_software = ADCpt(EpicsSignal, "TriggerSoftware")
|
||||
acquire = ADCpt(EpicsSignal, "Acquire")
|
||||
detector_state = ADCpt(EpicsSignalRO, "DetectorState_RBV")
|
||||
|
||||
|
||||
class TriggerSource(enum.IntEnum):
|
||||
"""Trigger signals for Eiger9M detector"""
|
||||
|
||||
AUTO = 0
|
||||
TRIGGER = 1
|
||||
GATING = 2
|
||||
BURST_TRIGGER = 3
|
||||
|
||||
|
||||
class DetectorState(enum.IntEnum):
|
||||
"""Detector states for Eiger9M detector"""
|
||||
|
||||
IDLE = 0
|
||||
ERROR = 1
|
||||
WAITING = 2
|
||||
FINISHED = 3
|
||||
TRANSMITTING = 4
|
||||
RUNNING = 5
|
||||
STOPPED = 6
|
||||
STILL_WAITING = 7
|
||||
INITIALIZING = 8
|
||||
DISCONNECTED = 9
|
||||
ABORTED = 10
|
||||
|
||||
|
||||
class Eiger9McSAXS(PSIDetectorBase):
|
||||
"""
|
||||
Eiger9M detector for CSAXS
|
||||
|
||||
Parent class: PSIDetectorBase
|
||||
|
||||
class attributes:
|
||||
custom_prepare_cls (FalconSetup) : Custom detector setup class for cSAXS,
|
||||
inherits from CustomDetectorMixin
|
||||
PSIDetectorBase.set_min_readout (float) : Minimum readout time for the detector
|
||||
Various EpicsPVs for controlling the detector
|
||||
"""
|
||||
|
||||
# Specify which functions are revealed to the user in BEC client
|
||||
USER_ACCESS = ["describe"]
|
||||
|
||||
# specify Setup class
|
||||
custom_prepare_cls = Eiger9MSetup
|
||||
# specify minimum readout time for detector
|
||||
MIN_READOUT = 3e-3
|
||||
# specify class attributes
|
||||
cam = ADCpt(SLSDetectorCam, "cam1:")
|
||||
|
||||
def set_trigger(self, trigger_source: TriggerSource) -> None:
|
||||
"""Set trigger source for the detector.
|
||||
Check the TriggerSource enum for possible values
|
||||
|
||||
Args:
|
||||
trigger_source (TriggerSource): Trigger source for the detector
|
||||
|
||||
"""
|
||||
value = trigger_source
|
||||
self.cam.trigger_mode.put(value)
|
||||
|
||||
def stage(self) -> list[object]:
|
||||
"""
|
||||
Add functionality to stage, and arm the detector
|
||||
|
||||
Additional call to:
|
||||
- custom_prepare.arm_acquisition()
|
||||
"""
|
||||
rtr = super().stage()
|
||||
self.custom_prepare.arm_acquisition()
|
||||
return rtr
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
eiger = Eiger9McSAXS(name="eiger", prefix="X12SA-ES-EIGER9M:", sim_mode=True)
|
||||
@@ -1,356 +0,0 @@
|
||||
import enum
|
||||
import os
|
||||
|
||||
from bec_lib import messages
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.logger import bec_logger
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV
|
||||
from ophyd.mca import EpicsMCARecord
|
||||
from ophyd_devices.epics.devices.psi_detector_base import CustomDetectorMixin, PSIDetectorBase
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class FalconError(Exception):
|
||||
"""Base class for exceptions in this module."""
|
||||
|
||||
|
||||
class FalconTimeoutError(FalconError):
|
||||
"""Raised when the Falcon does not respond in time."""
|
||||
|
||||
|
||||
class DetectorState(enum.IntEnum):
|
||||
"""Detector states for Falcon detector"""
|
||||
|
||||
DONE = 0
|
||||
ACQUIRING = 1
|
||||
|
||||
|
||||
class TriggerSource(enum.IntEnum):
|
||||
"""Trigger source for Falcon detector"""
|
||||
|
||||
USER = 0
|
||||
GATE = 1
|
||||
SYNC = 2
|
||||
|
||||
|
||||
class MappingSource(enum.IntEnum):
|
||||
"""Mapping source for Falcon detector"""
|
||||
|
||||
SPECTRUM = 0
|
||||
MAPPING = 1
|
||||
|
||||
|
||||
class EpicsDXPFalcon(Device):
|
||||
"""
|
||||
DXP parameters for Falcon detector
|
||||
|
||||
Base class to map EPICS PVs from DXP parameters to ophyd signals.
|
||||
"""
|
||||
|
||||
elapsed_live_time = Cpt(EpicsSignal, "ElapsedLiveTime")
|
||||
elapsed_real_time = Cpt(EpicsSignal, "ElapsedRealTime")
|
||||
elapsed_trigger_live_time = Cpt(EpicsSignal, "ElapsedTriggerLiveTime")
|
||||
|
||||
# Energy Filter PVs
|
||||
energy_threshold = Cpt(EpicsSignalWithRBV, "DetectionThreshold")
|
||||
min_pulse_separation = Cpt(EpicsSignalWithRBV, "MinPulsePairSeparation")
|
||||
detection_filter = Cpt(EpicsSignalWithRBV, "DetectionFilter", string=True)
|
||||
scale_factor = Cpt(EpicsSignalWithRBV, "ScaleFactor")
|
||||
risetime_optimisation = Cpt(EpicsSignalWithRBV, "RisetimeOptimization")
|
||||
|
||||
# Misc PVs
|
||||
detector_polarity = Cpt(EpicsSignalWithRBV, "DetectorPolarity")
|
||||
decay_time = Cpt(EpicsSignalWithRBV, "DecayTime")
|
||||
|
||||
current_pixel = Cpt(EpicsSignalRO, "CurrentPixel")
|
||||
|
||||
|
||||
class FalconHDF5Plugins(Device):
|
||||
"""
|
||||
HDF5 parameters for Falcon detector
|
||||
|
||||
Base class to map EPICS PVs from HDF5 Plugin to ophyd signals.
|
||||
"""
|
||||
|
||||
capture = Cpt(EpicsSignalWithRBV, "Capture")
|
||||
enable = Cpt(EpicsSignalWithRBV, "EnableCallbacks", string=True, kind="config")
|
||||
xml_file_name = Cpt(EpicsSignalWithRBV, "XMLFileName", string=True, kind="config")
|
||||
lazy_open = Cpt(EpicsSignalWithRBV, "LazyOpen", string=True, doc="0='No' 1='Yes'")
|
||||
temp_suffix = Cpt(EpicsSignalWithRBV, "TempSuffix", string=True)
|
||||
file_path = Cpt(EpicsSignalWithRBV, "FilePath", string=True, kind="config")
|
||||
file_name = Cpt(EpicsSignalWithRBV, "FileName", string=True, kind="config")
|
||||
file_template = Cpt(EpicsSignalWithRBV, "FileTemplate", string=True, kind="config")
|
||||
num_capture = Cpt(EpicsSignalWithRBV, "NumCapture", kind="config")
|
||||
file_write_mode = Cpt(EpicsSignalWithRBV, "FileWriteMode", kind="config")
|
||||
queue_size = Cpt(EpicsSignalWithRBV, "QueueSize", kind="config")
|
||||
array_counter = Cpt(EpicsSignalWithRBV, "ArrayCounter", kind="config")
|
||||
|
||||
|
||||
class FalconSetup(CustomDetectorMixin):
|
||||
"""
|
||||
Falcon setup class for cSAXS
|
||||
|
||||
Parent class: CustomDetectorMixin
|
||||
|
||||
"""
|
||||
|
||||
def initialize_default_parameter(self) -> None:
|
||||
"""
|
||||
Set default parameters for Falcon
|
||||
|
||||
This will set:
|
||||
- readout (float): readout time in seconds
|
||||
- value_pixel_per_buffer (int): number of spectra in buffer of Falcon Sitoro
|
||||
|
||||
"""
|
||||
self.parent.value_pixel_per_buffer = 20
|
||||
self.update_readout_time()
|
||||
|
||||
def update_readout_time(self) -> None:
|
||||
"""Set readout time for Eiger9M detector"""
|
||||
readout_time = (
|
||||
self.parent.scaninfo.readout_time
|
||||
if hasattr(self.parent.scaninfo, "readout_time")
|
||||
else self.parent.MIN_READOUT
|
||||
)
|
||||
self.parent.readout_time = max(readout_time, self.parent.MIN_READOUT)
|
||||
|
||||
def initialize_detector(self) -> None:
|
||||
"""Initialize Falcon detector"""
|
||||
self.stop_detector()
|
||||
self.stop_detector_backend()
|
||||
self.parent.set_trigger(
|
||||
mapping_mode=MappingSource.MAPPING, trigger_source=TriggerSource.GATE, ignore_gate=0
|
||||
)
|
||||
# 1 Realtime
|
||||
self.parent.preset_mode.put(1)
|
||||
# 0 Normal, 1 Inverted
|
||||
self.parent.input_logic_polarity.put(0)
|
||||
# 0 Manual 1 Auto
|
||||
self.parent.auto_pixels_per_buffer.put(0)
|
||||
# Sets the number of pixels/spectra in the buffer
|
||||
self.parent.pixels_per_buffer.put(self.parent.value_pixel_per_buffer)
|
||||
|
||||
def stop_detector(self) -> None:
|
||||
"""Stops detector"""
|
||||
|
||||
self.parent.stop_all.put(1)
|
||||
self.parent.erase_all.put(1)
|
||||
|
||||
signal_conditions = [
|
||||
(lambda: self.parent.state.read()[self.parent.state.name]["value"], DetectorState.DONE)
|
||||
]
|
||||
|
||||
if not self.wait_for_signals(
|
||||
signal_conditions=signal_conditions,
|
||||
timeout=self.parent.timeout - self.parent.timeout // 2,
|
||||
all_signals=False,
|
||||
):
|
||||
# Retry stop detector and wait for remaining time
|
||||
raise FalconTimeoutError(
|
||||
f"Failed to stop detector, timeout with state {signal_conditions[0][0]}"
|
||||
)
|
||||
|
||||
def stop_detector_backend(self) -> None:
|
||||
"""Stop the detector backend"""
|
||||
self.parent.hdf5.capture.put(0)
|
||||
|
||||
def initialize_detector_backend(self) -> None:
|
||||
"""Initialize the detector backend for Falcon."""
|
||||
self.parent.hdf5.enable.put(1)
|
||||
# file location of h5 layout for cSAXS
|
||||
self.parent.hdf5.xml_file_name.put("layout.xml")
|
||||
# TODO Check if lazy open is needed and wanted!
|
||||
self.parent.hdf5.lazy_open.put(1)
|
||||
self.parent.hdf5.temp_suffix.put("")
|
||||
# size of queue for number of spectra allowed in the buffer, if too small at high throughput, data is lost
|
||||
self.parent.hdf5.queue_size.put(2000)
|
||||
# Segmentation into Spectra within EPICS, 1 is activate, 0 is deactivate
|
||||
self.parent.nd_array_mode.put(1)
|
||||
|
||||
def prepare_detector(self) -> None:
|
||||
"""Prepare detector for acquisition"""
|
||||
self.parent.set_trigger(
|
||||
mapping_mode=MappingSource.MAPPING, trigger_source=TriggerSource.GATE, ignore_gate=0
|
||||
)
|
||||
self.parent.preset_real.put(self.parent.scaninfo.exp_time)
|
||||
self.parent.pixels_per_run.put(
|
||||
int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger)
|
||||
)
|
||||
|
||||
def prepare_data_backend(self) -> None:
|
||||
"""Prepare data backend for acquisition"""
|
||||
self.parent.filepath = self.parent.filewriter.compile_full_filename(
|
||||
f"{self.parent.name}.h5"
|
||||
)
|
||||
file_path, file_name = os.path.split(self.parent.filepath)
|
||||
self.parent.hdf5.file_path.put(file_path)
|
||||
self.parent.hdf5.file_name.put(file_name)
|
||||
self.parent.hdf5.file_template.put("%s%s")
|
||||
self.parent.hdf5.num_capture.put(
|
||||
int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger)
|
||||
)
|
||||
self.parent.hdf5.file_write_mode.put(2)
|
||||
# Reset spectrum counter in filewriter, used for indexing & identifying missing triggers
|
||||
self.parent.hdf5.array_counter.put(0)
|
||||
# Start file writing
|
||||
self.parent.hdf5.capture.put(1)
|
||||
|
||||
def arm_acquisition(self) -> None:
|
||||
"""Arm detector for acquisition"""
|
||||
self.parent.start_all.put(1)
|
||||
signal_conditions = [
|
||||
(
|
||||
lambda: self.parent.state.read()[self.parent.state.name]["value"],
|
||||
DetectorState.ACQUIRING,
|
||||
)
|
||||
]
|
||||
if not self.wait_for_signals(
|
||||
signal_conditions=signal_conditions,
|
||||
timeout=self.parent.timeout,
|
||||
check_stopped=True,
|
||||
all_signals=False,
|
||||
):
|
||||
raise FalconTimeoutError(
|
||||
f"Failed to arm the acquisition. Detector state {signal_conditions[0][0]}"
|
||||
)
|
||||
|
||||
def check_scan_id(self) -> None:
|
||||
"""Checks if scan_id has changed and stops the scan if it has"""
|
||||
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 publish_file_location(self, done: bool = False, successful: bool = None) -> None:
|
||||
"""
|
||||
Publish the filepath to REDIS.
|
||||
|
||||
We publish two events here:
|
||||
- file_event: event for the filewriter
|
||||
- public_file: event for any secondary service (e.g. radial integ code)
|
||||
|
||||
Args:
|
||||
done (bool): True if scan is finished
|
||||
successful (bool): True if scan was successful
|
||||
"""
|
||||
pipe = self.parent.connector.pipeline()
|
||||
if successful is None:
|
||||
msg = messages.FileMessage(file_path=self.parent.filepath, done=done)
|
||||
else:
|
||||
msg = messages.FileMessage(
|
||||
file_path=self.parent.filepath, done=done, successful=successful
|
||||
)
|
||||
self.parent.connector.set_and_publish(
|
||||
MessageEndpoints.public_file(self.parent.scaninfo.scan_id, self.parent.name),
|
||||
msg,
|
||||
pipe=pipe,
|
||||
)
|
||||
self.parent.connector.set_and_publish(
|
||||
MessageEndpoints.file_event(self.parent.name), msg, pipe=pipe
|
||||
)
|
||||
pipe.execute()
|
||||
|
||||
def finished(self) -> None:
|
||||
"""Check if scan finished succesfully"""
|
||||
total_frames = int(
|
||||
self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger
|
||||
)
|
||||
signal_conditions = [
|
||||
(self.parent.dxp.current_pixel.get, total_frames),
|
||||
(self.parent.hdf5.array_counter.get, total_frames),
|
||||
]
|
||||
if not self.wait_for_signals(
|
||||
signal_conditions=signal_conditions,
|
||||
timeout=self.parent.timeout,
|
||||
check_stopped=True,
|
||||
all_signals=True,
|
||||
):
|
||||
logger.debug(
|
||||
f"Falcon missed a trigger: received trigger {self.parent.dxp.current_pixel.get()},"
|
||||
f" send data {self.parent.hdf5.array_counter.get()} from total_frames"
|
||||
f" {total_frames}"
|
||||
)
|
||||
self.stop_detector()
|
||||
self.stop_detector_backend()
|
||||
|
||||
|
||||
class FalconcSAXS(PSIDetectorBase):
|
||||
"""
|
||||
Falcon Sitoro detector for CSAXS
|
||||
|
||||
Parent class: PSIDetectorBase
|
||||
|
||||
class attributes:
|
||||
custom_prepare_cls (FalconSetup) : Custom detector setup class for cSAXS,
|
||||
inherits from CustomDetectorMixin
|
||||
PSIDetectorBase.set_min_readout (float) : Minimum readout time for the detector
|
||||
dxp (EpicsDXPFalcon) : DXP parameters for Falcon detector
|
||||
mca (EpicsMCARecord) : MCA parameters for Falcon detector
|
||||
hdf5 (FalconHDF5Plugins) : HDF5 parameters for Falcon detector
|
||||
MIN_READOUT (float) : Minimum readout time for the detector
|
||||
"""
|
||||
|
||||
# Specify which functions are revealed to the user in BEC client
|
||||
USER_ACCESS = ["describe"]
|
||||
|
||||
# specify Setup class
|
||||
custom_prepare_cls = FalconSetup
|
||||
# specify minimum readout time for detector
|
||||
MIN_READOUT = 3e-3
|
||||
|
||||
# specify class attributes
|
||||
dxp = Cpt(EpicsDXPFalcon, "dxp1:")
|
||||
mca = Cpt(EpicsMCARecord, "mca1")
|
||||
hdf5 = Cpt(FalconHDF5Plugins, "HDF1:")
|
||||
|
||||
stop_all = Cpt(EpicsSignal, "StopAll")
|
||||
erase_all = Cpt(EpicsSignal, "EraseAll")
|
||||
start_all = Cpt(EpicsSignal, "StartAll")
|
||||
state = Cpt(EpicsSignal, "Acquiring")
|
||||
preset_mode = Cpt(EpicsSignal, "PresetMode") # 0 No preset 1 Real time 2 Events 3 Triggers
|
||||
preset_real = Cpt(EpicsSignal, "PresetReal")
|
||||
preset_events = Cpt(EpicsSignal, "PresetEvents")
|
||||
preset_triggers = Cpt(EpicsSignal, "PresetTriggers")
|
||||
triggers = Cpt(EpicsSignalRO, "MaxTriggers", lazy=True)
|
||||
events = Cpt(EpicsSignalRO, "MaxEvents", lazy=True)
|
||||
input_count_rate = Cpt(EpicsSignalRO, "MaxInputCountRate", lazy=True)
|
||||
output_count_rate = Cpt(EpicsSignalRO, "MaxOutputCountRate", lazy=True)
|
||||
collect_mode = Cpt(EpicsSignal, "CollectMode") # 0 MCA spectra, 1 MCA mapping
|
||||
pixel_advance_mode = Cpt(EpicsSignal, "PixelAdvanceMode")
|
||||
ignore_gate = Cpt(EpicsSignal, "IgnoreGate")
|
||||
input_logic_polarity = Cpt(EpicsSignal, "InputLogicPolarity")
|
||||
auto_pixels_per_buffer = Cpt(EpicsSignal, "AutoPixelsPerBuffer")
|
||||
pixels_per_buffer = Cpt(EpicsSignal, "PixelsPerBuffer")
|
||||
pixels_per_run = Cpt(EpicsSignal, "PixelsPerRun")
|
||||
nd_array_mode = Cpt(EpicsSignal, "NDArrayMode")
|
||||
|
||||
def set_trigger(
|
||||
self, mapping_mode: MappingSource, trigger_source: TriggerSource, ignore_gate: int = 0
|
||||
) -> None:
|
||||
"""
|
||||
Set triggering mode for detector
|
||||
|
||||
Args:
|
||||
mapping_mode (MappingSource): Mapping mode for the detector
|
||||
trigger_source (TriggerSource): Trigger source for the detector, pixel_advance_signal
|
||||
ignore_gate (int): Ignore gate from TTL signal; defaults to 0
|
||||
|
||||
"""
|
||||
mapping = int(mapping_mode)
|
||||
trigger = trigger_source
|
||||
self.collect_mode.put(mapping)
|
||||
self.pixel_advance_mode.put(trigger)
|
||||
self.ignore_gate.put(ignore_gate)
|
||||
|
||||
def stage(self) -> list[object]:
|
||||
"""Stage"""
|
||||
rtr = super().stage()
|
||||
self.custom_prepare.arm_acquisition()
|
||||
return rtr
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
falcon = FalconcSAXS(name="falcon", prefix="X12SA-SITORO:", sim_mode=True)
|
||||
@@ -1,116 +0,0 @@
|
||||
import time
|
||||
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device
|
||||
from ophyd import DynamicDeviceComponent as Dcpt
|
||||
from ophyd import EpicsSignal
|
||||
from prettytable import PrettyTable
|
||||
|
||||
|
||||
class FlomniSampleStorageError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class FlomniSampleStorage(Device):
|
||||
USER_ACCESS = [
|
||||
"is_sample_slot_used",
|
||||
"is_sample_in_gripper",
|
||||
"set_sample_slot",
|
||||
"unset_sample_slot",
|
||||
"set_sample_in_gripper",
|
||||
"unset_sample_in_gripper",
|
||||
"show_all",
|
||||
]
|
||||
SUB_VALUE = "value"
|
||||
_default_sub = SUB_VALUE
|
||||
sample_placed = {
|
||||
f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_flomni{i}:GET", {}) for i in range(21)
|
||||
}
|
||||
sample_placed = Dcpt(sample_placed)
|
||||
|
||||
sample_names = {
|
||||
f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_flomni{i}:GET.DESC", {"string": True})
|
||||
for i in range(21)
|
||||
}
|
||||
sample_names = Dcpt(sample_names)
|
||||
|
||||
sample_in_gripper = Cpt(
|
||||
EpicsSignal, name="sample_in_gripper", read_pv="XOMNY-SAMPLE_DB_flomni100:GET"
|
||||
)
|
||||
sample_in_gripper_name = Cpt(
|
||||
EpicsSignal,
|
||||
name="sample_in_gripper_name",
|
||||
read_pv="XOMNY-SAMPLE_DB_flomni100:GET.DESC",
|
||||
string=True,
|
||||
)
|
||||
|
||||
def __init__(self, prefix="", *, name, **kwargs):
|
||||
super().__init__(prefix, name=name, **kwargs)
|
||||
self.sample_placed.sample1.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_sample_slot(self, slot_nr: int, name: str) -> bool:
|
||||
if slot_nr > 20:
|
||||
raise FlomniSampleStorageError(f"Invalid slot number {slot_nr}.")
|
||||
|
||||
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, slot_nr: int) -> bool:
|
||||
if slot_nr > 20:
|
||||
raise FlomniSampleStorageError(f"Invalid slot number {slot_nr}.")
|
||||
|
||||
getattr(self.sample_placed, f"sample{slot_nr}").set(0)
|
||||
getattr(self.sample_names, f"sample{slot_nr}").set("-")
|
||||
|
||||
def set_sample_in_gripper(self, name: str) -> bool:
|
||||
self.sample_in_gripper.set(1)
|
||||
self.sample_in_gripper_name.set(name)
|
||||
|
||||
def unset_sample_in_gripper(self) -> bool:
|
||||
self.sample_in_gripper.set(0)
|
||||
self.sample_in_gripper_name.set("-")
|
||||
|
||||
def is_sample_slot_used(self, slot_nr: int) -> bool:
|
||||
val = getattr(self.sample_placed, f"sample{slot_nr}").get()
|
||||
return bool(val)
|
||||
|
||||
def is_sample_in_gripper(self) -> bool:
|
||||
val = self.sample_in_gripper.get()
|
||||
return bool(val)
|
||||
|
||||
def get_sample_name(self, slot_nr) -> str:
|
||||
val = getattr(self.sample_names, f"sample{slot_nr}").get()
|
||||
return str(val)
|
||||
|
||||
def show_all(self):
|
||||
t = PrettyTable()
|
||||
t.title = "flOMNI sample storage"
|
||||
field_names = [""]
|
||||
field_names.extend(str(ax) for ax in range(1, 11))
|
||||
for ct in range(0, 2):
|
||||
t.field_names = field_names
|
||||
row = ["Container " + str(ct)]
|
||||
row.extend(
|
||||
"used" if self.is_sample_slot_used(slot_nr) else "free"
|
||||
for slot_nr in range((ct * 10) + 1, (ct * 10) + 11)
|
||||
)
|
||||
t.add_row(row)
|
||||
print(t)
|
||||
print("\n\nFollowing samples are currently loaded:\n")
|
||||
for ct in range(1, 21):
|
||||
if self.is_sample_slot_used(ct):
|
||||
print(f" Position {ct:2.0f}: {self.get_sample_name(ct)}")
|
||||
if self.sample_in_gripper.get():
|
||||
print(f"\n Gripper: {self.sample_in_gripper_name.get()}\n")
|
||||
else:
|
||||
print(f"\n Gripper: no sample\n")
|
||||
|
||||
if self.is_sample_slot_used(0):
|
||||
print(f" flOMNI stage: {self.get_sample_name(0)}\n")
|
||||
else:
|
||||
print(f" flOMNI stage: no sample\n")
|
||||
@@ -1,310 +0,0 @@
|
||||
import enum
|
||||
import threading
|
||||
from collections import defaultdict
|
||||
|
||||
import numpy as np
|
||||
from bec_lib import MessageEndpoints, bec_logger, messages, threadlocked
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, EpicsSignal, EpicsSignalRO
|
||||
from ophyd_devices.epics.devices.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 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.set_trigger(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),
|
||||
)
|
||||
|
||||
@threadlocked
|
||||
def _on_mca_data(self, *args, obj=None, value=None, **kwargs) -> None:
|
||||
"""Callback function for scan progress"""
|
||||
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 prepare_detector(self) -> None:
|
||||
"""Prepare detector for scan"""
|
||||
self.set_acquisition_params()
|
||||
self.parent.set_trigger(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 finished(self) -> 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=self.parent.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 stop_detector(self) -> None:
|
||||
"""Stop detector"""
|
||||
self.parent.stop_all.set(1)
|
||||
|
||||
return super().stop_detector()
|
||||
|
||||
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 = ["describe", "_init_mcs"]
|
||||
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
|
||||
|
||||
# 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,
|
||||
read_attrs=None,
|
||||
configuration_attrs=None,
|
||||
parent=None,
|
||||
device_manager=None,
|
||||
sim_mode=False,
|
||||
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,
|
||||
read_attrs=read_attrs,
|
||||
configuration_attrs=configuration_attrs,
|
||||
parent=parent,
|
||||
device_manager=device_manager,
|
||||
sim_mode=sim_mode,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def set_trigger(self, trigger_source: TriggerSource) -> None:
|
||||
"""Set trigger mode from TriggerSource"""
|
||||
value = int(trigger_source)
|
||||
self.input_mode.set(value)
|
||||
|
||||
def stage(self) -> list[object]:
|
||||
"""stage the detector for upcoming acquisition"""
|
||||
rtr = super().stage()
|
||||
self.custom_prepare.arm_acquisition()
|
||||
return rtr
|
||||
|
||||
|
||||
# Automatically connect to test environmenr if directly invoked
|
||||
if __name__ == "__main__":
|
||||
mcs = MCScSAXS(name="mcs", prefix="X12SA-MCS:", sim_mode=True)
|
||||
@@ -1,282 +0,0 @@
|
||||
import time
|
||||
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device
|
||||
from ophyd import DynamicDeviceComponent as Dcpt
|
||||
from ophyd import EpicsSignal
|
||||
from prettytable import PrettyTable, FRAME
|
||||
|
||||
|
||||
class OMNYSampleStorageError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class OMNYSampleStorage(Device):
|
||||
USER_ACCESS = [
|
||||
"is_sample_slot_used",
|
||||
"is_sample_in_gripper",
|
||||
"set_sample_slot",
|
||||
"unset_sample_slot",
|
||||
"set_sample_in_gripper",
|
||||
"unset_sample_in_gripper",
|
||||
"set_sample_in_samplestage",
|
||||
"unset_sample_in_samplestage",
|
||||
"get_sample_name_in_samplestage",
|
||||
"get_sample_name",
|
||||
"is_sample_in_samplestage",
|
||||
"set_shuttle_slot",
|
||||
"unset_shuttle_slot",
|
||||
"get_shuttle_name_slot",
|
||||
"is_shuttle_slot_used",
|
||||
"search_shuttle_in_slot",
|
||||
"show_all",
|
||||
]
|
||||
SUB_VALUE = "value"
|
||||
_default_sub = SUB_VALUE
|
||||
|
||||
sample_shuttle_A_placed = {
|
||||
f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_shuttle_A:{i}", {}) for i in range(1, 7)
|
||||
}
|
||||
sample_shuttle_A_placed = Dcpt(sample_shuttle_A_placed)
|
||||
|
||||
sample_shuttle_B_placed = {
|
||||
f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_shuttle_B:{i}", {}) for i in range(1, 7)
|
||||
}
|
||||
sample_shuttle_B_placed = Dcpt(sample_shuttle_B_placed)
|
||||
|
||||
sample_shuttle_C_placed = {
|
||||
f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_shuttle_C:{i}", {}) for i in range(1, 7)
|
||||
}
|
||||
sample_shuttle_C_placed = Dcpt(sample_shuttle_C_placed)
|
||||
|
||||
sample_shuttle_C_placed = {
|
||||
f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_shuttle_C:{i}", {}) for i in range(1, 7)
|
||||
}
|
||||
sample_shuttle_C_placed = Dcpt(sample_shuttle_C_placed)
|
||||
|
||||
parking_placed = {
|
||||
f"parking{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_parking:{i}", {}) for i in range(1, 7)
|
||||
}
|
||||
parking_placed = Dcpt(parking_placed)
|
||||
|
||||
sample_placed = {
|
||||
f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_omny:{i}", {})
|
||||
for i in [10, 11, 12, 13, 14, 32, 33, 34, 100, 101]
|
||||
}
|
||||
sample_placed = Dcpt(sample_placed)
|
||||
|
||||
sample_shuttle_A_names = {
|
||||
f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_shuttle_A:{i}.DESC", {"string": True})
|
||||
for i in range(1, 7)
|
||||
}
|
||||
sample_shuttle_A_names = Dcpt(sample_shuttle_A_names)
|
||||
|
||||
sample_shuttle_B_names = {
|
||||
f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_shuttle_B:{i}.DESC", {"string": True})
|
||||
for i in range(1, 7)
|
||||
}
|
||||
sample_shuttle_B_names = Dcpt(sample_shuttle_B_names)
|
||||
|
||||
sample_shuttle_C_names = {
|
||||
f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_shuttle_C:{i}.DESC", {"string": True})
|
||||
for i in range(1, 7)
|
||||
}
|
||||
sample_shuttle_C_names = Dcpt(sample_shuttle_C_names)
|
||||
|
||||
parking_names = {
|
||||
f"parking{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_parking:{i}.DESC", {"string": True})
|
||||
for i in range(1, 7)
|
||||
}
|
||||
parking_names = Dcpt(parking_names)
|
||||
|
||||
sample_names = {
|
||||
f"sample{i}": (EpicsSignal, f"XOMNY-SAMPLE_DB_omny:{i}.DESC", {"string": True})
|
||||
for i in [10, 11, 12, 13, 14, 32, 33, 34, 100, 101]
|
||||
}
|
||||
sample_names = Dcpt(sample_names)
|
||||
|
||||
sample_in_gripper = Cpt(
|
||||
EpicsSignal, name="sample_in_gripper", read_pv="XOMNY-SAMPLE_DB_omny:110.VAL"
|
||||
)
|
||||
sample_in_gripper_name = Cpt(
|
||||
EpicsSignal,
|
||||
name="sample_in_gripper_name",
|
||||
read_pv="XOMNY-SAMPLE_DB_omny:110.DESC",
|
||||
string=True,
|
||||
)
|
||||
|
||||
sample_in_samplestage = Cpt(
|
||||
EpicsSignal, name="sample_in_samplestage", read_pv="XOMNY-SAMPLE_DB_omny:0.VAL"
|
||||
)
|
||||
sample_in_samplestage_name = Cpt(
|
||||
EpicsSignal,
|
||||
name="sample_in_samplestage_name",
|
||||
read_pv="XOMNY-SAMPLE_DB_omny:0.DESC",
|
||||
string=True,
|
||||
)
|
||||
|
||||
def __init__(self, prefix="", *, name, **kwargs):
|
||||
super().__init__(prefix, name=name, **kwargs)
|
||||
self.sample_shuttle_A_placed.sample1.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_sample_slot(self, container: str, slot_nr: int, name: str) -> bool:
|
||||
if slot_nr > 20:
|
||||
raise OMNYSampleStorageError(f"Invalid slot number {slot_nr}.")
|
||||
|
||||
if container == "A":
|
||||
getattr(self.sample_shuttle_A_placed, f"sample{slot_nr}").set(1)
|
||||
getattr(self.sample_shuttle_A_names, f"sample{slot_nr}").set(name)
|
||||
elif container == "B":
|
||||
getattr(self.sample_shuttle_B_placed, f"sample{slot_nr}").set(1)
|
||||
getattr(self.sample_shuttle_B_names, f"sample{slot_nr}").set(name)
|
||||
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)
|
||||
|
||||
def unset_sample_slot(self, shuttle: str, slot_nr: int) -> bool:
|
||||
if slot_nr > 20:
|
||||
raise OMNYSampleStorageError(f"Invalid slot number {slot_nr}.")
|
||||
|
||||
if shuttle == "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":
|
||||
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":
|
||||
getattr(self.sample_shuttle_C_placed, f"sample{slot_nr}").set(0)
|
||||
getattr(self.sample_shuttle_C_names, f"sample{slot_nr}").set("-")
|
||||
|
||||
def set_shuttle_slot(self, container: str, slot_nr: int) -> bool:
|
||||
if slot_nr > 6:
|
||||
raise OMNYSampleStorageError(f"Invalid slot number {slot_nr}.")
|
||||
getattr(self.parking_placed, f"parking{slot_nr}").set(1)
|
||||
getattr(self.parking_names, f"parking{slot_nr}").set(container)
|
||||
|
||||
def unset_shuttle_slot(self, slot_nr: int) -> bool:
|
||||
if slot_nr > 6:
|
||||
raise OMNYSampleStorageError(f"Invalid slot number {slot_nr}.")
|
||||
getattr(self.parking_placed, f"parking{slot_nr}").set(0)
|
||||
getattr(self.parking_names, f"parking{slot_nr}").set("none")
|
||||
|
||||
def set_sample_in_gripper(self, name: str) -> bool:
|
||||
self.sample_in_gripper.set(1)
|
||||
self.sample_in_gripper_name.set(name)
|
||||
|
||||
def unset_sample_in_gripper(self) -> bool:
|
||||
self.sample_in_gripper.set(0)
|
||||
self.sample_in_gripper_name.set("-")
|
||||
|
||||
def set_sample_in_samplestage(self, name: str) -> bool:
|
||||
self.sample_in_samplestage.set(1)
|
||||
self.sample_in_samplestage_name.set(name)
|
||||
|
||||
def unset_sample_in_samplestage(self) -> bool:
|
||||
self.sample_in_samplestage.set(0)
|
||||
self.sample_in_samplestage_name.set("-")
|
||||
|
||||
def is_sample_slot_used(self, container, slot_nr: int) -> bool:
|
||||
if container == "A":
|
||||
val = getattr(self.sample_shuttle_A_placed, f"sample{slot_nr}").get()
|
||||
if container == "B":
|
||||
val = getattr(self.sample_shuttle_B_placed, f"sample{slot_nr}").get()
|
||||
if container == "C":
|
||||
val = getattr(self.sample_shuttle_C_placed, f"sample{slot_nr}").get()
|
||||
elif container == "O":
|
||||
val = getattr(self.sample_placed, f"sample{slot_nr}").get()
|
||||
return bool(val)
|
||||
|
||||
def is_shuttle_slot_used(self, slot_nr: int) -> bool:
|
||||
val = getattr(self.parking_placed, f"parking{slot_nr}").get()
|
||||
return bool(val)
|
||||
|
||||
def is_sample_in_gripper(self) -> bool:
|
||||
val = self.sample_in_gripper.get()
|
||||
return bool(val)
|
||||
|
||||
def is_sample_in_samplestage(self) -> bool:
|
||||
val = self.sample_in_samplestage.get()
|
||||
return bool(val)
|
||||
|
||||
def get_sample_name(self, container, slot_nr) -> str:
|
||||
if container == "A":
|
||||
val = getattr(self.sample_shuttle_A_names, f"sample{slot_nr}").get()
|
||||
elif container == "B":
|
||||
val = getattr(self.sample_shuttle_B_names, f"sample{slot_nr}").get()
|
||||
elif container == "C":
|
||||
val = getattr(self.sample_shuttle_C_names, f"sample{slot_nr}").get()
|
||||
elif container == "O":
|
||||
val = getattr(self.sample_names, f"sample{slot_nr}").get()
|
||||
else:
|
||||
val = "unknown container"
|
||||
return str(val)
|
||||
|
||||
def get_shuttle_name_slot(self, slot_nr: int) -> str:
|
||||
val = getattr(self.parking_names, f"parking{slot_nr}").get()
|
||||
return str(val)
|
||||
|
||||
def get_sample_name_in_gripper(self) -> str:
|
||||
val = self.sample_in_gripper_name.get()
|
||||
return str(val)
|
||||
|
||||
def get_sample_name_in_samplestage(self) -> str:
|
||||
val = self.sample_in_samplestage_name.get()
|
||||
return str(val)
|
||||
|
||||
def search_shuttle_in_slot(self, shuttle: str) -> int:
|
||||
returnvalue = 0
|
||||
for i in range(1, 7):
|
||||
if self.get_shuttle_name_slot(i) == shuttle:
|
||||
returnvalue = i
|
||||
return returnvalue
|
||||
|
||||
def show_all(self):
|
||||
t = PrettyTable()
|
||||
red = "\x1b[91m"
|
||||
green = "\x1b[92m"
|
||||
white = "\x1b[0m"
|
||||
for ch in ["A", "B", "C"]:
|
||||
t.clear()
|
||||
shuttle_in_slot = self.search_shuttle_in_slot(ch)
|
||||
if shuttle_in_slot > 0:
|
||||
t.title = green + "Shuttle " + ch + " in OMNY slot " + str(shuttle_in_slot) + white
|
||||
else:
|
||||
t.title = red + "Shuttle " + ch + white
|
||||
field_names = [""]
|
||||
for ax in [1, 3, 5]:
|
||||
row = []
|
||||
row.extend([self.get_sample_name(ch, ax)])
|
||||
row.extend(str(ax))
|
||||
row.extend(str(ax + 1))
|
||||
row.extend([self.get_sample_name(ch, ax + 1)])
|
||||
t.add_row(row)
|
||||
t.header = False
|
||||
t.vrules = FRAME
|
||||
print(t)
|
||||
|
||||
if self.is_sample_in_samplestage():
|
||||
print(f"\n\n Sample stage: {self.get_sample_name_in_samplestage()}")
|
||||
else:
|
||||
print(f"\n\n Sample stage: no sample")
|
||||
|
||||
if self.is_sample_in_gripper():
|
||||
print(f"\n Gripper: {self.get_sample_name_in_gripper()}\n")
|
||||
else:
|
||||
print(f"\n Gripper: no sample\n")
|
||||
|
||||
t.clear()
|
||||
t.title = "Fixed positions in OMNY"
|
||||
for i in [10, 11, 12, 13, 14, 32, 33, 34, 100, 101]:
|
||||
row = []
|
||||
row.extend([f"Position {i:3d}"])
|
||||
if self.is_sample_slot_used("O", i):
|
||||
row.extend(self.get_sample_name("O", i))
|
||||
else:
|
||||
row.extend(["free"])
|
||||
t.add_row(row)
|
||||
print(t)
|
||||
@@ -1,416 +0,0 @@
|
||||
import enum
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
import requests
|
||||
from bec_lib import MessageEndpoints, bec_logger, messages
|
||||
from ophyd import ADComponent as ADCpt
|
||||
from ophyd import Device, EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV, Staged
|
||||
from ophyd_devices.epics.devices.psi_detector_base import CustomDetectorMixin, PSIDetectorBase
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
MIN_READOUT = 3e-3
|
||||
|
||||
|
||||
class PilatusError(Exception):
|
||||
"""Base class for exceptions in this module."""
|
||||
|
||||
|
||||
class PilatusTimeoutError(PilatusError):
|
||||
"""Raised when the Pilatus does not respond in time during unstage."""
|
||||
|
||||
|
||||
class TriggerSource(enum.IntEnum):
|
||||
"""Trigger source options for the detector"""
|
||||
|
||||
INTERNAL = 0
|
||||
EXT_ENABLE = 1
|
||||
EXT_TRIGGER = 2
|
||||
MULTI_TRIGGER = 3
|
||||
ALGINMENT = 4
|
||||
|
||||
|
||||
class SLSDetectorCam(Device):
|
||||
"""SLS Detector Camera - Pilatus
|
||||
|
||||
Base class to map EPICS PVs to ophyd signals.
|
||||
"""
|
||||
|
||||
num_images = ADCpt(EpicsSignalWithRBV, "NumImages")
|
||||
num_frames = ADCpt(EpicsSignalWithRBV, "NumExposures")
|
||||
delay_time = ADCpt(EpicsSignalWithRBV, "NumExposures")
|
||||
trigger_mode = ADCpt(EpicsSignalWithRBV, "TriggerMode")
|
||||
acquire = ADCpt(EpicsSignal, "Acquire")
|
||||
armed = ADCpt(EpicsSignalRO, "Armed")
|
||||
|
||||
read_file_timeout = ADCpt(EpicsSignal, "ImageFileTmot")
|
||||
detector_state = ADCpt(EpicsSignalRO, "StatusMessage_RBV")
|
||||
status_message_camserver = ADCpt(EpicsSignalRO, "StringFromServer_RBV", string=True)
|
||||
acquire_time = ADCpt(EpicsSignal, "AcquireTime")
|
||||
acquire_period = ADCpt(EpicsSignal, "AcquirePeriod")
|
||||
threshold_energy = ADCpt(EpicsSignalWithRBV, "ThresholdEnergy")
|
||||
file_path = ADCpt(EpicsSignalWithRBV, "FilePath")
|
||||
file_name = ADCpt(EpicsSignalWithRBV, "FileName")
|
||||
file_number = ADCpt(EpicsSignalWithRBV, "FileNumber")
|
||||
auto_increment = ADCpt(EpicsSignalWithRBV, "AutoIncrement")
|
||||
file_template = ADCpt(EpicsSignalWithRBV, "FileTemplate")
|
||||
file_format = ADCpt(EpicsSignalWithRBV, "FileNumber")
|
||||
gap_fill = ADCpt(EpicsSignalWithRBV, "GapFill")
|
||||
|
||||
|
||||
class PilatusSetup(CustomDetectorMixin):
|
||||
"""Pilatus setup class for cSAXS
|
||||
|
||||
Parent class: CustomDetectorMixin
|
||||
|
||||
"""
|
||||
|
||||
def initialize_default_parameter(self) -> None:
|
||||
"""Set default parameters for Eiger9M detector"""
|
||||
self.update_readout_time()
|
||||
|
||||
def update_readout_time(self) -> None:
|
||||
"""Set readout time for Eiger9M detector"""
|
||||
readout_time = (
|
||||
self.parent.scaninfo.readout_time
|
||||
if hasattr(self.parent.scaninfo, "readout_time")
|
||||
else self.parent.MIN_READOUT
|
||||
)
|
||||
self.parent.readout_time = max(readout_time, self.parent.MIN_READOUT)
|
||||
|
||||
def initialize_detector(self) -> None:
|
||||
"""Initialize detector"""
|
||||
# Stops the detector
|
||||
self.stop_detector()
|
||||
# Sets the trigger source to GATING
|
||||
self.parent.set_trigger(TriggerSource.EXT_ENABLE)
|
||||
|
||||
def prepare_detector(self) -> None:
|
||||
"""
|
||||
Prepare detector for scan.
|
||||
|
||||
Includes checking the detector threshold,
|
||||
setting the acquisition parameters and setting the trigger source
|
||||
"""
|
||||
self.set_detector_threshold()
|
||||
self.set_acquisition_params()
|
||||
self.parent.set_trigger(TriggerSource.EXT_ENABLE)
|
||||
|
||||
def set_detector_threshold(self) -> None:
|
||||
"""
|
||||
Set correct detector threshold to 1/2 of current X-ray energy, allow 5% tolerance
|
||||
|
||||
Threshold might be in ev or keV
|
||||
"""
|
||||
|
||||
# get current beam energy from device manageer
|
||||
mokev = self.parent.device_manager.devices.mokev.obj.read()[
|
||||
self.parent.device_manager.devices.mokev.name
|
||||
]["value"]
|
||||
factor = 1
|
||||
|
||||
# Check if energies are eV or keV, assume keV as the default
|
||||
unit = getattr(self.parent.cam.threshold_energy, "units", None)
|
||||
if unit is not None and unit == "eV":
|
||||
factor = 1000
|
||||
|
||||
# set energy on detector
|
||||
setpoint = int(mokev * factor)
|
||||
|
||||
# set threshold on detector
|
||||
threshold = self.parent.cam.threshold_energy.read()[self.parent.cam.threshold_energy.name][
|
||||
"value"
|
||||
]
|
||||
if not np.isclose(setpoint / 2, threshold, rtol=0.05):
|
||||
self.parent.cam.threshold_energy.set(setpoint / 2)
|
||||
|
||||
def set_acquisition_params(self) -> None:
|
||||
"""Set acquisition parameters for the detector"""
|
||||
|
||||
# Set number of images and frames (frames is for internal burst of detector)
|
||||
self.parent.cam.num_images.put(
|
||||
int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger)
|
||||
)
|
||||
self.parent.cam.num_frames.put(1)
|
||||
|
||||
# Update the readout time of the detector
|
||||
self.update_readout_time()
|
||||
|
||||
def create_directory(self, filepath: str) -> None:
|
||||
"""Create directory if it does not exist"""
|
||||
os.makedirs(filepath, exist_ok=True)
|
||||
|
||||
def stop_detector_backend(self) -> None:
|
||||
"""Stop the file writer zmq service for pilatus_2"""
|
||||
self.close_file_writer()
|
||||
time.sleep(0.1)
|
||||
self.stop_file_writer()
|
||||
time.sleep(0.1)
|
||||
|
||||
def close_file_writer(self) -> None:
|
||||
"""
|
||||
Close the file writer for pilatus_2
|
||||
|
||||
Delete the data from x12sa-pd-2
|
||||
|
||||
"""
|
||||
url = "http://x12sa-pd-2:8080/stream/pilatus_2"
|
||||
try:
|
||||
res = self.send_requests_delete(url=url)
|
||||
if not res.ok:
|
||||
res.raise_for_status()
|
||||
except Exception as exc:
|
||||
logger.info(f"Pilatus2 close threw Exception: {exc}")
|
||||
|
||||
def stop_file_writer(self) -> None:
|
||||
"""
|
||||
Stop the file writer for pilatus_2
|
||||
|
||||
Runs on xbl-daq-34
|
||||
"""
|
||||
url = "http://xbl-daq-34:8091/pilatus_2/stop"
|
||||
res = self.send_requests_put(url=url)
|
||||
if not res.ok:
|
||||
res.raise_for_status()
|
||||
|
||||
def prepare_data_backend(self) -> None:
|
||||
"""
|
||||
Prepare the detector backend of pilatus for a scan
|
||||
|
||||
A zmq service is running on xbl-daq-34 that is waiting
|
||||
for a zmq message to start the writer for the pilatus_2 x12sa-pd-2
|
||||
|
||||
"""
|
||||
|
||||
self.stop_detector_backend()
|
||||
|
||||
self.parent.filepath = self.parent.filewriter.compile_full_filename("pilatus_2.h5")
|
||||
self.parent.cam.file_path.put("/dev/shm/zmq/")
|
||||
self.parent.cam.file_name.put(
|
||||
f"{self.parent.scaninfo.username}_2_{self.parent.scaninfo.scan_number:05d}"
|
||||
)
|
||||
self.parent.cam.auto_increment.put(1) # auto increment
|
||||
self.parent.cam.file_number.put(0) # first iter
|
||||
self.parent.cam.file_format.put(0) # 0: TIFF
|
||||
self.parent.cam.file_template.put("%s%s_%5.5d.cbf")
|
||||
|
||||
# TODO better to remove hard coded path with link to home directory/pilatus_2
|
||||
basepath = f"/sls/X12SA/data/{self.parent.scaninfo.username}/Data10/pilatus_2/"
|
||||
self.parent.filepath_raw = os.path.join(
|
||||
basepath,
|
||||
self.parent.filewriter.get_scan_directory(self.parent.scaninfo.scan_number, 1000, 5),
|
||||
)
|
||||
# Make directory if needed
|
||||
self.create_directory(self.parent.filepath_raw)
|
||||
|
||||
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
||||
# start the stream on x12sa-pd-2
|
||||
url = "http://x12sa-pd-2:8080/stream/pilatus_2"
|
||||
data_msg = {
|
||||
"source": [
|
||||
{
|
||||
"searchPath": "/",
|
||||
"searchPattern": "glob:*.cbf",
|
||||
"destinationPath": self.parent.filepath_raw,
|
||||
}
|
||||
]
|
||||
}
|
||||
res = self.send_requests_put(url=url, data=data_msg, headers=headers)
|
||||
logger.info(f"{res.status_code} - {res.text} - {res.content}")
|
||||
|
||||
if not res.ok:
|
||||
res.raise_for_status()
|
||||
|
||||
# start the data receiver on xbl-daq-34
|
||||
url = "http://xbl-daq-34:8091/pilatus_2/run"
|
||||
data_msg = [
|
||||
"zmqWriter",
|
||||
self.parent.scaninfo.username,
|
||||
{
|
||||
"addr": "tcp://x12sa-pd-2:8888",
|
||||
"dst": ["file"],
|
||||
"numFrm": int(
|
||||
self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger
|
||||
),
|
||||
"timeout": 2000,
|
||||
"ifType": "PULL",
|
||||
"user": self.parent.scaninfo.username,
|
||||
},
|
||||
]
|
||||
res = self.send_requests_put(url=url, data=data_msg, headers=headers)
|
||||
logger.info(f"{res.status_code} - {res.text} - {res.content}")
|
||||
|
||||
if not res.ok:
|
||||
res.raise_for_status()
|
||||
|
||||
# Wait for server to become available again
|
||||
time.sleep(0.1)
|
||||
logger.info(f"{res.status_code} -{res.text} - {res.content}")
|
||||
|
||||
# Send requests.put to xbl-daq-34 to wait for data
|
||||
url = "http://xbl-daq-34:8091/pilatus_2/wait"
|
||||
data_msg = [
|
||||
"zmqWriter",
|
||||
self.parent.scaninfo.username,
|
||||
{
|
||||
"frmCnt": int(
|
||||
self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger
|
||||
),
|
||||
"timeout": 2000,
|
||||
},
|
||||
]
|
||||
try:
|
||||
res = self.send_requests_put(url=url, data=data_msg, headers=headers)
|
||||
logger.info(f"{res}")
|
||||
|
||||
if not res.ok:
|
||||
res.raise_for_status()
|
||||
except Exception as exc:
|
||||
logger.info(f"Pilatus2 wait threw Exception: {exc}")
|
||||
|
||||
def send_requests_put(self, url: str, data: list = None, headers: dict = None) -> object:
|
||||
"""
|
||||
Send a put request to the given url
|
||||
|
||||
Args:
|
||||
url (str): url to send the request to
|
||||
data (dict): data to be sent with the request (optional)
|
||||
headers (dict): headers to be sent with the request (optional)
|
||||
|
||||
Returns:
|
||||
status code of the request
|
||||
"""
|
||||
return requests.put(url=url, data=json.dumps(data), headers=headers, timeout=5)
|
||||
|
||||
def send_requests_delete(self, url: str, headers: dict = None) -> object:
|
||||
"""
|
||||
Send a delete request to the given url
|
||||
|
||||
Args:
|
||||
url (str): url to send the request to
|
||||
headers (dict): headers to be sent with the request (optional)
|
||||
|
||||
Returns:
|
||||
status code of the request
|
||||
"""
|
||||
return requests.delete(url=url, headers=headers, timeout=5)
|
||||
|
||||
def pre_scan(self) -> None:
|
||||
"""
|
||||
Pre_scan function call
|
||||
|
||||
This function is called just before the scan core.
|
||||
Here it is used to arm the detector for the acquisition
|
||||
|
||||
"""
|
||||
self.arm_acquisition()
|
||||
|
||||
def arm_acquisition(self) -> None:
|
||||
"""Arms the detector for the acquisition"""
|
||||
self.parent.cam.acquire.put(1)
|
||||
# TODO is this sleep needed? to be tested with detector and for how long
|
||||
time.sleep(0.5)
|
||||
|
||||
def publish_file_location(self, done: bool = False, successful: bool = None) -> None:
|
||||
"""
|
||||
Publish the filepath to REDIS and publish the event for the h5_converter
|
||||
|
||||
We publish two events here:
|
||||
- file_event: event for the filewriter
|
||||
- public_file: event for any secondary service (e.g. radial integ code)
|
||||
|
||||
Args:
|
||||
done (bool): True if scan is finished
|
||||
successful (bool): True if scan was successful
|
||||
"""
|
||||
pipe = self.parent.connector.pipeline()
|
||||
if successful is None:
|
||||
msg = messages.FileMessage(
|
||||
file_path=self.parent.filepath,
|
||||
done=done,
|
||||
metadata={"input_path": self.parent.filepath_raw},
|
||||
)
|
||||
else:
|
||||
msg = messages.FileMessage(
|
||||
file_path=self.parent.filepath,
|
||||
done=done,
|
||||
successful=successful,
|
||||
metadata={"input_path": self.parent.filepath_raw},
|
||||
)
|
||||
self.parent.connector.set_and_publish(
|
||||
MessageEndpoints.public_file(self.parent.scaninfo.scan_id, self.parent.name),
|
||||
msg,
|
||||
pipe=pipe,
|
||||
)
|
||||
self.parent.connector.set_and_publish(
|
||||
MessageEndpoints.file_event(self.parent.name), msg, pipe=pipe
|
||||
)
|
||||
pipe.execute()
|
||||
|
||||
def finished(self) -> None:
|
||||
"""Check if acquisition is finished."""
|
||||
# pylint: disable=protected-access
|
||||
# TODO: at the moment this relies on device.mcs.obj._staged attribute
|
||||
signal_conditions = [
|
||||
(lambda: self.parent.device_manager.devices.mcs.obj._staged, Staged.no)
|
||||
]
|
||||
if not self.wait_for_signals(
|
||||
signal_conditions=signal_conditions,
|
||||
timeout=self.parent.timeout,
|
||||
check_stopped=True,
|
||||
all_signals=True,
|
||||
):
|
||||
raise PilatusTimeoutError(
|
||||
f"Reached timeout with detector state {signal_conditions[0][0]}, std_daq state"
|
||||
f" {signal_conditions[1][0]} and received frames of {signal_conditions[2][0]} for"
|
||||
" the file writer"
|
||||
)
|
||||
self.stop_detector()
|
||||
self.stop_detector_backend()
|
||||
|
||||
def stop_detector(self) -> None:
|
||||
"""Stop detector"""
|
||||
self.parent.cam.acquire.put(0)
|
||||
|
||||
def check_scan_id(self) -> None:
|
||||
"""Checks if scan_id has changed and stops the scan if it has"""
|
||||
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
|
||||
|
||||
|
||||
class PilatuscSAXS(PSIDetectorBase):
|
||||
"""Pilatus_2 300k detector for CSAXS
|
||||
|
||||
Parent class: PSIDetectorBase
|
||||
|
||||
class attributes:
|
||||
custom_prepare_cls (Eiger9MSetup) : Custom detector setup class for cSAXS,
|
||||
inherits from CustomDetectorMixin
|
||||
cam (SLSDetectorCam) : Detector camera
|
||||
MIN_READOUT (float) : Minimum readout time for the detector
|
||||
|
||||
"""
|
||||
|
||||
# Specify which functions are revealed to the user in BEC client
|
||||
USER_ACCESS = ["describe"]
|
||||
|
||||
# specify Setup class
|
||||
custom_prepare_cls = PilatusSetup
|
||||
# specify minimum readout time for detector
|
||||
MIN_READOUT = 3e-3
|
||||
# specify class attributes
|
||||
cam = ADCpt(SLSDetectorCam, "cam1:")
|
||||
|
||||
def set_trigger(self, trigger_source: TriggerSource) -> None:
|
||||
"""Set trigger source for the detector"""
|
||||
value = trigger_source
|
||||
self.cam.trigger_mode.put(value)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pilatus_2 = PilatuscSAXS(name="pilatus_2", prefix="X12SA-ES-PILATUS300K:", sim_mode=True)
|
||||
@@ -1,304 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created on Wed Oct 13 18:06:15 2021
|
||||
|
||||
@author: mohacsi_i
|
||||
|
||||
IMPORTANT: Virtual monochromator axes should be implemented already in EPICS!!!
|
||||
"""
|
||||
|
||||
import time
|
||||
from math import asin, atan, isclose, sin, sqrt, tan
|
||||
|
||||
import numpy as np
|
||||
from ophyd import (
|
||||
Component,
|
||||
Device,
|
||||
EpicsMotor,
|
||||
EpicsSignal,
|
||||
EpicsSignalRO,
|
||||
Kind,
|
||||
PseudoPositioner,
|
||||
PseudoSingle,
|
||||
PVPositioner,
|
||||
Signal,
|
||||
)
|
||||
from ophyd.pseudopos import pseudo_position_argument, real_position_argument
|
||||
|
||||
|
||||
class PmMonoBender(PseudoPositioner):
|
||||
"""Monochromator bender
|
||||
|
||||
Small wrapper to combine the four monochromator bender motors.
|
||||
"""
|
||||
|
||||
# Real axes
|
||||
ai = Component(EpicsMotor, "TRYA", name="ai")
|
||||
bo = Component(EpicsMotor, "TRYB", name="bo")
|
||||
co = Component(EpicsMotor, "TRYC", name="co")
|
||||
di = Component(EpicsMotor, "TRYD", name="di")
|
||||
|
||||
# Virtual axis
|
||||
bend = Component(PseudoSingle, name="bend")
|
||||
|
||||
_real = ["ai", "bo", "co", "di"]
|
||||
|
||||
@pseudo_position_argument
|
||||
def forward(self, pseudo_pos):
|
||||
delta = pseudo_pos.bend - 0.25 * (
|
||||
self.ai.position + self.bo.position + self.co.position + self.di.position
|
||||
)
|
||||
return self.RealPosition(
|
||||
ai=self.ai.position + delta,
|
||||
bo=self.bo.position + delta,
|
||||
co=self.co.position + delta,
|
||||
di=self.di.position + delta,
|
||||
)
|
||||
|
||||
@real_position_argument
|
||||
def inverse(self, real_pos):
|
||||
return self.PseudoPosition(
|
||||
bend=0.25 * (real_pos.ai + real_pos.bo + real_pos.co + real_pos.di)
|
||||
)
|
||||
|
||||
|
||||
def r2d(radians):
|
||||
return radians * 180 / 3.141592
|
||||
|
||||
|
||||
def d2r(degrees):
|
||||
return degrees * 3.141592 / 180.0
|
||||
|
||||
|
||||
class PmDetectorRotation(PseudoPositioner):
|
||||
"""Detector rotation pseudo motor
|
||||
|
||||
Small wrapper to convert detector pusher position to rotation angle.
|
||||
"""
|
||||
|
||||
_tables_dt_push_dist_mm = 890
|
||||
# Real axes
|
||||
dtpush = Component(EpicsMotor, "", name="dtpush")
|
||||
|
||||
# Virtual axis
|
||||
dtth = Component(PseudoSingle, name="dtth")
|
||||
|
||||
_real = ["dtpush"]
|
||||
|
||||
@pseudo_position_argument
|
||||
def forward(self, pseudo_pos):
|
||||
return self.RealPosition(
|
||||
dtpush=d2r(tan(-3.14 / 180 * pseudo_pos.dtth)) * self._tables_dt_push_dist_mm
|
||||
)
|
||||
|
||||
@real_position_argument
|
||||
def inverse(self, real_pos):
|
||||
return self.PseudoPosition(dtth=r2d(-atan(real_pos.dtpush / self._tables_dt_push_dist_mm)))
|
||||
|
||||
|
||||
class GirderMotorX1(PVPositioner):
|
||||
"""Girder X translation pseudo motor"""
|
||||
|
||||
setpoint = Component(EpicsSignal, ":X_SET", name="sp")
|
||||
readback = Component(EpicsSignalRO, ":X1", name="rbv")
|
||||
done = Component(EpicsSignal, ":M-DMOV", name="dmov")
|
||||
|
||||
|
||||
class GirderMotorY1(PVPositioner):
|
||||
"""Girder Y translation pseudo motor"""
|
||||
|
||||
setpoint = Component(EpicsSignal, ":Y_SET", name="sp")
|
||||
readback = Component(EpicsSignalRO, ":Y1", name="rbv")
|
||||
done = Component(EpicsSignal, ":M-DMOV", name="dmov")
|
||||
|
||||
|
||||
class GirderMotorYAW(PVPositioner):
|
||||
"""Girder YAW pseudo motor"""
|
||||
|
||||
setpoint = Component(EpicsSignal, ":YAW_SET", name="sp")
|
||||
readback = Component(EpicsSignalRO, ":YAW1", name="rbv")
|
||||
done = Component(EpicsSignal, ":M-DMOV", name="dmov")
|
||||
|
||||
|
||||
class GirderMotorROLL(PVPositioner):
|
||||
"""Girder ROLL pseudo motor"""
|
||||
|
||||
setpoint = Component(EpicsSignal, ":ROLL_SET", name="sp")
|
||||
readback = Component(EpicsSignalRO, ":ROLL1", name="rbv")
|
||||
done = Component(EpicsSignal, ":M-DMOV", name="dmov")
|
||||
|
||||
|
||||
class GirderMotorPITCH(PVPositioner):
|
||||
"""Girder YAW pseudo motor"""
|
||||
|
||||
setpoint = Component(EpicsSignal, ":PITCH_SET", name="sp")
|
||||
readback = Component(EpicsSignalRO, ":PITCH1", name="rbv")
|
||||
done = Component(EpicsSignal, ":M-DMOV", name="dmov")
|
||||
|
||||
|
||||
class VirtualEpicsSignalRO(EpicsSignalRO):
|
||||
"""This is a test class to create derives signals from one or
|
||||
multiple original signals...
|
||||
"""
|
||||
|
||||
def calc(self, val):
|
||||
return val
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
raw = super().get(*args, **kwargs)
|
||||
return self.calc(raw)
|
||||
|
||||
|
||||
class MonoTheta1(VirtualEpicsSignalRO):
|
||||
"""Converts the pusher motor position to theta angle"""
|
||||
|
||||
_mono_a0_enc_scale1 = -1.0
|
||||
_mono_a1_lever_length1 = 206.706
|
||||
_mono_a2_pusher_offs1 = 6.85858
|
||||
_mono_a3_enc_offs1 = -16.9731
|
||||
|
||||
def calc(self, val):
|
||||
asin_arg = (val - self._mono_a2_pusher_offs1) / self._mono_a1_lever_length1
|
||||
theta1 = (
|
||||
self._mono_a0_enc_scale1 * asin(asin_arg) / 3.141592 * 180.0 + self._mono_a3_enc_offs1
|
||||
)
|
||||
return theta1
|
||||
|
||||
|
||||
class MonoTheta2(VirtualEpicsSignalRO):
|
||||
"""Converts the pusher motor position to theta angle"""
|
||||
|
||||
_mono_a3_enc_offs2 = -19.7072
|
||||
_mono_a2_pusher_offs2 = 5.93905
|
||||
_mono_a1_lever_length2 = 206.572
|
||||
_mono_a0_enc_scale2 = -1.0
|
||||
|
||||
def calc(self, val):
|
||||
asin_arg = (val - self._mono_a2_pusher_offs2) / self._mono_a1_lever_length2
|
||||
theta2 = (
|
||||
self._mono_a0_enc_scale2 * asin(asin_arg) / 3.141592 * 180.0 + self._mono_a3_enc_offs2
|
||||
)
|
||||
return theta2
|
||||
|
||||
|
||||
MONO_THETA2_OFFSETS_FILENAME = (
|
||||
"/sls/X12SA/data/gac-x12saop/spec/macros/spec_data/mono_th2_offsets.txt"
|
||||
)
|
||||
|
||||
|
||||
class EnergyKev(VirtualEpicsSignalRO):
|
||||
"""Converts the pusher motor position to energy in keV"""
|
||||
|
||||
_mono_add_offs = True
|
||||
_mono_a3_enc_offs2 = -19.7072
|
||||
_mono_a2_pusher_offs2 = 5.93905
|
||||
_mono_a1_lever_length2 = 206.572
|
||||
_mono_a0_enc_scale2 = -1.0
|
||||
_mono_hce = 12.39852066
|
||||
_mono_2d2 = 2 * 5.43102 / sqrt(3)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._th2_offsets = np.loadtxt(MONO_THETA2_OFFSETS_FILENAME)
|
||||
|
||||
def _mono_get_th2_offs(self, energy_keV):
|
||||
if self._th2_offsets is None:
|
||||
return 0.0
|
||||
|
||||
max_offs = np.max(self._th2_offsets[:, 1])
|
||||
|
||||
if max_offs > 0.2:
|
||||
raise ValueError(
|
||||
f"\nThe empirical moth2 corrections are as high as {max_offs} deg\nThis is unreasonable and the corrections will not be used.\n\n***PLEASE INFORM BEAMLINE SCIENTISTS***\n"
|
||||
)
|
||||
|
||||
offs = np.interp(energy_keV, self._th2_offsets[:, 0], self._th2_offsets[:, 1])
|
||||
# print(offs)
|
||||
return offs
|
||||
|
||||
def calc(self, val):
|
||||
_mono_sintheta2_to_Ekev = -self._mono_hce / self._mono_2d2
|
||||
asin_arg = (val - self._mono_a2_pusher_offs2) / self._mono_a1_lever_length2
|
||||
theta2_deg = (
|
||||
self._mono_a0_enc_scale2 * asin(asin_arg) / 3.141592 * 180.0 + self._mono_a3_enc_offs2
|
||||
)
|
||||
E_keV = _mono_sintheta2_to_Ekev / sin(theta2_deg / 180.0 * 3.141592)
|
||||
|
||||
if self._mono_add_offs:
|
||||
theta2_deg -= self._mono_get_th2_offs(E_keV)
|
||||
E_keV = _mono_sintheta2_to_Ekev / sin(theta2_deg / 180.0 * 3.141592)
|
||||
return E_keV
|
||||
|
||||
|
||||
class CurrentSum(Signal):
|
||||
"""Adds up four current signals from the parent"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.parent.ch1.subscribe(self._emit_value)
|
||||
|
||||
def _emit_value(self, **kwargs):
|
||||
timestamp = kwargs.pop("timestamp", time.time())
|
||||
self.wait_for_connection()
|
||||
self._run_subs(sub_type="value", timestamp=timestamp, obj=self)
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
# self.parent._cnt.set(1).wait()
|
||||
self._metadata["timestamp"] = time.time()
|
||||
total = (
|
||||
self.parent.ch1.get()
|
||||
+ self.parent.ch2.get()
|
||||
+ self.parent.ch3.get()
|
||||
+ self.parent.ch4.get()
|
||||
)
|
||||
return total
|
||||
|
||||
|
||||
class Bpm4i(Device):
|
||||
SUB_VALUE = "value"
|
||||
_default_sub = SUB_VALUE
|
||||
_cont = Component(EpicsSignal, "CONT", put_complete=True, kind=Kind.omitted)
|
||||
_cnt = Component(EpicsSignal, "CNT", put_complete=True, kind=Kind.omitted)
|
||||
ch1 = Component(EpicsSignalRO, "S2", auto_monitor=True, kind=Kind.omitted, name="ch1")
|
||||
ch2 = Component(EpicsSignalRO, "S3", auto_monitor=True, kind=Kind.omitted, name="ch2")
|
||||
ch3 = Component(EpicsSignalRO, "S4", auto_monitor=True, kind=Kind.omitted, name="ch3")
|
||||
ch4 = Component(EpicsSignalRO, "S5", auto_monitor=True, kind=Kind.omitted, name="ch4")
|
||||
sum = Component(CurrentSum, kind=Kind.hinted, name="sum")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
prefix="",
|
||||
*,
|
||||
name,
|
||||
kind=None,
|
||||
read_attrs=None,
|
||||
configuration_attrs=None,
|
||||
parent=None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(
|
||||
prefix,
|
||||
name=name,
|
||||
kind=kind,
|
||||
read_attrs=read_attrs,
|
||||
configuration_attrs=configuration_attrs,
|
||||
parent=parent,
|
||||
**kwargs,
|
||||
)
|
||||
self.sum.name = self.name
|
||||
# Ensure the scaler counts automatically
|
||||
self._cont.wait_for_connection()
|
||||
self._cont.set(1).wait()
|
||||
self.ch1.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)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
dut = Bpm4i("X12SA-OP1-SCALER.", name="bpm4")
|
||||
dut.wait_for_connection()
|
||||
print(dut.read())
|
||||
print(dut.describe())
|
||||
@@ -1,4 +0,0 @@
|
||||
from .fgalil_ophyd import FlomniGalilController, FlomniGalilMotor
|
||||
from .fupr_ophyd import FuprGalilController, FuprGalilMotor
|
||||
from .galil_ophyd import GalilController, GalilMotor
|
||||
from .sgalil_ophyd import SGalilMotor
|
||||
@@ -1,201 +0,0 @@
|
||||
<mxfile host="app.diagrams.net" modified="2023-07-31T14:55:08.470Z" agent="Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0" etag="hKC99drhJ4x6bY93jFGH" version="21.6.6" type="device">
|
||||
<diagram name="Page-1" id="b520641d-4fe3-3701-9064-5fc419738815">
|
||||
<mxGraphModel dx="2049" dy="1206" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1100" pageHeight="850" background="none" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
<mxCell id="21ea969265ad0168-6" value="SGalil stages" style="swimlane;html=1;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=#e0e0e0;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;swimlaneFillColor=#ffffff;align=center;rounded=1;shadow=0;comic=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=12" parent="1" vertex="1">
|
||||
<mxGeometry x="12.5" y="24" width="160" height="110" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="21ea969265ad0168-7" value="grid fly scan (2D), TTL signal at the beginning of each line" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="21ea969265ad0168-6" vertex="1">
|
||||
<mxGeometry y="26" width="160" height="54" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="21ea969265ad0168-14" value="DelayGenerator 4 - ddg4" style="swimlane;html=1;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=#e0e0e0;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;swimlaneFillColor=#ffffff;align=center;rounded=1;shadow=0;comic=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=12" parent="1" vertex="1">
|
||||
<mxGeometry x="270" y="40" width="160" height="160" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="21ea969265ad0168-15" value="ext. trigger" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="21ea969265ad0168-14" vertex="1">
|
||||
<mxGeometry y="26" width="160" height="26" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="21ea969265ad0168-16" value="operated in normal mode, controls fast shutter and triggers second DDG" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="21ea969265ad0168-14" vertex="1">
|
||||
<mxGeometry y="52" width="160" height="48" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-19" value="<div>channel AB</div>" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="21ea969265ad0168-14">
|
||||
<mxGeometry y="100" width="160" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-21" value="<div>channel T0<br></div>" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="21ea969265ad0168-14">
|
||||
<mxGeometry y="130" width="160" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="21ea969265ad0168-22" value="Logical Card" style="swimlane;html=1;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=#e0e0e0;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;swimlaneFillColor=#ffffff;align=center;rounded=1;shadow=0;comic=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=12" parent="1" vertex="1">
|
||||
<mxGeometry x="710" y="250" width="160" height="104" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="21ea969265ad0168-24" value="<div>both signals low -&gt; low<br>either on signal high -&gt; high<br>both signals high -&gt; low<br><br></div>" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="21ea969265ad0168-22" vertex="1">
|
||||
<mxGeometry y="26" width="160" height="54" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="21ea969265ad0168-26" value="MCS readout card" style="swimlane;html=1;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=#e0e0e0;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;swimlaneFillColor=#ffffff;align=center;rounded=1;shadow=0;comic=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=12" parent="1" vertex="1">
|
||||
<mxGeometry x="920" y="360" width="160" height="110" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="21ea969265ad0168-27" value="falling edge trigger<br>readout is triggered between to falling edges, thus logical board required" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" parent="21ea969265ad0168-26" vertex="1">
|
||||
<mxGeometry y="26" width="160" height="74" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="21ea969265ad0168-36" value="" style="edgeStyle=orthogonalEdgeStyle;html=1;labelBackgroundColor=none;startFill=0;startSize=8;endFill=1;endSize=8;fontFamily=Verdana;fontSize=12;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" source="21ea969265ad0168-6" target="21ea969265ad0168-15" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="21ea969265ad0168-38" value="" style="edgeStyle=orthogonalEdgeStyle;html=1;labelBackgroundColor=none;startFill=0;startSize=8;endFill=1;endSize=8;fontFamily=Verdana;fontSize=12;align=center;entryX=0;entryY=0.5;entryDx=0;entryDy=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="O8qpyw_Cq4v1m74naMs6-19" target="O8qpyw_Cq4v1m74naMs6-3" edge="1">
|
||||
<mxGeometry x="-0.0026" relative="1" as="geometry">
|
||||
<mxPoint x="430" y="150" as="sourcePoint" />
|
||||
<mxPoint x="680" y="60" as="targetPoint" />
|
||||
<Array as="points">
|
||||
<mxPoint x="660" y="155" />
|
||||
<mxPoint x="660" y="43" />
|
||||
</Array>
|
||||
<mxPoint as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="21ea969265ad0168-42" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=1;exitY=0.5;labelBackgroundColor=none;startFill=0;startSize=8;endFill=1;endSize=8;fontFamily=Verdana;fontSize=12;exitDx=0;exitDy=0;" parent="1" source="O8qpyw_Cq4v1m74naMs6-19" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="660" y="155" />
|
||||
<mxPoint x="660" y="290" />
|
||||
</Array>
|
||||
<mxPoint x="710" y="290" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="21ea969265ad0168-43" style="edgeStyle=orthogonalEdgeStyle;html=1;labelBackgroundColor=none;startFill=0;startSize=8;endFill=1;endSize=8;fontFamily=Verdana;fontSize=12;entryX=0;entryY=0.378;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="21ea969265ad0168-24" edge="1" target="21ea969265ad0168-27">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="880" y="331" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-2" value="Fast shutter: fsh" style="swimlane;html=1;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=#e0e0e0;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;swimlaneFillColor=#ffffff;align=center;rounded=1;shadow=0;comic=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=12" vertex="1" parent="1">
|
||||
<mxGeometry x="750" width="160" height="104" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-3" value="FSH opening time, e.g. 20ms" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="O8qpyw_Cq4v1m74naMs6-2">
|
||||
<mxGeometry y="26" width="160" height="34" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-11" value="TTL" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=default;" vertex="1" parent="1">
|
||||
<mxGeometry x="200" y="60" width="40" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-16" value="DelayGenerator 4 - ddg4" style="swimlane;html=1;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=#e0e0e0;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;swimlaneFillColor=#ffffff;align=center;rounded=1;shadow=0;comic=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=12" vertex="1" parent="1">
|
||||
<mxGeometry x="270" y="350" width="230" height="190" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-17" value="ext. trigger" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="O8qpyw_Cq4v1m74naMs6-16">
|
||||
<mxGeometry y="26" width="230" height="24" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-18" value="operated in burst mode:<br>burstCount: N_points<br>burstPeriod: (exp_time + readout time)<br>burstDelay: fsh opening (20ms)" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="O8qpyw_Cq4v1m74naMs6-16">
|
||||
<mxGeometry y="50" width="230" height="68" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-29" value="Channel AB" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="O8qpyw_Cq4v1m74naMs6-16">
|
||||
<mxGeometry y="118" width="230" height="24" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-28" value="Channel CD " style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="O8qpyw_Cq4v1m74naMs6-16">
|
||||
<mxGeometry y="142" width="230" height="24" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-30" value="Channel EF" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="O8qpyw_Cq4v1m74naMs6-16">
|
||||
<mxGeometry y="166" width="230" height="24" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-27" value="" style="edgeStyle=orthogonalEdgeStyle;html=1;labelBackgroundColor=none;startFill=0;startSize=8;endFill=1;endSize=8;fontFamily=Verdana;fontSize=12;align=center;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="O8qpyw_Cq4v1m74naMs6-21" target="O8qpyw_Cq4v1m74naMs6-17">
|
||||
<mxGeometry x="-0.0026" relative="1" as="geometry">
|
||||
<mxPoint x="220" y="262" as="sourcePoint" />
|
||||
<mxPoint x="240" y="390" as="targetPoint" />
|
||||
<Array as="points">
|
||||
<mxPoint x="510" y="185" />
|
||||
<mxPoint x="510" y="270" />
|
||||
<mxPoint x="200" y="270" />
|
||||
<mxPoint x="200" y="390" />
|
||||
</Array>
|
||||
<mxPoint as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-31" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=1;exitY=0.5;labelBackgroundColor=none;startFill=0;startSize=8;endFill=1;endSize=8;fontFamily=Verdana;fontSize=12;exitDx=0;exitDy=0;" edge="1" parent="1" source="O8qpyw_Cq4v1m74naMs6-28" target="O8qpyw_Cq4v1m74naMs6-36">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="640" y="504" />
|
||||
<mxPoint x="640" y="610" />
|
||||
</Array>
|
||||
<mxPoint x="500" y="480" as="sourcePoint" />
|
||||
<mxPoint x="910" y="610" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-32" style="edgeStyle=orthogonalEdgeStyle;html=1;labelBackgroundColor=none;startFill=0;startSize=8;endFill=1;endSize=8;fontFamily=Verdana;fontSize=12;entryX=0;entryY=0.25;entryDx=0;entryDy=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="O8qpyw_Cq4v1m74naMs6-28" target="O8qpyw_Cq4v1m74naMs6-34">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="640" y="504" />
|
||||
<mxPoint x="640" y="610" />
|
||||
<mxPoint x="760" y="610" />
|
||||
<mxPoint x="760" y="528" />
|
||||
</Array>
|
||||
<mxPoint x="500" y="500" as="sourcePoint" />
|
||||
<mxPoint x="880" y="540" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-33" style="edgeStyle=orthogonalEdgeStyle;html=1;labelBackgroundColor=none;startFill=0;startSize=8;endFill=1;endSize=8;fontFamily=Verdana;fontSize=12;entryX=0.008;entryY=0.154;entryDx=0;entryDy=0;entryPerimeter=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="O8qpyw_Cq4v1m74naMs6-28" target="O8qpyw_Cq4v1m74naMs6-38">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="640" y="504" />
|
||||
<mxPoint x="640" y="610" />
|
||||
<mxPoint x="760" y="610" />
|
||||
<mxPoint x="760" y="701" />
|
||||
</Array>
|
||||
<mxPoint x="500" y="480" as="sourcePoint" />
|
||||
<mxPoint x="880" y="700" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-34" value="eiger" style="swimlane;html=1;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=#e0e0e0;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;swimlaneFillColor=#ffffff;align=center;rounded=1;shadow=0;comic=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=12" vertex="1" parent="1">
|
||||
<mxGeometry x="920" y="510" width="160" height="70" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-35" value="rising edge<br>set exp_time on device" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="O8qpyw_Cq4v1m74naMs6-34">
|
||||
<mxGeometry y="26" width="160" height="44" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-36" value="falcon" style="swimlane;html=1;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=#e0e0e0;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;swimlaneFillColor=#ffffff;align=center;rounded=1;shadow=0;comic=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=12" vertex="1" parent="1">
|
||||
<mxGeometry x="920" y="600" width="160" height="70" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-37" value="rising edge <br>set exp_time on device" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="O8qpyw_Cq4v1m74naMs6-36">
|
||||
<mxGeometry y="26" width="160" height="44" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-38" value="Pilatus 300k" style="swimlane;html=1;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=#e0e0e0;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;swimlaneFillColor=#ffffff;align=center;rounded=1;shadow=0;comic=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=12" vertex="1" parent="1">
|
||||
<mxGeometry x="920" y="690" width="160" height="70" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-39" value="rising edge <br>set exp_time on device" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="O8qpyw_Cq4v1m74naMs6-38">
|
||||
<mxGeometry y="26" width="160" height="44" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-40" style="edgeStyle=orthogonalEdgeStyle;html=1;labelBackgroundColor=none;startFill=0;startSize=8;endFill=1;endSize=8;fontFamily=Verdana;fontSize=12;entryX=-0.005;entryY=1.002;entryDx=0;entryDy=0;entryPerimeter=0;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="O8qpyw_Cq4v1m74naMs6-29" target="21ea969265ad0168-24">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="560" y="480" />
|
||||
<mxPoint x="560" y="330" />
|
||||
</Array>
|
||||
<mxPoint x="510" y="520" as="sourcePoint" />
|
||||
<mxPoint x="700" y="330" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-41" style="edgeStyle=orthogonalEdgeStyle;html=1;labelBackgroundColor=none;startFill=0;startSize=8;endFill=1;endSize=8;fontFamily=Verdana;fontSize=12;exitX=1;exitY=0.167;exitDx=0;exitDy=0;exitPerimeter=0;" edge="1" parent="1" source="O8qpyw_Cq4v1m74naMs6-30">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="530" y="520" />
|
||||
<mxPoint x="530" y="780" />
|
||||
</Array>
|
||||
<mxPoint x="500" y="530" as="sourcePoint" />
|
||||
<mxPoint x="610" y="780" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-42" value="SGalil positions encoder" style="swimlane;html=1;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=#e0e0e0;horizontalStack=0;resizeParent=1;resizeLast=0;collapsible=1;marginBottom=0;swimlaneFillColor=#ffffff;align=center;rounded=1;shadow=0;comic=0;labelBackgroundColor=none;strokeWidth=1;fontFamily=Verdana;fontSize=12" vertex="1" parent="1">
|
||||
<mxGeometry x="610" y="740" width="160" height="70" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-43" value="triggered on falling edge" style="text;html=1;strokeColor=none;fillColor=none;spacingLeft=4;spacingRight=4;whiteSpace=wrap;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="O8qpyw_Cq4v1m74naMs6-42">
|
||||
<mxGeometry y="26" width="160" height="44" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-44" value="<div align="justify"><span style="background-color: rgb(255, 255, 255);">channel AB</span></div><div align="justify"><div><span style="background-color: rgb(255, 255, 255);">delay:0<br></span></div><span style="background-color: rgb(255, 255, 255);">width: fsh opening (20ms)<br>+ N_points * exp_time<br>+ (N_points-1) * readout time<br></span></div>" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=default;" vertex="1" parent="1">
|
||||
<mxGeometry x="450" y="44" width="180" height="90" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-45" value="<div align="justify"><span style="background-color: rgb(255, 255, 255);">channel AB</span></div><div align="justify"><div><span style="background-color: rgb(255, 255, 255);">delay:0<br></span></div><span style="background-color: rgb(255, 255, 255);">width: exp_time <br>or less<br></span></div>" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=default;" vertex="1" parent="1">
|
||||
<mxGeometry x="530" y="300" width="110" height="70" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-46" value="<div align="justify"><span style="background-color: rgb(255, 255, 255);">channel CD<br>split into 3 signals<br></span></div><div align="justify"><div><span style="background-color: rgb(255, 255, 255);">delay:0<br></span></div><span style="background-color: rgb(255, 255, 255);">width: exp_time <br>or less<br></span></div>" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=default;" vertex="1" parent="1">
|
||||
<mxGeometry x="575" y="450" width="120" height="90" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O8qpyw_Cq4v1m74naMs6-47" value="<div align="justify"><span style="background-color: rgb(255, 255, 255);">channel EF<br></span></div><div align="justify"><div><span style="background-color: rgb(255, 255, 255);">delay:0<br></span></div><span style="background-color: rgb(255, 255, 255);">width: exp_time/2 <br></span></div>" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=default;" vertex="1" parent="1">
|
||||
<mxGeometry x="480" y="610" width="120" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 157 KiB |
@@ -1,370 +0,0 @@
|
||||
import threading
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
from bec_lib 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.galil.galil_ophyd import (
|
||||
BECConfigError,
|
||||
GalilAxesReferenced,
|
||||
GalilController,
|
||||
GalilError,
|
||||
GalilMotorIsMoving,
|
||||
GalilMotorResolution,
|
||||
GalilSetpointSignal,
|
||||
GalilSignalRO,
|
||||
retry_once,
|
||||
)
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class FlomniGalilController(GalilController):
|
||||
USER_ACCESS = [
|
||||
"describe",
|
||||
"show_running_threads",
|
||||
"galil_show_all",
|
||||
"socket_put_and_receive",
|
||||
"socket_put_confirmed",
|
||||
"drive_axis_to_limit",
|
||||
"find_reference",
|
||||
"get_motor_limit_switch",
|
||||
"fosaz_light_curtain_is_triggered",
|
||||
"is_motor_on",
|
||||
"all_axes_referenced",
|
||||
"lights_off",
|
||||
"lights_on",
|
||||
]
|
||||
|
||||
def is_axis_moving(self, axis_Id, axis_Id_numeric) -> bool:
|
||||
if axis_Id is None and axis_Id_numeric is not None:
|
||||
axis_Id = self.axis_Id_numeric_to_alpha(axis_Id_numeric)
|
||||
active_thread = self.is_thread_active(0)
|
||||
motor_is_on = self.is_motor_on(axis_Id)
|
||||
return bool(active_thread or motor_is_on)
|
||||
|
||||
def all_axes_referenced(self) -> bool:
|
||||
# TODO: check if all axes are referenced in all controllers
|
||||
return super().all_axes_referenced()
|
||||
|
||||
def fosaz_light_curtain_is_triggered(self) -> bool:
|
||||
"""
|
||||
Check the light curtain status for fosaz
|
||||
|
||||
Returns:
|
||||
bool: True if the light curtain is triggered
|
||||
"""
|
||||
|
||||
return int(float(self.socket_put_and_receive("MG @IN[14]").strip())) == 1
|
||||
|
||||
def lights_off(self) -> None:
|
||||
"""
|
||||
Turn off the lights
|
||||
"""
|
||||
self.socket_put_confirmed("CB15")
|
||||
|
||||
def lights_on(self) -> None:
|
||||
"""
|
||||
Turn on the lights
|
||||
"""
|
||||
self.socket_put_confirmed("SB15")
|
||||
|
||||
|
||||
class FlomniGalilReadbackSignal(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()
|
||||
return val
|
||||
|
||||
|
||||
class FlomniGalilSetpointSignal(GalilSetpointSignal):
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def _socket_set(self, val: float) -> None:
|
||||
"""Set a new target value / setpoint value. Before submission, the target value is adjusted for the axis' sign.
|
||||
Furthermore, it is ensured that all axes are referenced before a new setpoint is submitted.
|
||||
|
||||
Args:
|
||||
val (float): Target value / setpoint value
|
||||
|
||||
Raises:
|
||||
GalilError: Raised if not all axes are referenced.
|
||||
|
||||
"""
|
||||
target_val = val * self.parent.sign
|
||||
self.setpoint = target_val
|
||||
axes_referenced = self.controller.all_axes_referenced()
|
||||
if axes_referenced:
|
||||
while self.controller.is_thread_active(0):
|
||||
time.sleep(0.1)
|
||||
|
||||
self.controller.socket_put_confirmed(f"naxis={self.parent.axis_Id_numeric}")
|
||||
self.controller.socket_put_confirmed(f"ntarget={target_val:.3f}")
|
||||
self.controller.socket_put_confirmed("movereq=1")
|
||||
self.controller.socket_put_confirmed("XQ#NEWPAR")
|
||||
while self.controller.is_thread_active(0):
|
||||
time.sleep(0.005)
|
||||
else:
|
||||
raise GalilError("Not all axes are referenced.")
|
||||
|
||||
|
||||
class FlomniGalilMotorResolution(GalilMotorResolution):
|
||||
pass
|
||||
|
||||
|
||||
class FlomniGalilMotorIsMoving(GalilMotorIsMoving):
|
||||
pass
|
||||
|
||||
|
||||
class FlomniGalilAxesReferenced(GalilAxesReferenced):
|
||||
pass
|
||||
|
||||
|
||||
class FlomniGalilMotor(Device, PositionerBase):
|
||||
USER_ACCESS = ["controller"]
|
||||
readback = Cpt(FlomniGalilReadbackSignal, signal_name="readback", kind="hinted")
|
||||
user_setpoint = Cpt(FlomniGalilSetpointSignal, signal_name="setpoint")
|
||||
motor_resolution = Cpt(FlomniGalilMotorResolution, signal_name="resolution", kind="config")
|
||||
motor_is_moving = Cpt(FlomniGalilMotorIsMoving, signal_name="motor_is_moving", kind="normal")
|
||||
all_axes_referenced = Cpt(
|
||||
FlomniGalilAxesReferenced, 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="mpc2680.psi.ch",
|
||||
port=8081,
|
||||
limits=None,
|
||||
sign=1,
|
||||
socket_cls=SocketIO,
|
||||
device_manager=None,
|
||||
**kwargs,
|
||||
):
|
||||
self.controller = FlomniGalilController(
|
||||
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")
|
||||
self.pid_x_correction = 0
|
||||
|
||||
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 = ord(val.lower()) - 97
|
||||
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 = val
|
||||
self._axis_Id_numeric = (chr(val + 97)).capitalize()
|
||||
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 stage(self) -> list[object]:
|
||||
return super().stage()
|
||||
|
||||
def unstage(self) -> list[object]:
|
||||
return super().unstage()
|
||||
|
||||
def stop(self, *, success=False):
|
||||
self.controller.stop_all_axes()
|
||||
return super().stop(success=success)
|
||||
|
||||
|
||||
# if __name__ == "__main__":
|
||||
# mock = False
|
||||
# if not mock:
|
||||
# leyey = GalilMotor("H", name="leyey", host="mpc2680.psi.ch", port=8081, sign=-1)
|
||||
# leyey.stage()
|
||||
# status = leyey.move(0, wait=True)
|
||||
# status = leyey.move(10, wait=True)
|
||||
# leyey.read()
|
||||
|
||||
# leyey.get()
|
||||
# leyey.describe()
|
||||
|
||||
# leyey.unstage()
|
||||
# else:
|
||||
# from ophyd_devices.utils.socket import SocketMock
|
||||
|
||||
# leyex = GalilMotor(
|
||||
# "G", name="leyex", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock
|
||||
# )
|
||||
# leyey = GalilMotor(
|
||||
# "H", name="leyey", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock
|
||||
# )
|
||||
# leyex.stage()
|
||||
# # leyey.stage()
|
||||
|
||||
# leyex.controller.galil_show_all()
|
||||
@@ -1,318 +0,0 @@
|
||||
import functools
|
||||
import threading
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
from bec_lib 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, ReadOnlyError
|
||||
from ophyd_devices.utils.controller import Controller, threadlocked
|
||||
from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected
|
||||
from prettytable import PrettyTable
|
||||
|
||||
from csaxs_bec.devices.galil.galil_ophyd import (
|
||||
BECConfigError,
|
||||
GalilAxesReferenced,
|
||||
GalilCommunicationError,
|
||||
GalilController,
|
||||
GalilError,
|
||||
GalilMotorIsMoving,
|
||||
GalilMotorResolution,
|
||||
GalilReadbackSignal,
|
||||
GalilSetpointSignal,
|
||||
retry_once,
|
||||
)
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class FuprGalilController(GalilController):
|
||||
_axes_per_controller = 1
|
||||
|
||||
def is_axis_moving(self, axis_Id, axis_Id_numeric) -> bool:
|
||||
if axis_Id is None and axis_Id_numeric is not None:
|
||||
axis_Id = self.axis_Id_numeric_to_alpha(axis_Id_numeric)
|
||||
is_moving = bool(float(self.socket_put_and_receive(f"MG_BG{axis_Id}")) != 0)
|
||||
return is_moving
|
||||
|
||||
def axis_is_referenced(self, axis_Id) -> bool:
|
||||
return self.all_axes_referenced()
|
||||
|
||||
def all_axes_referenced(self) -> bool:
|
||||
return bool(float(self.socket_put_and_receive("MG axisref").strip()))
|
||||
|
||||
def drive_axis_to_limit(self, axis_Id_numeric, direction: str) -> None:
|
||||
raise NotImplementedError("This function is not implemented for the FuprGalilController.")
|
||||
|
||||
|
||||
class FuprGalilReadbackSignal(GalilReadbackSignal):
|
||||
@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()
|
||||
return current_pos / step_mm
|
||||
|
||||
def read(self):
|
||||
self._metadata["timestamp"] = time.time()
|
||||
val = super().read()
|
||||
if self.parent.axis_Id_numeric == 0:
|
||||
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
|
||||
|
||||
|
||||
class FuprGalilSetpointSignal(GalilSetpointSignal):
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def _socket_set(self, val: float) -> None:
|
||||
"""Set a new target value / setpoint value. Before submission, the target value is adjusted for the axis' sign.
|
||||
Furthermore, it is ensured that all axes are referenced before a new setpoint is submitted.
|
||||
|
||||
Args:
|
||||
val (float): Target value / setpoint value
|
||||
|
||||
Raises:
|
||||
GalilError: Raised if not all axes are referenced.
|
||||
|
||||
"""
|
||||
target_val = val * self.parent.sign
|
||||
self.setpoint = target_val
|
||||
axes_referenced = self.controller.all_axes_referenced()
|
||||
if axes_referenced:
|
||||
self.controller.socket_put_confirmed(
|
||||
f"PA{self.parent.axis_Id}={int(self.setpoint*self.parent.MOTOR_RESOLUTION)}"
|
||||
)
|
||||
self.controller.socket_put_confirmed(f"BG{self.parent.axis_Id}")
|
||||
else:
|
||||
raise GalilError("Not all axes are referenced.")
|
||||
|
||||
|
||||
class FuprGalilMotorResolution(GalilMotorResolution):
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def _socket_get(self):
|
||||
return self.parent.MOTOR_RESOLUTION
|
||||
|
||||
|
||||
class FuprGalilMotorIsMoving(GalilMotorIsMoving):
|
||||
pass
|
||||
|
||||
|
||||
class FuprGalilAxesReferenced(GalilAxesReferenced):
|
||||
pass
|
||||
|
||||
|
||||
class FuprGalilMotor(Device, PositionerBase):
|
||||
USER_ACCESS = ["controller"]
|
||||
MOTOR_RESOLUTION = 25600
|
||||
readback = Cpt(FuprGalilReadbackSignal, signal_name="readback", kind="hinted")
|
||||
user_setpoint = Cpt(FuprGalilSetpointSignal, signal_name="setpoint")
|
||||
motor_resolution = Cpt(FuprGalilMotorResolution, signal_name="resolution", kind="config")
|
||||
motor_is_moving = Cpt(FuprGalilMotorIsMoving, signal_name="motor_is_moving", kind="normal")
|
||||
all_axes_referenced = Cpt(
|
||||
FuprGalilAxesReferenced, 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="mpc2680.psi.ch",
|
||||
port=8081,
|
||||
limits=None,
|
||||
sign=1,
|
||||
socket_cls=SocketIO,
|
||||
device_manager=None,
|
||||
**kwargs,
|
||||
):
|
||||
self.controller = FuprGalilController(
|
||||
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", "rtx")
|
||||
|
||||
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(f"Only single-character axis_Ids are supported.")
|
||||
self._axis_Id_alpha = val
|
||||
self._axis_Id_numeric = ord(val.lower()) - 97
|
||||
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(f"Numeric value exceeds supported range.")
|
||||
self._axis_Id_alpha = val
|
||||
self._axis_Id_numeric = (chr(val + 97)).capitalize()
|
||||
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 stage(self) -> list[object]:
|
||||
return super().stage()
|
||||
|
||||
def unstage(self) -> list[object]:
|
||||
return super().unstage()
|
||||
|
||||
def stop(self, *, success=False):
|
||||
self.controller.stop_all_axes()
|
||||
return super().stop(success=success)
|
||||
@@ -1,604 +0,0 @@
|
||||
import functools
|
||||
import threading
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
from bec_lib 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, ReadOnlyError
|
||||
from ophyd_devices.utils.controller import Controller, threadlocked
|
||||
from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected
|
||||
from prettytable import PrettyTable
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class GalilCommunicationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class GalilError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BECConfigError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
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 GalilController(Controller):
|
||||
_axes_per_controller = 8
|
||||
USER_ACCESS = [
|
||||
"describe",
|
||||
"show_running_threads",
|
||||
"galil_show_all",
|
||||
"socket_put_and_receive",
|
||||
"socket_put_confirmed",
|
||||
"lgalil_is_air_off_and_orchestra_enabled",
|
||||
"drive_axis_to_limit",
|
||||
"find_reference",
|
||||
"get_motor_limit_switch",
|
||||
"is_motor_on",
|
||||
"all_axes_referenced",
|
||||
]
|
||||
|
||||
@threadlocked
|
||||
def socket_put(self, val: str) -> None:
|
||||
self.sock.put(f"{val}\r".encode())
|
||||
|
||||
@threadlocked
|
||||
def socket_get(self) -> str:
|
||||
return self.sock.receive().decode()
|
||||
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def socket_put_and_receive(self, val: str, remove_trailing_chars=True) -> str:
|
||||
self.socket_put(val)
|
||||
if remove_trailing_chars:
|
||||
return self._remove_trailing_characters(self.sock.receive().decode())
|
||||
return self.socket_get()
|
||||
|
||||
@retry_once
|
||||
def socket_put_confirmed(self, val: str) -> None:
|
||||
"""Send message to controller and ensure that it is received by checking that the socket receives a colon.
|
||||
|
||||
Args:
|
||||
val (str): Message that should be sent to the socket
|
||||
|
||||
Raises:
|
||||
GalilCommunicationError: Raised if the return value is not a colon.
|
||||
|
||||
"""
|
||||
return_val = self.socket_put_and_receive(val)
|
||||
if return_val != ":":
|
||||
raise GalilCommunicationError(
|
||||
f"Expected return value of ':' but instead received {return_val}"
|
||||
)
|
||||
|
||||
def is_axis_moving(self, axis_Id, axis_Id_numeric) -> bool:
|
||||
if axis_Id is None and axis_Id_numeric is not None:
|
||||
axis_Id = self.axis_Id_numeric_to_alpha(axis_Id_numeric)
|
||||
is_moving = bool(float(self.socket_put_and_receive(f"MG_BG{axis_Id}")) != 0)
|
||||
backlash_is_active = bool(float(self.socket_put_and_receive(f"MGbcklact[{axis_Id_numeric}]")) != 0)
|
||||
return bool(
|
||||
is_moving or backlash_is_active or self.is_thread_active(0) or self.is_thread_active(2)
|
||||
)
|
||||
|
||||
def is_thread_active(self, thread_id: int) -> bool:
|
||||
val = float(self.socket_put_and_receive(f"MG_XQ{thread_id}"))
|
||||
if val == -1:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _remove_trailing_characters(self, var) -> str:
|
||||
if len(var) > 1:
|
||||
return var.split("\r\n")[0]
|
||||
return var
|
||||
|
||||
def stop_all_axes(self) -> str:
|
||||
return self.socket_put_and_receive(f"XQ#STOP,1")
|
||||
|
||||
def lgalil_is_air_off_and_orchestra_enabled(self) -> bool:
|
||||
# TODO: move this to the LamNI-specific controller
|
||||
rt_not_blocked_by_galil = bool(self.socket_put_and_receive(f"MG@OUT[9]"))
|
||||
air_off = bool(self.socket_put_and_receive(f"MG@OUT[13]"))
|
||||
return rt_not_blocked_by_galil and air_off
|
||||
|
||||
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 all_axes_referenced(self) -> bool:
|
||||
"""
|
||||
Check if all axes are referenced.
|
||||
"""
|
||||
return bool(float(self.socket_put_and_receive("MG allaxref").strip()))
|
||||
|
||||
def drive_axis_to_limit(self, axis_Id_numeric: int, direction: str) -> None:
|
||||
"""
|
||||
Drive an axis to the limit in a specified direction.
|
||||
|
||||
Args:
|
||||
axis_Id_numeric (int): Axis number
|
||||
direction (str): Direction in which the axis should be driven to the limit. Either 'forward' or 'reverse'.
|
||||
"""
|
||||
if direction == "forward":
|
||||
direction_flag = 1
|
||||
elif direction == "reverse":
|
||||
direction_flag = -1
|
||||
else:
|
||||
raise ValueError(f"Invalid direction {direction}")
|
||||
|
||||
self.socket_put_confirmed(f"naxis={axis_Id_numeric}")
|
||||
self.socket_put_confirmed(f"ndir={direction_flag}")
|
||||
self.socket_put_confirmed("XQ#NEWPAR")
|
||||
time.sleep(0.005)
|
||||
self.socket_put_confirmed("XQ#FES")
|
||||
time.sleep(0.01)
|
||||
while self.is_axis_moving(None, axis_Id_numeric):
|
||||
time.sleep(0.01)
|
||||
|
||||
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]
|
||||
elif direction == "reverse":
|
||||
limit = self.get_motor_limit_switch(axis_Id)[0]
|
||||
|
||||
if not limit:
|
||||
raise GalilError(f"Failed to drive axis {axis_Id}/{axis_Id_numeric} to limit.")
|
||||
|
||||
def find_reference(self, axis_Id_numeric: int) -> None:
|
||||
"""
|
||||
Find the reference of an axis.
|
||||
|
||||
Args:
|
||||
axis_Id_numeric (int): Axis number
|
||||
"""
|
||||
self.socket_put_confirmed(f"naxis={axis_Id_numeric}")
|
||||
self.socket_put_and_receive("XQ#NEWPAR")
|
||||
self.socket_put_confirmed("XQ#FRM")
|
||||
time.sleep(0.1)
|
||||
while self.is_axis_moving(None, axis_Id_numeric):
|
||||
time.sleep(0.1)
|
||||
|
||||
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}.")
|
||||
|
||||
def show_running_threads(self) -> None:
|
||||
t = PrettyTable()
|
||||
t.title = f"Threads on {self.sock.host}:{self.sock.port}"
|
||||
t.field_names = [str(ax) for ax in range(self._axes_per_controller)]
|
||||
t.add_row(
|
||||
[
|
||||
"active" if self.is_thread_active(t) else "inactive"
|
||||
for t in range(self._axes_per_controller)
|
||||
]
|
||||
)
|
||||
print(t)
|
||||
|
||||
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.
|
||||
|
||||
Args:
|
||||
axis_Id (str): Axis identifier (e.g. 'A', 'B', 'C', ...)
|
||||
|
||||
Returns:
|
||||
list: List of two booleans indicating if the low and high limit switch is active, respectively.
|
||||
"""
|
||||
ret = self.socket_put_and_receive(f"MG _LR{axis_Id}, _LF{axis_Id}")
|
||||
low, high = ret.strip().split(" ")
|
||||
return [not bool(float(low)), not bool(float(high))]
|
||||
|
||||
def describe(self) -> None:
|
||||
t = PrettyTable()
|
||||
t.title = f"{self.__class__.__name__} on {self.sock.host}:{self.sock.port}"
|
||||
t.field_names = [
|
||||
"Axis",
|
||||
"Name",
|
||||
"Connected",
|
||||
"Referenced",
|
||||
"Motor On",
|
||||
"Limits",
|
||||
"Position",
|
||||
]
|
||||
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"),
|
||||
]
|
||||
)
|
||||
else:
|
||||
t.add_row([None for t in t.field_names])
|
||||
print(t)
|
||||
|
||||
self.show_running_threads()
|
||||
|
||||
def galil_show_all(self) -> None:
|
||||
for controller in self._controller_instances.values():
|
||||
if isinstance(controller, GalilController):
|
||||
controller.describe()
|
||||
|
||||
@staticmethod
|
||||
def axis_Id_to_numeric(axis_Id: str) -> int:
|
||||
return ord(axis_Id.lower()) - 97
|
||||
|
||||
@staticmethod
|
||||
def axis_Id_numeric_to_alpha(axis_Id_numeric: int) -> str:
|
||||
return (chr(axis_Id_numeric + 97)).capitalize()
|
||||
|
||||
|
||||
class GalilSignalBase(SocketSignal):
|
||||
def __init__(self, signal_name, **kwargs):
|
||||
self.signal_name = signal_name
|
||||
super().__init__(**kwargs)
|
||||
self.controller = self.parent.controller
|
||||
self.sock = self.parent.controller.sock
|
||||
|
||||
|
||||
class GalilSignalRO(GalilSignalBase):
|
||||
def __init__(self, signal_name, **kwargs):
|
||||
super().__init__(signal_name, **kwargs)
|
||||
self._metadata["write_access"] = False
|
||||
|
||||
def _socket_set(self, val):
|
||||
raise ReadOnlyError("Read-only signals cannot be set")
|
||||
|
||||
|
||||
class GalilReadbackSignal(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.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
|
||||
|
||||
|
||||
class GalilSetpointSignal(GalilSignalBase):
|
||||
setpoint = 0
|
||||
|
||||
def _socket_get(self) -> float:
|
||||
"""Get command for receiving the setpoint / target value.
|
||||
The value is not pulled from the controller but instead just the last setpoint used.
|
||||
|
||||
Returns:
|
||||
float: setpoint / target value
|
||||
"""
|
||||
return self.setpoint * self.parent.sign
|
||||
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def _socket_set(self, val: float) -> None:
|
||||
"""Set a new target value / setpoint value. Before submission, the target value is adjusted for the axis' sign.
|
||||
Furthermore, it is ensured that all axes are referenced before a new setpoint is submitted.
|
||||
|
||||
Args:
|
||||
val (float): Target value / setpoint value
|
||||
|
||||
Raises:
|
||||
GalilError: Raised if not all axes are referenced.
|
||||
|
||||
"""
|
||||
target_val = val * self.parent.sign
|
||||
self.setpoint = target_val
|
||||
axes_referenced = self.controller.all_axes_referenced()
|
||||
if axes_referenced:
|
||||
while self.controller.is_thread_active(0):
|
||||
time.sleep(0.1)
|
||||
|
||||
if self.parent.axis_Id_numeric == 2:
|
||||
angle_status = self.parent.device_manager.devices[
|
||||
self.parent.rt
|
||||
].obj.controller.feedback_status_angle_lamni()
|
||||
|
||||
if angle_status:
|
||||
self.controller.socket_put_confirmed("angintf=1")
|
||||
|
||||
self.controller.socket_put_confirmed(f"naxis={self.parent.axis_Id_numeric}")
|
||||
self.controller.socket_put_confirmed(f"ntarget={target_val:.3f}")
|
||||
self.controller.socket_put_confirmed("movereq=1")
|
||||
self.controller.socket_put_confirmed("XQ#NEWPAR")
|
||||
while self.controller.is_thread_active(0):
|
||||
time.sleep(0.005)
|
||||
else:
|
||||
raise GalilError("Not all axes are referenced.")
|
||||
|
||||
|
||||
class GalilMotorResolution(GalilSignalRO):
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def _socket_get(self):
|
||||
return float(
|
||||
self.controller.socket_put_and_receive(f"MG stppermm[{self.parent.axis_Id_numeric}]")
|
||||
)
|
||||
|
||||
|
||||
class GalilMotorIsMoving(GalilSignalRO):
|
||||
@threadlocked
|
||||
def _socket_get(self):
|
||||
return self.controller.is_axis_moving(self.parent.axis_Id, self.parent.axis_Id_numeric)
|
||||
|
||||
def get(self):
|
||||
val = super().get()
|
||||
if val is not None:
|
||||
self._run_subs(sub_type=self.SUB_VALUE, value=val, timestamp=time.time())
|
||||
return val
|
||||
|
||||
|
||||
class GalilAxesReferenced(GalilSignalRO):
|
||||
@threadlocked
|
||||
def _socket_get(self):
|
||||
return self.controller.all_axes_referenced()
|
||||
|
||||
|
||||
class GalilMotor(Device, PositionerBase):
|
||||
USER_ACCESS = ["controller"]
|
||||
readback = Cpt(GalilReadbackSignal, 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="mpc2680.psi.ch",
|
||||
port=8081,
|
||||
limits=None,
|
||||
sign=1,
|
||||
socket_cls=SocketIO,
|
||||
device_manager=None,
|
||||
**kwargs,
|
||||
):
|
||||
self.controller = GalilController(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(f"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(f"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 stage(self) -> list[object]:
|
||||
return super().stage()
|
||||
|
||||
def unstage(self) -> list[object]:
|
||||
return super().unstage()
|
||||
|
||||
def stop(self, *, success=False):
|
||||
self.controller.stop_all_axes()
|
||||
return super().stop(success=success)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# pytest: skip-file
|
||||
mock = False
|
||||
if not mock:
|
||||
leyey = GalilMotor("H", name="leyey", host="mpc2680.psi.ch", port=8081, sign=-1)
|
||||
leyey.stage()
|
||||
status = leyey.move(0, wait=True)
|
||||
status = leyey.move(10, wait=True)
|
||||
leyey.read()
|
||||
|
||||
leyey.get()
|
||||
leyey.describe()
|
||||
|
||||
leyey.unstage()
|
||||
else:
|
||||
from ophyd_devices.utils.socket import SocketMock
|
||||
|
||||
leyex = GalilMotor(
|
||||
"G", name="leyex", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock
|
||||
)
|
||||
leyey = GalilMotor(
|
||||
"H", name="leyey", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock
|
||||
)
|
||||
leyex.stage()
|
||||
# leyey.stage()
|
||||
|
||||
leyex.controller.galil_show_all()
|
||||
@@ -1,415 +0,0 @@
|
||||
'******************************************************************************
|
||||
' scanning saxs stages controller code
|
||||
' version 0.1 20160113, holler, based on example code
|
||||
' version 0.2 20160321, holler, small adjustments
|
||||
' version 0.3 20160323, position sampling
|
||||
' version 0.4 grid scan implemented
|
||||
' version 0.5 20160426, shutter control from controller
|
||||
' version 0.6 20160614, code for manual stage tuning added
|
||||
' version 0.7 20170609, prefact added
|
||||
' version 0.8 20190327, stepper motor x axis E, encoder axis A
|
||||
' DC motor y axis C
|
||||
' version 0.9 20190403, Pos sampling averaging and ring buffered
|
||||
' internal grid scan updated
|
||||
' 20190507 various fixes during comm. at beamline
|
||||
' off now in microns for higher resolution
|
||||
' version 1.1 20191021, position samples were off compared to xrays
|
||||
' use AL and RL commands for position latch
|
||||
' to reduce delay for axis C (continuous)
|
||||
' switch to DI3 required, averaging removed
|
||||
' version 1.2 20200500, switch stepper motor to axis F
|
||||
' because motor driver axis E defective
|
||||
' version 2.0 20230816, adjustments in premove with BEC
|
||||
' DO8 controls the shutter
|
||||
' DI1 1 during exposure for pos sampling
|
||||
' Thread overview
|
||||
'******************************************************************************
|
||||
#AUTO
|
||||
DA*,*[];'DEALLOCATE ARRAYS
|
||||
ssaxs_v=1.3
|
||||
IA129,129,122,26
|
||||
'acctim determines pre motion
|
||||
acctim=2.5
|
||||
prvspeed=0
|
||||
posest=0
|
||||
'prefact increases the distance for pre acceleration
|
||||
'if in acctim limits
|
||||
prefact=2.5
|
||||
off=0
|
||||
DM aposavg[2000]
|
||||
DM cposavg[2000]
|
||||
nums=1
|
||||
JS#INIT
|
||||
JS#SETPLAT
|
||||
EN
|
||||
#CALIBC
|
||||
'ACC=1000000
|
||||
'DCC=1000000
|
||||
'SPC=1*mm
|
||||
PAC=2*mm
|
||||
BGC;AMC
|
||||
WT 1000
|
||||
PAC=5*mm
|
||||
BGC;AMC
|
||||
WT 1000
|
||||
JP#CALIBC
|
||||
EN
|
||||
'default settings
|
||||
#SETTOMO
|
||||
IF(allaxref=1)
|
||||
EN
|
||||
ENDIF
|
||||
KPC=100
|
||||
PLC=0.3
|
||||
KIC=10
|
||||
KDC=30
|
||||
ILC=9
|
||||
FAC=10
|
||||
FVC=240
|
||||
EN
|
||||
'set tuning parameters for scanning saxs plate
|
||||
#SETPLAT
|
||||
IF(allaxref=1)
|
||||
EN
|
||||
ENDIF
|
||||
KPC=100
|
||||
PLC=0.3
|
||||
KIC=10
|
||||
KDC=30
|
||||
ILC=9
|
||||
FAC=10
|
||||
FVC=240
|
||||
EN
|
||||
'called with AB before execution
|
||||
#SAMPLE
|
||||
posct=0
|
||||
sposct=0
|
||||
IF(_XQ2=-1)
|
||||
'arm position latch rising axis C
|
||||
'set latch direction rising
|
||||
'we do this prior the loop, to do the switching later asap
|
||||
CN ,,1
|
||||
ALC
|
||||
XQ#SAMPLEL,2
|
||||
ELSE
|
||||
runerr=1
|
||||
ENDIF
|
||||
EN
|
||||
#SAMPLEL
|
||||
'wait for latch
|
||||
AI3
|
||||
'WT1
|
||||
''#WAITLT1
|
||||
''JP#WAITLT1,(_ALC=1);'WAIT UNTIL CAPTURED'
|
||||
'write encoder position to a array
|
||||
aposstrt=_TPA
|
||||
'write latched position to c array
|
||||
cposstrt=_RLC
|
||||
'change latch direction falling axis C
|
||||
CN ,,-1
|
||||
ALC
|
||||
'wait for latch
|
||||
AI-3
|
||||
'WT1
|
||||
''#WAITLT2
|
||||
''JP#WAITLT2,(_ALC=1);'WAIT UNTIL CAPTURED
|
||||
'write encoder position to a array
|
||||
aposend=_TPA
|
||||
'write latched position to c array
|
||||
cposend=_RLC
|
||||
'arm position latch rising axis C for next cycle
|
||||
'set latch direction rising
|
||||
CN ,,1
|
||||
ALC
|
||||
aposavg[posct]=((aposstrt+aposend)/2)
|
||||
cposavg[posct]=((cposstrt+cposend)/2)
|
||||
posct=posct+1
|
||||
sposct=sposct+1
|
||||
IF(posct>1999)
|
||||
posct=0
|
||||
ENDIF
|
||||
JP #SAMPLEL,(posct<nums)
|
||||
EN
|
||||
#SCANG
|
||||
'a_start, a_end, speed is line axis
|
||||
'b_start, gridmax, b_step is grid axis
|
||||
gridct=0
|
||||
'nums=1000
|
||||
'wait for detector
|
||||
WT350
|
||||
IF(_XQ3=(-1))
|
||||
XQ#SCANGL,3
|
||||
ENDIF
|
||||
EN
|
||||
#SCANGL
|
||||
gridpos=b_start+(gridct*b_step)
|
||||
targE=gridpos
|
||||
JS#POSE
|
||||
WT10
|
||||
IF(@INT[(gridct/2)]<>(gridct/2))
|
||||
start=a_start
|
||||
end=a_end
|
||||
ELSE
|
||||
start=a_end
|
||||
end=a_start
|
||||
ENDIF
|
||||
'XQ#SCANL,5
|
||||
'scanstat=-1
|
||||
'#lineact
|
||||
'WT2
|
||||
'JP#lineact,(scanstat<>0)
|
||||
JS#SCANL
|
||||
gridct=gridct+1
|
||||
JP #SCANGL,(gridct<=gridmax)
|
||||
'close shutter
|
||||
CB8
|
||||
EN
|
||||
#TEMP1
|
||||
EN
|
||||
'
|
||||
#SCANL
|
||||
'based on acceleration of 500 mm/s^2
|
||||
'and a max speed of 2 mm/s
|
||||
'the max distance needed for acceleration is 4 microns
|
||||
'so we pre-move 10 microns
|
||||
'variables to set in mm, mm/s
|
||||
'start = start position
|
||||
'end = end position
|
||||
'speed = velocity
|
||||
'the scan axis is defined in the init section
|
||||
IF(allaxref=0)
|
||||
EN
|
||||
ENDIF
|
||||
'
|
||||
IF(end>start)
|
||||
dir=1
|
||||
ELSE
|
||||
dir=-1
|
||||
ENDIF
|
||||
'measure required premove
|
||||
IF((@ABS[(prvspeed-speed)])>0.001)
|
||||
premv=speed*mm*5
|
||||
IF(premv>(3*mm))
|
||||
premv=3*mm
|
||||
ENDIF
|
||||
measpre=1
|
||||
ELSE
|
||||
measpre=0
|
||||
ENDIF
|
||||
'for internal grid scans reduce overshoot
|
||||
'IF(_XQ3<>-1)
|
||||
'redpremv=0.1 ;'case for int grid scan
|
||||
'ELSE
|
||||
'redpremv=1 ;'case for line based scans
|
||||
'ENDIF
|
||||
'we are doing an internal grid
|
||||
'reduce overshoot
|
||||
prepos=(start*mm)-(dir*redpremv*(premv+(off/1000*mm)))
|
||||
'move to pre-start position if needed
|
||||
'prepos=(start*mm)-(dir*speed*acctim*mm)
|
||||
'IF(@ABS[(speed*acctim)]<0.01)
|
||||
'prepos=(start*mm)-(dir*0.01*mm*prefact)
|
||||
'ENDIF
|
||||
'IF(@ABS[(speed*acctim)]>0.1)
|
||||
'ENDIF
|
||||
IF((@ABS[(_TDC-prepos)])>(0.002*mm))
|
||||
scanstat=1
|
||||
SPC=2*mm
|
||||
PAC=prepos
|
||||
BGC
|
||||
AMC
|
||||
'open the shutter
|
||||
SB8
|
||||
WT10
|
||||
ENDIF
|
||||
IF((_LFC<>0)&(_LRC=<>0))
|
||||
scanstat=2
|
||||
SPC=@RND[speed*mm]
|
||||
'arm trigger
|
||||
trigpos=(start*mm)+(dir*off/1000*mm)
|
||||
IF(dir=1)
|
||||
OCC=trigpos,0
|
||||
ENDIF
|
||||
IF(dir=-1)
|
||||
OCC=trigpos,-65536;
|
||||
ENDIF
|
||||
'PAC=((end*mm)+(dir*premv))
|
||||
PAC=((end*mm)+(dir*redpremv*(premv+(off/1000*mm))))
|
||||
BGC
|
||||
IF(measpre=1)
|
||||
calstart=_TDC
|
||||
WT25
|
||||
#CALIBV
|
||||
prevvel=_TVC
|
||||
WT10
|
||||
JP#CALIBV,((@ABS[(prevvel-(_TVC/mm))])<15)
|
||||
calend=_TDC
|
||||
premv=((@ABS[(calend-calstart)])*3)
|
||||
'case of grid scan
|
||||
prvspeed=speed
|
||||
ENDIF
|
||||
'
|
||||
AMC
|
||||
ENDIF
|
||||
scanstat=0
|
||||
'close the shutter
|
||||
'#SHUTWT
|
||||
'JP#SHUTWT,(@IN[1]=1)
|
||||
'WT10
|
||||
'JP#SHUTWT,(@IN[1]=1)
|
||||
'
|
||||
'we are doing an internal grid
|
||||
IF(_XQ3=-1)
|
||||
CB8
|
||||
ENDIF
|
||||
EN
|
||||
'
|
||||
#POSE
|
||||
posctr=0
|
||||
posest=1
|
||||
sttime=TIME
|
||||
PTF=1;' Position Tracking aktiv
|
||||
errE=(targE-(_TPA/mm));' Fehler in mm
|
||||
IF((@ABS[errE])>0.2)
|
||||
SPF=12*stpmm
|
||||
ENDIF
|
||||
#CORRE
|
||||
posest=2
|
||||
errE=(targE-(_TPA/mm));' Fehler in mm
|
||||
PAF=_TDF+(errE*stpmm)
|
||||
IF((@ABS[errE])<0.1)
|
||||
SPF=5*stpmm
|
||||
ENDIF
|
||||
IF((@ABS[errE])<0.0001)
|
||||
posctr=posctr+1
|
||||
IF(posctr>5)
|
||||
STF
|
||||
'MG TIME-sttime, (targE-(_TPA/mm))*1000
|
||||
EN
|
||||
ENDIF
|
||||
ELSE
|
||||
posctr=0
|
||||
ENDIF
|
||||
WT5
|
||||
JP#CORRE
|
||||
posest=3
|
||||
MCF
|
||||
EN
|
||||
'
|
||||
#ZZ
|
||||
targE=120;XQ#POSE
|
||||
WT12000
|
||||
targE=20;XQ#POSE
|
||||
WT12000
|
||||
JP#ZZ
|
||||
EN
|
||||
#FINDREF
|
||||
SB1;' Bremse C-Achse loesen (nur in Verbindung mit SHC)
|
||||
SHC
|
||||
SHF
|
||||
JS#LIMSWI
|
||||
JS#REFE
|
||||
JS#REFC
|
||||
allaxref=1
|
||||
targE=0
|
||||
EN
|
||||
'
|
||||
#REFE
|
||||
SHF
|
||||
JGF=-2*stpmm
|
||||
BGF
|
||||
'MG "suche negativen Endschalter E"
|
||||
AMF
|
||||
WT100
|
||||
'step counter zero
|
||||
DPF=0
|
||||
'encoder zero
|
||||
DPA=0
|
||||
EN
|
||||
'
|
||||
#REFC
|
||||
SB1;' Bremse loesen (nur in Verbindung mit SHC)
|
||||
SHC
|
||||
JGC=-2*mm
|
||||
BGC
|
||||
'MG "suche negativen Endschalter C"
|
||||
AMC
|
||||
WT100
|
||||
DPC=0
|
||||
EN
|
||||
'
|
||||
#LIMSWI
|
||||
scanstat=0
|
||||
JS#LFF,_LFF=0;' +LIMIT E
|
||||
JS#LRF,_LRF=0;' -LIMIT E
|
||||
JS#LFC,_LFC=0;' +LIMIT C
|
||||
JS#LRC,_LRC=0;' -LIMIT C
|
||||
RE;' RETURN FROM ERROR INTERUPT
|
||||
'
|
||||
#LRF
|
||||
'MG "- LIMIT E-ACHSE "
|
||||
STF
|
||||
AMF
|
||||
JGF=stpmm
|
||||
BGF
|
||||
#LOOPA1
|
||||
JP#LOOPA1,_LRF=0
|
||||
STF
|
||||
MCF
|
||||
EN
|
||||
'
|
||||
#LFF
|
||||
'MG "+ LIMIT E-ACHSE "
|
||||
STF
|
||||
AMF
|
||||
JGF=-stpmm
|
||||
BGF
|
||||
#LOOPA2
|
||||
JP#LOOPA2,_LFF=0
|
||||
STF
|
||||
MCF
|
||||
EN
|
||||
'
|
||||
#LRC
|
||||
'MG "- LIMIT C-ACHSE "
|
||||
STC
|
||||
AMC
|
||||
JGC=mm
|
||||
BGC
|
||||
#LOOPC1
|
||||
JP#LOOPC1,_LRC=0
|
||||
STC
|
||||
AMC
|
||||
EN
|
||||
'
|
||||
#LFC
|
||||
'MG "- LIMIT C-ACHSE "
|
||||
STC
|
||||
AMC
|
||||
JGC=-mm
|
||||
BGC
|
||||
#LOOPC2
|
||||
JP#LOOPC2,_LFC=0
|
||||
STC
|
||||
AMC
|
||||
EN
|
||||
'
|
||||
#INIT
|
||||
'define fast scan axis redefined by spec
|
||||
scanstat=0
|
||||
mm=10000;' 100nm Aufloesung im encoder
|
||||
stpmm=50000;' microsteps axis E/mm
|
||||
ratio=5;'
|
||||
allaxref=0
|
||||
'acceleration rates
|
||||
ACF=3000000
|
||||
DCF=3000000
|
||||
SPF=2*mm
|
||||
OEF=0;' OF ON ERROR axis E ausgeschaltet
|
||||
MTF=2
|
||||
KSF=0.5;' Smoothing ausgeschaltet
|
||||
ACC=10*mm
|
||||
DCC=10*mm
|
||||
DVC=1;' Dual loop deaktiviert
|
||||
EN
|
||||
'
|
||||
@@ -1,713 +0,0 @@
|
||||
import functools
|
||||
import threading
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
from bec_lib import bec_logger
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, DeviceStatus, PositionerBase, Signal
|
||||
from ophyd.status import wait as status_wait
|
||||
from ophyd.utils import LimitError, ReadOnlyError
|
||||
from ophyd_devices.utils.controller import Controller, threadlocked
|
||||
from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected
|
||||
from prettytable import PrettyTable
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class GalilCommunicationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class GalilError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BECConfigError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
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 GalilController(Controller):
|
||||
USER_ACCESS = [
|
||||
"describe",
|
||||
"show_running_threads",
|
||||
"galil_show_all",
|
||||
"socket_put_and_receive",
|
||||
"socket_put_confirmed",
|
||||
"sgalil_reference",
|
||||
"fly_grid_scan",
|
||||
"read_encoder_position",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
name="GalilController",
|
||||
kind=None,
|
||||
parent=None,
|
||||
socket=None,
|
||||
attr_name="",
|
||||
labels=None,
|
||||
):
|
||||
if not hasattr(self, "_initialized") or not self._initialized:
|
||||
self._galil_axis_per_controller = 8
|
||||
self._axis = [None for axis_num in range(self._galil_axis_per_controller)]
|
||||
super().__init__(
|
||||
name=name,
|
||||
socket=socket,
|
||||
attr_name=attr_name,
|
||||
parent=parent,
|
||||
labels=labels,
|
||||
kind=kind,
|
||||
)
|
||||
|
||||
def on(self, controller_num=0) -> None:
|
||||
"""Open a new socket connection to the controller"""
|
||||
if not self.connected:
|
||||
self.sock.open()
|
||||
self.connected = True
|
||||
else:
|
||||
logger.info("The connection has already been established.")
|
||||
# warnings.warn(f"The connection has already been established.", stacklevel=2)
|
||||
|
||||
def off(self) -> None:
|
||||
"""Close the socket connection to the controller"""
|
||||
if self.connected:
|
||||
self.sock.close()
|
||||
self.connected = False
|
||||
else:
|
||||
logger.info("The connection is already closed.")
|
||||
|
||||
def set_axis(self, axis: Device, axis_nr: int) -> None:
|
||||
"""Assign an axis to a device instance.
|
||||
|
||||
Args:
|
||||
axis (Device): Device instance (e.g. GalilMotor)
|
||||
axis_nr (int): Controller axis number
|
||||
|
||||
"""
|
||||
self._axis[axis_nr] = axis
|
||||
|
||||
@threadlocked
|
||||
def socket_put(self, val: str) -> None:
|
||||
time.sleep(0.01)
|
||||
self.sock.put(f"{val}\r".encode())
|
||||
|
||||
@threadlocked
|
||||
def socket_get(self) -> str:
|
||||
time.sleep(0.01)
|
||||
return self.sock.receive().decode()
|
||||
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def socket_put_and_receive(self, val: str, remove_trailing_chars=True) -> str:
|
||||
self.socket_put(val)
|
||||
if remove_trailing_chars:
|
||||
return self._remove_trailing_characters(self.sock.receive().decode())
|
||||
return self.socket_get()
|
||||
|
||||
@retry_once
|
||||
def socket_put_confirmed(self, val: str) -> None:
|
||||
"""Send message to controller and ensure that it is received by checking that the socket receives a colon.
|
||||
|
||||
Args:
|
||||
val (str): Message that should be sent to the socket
|
||||
|
||||
Raises:
|
||||
GalilCommunicationError: Raised if the return value is not a colon.
|
||||
|
||||
"""
|
||||
return_val = self.socket_put_and_receive(val)
|
||||
if return_val != ":":
|
||||
raise GalilCommunicationError(
|
||||
f"Expected return value of ':' but instead received {return_val}"
|
||||
)
|
||||
|
||||
def is_axis_moving(self, axis_Id, axis_Id_numeric) -> bool:
|
||||
is_moving = bool(float(self.socket_put_and_receive(f"MG_BG{axis_Id}")) != 0)
|
||||
# backlash_is_active = bool(float(self.socket_put_and_receive(f"MGbcklact[axis]")) != 0)
|
||||
return bool(is_moving) # bool(is_moving or backlash_is_active)
|
||||
|
||||
def is_thread_active(self, thread_id: int) -> bool:
|
||||
val = float(self.socket_put_and_receive(f"MG_XQ{thread_id}"))
|
||||
if val == -1:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _remove_trailing_characters(self, var) -> str:
|
||||
if len(var) > 1:
|
||||
return var.split("\r\n")[0]
|
||||
return var
|
||||
|
||||
def stop_all_axes(self) -> str:
|
||||
# return self.socket_put_and_receive(f"XQ#STOP,1")
|
||||
# Command stops all threads and motors!
|
||||
self.socket_put_and_receive(f"CB8")
|
||||
return self.socket_put_and_receive(f"ST")
|
||||
|
||||
def axis_is_referenced(self) -> bool:
|
||||
return bool(float(self.socket_put_and_receive(f"MG allaxref").strip()))
|
||||
|
||||
def show_running_threads(self) -> None:
|
||||
t = PrettyTable()
|
||||
t.title = f"Threads on {self.sock.host}:{self.sock.port}"
|
||||
t.field_names = [str(ax) for ax in range(self._galil_axis_per_controller)]
|
||||
t.add_row(
|
||||
[
|
||||
"active" if self.is_thread_active(t) else "inactive"
|
||||
for t in range(self._galil_axis_per_controller)
|
||||
]
|
||||
)
|
||||
print(t)
|
||||
|
||||
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:
|
||||
# SGalil specific
|
||||
if axis_Id == "C":
|
||||
ret = self.socket_put_and_receive(f"MG _LF{axis_Id}, _LR{axis_Id}")
|
||||
high, low = ret.strip().split(" ")
|
||||
elif axis_Id == "E":
|
||||
ret = self.socket_put_and_receive(f"MG _LF{'F'}, _LR{'F'}")
|
||||
high, low = ret.strip().split(" ")
|
||||
return [not bool(float(low)), not bool(float(high))]
|
||||
|
||||
def describe(self) -> None:
|
||||
t = PrettyTable()
|
||||
t.title = f"{self.__class__.__name__} on {self.sock.host}:{self.sock.port}"
|
||||
t.field_names = [
|
||||
"Axis",
|
||||
"Name",
|
||||
"Connected",
|
||||
"Referenced",
|
||||
"Motor On",
|
||||
"Limits",
|
||||
"Position",
|
||||
]
|
||||
for ax in range(self._galil_axis_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(),
|
||||
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)
|
||||
|
||||
self.show_running_threads()
|
||||
|
||||
def galil_show_all(self) -> None:
|
||||
for controller in self._controller_instances.values():
|
||||
if isinstance(controller, GalilController):
|
||||
controller.describe()
|
||||
|
||||
def sgalil_reference(self) -> None:
|
||||
"""Reference all axes of the controller"""
|
||||
if self.axis_is_referenced():
|
||||
print("All axes are already referenced.\n")
|
||||
return
|
||||
# Make sure no axes are moving, is this necessary?
|
||||
self.stop_all_axes()
|
||||
self.socket_put_and_receive(f"XQ#FINDREF")
|
||||
print("Referencing. Please wait, timeout after 100s...\n")
|
||||
|
||||
timeout = time.time() + 100
|
||||
while not self.axis_is_referenced():
|
||||
if time.time() > timeout:
|
||||
print("Abort reference sequence, timeout reached\n")
|
||||
break
|
||||
time.sleep(0.5)
|
||||
|
||||
# @threadlocked
|
||||
def fly_grid_scan(
|
||||
self,
|
||||
status: DeviceStatus,
|
||||
start_y: float,
|
||||
end_y: float,
|
||||
interval_y: int,
|
||||
start_x: float,
|
||||
end_x: float,
|
||||
interval_x: int,
|
||||
exp_time: float,
|
||||
readout_time: float,
|
||||
**kwargs,
|
||||
) -> tuple:
|
||||
"""_summary_
|
||||
|
||||
Args:
|
||||
start_y (float): start position of y axis (fast axis)
|
||||
end_y (float): end position of y axis (fast axis)
|
||||
interval_y (int): number of points in y axis
|
||||
start_x (float): start position of x axis (slow axis)
|
||||
end_x (float): end position of x axis (slow axis)
|
||||
interval_x (int): number of points in x axis
|
||||
exp_time (float): exposure time in seconds
|
||||
readout_time (float): readout time in seconds, minimum of .5e-3s (0.5ms)
|
||||
|
||||
Raises:
|
||||
|
||||
LimitError: Raised if any position of motion is outside of the limits
|
||||
LimitError: Raised if the speed is above 2mm/s or below 0.02mm/s
|
||||
|
||||
"""
|
||||
#
|
||||
if not self.axis_is_referenced():
|
||||
raise GalilError("Axis are not referenced")
|
||||
sign_y = self._axis[ord("c") - 97].sign
|
||||
sign_x = self._axis[ord("e") - 97].sign
|
||||
# Check limits
|
||||
# TODO check sign of stage, or not necessary
|
||||
check_values = [start_y, end_y, start_x, end_x]
|
||||
for val in check_values:
|
||||
self.check_value(val)
|
||||
|
||||
start_x *= sign_x
|
||||
end_x *= sign_x
|
||||
start_y *= sign_y
|
||||
end_y *= sign_y
|
||||
|
||||
speed = np.abs(end_y - start_y) / (
|
||||
(interval_y) * exp_time + (interval_y - 1) * readout_time
|
||||
)
|
||||
if speed > 2.00 or speed < 0.02:
|
||||
raise LimitError(
|
||||
f"Speed of {speed:.03f}mm/s is outside of acceptable range of 0.02 to 2 mm/s"
|
||||
)
|
||||
|
||||
gridmax = int(interval_x - 1)
|
||||
step_grid = (end_x - start_x) / interval_x
|
||||
n_samples = int(interval_y * interval_x)
|
||||
|
||||
# Hard coded to maximum offset of 0.1mm to avoid long motions.
|
||||
self.socket_put_and_receive(f"off={(0):f}")
|
||||
self.socket_put_and_receive(f"a_start={start_y:.04f};a_end={end_y:.04f};speed={speed:.04f}")
|
||||
self.socket_put_and_receive(
|
||||
f"b_start={start_x:.04f};gridmax={gridmax:d};b_step={step_grid:.04f}"
|
||||
)
|
||||
self.socket_put_and_receive(f"nums={n_samples}")
|
||||
self.socket_put_and_receive("XQ#SAMPLE")
|
||||
# sleep 50ms to avoid controller running into
|
||||
time.sleep(0.1)
|
||||
self.socket_put_and_receive("XQ#SCANG")
|
||||
# self._block_while_active(3)
|
||||
# time.sleep(0.1)
|
||||
threading.Thread(target=self._block_while_active, args=(3, status), daemon=True).start()
|
||||
# self._while_in_motion(3, n_samples)
|
||||
|
||||
def _block_while_active(self, thread_id: int, status) -> None:
|
||||
while self.is_thread_active(thread_id):
|
||||
time.sleep(1)
|
||||
time.sleep(1)
|
||||
while self.is_thread_active(thread_id):
|
||||
time.sleep(1)
|
||||
status.set_finished()
|
||||
|
||||
# TODO this is for reading out positions, readout is limited by stage triggering
|
||||
def _while_in_motion(self, thread_id: int, n_samples: int) -> tuple:
|
||||
last_readout = 0
|
||||
val_axis2 = [] # y axis
|
||||
val_axis4 = [] # x axis
|
||||
while self.is_thread_active(thread_id):
|
||||
posct = int(self.socket_put_and_receive(f"MGposct").strip().split(".")[0])
|
||||
logger.info(f"SGalil is scanning - latest enconder position {posct+1} from {n_samples}")
|
||||
time.sleep(1)
|
||||
if posct > last_readout:
|
||||
positions = self.read_encoder_position(last_readout, posct)
|
||||
val_axis4.extend(positions[0])
|
||||
val_axis2.extend(positions[1])
|
||||
last_readout = posct + 1
|
||||
logger.info(len(val_axis2))
|
||||
time.sleep(1)
|
||||
# Readout of last positions after scan finished
|
||||
posct = int(self.socket_put_and_receive(f"MGposct").strip().split(".")[0])
|
||||
logger.info(f"SGalil is scanning - latest enconder position {posct} from {n_samples}")
|
||||
if posct > last_readout:
|
||||
positions = self.read_encoder_position(last_readout, posct)
|
||||
val_axis4.extend(positions[0])
|
||||
val_axis2.extend(positions[1])
|
||||
|
||||
return val_axis4, val_axis2
|
||||
|
||||
def read_encoder_position(self, fromval: int, toval: int) -> tuple:
|
||||
val_axis2 = [] # y axis
|
||||
val_axis4 = [] # x axis
|
||||
for ii in range(fromval, toval + 1):
|
||||
rts = self.socket_put_and_receive(f"MGaposavg[{ii%2000}]*10,cposavg[{ii%2000}]*10")
|
||||
if rts == ":":
|
||||
val_axis4.append(rts)
|
||||
val_axis2.append(rts)
|
||||
continue
|
||||
|
||||
val_axis4.append(float(rts.strip().split(" ")[0]) / 100000)
|
||||
val_axis2.append(float(rts.strip().split(" ")[1]) / 100000)
|
||||
return val_axis4, val_axis2
|
||||
|
||||
|
||||
class GalilSignalBase(SocketSignal):
|
||||
def __init__(self, signal_name, **kwargs):
|
||||
self.signal_name = signal_name
|
||||
super().__init__(**kwargs)
|
||||
self.controller = self.parent.controller
|
||||
self.sock = self.parent.controller.sock
|
||||
|
||||
|
||||
class GalilSignalRO(GalilSignalBase):
|
||||
def __init__(self, signal_name, **kwargs):
|
||||
super().__init__(signal_name, **kwargs)
|
||||
self._metadata["write_access"] = False
|
||||
|
||||
def _socket_set(self, val):
|
||||
raise ReadOnlyError("Read-only signals cannot be set")
|
||||
|
||||
|
||||
class GalilReadbackSignal(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.
|
||||
"""
|
||||
if self.parent.axis_Id_numeric == 2:
|
||||
current_pos = float(
|
||||
self.controller.socket_put_and_receive(f"MG _TP{self.parent.axis_Id}/mm")
|
||||
)
|
||||
elif self.parent.axis_Id_numeric == 4:
|
||||
# hardware controller readback from axis 4 is on axis 0, A instead of E
|
||||
current_pos = float(self.controller.socket_put_and_receive(f"MG _TP{'A'}/mm"))
|
||||
current_pos *= self.parent.sign
|
||||
return current_pos
|
||||
|
||||
def read(self):
|
||||
self._metadata["timestamp"] = time.time()
|
||||
val = super().read()
|
||||
return val
|
||||
|
||||
|
||||
class GalilSetpointSignal(GalilSignalBase):
|
||||
setpoint = 0
|
||||
|
||||
def _socket_get(self) -> float:
|
||||
"""Get command for receiving the setpoint / target value.
|
||||
The value is not pulled from the controller but instead just the last setpoint used.
|
||||
|
||||
Returns:
|
||||
float: setpoint / target value
|
||||
"""
|
||||
return self.setpoint * self.parent.sign
|
||||
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def _socket_set(self, val: float) -> None:
|
||||
"""Set a new target value / setpoint value. Before submission, the target value is adjusted for the axis' sign.
|
||||
Furthermore, it is ensured that all axes are referenced before a new setpoint is submitted.
|
||||
|
||||
Args:
|
||||
val (float): Target value / setpoint value
|
||||
|
||||
Raises:
|
||||
GalilError: Raised if not all axes are referenced.
|
||||
|
||||
"""
|
||||
target_val = val * self.parent.sign
|
||||
self.setpoint = target_val
|
||||
axes_referenced = self.controller.axis_is_referenced()
|
||||
if not axes_referenced:
|
||||
raise GalilError(
|
||||
"Not all axes are referenced. Please use controller.sgalil_reference(). BE AWARE that axes start moving, potentially beyond limits, make sure full range of motion is safe"
|
||||
)
|
||||
while self.controller.is_thread_active(0):
|
||||
time.sleep(0.1)
|
||||
|
||||
if self.parent.axis_Id_numeric == 2:
|
||||
self.controller.socket_put_confirmed(f"PA{self.parent.axis_Id}={target_val:.4f}*mm")
|
||||
self.controller.socket_put_and_receive(f"BG{self.parent.axis_Id}")
|
||||
elif self.parent.axis_Id_numeric == 4:
|
||||
self.controller.socket_put_confirmed(f"targ{self.parent.axis_Id}={target_val:.4f}")
|
||||
self.controller.socket_put_and_receive(f"XQ#POSE,{self.parent.axis_Id_numeric}")
|
||||
while self.controller.is_thread_active(0):
|
||||
time.sleep(0.005)
|
||||
|
||||
|
||||
class GalilMotorIsMoving(GalilSignalRO):
|
||||
@threadlocked
|
||||
def _socket_get(self):
|
||||
if self.parent.axis_Id_numeric == 2:
|
||||
ret = self.controller.is_axis_moving(self.parent.axis_Id, self.parent.axis_Id_numeric)
|
||||
return ret
|
||||
if self.parent.axis_Id_numeric == 4:
|
||||
# Motion signal from axis 4 is mapped to axis 5
|
||||
ret = self.controller.is_axis_moving("F", 5)
|
||||
return ret or self.controller.is_thread_active(4)
|
||||
|
||||
def get(self):
|
||||
val = super().get()
|
||||
if val is not None:
|
||||
self._run_subs(sub_type=self.SUB_VALUE, value=val, timestamp=time.time())
|
||||
return val
|
||||
|
||||
|
||||
class GalilAxesReferenced(GalilSignalRO):
|
||||
@threadlocked
|
||||
def _socket_get(self):
|
||||
return self.controller.socket_put_and_receive("MG allaxref")
|
||||
|
||||
|
||||
class SGalilMotor(Device, PositionerBase):
|
||||
""" "SGalil Motors at cSAXS have a
|
||||
DC motor (y axis - vertical) - implemented as C
|
||||
and a step motor (x-axis horizontal) - implemented as E
|
||||
that require different communication for control
|
||||
"""
|
||||
|
||||
USER_ACCESS = ["controller"]
|
||||
readback = Cpt(GalilReadbackSignal, signal_name="readback", kind="hinted")
|
||||
user_setpoint = Cpt(GalilSetpointSignal, signal_name="setpoint")
|
||||
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="129.129.122.26",
|
||||
port=23,
|
||||
limits=None,
|
||||
sign=1,
|
||||
socket_cls=SocketIO,
|
||||
device_manager=None,
|
||||
**kwargs,
|
||||
):
|
||||
self.axis_Id = axis_Id
|
||||
self.sign = sign
|
||||
self.controller = GalilController(socket=socket_cls(host=host, port=port))
|
||||
self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric)
|
||||
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(1.5)
|
||||
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(f"Only single-character axis_Ids are supported.")
|
||||
if val not in ["C", "E"]:
|
||||
raise ValueError(
|
||||
f"axis_id {val} is currently not supported, please use either 'C' or 'E'."
|
||||
)
|
||||
self._axis_Id_alpha = val
|
||||
self._axis_Id_numeric = ord(val.lower()) - 97
|
||||
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 not in [2, 4]:
|
||||
raise ValueError(f"Numeric value {val} is not supported, it must be either 2 or 4.")
|
||||
self._axis_Id_alpha = val
|
||||
self._axis_Id_numeric = (chr(val + 97)).capitalize()
|
||||
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 stop(self, *, success=False):
|
||||
self.controller.stop_all_axes()
|
||||
# last_speed = self.controller.socket_put_and_receive("MG")
|
||||
rtr = self.controller.socket_put_and_receive(f"SPC={2*10000}")
|
||||
logger.info(f"{rtr}")
|
||||
# logger.info(f'Motor stopped, restored speed for samy from {last_speed}mm/s to 2mm/s')
|
||||
return super().stop(success=success)
|
||||
|
||||
def kickoff(self) -> DeviceStatus:
|
||||
status = DeviceStatus(self)
|
||||
self.controller.fly_grid_scan(
|
||||
status,
|
||||
self._kickoff_params.get("start_y"),
|
||||
self._kickoff_params.get("end_y"),
|
||||
self._kickoff_params.get("interval_y"),
|
||||
self._kickoff_params.get("start_x"),
|
||||
self._kickoff_params.get("end_x"),
|
||||
self._kickoff_params.get("interval_x"),
|
||||
self._kickoff_params.get("exp_time"),
|
||||
self._kickoff_params.get("readout_time"),
|
||||
)
|
||||
return status
|
||||
|
||||
def configure(self, parameter: dict, **kwargs) -> None:
|
||||
self._kickoff_params = parameter
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
mock = False
|
||||
if not mock:
|
||||
samy = SGalilMotor("C", name="samy", host="129.129.122.26", port=23, sign=-1)
|
||||
samx = SGalilMotor("E", name="samx", host="129.129.122.26", port=23, sign=-1)
|
||||
else:
|
||||
from ophyd_devices.utils.socket import SocketMock
|
||||
|
||||
samx = SGalilMotor("E", name="samx", host="129.129.122.26", port=23, socket_cls=SocketMock)
|
||||
samy = SGalilMotor("C", name="samy", host="129.129.122.26", port=23, socket_cls=SocketMock)
|
||||
|
||||
samx.controller.galil_show_all()
|
||||
@@ -1,79 +0,0 @@
|
||||
# Documentation SGalil ophyd wrapper
|
||||
Ophyd wrapper for the SGalil controller and stages.
|
||||
## TODO tests and evaluate whether its good to combine common functionaltiy with galil lamni/omny/flomni controller
|
||||
## Integration of the device in IPython kernel
|
||||
BEC needs to be able to reach the host TCP to initiate a connection to the device.
|
||||
```Python
|
||||
from csaxs_bec.devices.galil.sgalil_ophyd import SGalilMotor
|
||||
samx = SGalilMotor("E", name="samx", host="129.129.122.26", port=23, sign=-1)
|
||||
samy = SGalilMotor("C", name="samy", host="129.129.122.26", port=23, sign=-1)
|
||||
# connect to the controller
|
||||
samx.controller.on()
|
||||
samx.read()
|
||||
samx.move(5)
|
||||
dir(samx)# for full printout of commands
|
||||
# useful for development, check below socket communication with sgalil controller
|
||||
samx.controller.socket_put_and_receive('#string: message_to_controller')
|
||||
```
|
||||
## TODO Integration of device in BEC device config!
|
||||
to be tested too
|
||||
|
||||
## Fly scans
|
||||
2D grid fly scan as implemented on the controller.
|
||||
TTL triggers are sent for the start of each line.
|
||||
The scan on the controller needs to be matched with an appropriate triggering scheme, as for instance shown in the attached scheme together with the Stanford Research DG645 device at cSAXS.
|
||||

|
||||
```Python
|
||||
samx.controller.(start_y, end_y, interval_y, start_x, end_x, interval_x, exp_time, readtime)
|
||||
# for example
|
||||
samx.controller.fly_grid_scan(start_y= 16, end_y= 24, interval_y= 100, start_x= 18, end_x= 17.6, interval_x= 2, exp_time= 0.08, readtime= 0.005)
|
||||
```
|
||||
|
||||
## TODO implement line scan
|
||||
Check SPEC implementation for line scans with sgalil controller, and complement it with a suitable triggering scheme of the DG645.
|
||||
|
||||
## TODO readout of positions in encoder
|
||||
Should this be integrated in the flyscan or not.
|
||||
To be explored where this is most suitable.
|
||||
|
||||
## Socket communication with sgalil controller
|
||||
### vertical axis (samy)
|
||||
- initiate with axis 2, C
|
||||
- in motion: "MG _BG{axis_char}", e.g. "MG _BGC" , 0 or 1
|
||||
- limit switch not pressed: "MG _LR{axis_char}, _LF{axis_char}" , 0 or 1
|
||||
- position: "MG _TP{axis_char}/mm" , position in mm
|
||||
- Axis referenced: "MG allaxref", 0 or 1
|
||||
- stop all axis: "XQ#STOP,1"
|
||||
- is motor on: "MG _MO{axis_char}", 0 or 1
|
||||
- is thread active: "MG _XQ{thread_id}", 0 or 1
|
||||
**Specific for sgalil_y**
|
||||
- set_motion_speed: "SP{axis_char}=2*mm", 2mm/s is max speed
|
||||
- set_final_pos: "PA{axis_char}={val:04f}*mm", target pos in mm
|
||||
- start motion: "BG{axis_char}", start motion
|
||||
### horizontal axis (samx)
|
||||
note: some hardware modifications were done that require access to different channels in the encoder. Encoder, motor and limit switches are not controlled by the same endpoint/axis of the controller... see below
|
||||
- initiate with axis 4, E
|
||||
**Specific for sgalil_x**
|
||||
- set_final_pos: "targ{axis_char}={val:04f}", e.g. "targE=2.0000"
|
||||
- start motion: "XQ#POSE,{axis_char}"
|
||||
- For *in motion* and *limit switch not pressed* commands,
|
||||
the key changes to AXIS 5 || F, e.g. "MG _BGF"
|
||||
- For *position* switch to Axis 0 || A, e.g. "MG _TPA/mm"
|
||||
|
||||
### flyscan 2D grid commanes:
|
||||
Last command ('XQ#SCANG') has to come with sufficient delay, important for setting up dedicated scans
|
||||
f***ast axis***
|
||||
- self.socket_put_and_receive(f'a_start={start_y:.04f};a_end={end_y:.04f};speed={speed:.04f}')
|
||||
***slow axis***
|
||||
- self.socket_put_and_receive(f'b_start={start_x:.04f};gridmax={gridmax:d};b_step={step_grid:.04f}')
|
||||
- self.socket_put_and_receive(f'nums={n_samples}') # Declare number of triggers for encoder
|
||||
- self.socket_put_and_receive('XQ#SAMPLE') # Reset encoder counting --> sampling starts with 0
|
||||
Start scan (be aware, needs some waiting from before)
|
||||
- self.socket_put_and_receive('XQ#SCANG')
|
||||
|
||||
### Encoder readings!
|
||||
The encoder readout is triggered by an TTL pulse.
|
||||
Unfortunately, TTL triggers to the encoder can only be accepted with at least 12.5ms time between rising/falling edges. Therefore, maximum readout has to be ~25Hz, rather 30Hz (experimentally determined).
|
||||
Socket commands for the readout:
|
||||
- self.socket_put_and_receive('MGsposct') # get current position counter
|
||||
- self.socket_put_and_receive('MGaposavg[{ii%2000}]*10, cposavg[{ii%2000}]*10,') # loop over ii
|
||||
@@ -1 +0,0 @@
|
||||
from .npoint import NPointAxis, NPointController
|
||||
@@ -1,545 +0,0 @@
|
||||
import functools
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
|
||||
from ophyd_devices.utils.controller import threadlocked
|
||||
from ophyd_devices.utils.socket import raise_if_disconnected
|
||||
from prettytable import PrettyTable
|
||||
from typeguard import typechecked
|
||||
|
||||
|
||||
def channel_checked(fcn):
|
||||
"""Decorator to catch attempted access to channels that are not available."""
|
||||
|
||||
@functools.wraps(fcn)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
self._check_channel(args[0])
|
||||
return fcn(self, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class SocketIO:
|
||||
"""SocketIO helper class for TCP IP connections"""
|
||||
|
||||
def __init__(self, sock=None):
|
||||
self.is_open = False
|
||||
if sock is None:
|
||||
self.open()
|
||||
else:
|
||||
self.sock = sock
|
||||
|
||||
def connect(self, host, port):
|
||||
print(f"connecting to {host} port {port}")
|
||||
# self.sock.create_connection((host, port))
|
||||
self.sock.connect((host, port))
|
||||
|
||||
def _put(self, msg_bytes):
|
||||
return self.sock.send(msg_bytes)
|
||||
|
||||
def _recv(self, buffer_length=1024):
|
||||
return self.sock.recv(buffer_length)
|
||||
|
||||
def _initialize_socket(self):
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.sock.settimeout(5)
|
||||
|
||||
def put(self, msg):
|
||||
return self._put(msg)
|
||||
|
||||
def receive(self, buffer_length=1024):
|
||||
return self._recv(buffer_length=buffer_length)
|
||||
|
||||
def open(self):
|
||||
self._initialize_socket()
|
||||
self.is_open = True
|
||||
|
||||
def close(self):
|
||||
self.sock.close()
|
||||
self.sock = None
|
||||
self.is_open = False
|
||||
|
||||
|
||||
class NPointController:
|
||||
_controller_instance = None
|
||||
|
||||
NUM_CHANNELS = 3
|
||||
_read_single_loc_bit = "A0"
|
||||
_write_single_loc_bit = "A2"
|
||||
_trailing_bit = "55"
|
||||
_range_offset = "78"
|
||||
_channel_base = ["11", "83"]
|
||||
|
||||
def __init__(
|
||||
self, comm_socket: SocketIO, server_ip: str = "129.129.99.87", server_port: int = 23
|
||||
) -> None:
|
||||
self._lock = threading.RLock()
|
||||
super().__init__()
|
||||
self._server_and_port_name = (server_ip, server_port)
|
||||
self.socket = comm_socket
|
||||
self.connected = False
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if not NPointController._controller_instance:
|
||||
NPointController._controller_instance = object.__new__(cls)
|
||||
return NPointController._controller_instance
|
||||
|
||||
@classmethod
|
||||
def create(cls):
|
||||
return cls(SocketIO())
|
||||
|
||||
def show_all(self) -> None:
|
||||
"""Display current status of all channels
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
if not self.connected:
|
||||
print("npoint controller is currently disabled.")
|
||||
return
|
||||
print(f"Connected to controller at {self._server_and_port_name}")
|
||||
t = PrettyTable()
|
||||
t.field_names = ["Channel", "Range", "Position", "Target"]
|
||||
for ii in range(self.NUM_CHANNELS):
|
||||
t.add_row(
|
||||
[ii, self._get_range(ii), self._get_current_pos(ii), self._get_target_pos(ii)]
|
||||
)
|
||||
print(t)
|
||||
|
||||
@threadlocked
|
||||
def on(self) -> None:
|
||||
"""Enable the NPoint controller and open a new socket.
|
||||
|
||||
Raises:
|
||||
TimeoutError: Raised if the socket connection raises a timeout.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
if self.connected:
|
||||
print("You are already connected to the NPoint controller.")
|
||||
return
|
||||
if not self.socket.is_open:
|
||||
self.socket.open()
|
||||
try:
|
||||
self.socket.connect(self._server_and_port_name[0], self._server_and_port_name[1])
|
||||
except socket.timeout:
|
||||
raise TimeoutError(
|
||||
f"Failed to connect to the specified server and port {self._server_and_port_name}."
|
||||
)
|
||||
except OSError:
|
||||
print("ERROR while connecting. Let's try again")
|
||||
self.socket.close()
|
||||
time.sleep(0.5)
|
||||
self.socket.open()
|
||||
self.socket.connect(self._server_and_port_name[0], self._server_and_port_name[1])
|
||||
self.connected = True
|
||||
|
||||
@threadlocked
|
||||
def off(self) -> None:
|
||||
"""Disable the controller and close the socket.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
self.socket.close()
|
||||
self.connected = False
|
||||
|
||||
@channel_checked
|
||||
def _get_range(self, channel: int) -> int:
|
||||
"""Get the range of the specified channel axis.
|
||||
|
||||
Args:
|
||||
channel (int): Channel for which the range should be requested.
|
||||
|
||||
Raises:
|
||||
RuntimeError: Raised if the received message doesn't have the expected number of bytes (10).
|
||||
|
||||
Returns:
|
||||
int: Range
|
||||
"""
|
||||
|
||||
# for first channel: 0x11 83 10 78
|
||||
addr = self._channel_base.copy()
|
||||
addr.extend([f"{16 + 16 * channel:x}", self._range_offset])
|
||||
send_buffer = self.__read_single_location_buffer(addr)
|
||||
|
||||
recvd = self._put_and_receive(send_buffer)
|
||||
if len(recvd) != 10:
|
||||
raise RuntimeError(
|
||||
f"Received buffer is corrupted. Expected 10 bytes and instead got {len(recvd)}"
|
||||
)
|
||||
device_range = self._hex_list_to_int(recvd[5:-1], signed=False)
|
||||
return device_range
|
||||
|
||||
@channel_checked
|
||||
def _get_current_pos(self, channel: int) -> float:
|
||||
# for first channel: 0x11 83 13 34
|
||||
addr = self._channel_base.copy()
|
||||
addr.extend([f"{19 + 16 * channel:x}", "34"])
|
||||
send_buffer = self.__read_single_location_buffer(addr)
|
||||
|
||||
recvd = self._put_and_receive(send_buffer)
|
||||
|
||||
pos_buffer = recvd[5:-1]
|
||||
pos = self._hex_list_to_int(pos_buffer) / 1048574 * 100
|
||||
return pos
|
||||
|
||||
@channel_checked
|
||||
def _set_target_pos(self, channel: int, pos: float) -> None:
|
||||
# for first channel: 0x11 83 12 18 00 00 00 00
|
||||
addr = self._channel_base.copy()
|
||||
addr.extend([f"{18 + channel * 16:x}", "18"])
|
||||
|
||||
target = int(round(1048574 / 100 * pos))
|
||||
data = [f"{m:02x}" for m in target.to_bytes(4, byteorder="big", signed=True)]
|
||||
|
||||
send_buffer = self.__write_single_location_buffer(addr, data)
|
||||
self._put(send_buffer)
|
||||
|
||||
@channel_checked
|
||||
def _get_target_pos(self, channel: int) -> float:
|
||||
# for first channel: 0x11 83 12 18
|
||||
addr = self._channel_base.copy()
|
||||
addr.extend([f"{18 + channel * 16:x}", "18"])
|
||||
send_buffer = self.__read_single_location_buffer(addr)
|
||||
|
||||
recvd = self._put_and_receive(send_buffer)
|
||||
pos_buffer = recvd[5:-1]
|
||||
pos = self._hex_list_to_int(pos_buffer) / 1048574 * 100
|
||||
return pos
|
||||
|
||||
@channel_checked
|
||||
def _set_servo(self, channel: int, enable: bool) -> None:
|
||||
print("Not tested")
|
||||
return
|
||||
# for first channel: 0x11 83 10 84 00 00 00 00
|
||||
addr = self._channel_base.copy()
|
||||
addr.extend([f"{16 + channel * 16:x}", "84"])
|
||||
|
||||
if enable:
|
||||
data = ["00"] * 3 + ["01"]
|
||||
else:
|
||||
data = ["00"] * 4
|
||||
send_buffer = self.__write_single_location_buffer(addr, data)
|
||||
|
||||
self._put(send_buffer)
|
||||
|
||||
@channel_checked
|
||||
def _get_servo(self, channel: int) -> int:
|
||||
# for first channel: 0x11 83 10 84 00 00 00 00
|
||||
addr = self._channel_base.copy()
|
||||
addr.extend([f"{16 + channel * 16:x}", "84"])
|
||||
send_buffer = self.__read_single_location_buffer(addr)
|
||||
|
||||
recvd = self._put_and_receive(send_buffer)
|
||||
buffer = recvd[5:-1]
|
||||
status = self._hex_list_to_int(buffer)
|
||||
return status
|
||||
|
||||
@threadlocked
|
||||
def _put(self, buffer: list) -> None:
|
||||
"""Translates a list of hex values to bytes and sends them to the socket.
|
||||
|
||||
Args:
|
||||
buffer (list): List of hex values without leading 0x
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
buffer = b"".join([bytes.fromhex(m) for m in buffer])
|
||||
self.socket.put(buffer)
|
||||
|
||||
@threadlocked
|
||||
def _put_and_receive(self, msg_hex_list: list) -> list:
|
||||
"""Send msg to socket and wait for a reply.
|
||||
|
||||
Args:
|
||||
msg_hex_list (list): List of hex values without leading 0x.
|
||||
|
||||
Returns:
|
||||
list: Received message as a list of hex values
|
||||
"""
|
||||
|
||||
buffer = b"".join([bytes.fromhex(m) for m in msg_hex_list])
|
||||
self.socket.put(buffer)
|
||||
recv_msg = self.socket.receive()
|
||||
recv_hex_list = [hex(m) for m in recv_msg]
|
||||
self._verify_received_msg(msg_hex_list, recv_hex_list)
|
||||
return recv_hex_list
|
||||
|
||||
def _verify_received_msg(self, in_list: list, out_list: list) -> None:
|
||||
"""Ensure that the first address bits of sent and received messages are the same.
|
||||
|
||||
Args:
|
||||
in_list (list): list containing the sent message
|
||||
out_list (list): list containing the received message
|
||||
|
||||
Raises:
|
||||
RuntimeError: Raised if first two address bits of 'in' and 'out' are not identical
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
# first, translate hex (str) values to int
|
||||
in_list_int = [int(val, 16) for val in in_list]
|
||||
out_list_int = [int(val, 16) for val in out_list]
|
||||
|
||||
# first ints of the reply should be the same. Otherwise something went wrong
|
||||
if not in_list_int[:2] == out_list_int[:2]:
|
||||
raise RuntimeError("Connection failure. Please restart the controller.")
|
||||
|
||||
def _check_channel(self, channel: int) -> None:
|
||||
if channel >= self.NUM_CHANNELS:
|
||||
raise ValueError(
|
||||
f"Channel {channel+1} exceeds the available number of channels ({self.NUM_CHANNELS})"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _hex_list_to_int(in_buffer: list, byteorder="little", signed=True) -> int:
|
||||
"""Translate hex list to int.
|
||||
|
||||
Args:
|
||||
in_buffer (list): Input buffer; received as list of hex values
|
||||
byteorder (str, optional): Byteorder of in_buffer. Defaults to "little".
|
||||
signed (bool, optional): Whether the hex list represents a signed int. Defaults to True.
|
||||
|
||||
Returns:
|
||||
int: Translated integer.
|
||||
"""
|
||||
if byteorder == "little":
|
||||
in_buffer.reverse()
|
||||
|
||||
# make sure that all hex strings have the same format ("FF")
|
||||
val_hex = [f"{int(m, 16):02x}" for m in in_buffer]
|
||||
|
||||
val_bytes = [bytes.fromhex(m) for m in val_hex]
|
||||
val = int.from_bytes(b"".join(val_bytes), byteorder="big", signed=signed)
|
||||
return val
|
||||
|
||||
@staticmethod
|
||||
def __read_single_location_buffer(addr) -> list:
|
||||
"""Prepare buffer for reading from a single memory location (hex address).
|
||||
Number of bytes: 6
|
||||
Format: 0xA0 [addr] 0x55
|
||||
Return Value: 0xA0 [addr] [data] 0x55
|
||||
Sample Hex Transmission from PC to LC.400: A0 18 12 83 11 55
|
||||
Sample Hex Return Transmission from LC.400 to PC: A0 18 12 83 11 64 00 00 00 55
|
||||
|
||||
Args:
|
||||
addr (list): Hex address to read from
|
||||
|
||||
Returns:
|
||||
list: List of hex values representing the read instruction.
|
||||
"""
|
||||
buffer = []
|
||||
buffer.append(NPointController._read_single_loc_bit)
|
||||
if isinstance(addr, list):
|
||||
addr.reverse()
|
||||
buffer.extend(addr)
|
||||
else:
|
||||
buffer.append(addr)
|
||||
buffer.append(NPointController._trailing_bit)
|
||||
|
||||
return buffer
|
||||
|
||||
@staticmethod
|
||||
def __write_single_location_buffer(addr: list, data: list) -> list:
|
||||
"""Prepare buffer for writing to a single memory location (hex address).
|
||||
Number of bytes: 10
|
||||
Format: 0xA2 [addr] [data] 0x55
|
||||
Return Value: none
|
||||
Sample Hex Transmission from PC to C.400: A2 18 12 83 11 E8 03 00 00 55
|
||||
|
||||
Args:
|
||||
addr (list): List of hex values representing the address to write to.
|
||||
data (list): List of hex values representing the data that should be written.
|
||||
|
||||
Returns:
|
||||
list: List of hex values representing the write instruction.
|
||||
"""
|
||||
buffer = []
|
||||
buffer.append(NPointController._write_single_loc_bit)
|
||||
if isinstance(addr, list):
|
||||
addr.reverse()
|
||||
buffer.extend(addr)
|
||||
else:
|
||||
buffer.append(addr)
|
||||
|
||||
if isinstance(data, list):
|
||||
data.reverse()
|
||||
buffer.extend(data)
|
||||
else:
|
||||
buffer.append(data)
|
||||
buffer.append(NPointController._trailing_bit)
|
||||
return buffer
|
||||
|
||||
@staticmethod
|
||||
def __read_array():
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def __write_next_command():
|
||||
raise NotImplementedError
|
||||
|
||||
def __del__(self):
|
||||
if self.connected:
|
||||
print("Closing npoint socket")
|
||||
self.off()
|
||||
|
||||
|
||||
class NPointAxis:
|
||||
def __init__(self, controller: NPointController, channel: int, name: str) -> None:
|
||||
super().__init__()
|
||||
self._axis_range = 100
|
||||
self.controller = controller
|
||||
self.channel = channel
|
||||
self.name = name
|
||||
self.controller._check_channel(channel)
|
||||
self._settling_time = 0.1
|
||||
|
||||
if self.settling_time == 0:
|
||||
self.settling_time = 0.1
|
||||
print(f"Setting the npoint settling time to {self.settling_time:.2f} s.")
|
||||
print(
|
||||
"You can set the settling time depending on the stage tuning\nusing the settling_time property."
|
||||
)
|
||||
print("This is the waiting time before the counting is done.")
|
||||
|
||||
def show_all(self) -> None:
|
||||
self.controller.show_all()
|
||||
|
||||
@raise_if_disconnected
|
||||
def get(self) -> float:
|
||||
"""Get current position for this channel.
|
||||
|
||||
Raises:
|
||||
RuntimeError: Raised if channel is not connected.
|
||||
|
||||
Returns:
|
||||
float: position
|
||||
"""
|
||||
return self.controller._get_current_pos(self.channel)
|
||||
|
||||
@raise_if_disconnected
|
||||
def get_target_pos(self) -> float:
|
||||
"""Get target position for this channel.
|
||||
|
||||
Raises:
|
||||
RuntimeError: Raised if channel is not connected.
|
||||
|
||||
Returns:
|
||||
float: position
|
||||
"""
|
||||
return self.controller._get_target_pos(self.channel)
|
||||
|
||||
@raise_if_disconnected
|
||||
@typechecked
|
||||
def set(self, pos: float) -> None:
|
||||
"""Set a new target position and wait until settled (settling_time).
|
||||
|
||||
Args:
|
||||
pos (float): New target position
|
||||
|
||||
Raises:
|
||||
RuntimeError: Raised if channel is not connected.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
self.controller._set_target_pos(self.channel, pos)
|
||||
time.sleep(self.settling_time)
|
||||
|
||||
@property
|
||||
def connected(self) -> bool:
|
||||
return self.controller.connected
|
||||
|
||||
@property
|
||||
@raise_if_disconnected
|
||||
def servo(self) -> int:
|
||||
"""Get servo status
|
||||
|
||||
Raises:
|
||||
RuntimeError: Raised if channel is not connected.
|
||||
|
||||
Returns:
|
||||
int: Servo status
|
||||
"""
|
||||
return self.controller._get_servo(self.channel)
|
||||
|
||||
@servo.setter
|
||||
@raise_if_disconnected
|
||||
@typechecked
|
||||
def servo(self, val: bool) -> None:
|
||||
"""Set servo status
|
||||
|
||||
Args:
|
||||
val (bool): Servo status
|
||||
|
||||
Raises:
|
||||
RuntimeError: Raised if channel is not connected.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
self.controller._set_servo(self.channel, val)
|
||||
|
||||
@property
|
||||
def settling_time(self) -> float:
|
||||
return self._settling_time
|
||||
|
||||
@settling_time.setter
|
||||
@typechecked
|
||||
def settling_time(self, val: float) -> None:
|
||||
self._settling_time = val
|
||||
print(f"Setting the npoint settling time to {val:.2f} s.")
|
||||
|
||||
|
||||
class NPointEpics(NPointAxis):
|
||||
def __init__(self, controller: NPointController, channel: int, name: str) -> None:
|
||||
super().__init__(controller, channel, name)
|
||||
self.low_limit = -50
|
||||
self.high_limit = 50
|
||||
self._prefix = name
|
||||
|
||||
def get_pv(self) -> str:
|
||||
return self.name
|
||||
|
||||
def get_position(self, readback=True) -> float:
|
||||
if readback:
|
||||
return self.get()
|
||||
else:
|
||||
return self.get_target_pos()
|
||||
|
||||
def within_limits(self, pos: float) -> bool:
|
||||
return pos > self.low_limit and pos < self.high_limit
|
||||
|
||||
def move(self, position: float, wait=True) -> None:
|
||||
self.set(position)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
## EXAMPLES ##
|
||||
#
|
||||
# Create controller and socket instance explicitly:
|
||||
# controller = NPointController(SocketIO())
|
||||
# npointx = NPointAxis(controller, 0, "nx")
|
||||
# npointy = NPointAxis(controller, 1, "ny")
|
||||
|
||||
# Create controller instance explicitly
|
||||
# controller = NPointController.create()
|
||||
# npointx = NPointAxis(controller, 0, "nx")
|
||||
# npointy = NPointAxis(controller, 1, "ny")
|
||||
|
||||
# Single-line axis:
|
||||
# npointx = NPointAxis(NPointController.create(), 0, "nx")
|
||||
#
|
||||
# EPICS wrapper:
|
||||
# nx = NPointEpics(NPointController.create(), 0, "nx")
|
||||
|
||||
controller = NPointController.create()
|
||||
npointx = NPointAxis(NPointController.create(), 0, "nx")
|
||||
npointy = NPointAxis(NPointController.create(), 0, "ny")
|
||||
@@ -1,2 +0,0 @@
|
||||
from .rt_flomni_ophyd import RtFlomniController, RtFlomniMotor
|
||||
from .rt_lamni_ophyd import RtLamniController, RtLamniMotor
|
||||
@@ -1,811 +0,0 @@
|
||||
import threading
|
||||
import time
|
||||
from typing import List
|
||||
|
||||
import numpy as np
|
||||
from bec_lib import MessageEndpoints, bec_logger, messages
|
||||
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 csaxs_bec.devices.rt_lamni.rt_ophyd import (
|
||||
BECConfigError,
|
||||
RtCommunicationError,
|
||||
RtController,
|
||||
RtError,
|
||||
RtReadbackSignal,
|
||||
RtSetpointSignal,
|
||||
RtSignalRO,
|
||||
retry_once,
|
||||
)
|
||||
from ophyd_devices.utils.controller import threadlocked
|
||||
from ophyd_devices.utils.socket import SocketIO, raise_if_disconnected
|
||||
from prettytable import PrettyTable
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class RtFlomniController(RtController):
|
||||
USER_ACCESS = [
|
||||
"socket_put_and_receive",
|
||||
"set_rotation_angle",
|
||||
"feedback_disable",
|
||||
"feedback_enable_without_reset",
|
||||
"feedback_enable_with_reset",
|
||||
"feedback_is_running",
|
||||
"add_pos_to_scan",
|
||||
"get_pid_x",
|
||||
"move_samx_to_scan_region",
|
||||
"clear_trajectory_generator",
|
||||
"show_cyclic_error_compensation",
|
||||
"laser_tracker_on",
|
||||
"laser_tracker_off",
|
||||
"laser_tracker_show_all",
|
||||
"show_signal_strength_interferometer",
|
||||
"read_ssi_interferometer",
|
||||
"laser_tracker_check_signalstrength",
|
||||
"laser_tracker_check_enabled",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
name=None,
|
||||
socket_cls=None,
|
||||
socket_host=None,
|
||||
socket_port=None,
|
||||
attr_name="",
|
||||
parent=None,
|
||||
labels=None,
|
||||
kind=None,
|
||||
):
|
||||
super().__init__(
|
||||
name=name,
|
||||
socket_cls=socket_cls,
|
||||
socket_host=socket_host,
|
||||
socket_port=socket_port,
|
||||
attr_name=attr_name,
|
||||
parent=parent,
|
||||
labels=labels,
|
||||
kind=kind,
|
||||
)
|
||||
self.tracker_info = {}
|
||||
self._min_scan_buffer_reached = False
|
||||
self.rt_pid_voltage = None
|
||||
|
||||
def add_pos_to_scan(self, positions) -> None:
|
||||
def send_positions(parent, positions):
|
||||
parent._min_scan_buffer_reached = False
|
||||
start_time = time.time()
|
||||
for pos_index, pos in enumerate(positions):
|
||||
parent.socket_put_and_receive(f"s{pos[0]:.05f},{pos[1]:.05f},{pos[2]:.05f}")
|
||||
if pos_index > 100:
|
||||
parent._min_scan_buffer_reached = True
|
||||
parent._min_scan_buffer_reached = True
|
||||
logger.info(
|
||||
f"Sending {len(positions)} positions took {time.time()-start_time} seconds."
|
||||
)
|
||||
|
||||
threading.Thread(target=send_positions, args=(self, positions), daemon=True).start()
|
||||
|
||||
def move_to_zero(self):
|
||||
self.socket_put("pa0,0")
|
||||
self.get_axis_by_name("rtx").user_setpoint.setpoint = 0
|
||||
self.socket_put("pa1,0")
|
||||
self.get_axis_by_name("rty").user_setpoint.setpoint = 0
|
||||
self.socket_put("pa2,0")
|
||||
self.get_axis_by_name("rtz").user_setpoint.setpoint = 0
|
||||
time.sleep(0.05)
|
||||
|
||||
def feedback_is_running(self) -> bool:
|
||||
status = int(float(self.socket_put_and_receive("l2").strip()))
|
||||
if status == 1:
|
||||
return False
|
||||
return True
|
||||
|
||||
def feedback_enable_with_reset(self):
|
||||
self.socket_put("l0") # disable feedback
|
||||
|
||||
self.move_to_zero()
|
||||
|
||||
if not self.slew_rate_limiters_on_target() or np.abs(self.pid_y()) > 0.1:
|
||||
print("Please wait, slew rate limiters not on target.")
|
||||
logger.info("Please wait, slew rate limiters not on target.")
|
||||
while not self.slew_rate_limiters_on_target() or np.abs(self.pid_y()) > 0.1:
|
||||
time.sleep(0.05)
|
||||
|
||||
self.get_device_manager().devices.rty.update_user_parameter({"tomo_additional_offsety": 0})
|
||||
self.clear_trajectory_generator()
|
||||
|
||||
self.laser_tracker_on()
|
||||
|
||||
# move to 0. FUPR will set the rotation angle during readout
|
||||
self.get_device_manager().devices.fsamroy.obj.move(0, wait=True)
|
||||
|
||||
fsamx = self.get_device_manager().devices.fsamx
|
||||
|
||||
fsamx.obj.pid_x_correction = 0
|
||||
fsamx.obj.controller.socket_put_confirmed("axspeed[4]=0.1*stppermm[4]")
|
||||
fsamx_in = fsamx.user_parameter.get("in")
|
||||
if not np.isclose(fsamx.obj.readback.get(), fsamx_in, atol=0.3):
|
||||
print(
|
||||
"Something is wrong. fsamx is very far from the samx_in position. Don't dare correct automatically."
|
||||
)
|
||||
raise RtError(
|
||||
"Something is wrong. fsamx is very far from the samx_in position. Don't dare correct automatically."
|
||||
)
|
||||
|
||||
if not np.isclose(fsamx.obj.readback.get(), fsamx_in, atol=0.01):
|
||||
fsamx.read_only = False
|
||||
fsamx.obj.move(fsamx_in, wait=True)
|
||||
fsamx.read_only = True
|
||||
time.sleep(1)
|
||||
|
||||
self.socket_put("l1")
|
||||
time.sleep(0.4)
|
||||
|
||||
if not self.feedback_is_running():
|
||||
print("Feedback is not running; likely an error in the interferometer.")
|
||||
raise RtError("Feedback is not running; likely an error in the interferometer.")
|
||||
|
||||
time.sleep(1.5)
|
||||
self.show_cyclic_error_compensation()
|
||||
|
||||
self.rt_pid_voltage = self.get_pid_x()
|
||||
rtx = self.get_device_manager().devices.rtx
|
||||
rtx.update_user_parameter({"rt_pid_voltage": self.rt_pid_voltage})
|
||||
|
||||
self.set_device_enabled("fsamx", False)
|
||||
self.set_device_enabled("fsamy", False)
|
||||
self.set_device_enabled("foptx", False)
|
||||
self.set_device_enabled("fopty", False)
|
||||
|
||||
def move_samx_to_scan_region(self, fovx: float, cenx: float):
|
||||
time.sleep(0.05)
|
||||
if self.rt_pid_voltage is None:
|
||||
rtx = self.get_device_manager().devices.rtx
|
||||
self.rt_pid_voltage = rtx.user_parameter.get("rt_pid_voltage")
|
||||
if self.rt_pid_voltage is None:
|
||||
raise RtError(
|
||||
"rt_pid_voltage not set in rtx user parameters. Please run feedback_enable_with_reset first."
|
||||
)
|
||||
logger.info(f"Using PID voltage from rtx user parameter: {self.rt_pid_voltage}")
|
||||
expected_voltage = self.rt_pid_voltage + fovx / 2 * 7 / 100
|
||||
logger.info(f"Expected PID voltage: {expected_voltage}")
|
||||
logger.info(f"Current PID voltage: {self.get_pid_x()}")
|
||||
|
||||
wait_on_exit = False
|
||||
while True:
|
||||
if np.abs(self.get_pid_x() - expected_voltage) < 1:
|
||||
break
|
||||
wait_on_exit = True
|
||||
self.socket_put("v0")
|
||||
fsamx = self.get_device_manager().devices.fsamx
|
||||
fsamx.read_only = False
|
||||
fsamx.obj.controller.socket_put_confirmed("axspeed[4]=0.1*stppermm[4]")
|
||||
fsamx.obj.pid_x_correction -= (self.get_pid_x() - expected_voltage) * 0.007
|
||||
logger.info(f"Correcting fsamx by {fsamx.obj.pid_x_correction}")
|
||||
fsamx_in = fsamx.user_parameter.get("in")
|
||||
fsamx.obj.move(fsamx_in + cenx / 1000 + fsamx.obj.pid_x_correction, wait=True)
|
||||
fsamx.read_only = True
|
||||
time.sleep(0.1)
|
||||
self.laser_tracker_on()
|
||||
time.sleep(0.01)
|
||||
|
||||
if wait_on_exit:
|
||||
time.sleep(1)
|
||||
|
||||
self.socket_put("v1")
|
||||
|
||||
@threadlocked
|
||||
def clear_trajectory_generator(self):
|
||||
self.socket_put("sc")
|
||||
logger.info("flomni scan stopped and deleted, moving to start position")
|
||||
|
||||
def feedback_enable_without_reset(self):
|
||||
self.laser_tracker_on()
|
||||
self.socket_put("l3")
|
||||
time.sleep(0.01)
|
||||
|
||||
if not self.feedback_is_running():
|
||||
print("Feedback is not running; likely an error in the interferometer.")
|
||||
raise RtError("Feedback is not running; likely an error in the interferometer.")
|
||||
|
||||
self.set_device_enabled("fsamx", False)
|
||||
self.set_device_enabled("fsamy", False)
|
||||
self.set_device_enabled("foptx", False)
|
||||
self.set_device_enabled("fopty", False)
|
||||
|
||||
def feedback_disable(self):
|
||||
self.clear_trajectory_generator()
|
||||
self.move_to_zero()
|
||||
self.socket_put("l0")
|
||||
|
||||
self.set_device_enabled("fsamx", True)
|
||||
self.set_device_enabled("fsamy", True)
|
||||
self.set_device_enabled("foptx", True)
|
||||
self.set_device_enabled("fopty", True)
|
||||
|
||||
fsamx = self.get_device_manager().devices.fsamx
|
||||
fsamx.obj.controller.socket_put_confirmed("axspeed[4]=025*stppermm[4]")
|
||||
print("rt feedback is now disalbed.")
|
||||
|
||||
def get_pid_x(self) -> float:
|
||||
voltage = float(self.socket_put_and_receive("g").strip())
|
||||
return voltage
|
||||
|
||||
def show_cyclic_error_compensation(self):
|
||||
cec0 = int(float(self.socket_put_and_receive("w0").strip()))
|
||||
cec1 = int(float(self.socket_put_and_receive("w1").strip()))
|
||||
|
||||
if cec0 == 32:
|
||||
logger.info("Cyclic Error Compensation: y-axis is initialized")
|
||||
else:
|
||||
logger.info("Cyclic Error Compensation: y-axis is NOT initialized")
|
||||
print("Cyclic Error Compensation: y-axis is NOT initialized")
|
||||
if cec1 == 32:
|
||||
logger.info("Cyclic Error Compensation: x-axis is initialized")
|
||||
else:
|
||||
logger.info("Cyclic Error Compensation: x-axis is NOT initialized")
|
||||
print("Cyclic Error Compensation: x-axis is NOT initialized")
|
||||
|
||||
def set_rotation_angle(self, val: float) -> None:
|
||||
self.socket_put(f"a{val/180*np.pi}")
|
||||
|
||||
def laser_tracker_check_enabled(self) -> bool:
|
||||
self.laser_update_tracker_info()
|
||||
if self.tracker_info["enabled_z"] and self.tracker_info["enabled_y"]:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def laser_tracker_on(self):
|
||||
if not self.laser_tracker_check_enabled():
|
||||
logger.info("Enabling the laser tracker. Please wait...")
|
||||
print("Enabling the laser tracker. Please wait...")
|
||||
|
||||
tracker_intensity = self.tracker_info["tracker_intensity"]
|
||||
if (
|
||||
tracker_intensity < self.tracker_info["threshold_intensity_y"]
|
||||
or tracker_intensity < self.tracker_info["threshold_intensity_z"]
|
||||
):
|
||||
logger.info(self.tracker_info)
|
||||
print("The tracker cannot be enabled because the beam intensity it low.")
|
||||
raise RtError("The tracker cannot be enabled because the beam intensity it low.")
|
||||
|
||||
self.move_to_zero()
|
||||
self.socket_put("T1")
|
||||
time.sleep(0.5)
|
||||
|
||||
self.get_device_manager().devices.ftrackz.obj.controller.socket_put_confirmed(
|
||||
"trackyct=0"
|
||||
)
|
||||
self.get_device_manager().devices.ftrackz.obj.controller.socket_put_confirmed(
|
||||
"trackzct=0"
|
||||
)
|
||||
|
||||
self.laser_tracker_wait_on_target()
|
||||
logger.info("Laser tracker running!")
|
||||
print("Laser tracker running!")
|
||||
|
||||
def laser_tracker_off(self):
|
||||
if self.feedback_is_running():
|
||||
print(
|
||||
"Interferometer feedback is running. Cannot disable the tracker. First disable the feedback using rt_feedback_disable()"
|
||||
)
|
||||
else:
|
||||
self.socket_put("T0")
|
||||
logger.info("Disabled the laser tracker")
|
||||
print("Disabled the laser tracker")
|
||||
|
||||
def laser_tracker_show_all(self):
|
||||
self.laser_update_tracker_info()
|
||||
t = PrettyTable()
|
||||
t.title = f"Laser Tracker Info"
|
||||
t.field_names = ["Name", "Value"]
|
||||
for key, val in self.tracker_info.items():
|
||||
t.add_row([key, val])
|
||||
print(t)
|
||||
|
||||
def laser_update_tracker_info(self):
|
||||
ret = self.socket_put_and_receive("Ts")
|
||||
|
||||
# remove trailing \n
|
||||
ret = ret.split("\n")[0]
|
||||
|
||||
tracker_values = [float(val) for val in ret.split(",")]
|
||||
self.tracker_info = {
|
||||
"tracker_intensity": tracker_values[2],
|
||||
"threshold_intensity_y": tracker_values[8],
|
||||
"enabled_y": bool(tracker_values[10]),
|
||||
"beampos_y": tracker_values[5],
|
||||
"target_y": tracker_values[6],
|
||||
"piezo_voltage_y": tracker_values[9],
|
||||
"threshold_intensity_z": tracker_values[3],
|
||||
"enabled_z": bool(tracker_values[10]),
|
||||
"beampos_z": tracker_values[0],
|
||||
"target_z": tracker_values[1],
|
||||
"piezo_voltage_z": tracker_values[4],
|
||||
}
|
||||
|
||||
def laser_tracker_galil_enable(self):
|
||||
ftrackz_con = self.get_device_manager().devices.ftrackz.obj.controller
|
||||
ftrackz_con.socket_put_confirmed("tracken=1")
|
||||
ftrackz_con.socket_put_confirmed("trackyct=0")
|
||||
ftrackz_con.socket_put_confirmed("trackzct=0")
|
||||
ftrackz_con.socket_put_confirmed("XQ#Tracker")
|
||||
|
||||
def laser_tracker_on_target(self) -> bool:
|
||||
self.laser_update_tracker_info()
|
||||
if np.isclose(
|
||||
self.tracker_info["beampos_y"], self.tracker_info["target_y"], atol=0.02
|
||||
) and np.isclose(self.tracker_info["beampos_z"], self.tracker_info["target_z"], atol=0.02):
|
||||
return True
|
||||
return False
|
||||
|
||||
def laser_tracker_wait_on_target(self):
|
||||
max_repeat = 25
|
||||
count = 0
|
||||
while not self.laser_tracker_on_target():
|
||||
self.laser_tracker_galil_enable()
|
||||
logger.info("Waiting for laser tracker to reach target.")
|
||||
time.sleep(0.5)
|
||||
count += 1
|
||||
if count > max_repeat:
|
||||
print("Failed to reach laser target position.")
|
||||
raise RtError("Failed to reach laser target position.")
|
||||
|
||||
def slew_rate_limiters_on_target(self) -> bool:
|
||||
ret = int(float(self.socket_put_and_receive("y").strip()))
|
||||
if ret == 3:
|
||||
return True
|
||||
return False
|
||||
|
||||
def pid_y(self) -> float:
|
||||
ret = float(self.socket_put_and_receive("G").strip())
|
||||
return ret
|
||||
|
||||
def read_ssi_interferometer(self, axis_number):
|
||||
val = float(self.socket_put_and_receive(f"j{axis_number}").strip())
|
||||
return val
|
||||
|
||||
def laser_tracker_check_signalstrength(self):
|
||||
if not self.laser_tracker_check_enabled():
|
||||
returnval = "disabled"
|
||||
else:
|
||||
returnval = "ok"
|
||||
self.laser_tracker_wait_on_target()
|
||||
|
||||
signal = self.read_ssi_interferometer(1)
|
||||
rtx = self.get_device_manager().devices.rtx
|
||||
min_signal = rtx.user_parameter.get("min_signal")
|
||||
low_signal = rtx.user_parameter.get("low_signal")
|
||||
if signal < min_signal:
|
||||
time.sleep(1)
|
||||
if signal < min_signal:
|
||||
print(
|
||||
f"\x1b[91mThe signal of the tracker {signal} is below the minimum required signal of {min_signal}. Readjustment requred!\x1b[0m"
|
||||
)
|
||||
returnval = "toolow"
|
||||
# raise RtError("The interferometer signal of tracker is too low.")
|
||||
elif signal < low_signal:
|
||||
print(
|
||||
f"\x1b[91mThe signal of the tracker {signal} is below the warning limit of {low_signal}. Readjustment recommended!\x1b[0m"
|
||||
)
|
||||
returnval = "low"
|
||||
return returnval
|
||||
|
||||
def show_signal_strength_interferometer(self):
|
||||
t = PrettyTable()
|
||||
t.title = f"Interferometer signal strength"
|
||||
t.field_names = ["Axis", "Value"]
|
||||
for i in range(4):
|
||||
t.add_row([i, self.read_ssi_interferometer(i)])
|
||||
print(t)
|
||||
|
||||
def _get_signals_from_table(self, return_table) -> dict:
|
||||
self.average_stdeviations_x_st_fzp += float(return_table[4])
|
||||
self.average_stdeviations_y_st_fzp += float(return_table[7])
|
||||
signals = {
|
||||
"target_x": {"value": float(return_table[2])},
|
||||
"average_x_st_fzp": {"value": float(return_table[3])},
|
||||
"stdev_x_st_fzp": {"value": float(return_table[4])},
|
||||
"target_y": {"value": float(return_table[5])},
|
||||
"average_y_st_fzp": {"value": float(return_table[6])},
|
||||
"stdev_y_st_fzp": {"value": float(return_table[7])},
|
||||
"average_rotz": {"value": float(return_table[8])},
|
||||
"stdev_rotz": {"value": float(return_table[9])},
|
||||
"average_stdeviations_x_st_fzp": {
|
||||
"value": self.average_stdeviations_x_st_fzp / (int(return_table[0]) + 1)
|
||||
},
|
||||
"average_stdeviations_y_st_fzp": {
|
||||
"value": self.average_stdeviations_y_st_fzp / (int(return_table[0]) + 1)
|
||||
},
|
||||
}
|
||||
return signals
|
||||
|
||||
@threadlocked
|
||||
def start_scan(self):
|
||||
if not self.feedback_is_running():
|
||||
logger.error(
|
||||
"Cannot start scan because feedback loop is not running or there is an"
|
||||
" interferometer error."
|
||||
)
|
||||
raise RtError(
|
||||
"Cannot start scan because feedback loop is not running or there is an"
|
||||
" interferometer error."
|
||||
)
|
||||
# here exception
|
||||
(mode, number_of_positions_planned, current_position_in_scan) = self.get_scan_status()
|
||||
|
||||
if number_of_positions_planned == 0:
|
||||
logger.error("Cannot start scan because no target positions are planned.")
|
||||
raise RtError("Cannot start scan because no target positions are planned.")
|
||||
# hier exception
|
||||
# start a point-by-point scan (for cont scan in flomni it would be "sa")
|
||||
self.socket_put_and_receive("sd")
|
||||
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def get_scan_status(self):
|
||||
return_table = (self.socket_put_and_receive("sr")).split(",")
|
||||
if len(return_table) != 3:
|
||||
raise RtCommunicationError(
|
||||
f"Expected to receive 3 return values. Instead received {return_table}"
|
||||
)
|
||||
mode = int(float(return_table[0]))
|
||||
# mode 0: direct positioning
|
||||
# mode 1: running internal timer (not tested/used anymore)
|
||||
# mode 2: rt point scan running
|
||||
# mode 3: rt point scan starting
|
||||
# mode 5/6: rt continuous scanning (not available in LamNI)
|
||||
number_of_positions_planned = int(float(return_table[1]))
|
||||
current_position_in_scan = int(float(return_table[2]))
|
||||
return (mode, number_of_positions_planned, current_position_in_scan)
|
||||
|
||||
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 read_positions_from_sampler(self):
|
||||
# this was for reading after the scan completed
|
||||
number_of_samples_to_read = 1 # self.get_scan_status()[1] #number of valid samples, will be updated upon first data read
|
||||
|
||||
read_counter = 0
|
||||
|
||||
self.average_stdeviations_x_st_fzp = 0
|
||||
self.average_stdeviations_y_st_fzp = 0
|
||||
self.average_lamni_angle = 0
|
||||
|
||||
mode, number_of_positions_planned, current_position_in_scan = self.get_scan_status()
|
||||
|
||||
# if not (mode==2 or mode==3):
|
||||
# error
|
||||
self.get_device_manager().connector.set(
|
||||
MessageEndpoints.device_status("rt_scan"),
|
||||
messages.DeviceStatusMessage(
|
||||
device="rt_scan", status=1, metadata=self.readout_metadata
|
||||
).dumps(),
|
||||
)
|
||||
# while scan is running
|
||||
while mode > 0:
|
||||
# logger.info(f"Current scan position {current_position_in_scan} out of {number_of_positions_planned}")
|
||||
mode, number_of_positions_planned, current_position_in_scan = self.get_scan_status()
|
||||
time.sleep(0.01)
|
||||
if current_position_in_scan > 5:
|
||||
while current_position_in_scan > read_counter + 1:
|
||||
return_table = (self.socket_put_and_receive(f"r{read_counter}")).split(",")
|
||||
# logger.info(f"{return_table}")
|
||||
logger.info(f"Read {read_counter} out of {number_of_positions_planned}")
|
||||
|
||||
read_counter = read_counter + 1
|
||||
|
||||
signals = self._get_signals_from_table(return_table)
|
||||
|
||||
self.publish_device_data(signals=signals, point_id=int(return_table[0]))
|
||||
|
||||
time.sleep(0.05)
|
||||
|
||||
# read the last samples even though scan is finished already
|
||||
while number_of_positions_planned > read_counter:
|
||||
return_table = (self.socket_put_and_receive(f"r{read_counter}")).split(",")
|
||||
logger.info(f"Read {read_counter} out of {number_of_positions_planned}")
|
||||
# logger.info(f"{return_table}")
|
||||
read_counter = read_counter + 1
|
||||
|
||||
signals = self._get_signals_from_table(return_table)
|
||||
self.publish_device_data(signals=signals, point_id=int(return_table[0]))
|
||||
|
||||
self.get_device_manager().connector.set(
|
||||
MessageEndpoints.device_status("rt_scan"),
|
||||
messages.DeviceStatusMessage(
|
||||
device="rt_scan", status=0, metadata=self.readout_metadata
|
||||
).dumps(),
|
||||
)
|
||||
|
||||
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}."
|
||||
)
|
||||
|
||||
def publish_device_data(self, signals, point_id):
|
||||
self.get_device_manager().connector.set_and_publish(
|
||||
MessageEndpoints.device_read("rt_flomni"),
|
||||
messages.DeviceMessage(
|
||||
signals=signals, metadata={"point_id": point_id, **self.readout_metadata}
|
||||
).dumps(),
|
||||
)
|
||||
|
||||
def start_readout(self):
|
||||
readout = threading.Thread(target=self.read_positions_from_sampler)
|
||||
readout.start()
|
||||
|
||||
def kickoff(self, metadata):
|
||||
self.readout_metadata = metadata
|
||||
while not self._min_scan_buffer_reached:
|
||||
time.sleep(0.001)
|
||||
self.start_scan()
|
||||
time.sleep(0.1)
|
||||
self.start_readout()
|
||||
|
||||
|
||||
class RtFlomniReadbackSignal(RtReadbackSignal):
|
||||
@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.
|
||||
"""
|
||||
time.sleep(0.1)
|
||||
return_table = (self.controller.socket_put_and_receive(f"pr")).split(",")
|
||||
|
||||
current_pos = float(return_table[self.parent.axis_Id_numeric])
|
||||
|
||||
current_pos *= self.parent.sign
|
||||
self.parent.user_setpoint.setpoint = current_pos
|
||||
return current_pos
|
||||
|
||||
|
||||
class RtFlomniSetpointSignal(RtSetpointSignal):
|
||||
setpoint = 0
|
||||
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def _socket_set(self, val: float) -> None:
|
||||
"""Set a new target value / setpoint value. Before submission, the target value is adjusted for the axis' sign.
|
||||
Furthermore, it is ensured that all axes are referenced before a new setpoint is submitted.
|
||||
|
||||
Args:
|
||||
val (float): Target value / setpoint value
|
||||
|
||||
Raises:
|
||||
RtError: Raised if interferometer feedback is disabled.
|
||||
|
||||
"""
|
||||
if not self.parent.controller.feedback_is_running():
|
||||
print(
|
||||
"The interferometer feedback is not running. Either it is turned off or and"
|
||||
" interferometer error occured."
|
||||
)
|
||||
raise RtError(
|
||||
"The interferometer feedback is not running. Either it is turned off or and"
|
||||
" interferometer error occured."
|
||||
)
|
||||
self.set_with_feedback_disabled(val)
|
||||
|
||||
def set_with_feedback_disabled(self, val):
|
||||
target_val = val * self.parent.sign
|
||||
self.setpoint = target_val
|
||||
self.controller.socket_put(f"pa{self.parent.axis_Id_numeric},{target_val:.4f}")
|
||||
|
||||
|
||||
class RtFlomniFeedbackRunning(RtSignalRO):
|
||||
@threadlocked
|
||||
def _socket_get(self):
|
||||
return int(self.parent.controller.feedback_is_running())
|
||||
|
||||
|
||||
class RtFlomniMotor(Device, PositionerBase):
|
||||
USER_ACCESS = ["controller"]
|
||||
readback = Cpt(RtFlomniReadbackSignal, signal_name="readback", kind="hinted")
|
||||
user_setpoint = Cpt(RtFlomniSetpointSignal, signal_name="setpoint")
|
||||
|
||||
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="mpc2844.psi.ch",
|
||||
port=2222,
|
||||
sign=1,
|
||||
socket_cls=SocketIO,
|
||||
device_manager=None,
|
||||
limits=None,
|
||||
**kwargs,
|
||||
):
|
||||
self.axis_Id = axis_Id
|
||||
self.sign = sign
|
||||
self.controller = RtFlomniController(
|
||||
socket_cls=socket_cls, socket_host=host, socket_port=port
|
||||
)
|
||||
self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric)
|
||||
self.device_manager = device_manager
|
||||
self.tolerance = kwargs.pop("tolerance", 0.5)
|
||||
|
||||
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._stopped = False
|
||||
|
||||
# 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 not self.controller.slew_rate_limiters_on_target() and not self._stopped:
|
||||
print("motor is moving")
|
||||
val = self.readback.read()
|
||||
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
|
||||
time.sleep(0.01)
|
||||
print("Move finished")
|
||||
self._done_moving(success=(not self._stopped))
|
||||
|
||||
self._stopped = False
|
||||
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(f"Only single-character axis_Ids are supported.")
|
||||
self._axis_Id_alpha = val
|
||||
self._axis_Id_numeric = ord(val.lower()) - 97
|
||||
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(f"Numeric value exceeds supported range.")
|
||||
self._axis_Id_alpha = val
|
||||
self._axis_Id_numeric = (chr(val + 97)).capitalize()
|
||||
else:
|
||||
raise TypeError(f"Expected value of type int but received {type(val)}")
|
||||
|
||||
def kickoff(self, metadata, **kwargs) -> None:
|
||||
self.controller.kickoff(metadata)
|
||||
|
||||
@property
|
||||
def egu(self):
|
||||
"""The engineering units (EGU) for positions"""
|
||||
return "um"
|
||||
|
||||
# how is this used later?
|
||||
|
||||
def stage(self) -> List[object]:
|
||||
return super().stage()
|
||||
|
||||
def unstage(self) -> List[object]:
|
||||
return super().unstage()
|
||||
|
||||
def stop(self, *, success=False):
|
||||
self.controller.stop_all_axes()
|
||||
self._stopped = True
|
||||
return super().stop(success=success)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
rtcontroller = RtFlomniController(
|
||||
socket_cls=SocketIO, socket_host="mpc2844.psi.ch", socket_port=2222
|
||||
)
|
||||
rtcontroller.on()
|
||||
rtcontroller.laser_tracker_on()
|
||||
@@ -1,847 +0,0 @@
|
||||
import functools
|
||||
import threading
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
from bec_lib import MessageEndpoints, bec_logger, messages
|
||||
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, ReadOnlyError
|
||||
from ophyd_devices.utils.controller import Controller, threadlocked
|
||||
from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class RtLamniCommunicationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class RtLamniError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BECConfigError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def retry_once(fcn):
|
||||
"""Decorator to rerun a function in case a CommunicationError 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 (RtLamniCommunicationError, RtLamniError):
|
||||
val = fcn(self, *args, **kwargs)
|
||||
return val
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class RtLamniController(Controller):
|
||||
USER_ACCESS = [
|
||||
"socket_put_and_receive",
|
||||
"set_rotation_angle",
|
||||
"feedback_disable",
|
||||
"feedback_enable_without_reset",
|
||||
"feedback_disable_and_even_reset_lamni_angle_interferometer",
|
||||
"feedback_enable_with_reset",
|
||||
"add_pos_to_scan",
|
||||
"clear_trajectory_generator",
|
||||
"_set_axis_velocity",
|
||||
"_set_axis_velocity_maximum_speed",
|
||||
"_position_sampling_single_read",
|
||||
"_position_sampling_single_reset_and_start_sampling",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
name="RtLamniController",
|
||||
kind=None,
|
||||
parent=None,
|
||||
socket=None,
|
||||
attr_name="",
|
||||
labels=None,
|
||||
):
|
||||
if not hasattr(self, "_initialized") or not self._initialized:
|
||||
self._rtlamni_axis_per_controller = 3
|
||||
self._axis = [None for axis_num in range(self._rtlamni_axis_per_controller)]
|
||||
self._min_scan_buffer_reached = False
|
||||
super().__init__(
|
||||
name=name,
|
||||
socket=socket,
|
||||
attr_name=attr_name,
|
||||
parent=parent,
|
||||
labels=labels,
|
||||
kind=kind,
|
||||
)
|
||||
self.readout_metadata = {}
|
||||
|
||||
def on(self, controller_num=0) -> None:
|
||||
"""Open a new socket connection to the controller"""
|
||||
if not self.connected:
|
||||
try:
|
||||
self.sock.open()
|
||||
# discuss - after disconnect takes a while for the server to be ready again
|
||||
max_retries = 10
|
||||
tries = 0
|
||||
while not self.connected:
|
||||
try:
|
||||
welcome_message = self.sock.receive()
|
||||
self.connected = True
|
||||
except ConnectionResetError as conn_reset:
|
||||
if tries > max_retries:
|
||||
raise conn_reset
|
||||
tries += 1
|
||||
time.sleep(2)
|
||||
except ConnectionRefusedError as conn_error:
|
||||
logger.error("Failed to open a connection to RTLamNI.")
|
||||
raise RtLamniCommunicationError from conn_error
|
||||
|
||||
else:
|
||||
logger.info("The connection has already been established.")
|
||||
# warnings.warn(f"The connection has already been established.", stacklevel=2)
|
||||
|
||||
self._update_flyer_device_info()
|
||||
|
||||
def off(self) -> None:
|
||||
"""Close the socket connection to the controller"""
|
||||
if self.connected:
|
||||
self.sock.close()
|
||||
self.connected = False
|
||||
else:
|
||||
logger.info("The connection is already closed.")
|
||||
|
||||
def set_axis(self, axis: Device, axis_nr: int) -> None:
|
||||
"""Assign an axis to a device instance.
|
||||
|
||||
Args:
|
||||
axis (Device): Device instance (e.g. GalilMotor)
|
||||
axis_nr (int): Controller axis number
|
||||
|
||||
"""
|
||||
self._axis[axis_nr] = axis
|
||||
|
||||
@threadlocked
|
||||
def socket_put(self, val: str) -> None:
|
||||
self.sock.put(f"{val}\n".encode())
|
||||
|
||||
@threadlocked
|
||||
def socket_get(self) -> str:
|
||||
return self.sock.receive().decode()
|
||||
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def socket_put_and_receive(self, val: str, remove_trailing_chars=True) -> str:
|
||||
self.socket_put(val)
|
||||
if remove_trailing_chars:
|
||||
return self._remove_trailing_characters(self.sock.receive().decode())
|
||||
return self.socket_get()
|
||||
|
||||
def is_axis_moving(self, axis_Id) -> bool:
|
||||
# this checks that axis is on target
|
||||
axis_is_on_target = bool(float(self.socket_put_and_receive(f"o")))
|
||||
return not axis_is_on_target
|
||||
|
||||
# def is_thread_active(self, thread_id: int) -> bool:
|
||||
# val = float(self.socket_put_and_receive(f"MG_XQ{thread_id}"))
|
||||
# if val == -1:
|
||||
# return False
|
||||
# return True
|
||||
|
||||
def _remove_trailing_characters(self, var) -> str:
|
||||
if len(var) > 1:
|
||||
return var.split("\r\n")[0]
|
||||
return var
|
||||
|
||||
@threadlocked
|
||||
def set_rotation_angle(self, val: float):
|
||||
self.socket_put(f"a{(val-300+30.538)/180*np.pi}")
|
||||
|
||||
@threadlocked
|
||||
def stop_all_axes(self):
|
||||
self.socket_put("sc")
|
||||
|
||||
@threadlocked
|
||||
def feedback_disable(self):
|
||||
self.socket_put("J0")
|
||||
logger.info("LamNI Feedback disabled.")
|
||||
self.set_device_enabled("lsamx", True)
|
||||
self.set_device_enabled("lsamy", True)
|
||||
self.set_device_enabled("loptx", True)
|
||||
self.set_device_enabled("lopty", True)
|
||||
self.set_device_enabled("loptz", True)
|
||||
|
||||
@threadlocked
|
||||
def _set_axis_velocity(self, um_per_s):
|
||||
self.socket_put(f"V{um_per_s}")
|
||||
|
||||
@threadlocked
|
||||
def _set_axis_velocity_maximum_speed(self):
|
||||
self.socket_put(f"V0")
|
||||
|
||||
# for developement of soft continuous scanning
|
||||
@threadlocked
|
||||
def _position_sampling_single_reset_and_start_sampling(self):
|
||||
self.socket_put(f"Ss")
|
||||
|
||||
@threadlocked
|
||||
def _position_sampling_single_read(self):
|
||||
(number_of_samples, sum0, sum0_2, sum1, sum1_2, sum2, sum2_2) = self.socket_put_and_receive(
|
||||
f"Sr"
|
||||
).split(",")
|
||||
avg_x = float(sum1) / int(number_of_samples)
|
||||
avg_y = float(sum0) / int(number_of_samples)
|
||||
stdev_x = np.sqrt(
|
||||
float(sum1_2) / int(number_of_samples)
|
||||
- np.power(float(sum1) / int(number_of_samples), 2)
|
||||
)
|
||||
stdev_y = np.sqrt(
|
||||
float(sum0_2) / int(number_of_samples)
|
||||
- np.power(float(sum0) / int(number_of_samples), 2)
|
||||
)
|
||||
return (avg_x, avg_y, stdev_x, stdev_y)
|
||||
|
||||
@threadlocked
|
||||
def feedback_enable_without_reset(self):
|
||||
# read current interferometer position
|
||||
return_table = (self.socket_put_and_receive(f"J4")).split(",")
|
||||
x_curr = float(return_table[2])
|
||||
y_curr = float(return_table[1])
|
||||
# set these as closed loop target position
|
||||
self.socket_put(f"pa0,{x_curr:.4f}")
|
||||
self.socket_put(f"pa1,{y_curr:.4f}")
|
||||
self.get_device_manager().devices.rtx.obj.user_setpoint.set_with_feedback_disabled(x_curr)
|
||||
self.get_device_manager().devices.rty.obj.user_setpoint.set_with_feedback_disabled(y_curr)
|
||||
self.socket_put("J5")
|
||||
logger.info("LamNI Feedback enabled (without reset).")
|
||||
self.set_device_enabled("lsamx", False)
|
||||
self.set_device_enabled("lsamy", False)
|
||||
self.set_device_enabled("loptx", False)
|
||||
self.set_device_enabled("lopty", False)
|
||||
self.set_device_enabled("loptz", False)
|
||||
|
||||
@threadlocked
|
||||
def feedback_disable_and_even_reset_lamni_angle_interferometer(self):
|
||||
self.socket_put("J6")
|
||||
logger.info("LamNI Feedback disabled including the angular interferometer.")
|
||||
self.set_device_enabled("lsamx", True)
|
||||
self.set_device_enabled("lsamy", True)
|
||||
self.set_device_enabled("loptx", True)
|
||||
self.set_device_enabled("lopty", True)
|
||||
self.set_device_enabled("loptz", True)
|
||||
|
||||
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 get_axis_by_name(self, name):
|
||||
for axis in self._axis:
|
||||
if axis:
|
||||
if axis.name == name:
|
||||
return axis
|
||||
raise RuntimeError(f"Could not find an axis with name {name}")
|
||||
|
||||
@threadlocked
|
||||
def clear_trajectory_generator(self):
|
||||
self.socket_put("sc")
|
||||
logger.info("LamNI scan stopped and deleted, moving to start position")
|
||||
|
||||
def add_pos_to_scan(self, positions) -> None:
|
||||
def send_positions(parent, positions):
|
||||
parent._min_scan_buffer_reached = False
|
||||
for pos_index, pos in enumerate(positions):
|
||||
parent.socket_put_and_receive(f"s{pos[0]:.05f},{pos[1]:05f},0")
|
||||
if pos_index > 100:
|
||||
parent._min_scan_buffer_reached = True
|
||||
parent._min_scan_buffer_reached = True
|
||||
|
||||
threading.Thread(target=send_positions, args=(self, positions), daemon=True).start()
|
||||
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def get_scan_status(self):
|
||||
return_table = (self.socket_put_and_receive(f"sr")).split(",")
|
||||
if len(return_table) != 3:
|
||||
raise RtLamniCommunicationError(
|
||||
f"Expected to receive 3 return values. Instead received {return_table}"
|
||||
)
|
||||
mode = int(return_table[0])
|
||||
# mode 0: direct positioning
|
||||
# mode 1: running internal timer (not tested/used anymore)
|
||||
# mode 2: rt point scan running
|
||||
# mode 3: rt point scan starting
|
||||
# mode 5/6: rt continuous scanning (not available in LamNI)
|
||||
number_of_positions_planned = int(return_table[1])
|
||||
current_position_in_scan = int(return_table[2])
|
||||
return (mode, number_of_positions_planned, current_position_in_scan)
|
||||
|
||||
@threadlocked
|
||||
def start_scan(self):
|
||||
interferometer_feedback_not_running = int((self.socket_put_and_receive("J2")).split(",")[0])
|
||||
if interferometer_feedback_not_running == 1:
|
||||
logger.error(
|
||||
"Cannot start scan because feedback loop is not running or there is an interferometer error."
|
||||
)
|
||||
raise RtLamniError(
|
||||
"Cannot start scan because feedback loop is not running or there is an interferometer error."
|
||||
)
|
||||
# here exception
|
||||
(mode, number_of_positions_planned, current_position_in_scan) = self.get_scan_status()
|
||||
|
||||
if number_of_positions_planned == 0:
|
||||
logger.error("Cannot start scan because no target positions are planned.")
|
||||
raise RtLamniError("Cannot start scan because no target positions are planned.")
|
||||
# hier exception
|
||||
# start a point-by-point scan (for cont scan in flomni it would be "sa")
|
||||
self.socket_put_and_receive("sd")
|
||||
|
||||
def start_readout(self):
|
||||
readout = threading.Thread(target=self.read_positions_from_sampler)
|
||||
readout.start()
|
||||
|
||||
def _update_flyer_device_info(self):
|
||||
flyer_info = self._get_flyer_device_info()
|
||||
self.get_device_manager().connector.set(
|
||||
MessageEndpoints.device_info("rt_scan"),
|
||||
messages.DeviceInfoMessage(device="rt_scan", info=flyer_info),
|
||||
)
|
||||
|
||||
def _get_flyer_device_info(self) -> dict:
|
||||
return {
|
||||
"device_name": self.name,
|
||||
"device_attr_name": getattr(self, "attr_name", ""),
|
||||
"device_dotted_name": getattr(self, "dotted_name", ""),
|
||||
"device_info": {
|
||||
"device_base_class": "ophydobject",
|
||||
"signals": [],
|
||||
"hints": {"fields": ["average_x_st_fzp", "average_y_st_fzp"]},
|
||||
"describe": {},
|
||||
"describe_configuration": {},
|
||||
"sub_devices": [],
|
||||
"custom_user_access": [],
|
||||
},
|
||||
}
|
||||
|
||||
def kickoff(self, metadata):
|
||||
self.readout_metadata = metadata
|
||||
while not self._min_scan_buffer_reached:
|
||||
time.sleep(0.001)
|
||||
self.start_scan()
|
||||
time.sleep(0.1)
|
||||
self.start_readout()
|
||||
|
||||
def _get_signals_from_table(self, return_table) -> dict:
|
||||
self.average_stdeviations_x_st_fzp += float(return_table[5])
|
||||
self.average_stdeviations_y_st_fzp += float(return_table[8])
|
||||
self.average_lamni_angle += float(return_table[19])
|
||||
signals = {
|
||||
"target_x": {"value": float(return_table[3])},
|
||||
"average_x_st_fzp": {"value": float(return_table[4])},
|
||||
"stdev_x_st_fzp": {"value": float(return_table[5])},
|
||||
"target_y": {"value": float(return_table[6])},
|
||||
"average_y_st_fzp": {"value": float(return_table[7])},
|
||||
"stdev_y_st_fzp": {"value": float(return_table[8])},
|
||||
"average_cap1": {"value": float(return_table[9])},
|
||||
"stdev_cap1": {"value": float(return_table[10])},
|
||||
"average_cap2": {"value": float(return_table[11])},
|
||||
"stdev_cap2": {"value": float(return_table[12])},
|
||||
"average_cap3": {"value": float(return_table[13])},
|
||||
"stdev_cap3": {"value": float(return_table[14])},
|
||||
"average_cap4": {"value": float(return_table[15])},
|
||||
"stdev_cap4": {"value": float(return_table[16])},
|
||||
"average_cap5": {"value": float(return_table[17])},
|
||||
"stdev_cap5": {"value": float(return_table[18])},
|
||||
"average_angle_interf_ST": {"value": float(return_table[19])},
|
||||
"stdev_angle_interf_ST": {"value": float(return_table[20])},
|
||||
"average_stdeviations_x_st_fzp": {
|
||||
"value": self.average_stdeviations_x_st_fzp / (int(return_table[0]) + 1)
|
||||
},
|
||||
"average_stdeviations_y_st_fzp": {
|
||||
"value": self.average_stdeviations_y_st_fzp / (int(return_table[0]) + 1)
|
||||
},
|
||||
"average_lamni_angle": {"value": self.average_lamni_angle / (int(return_table[0]) + 1)},
|
||||
}
|
||||
return signals
|
||||
|
||||
def read_positions_from_sampler(self):
|
||||
# this was for reading after the scan completed
|
||||
number_of_samples_to_read = 1 # self.get_scan_status()[1] #number of valid samples, will be updated upon first data read
|
||||
|
||||
read_counter = 0
|
||||
previous_point_in_scan = 0
|
||||
|
||||
self.average_stdeviations_x_st_fzp = 0
|
||||
self.average_stdeviations_y_st_fzp = 0
|
||||
self.average_lamni_angle = 0
|
||||
|
||||
mode, number_of_positions_planned, current_position_in_scan = self.get_scan_status()
|
||||
|
||||
# if not (mode==2 or mode==3):
|
||||
# error
|
||||
self.get_device_manager().connector.set(
|
||||
MessageEndpoints.device_status("rt_scan"),
|
||||
messages.DeviceStatusMessage(
|
||||
device="rt_scan", status=1, metadata=self.readout_metadata
|
||||
),
|
||||
)
|
||||
# while scan is running
|
||||
while mode > 0:
|
||||
# logger.info(f"Current scan position {current_position_in_scan} out of {number_of_positions_planned}")
|
||||
mode, number_of_positions_planned, current_position_in_scan = self.get_scan_status()
|
||||
time.sleep(0.01)
|
||||
if current_position_in_scan > 5:
|
||||
while current_position_in_scan > read_counter + 1:
|
||||
return_table = (self.socket_put_and_receive(f"r{read_counter}")).split(",")
|
||||
# logger.info(f"{return_table}")
|
||||
logger.info(f"Read {read_counter} out of {number_of_positions_planned}")
|
||||
|
||||
read_counter = read_counter + 1
|
||||
|
||||
signals = self._get_signals_from_table(return_table)
|
||||
|
||||
self.publish_device_data(signals=signals, point_id=int(return_table[0]))
|
||||
|
||||
time.sleep(0.05)
|
||||
|
||||
# read the last samples even though scan is finished already
|
||||
while number_of_positions_planned > read_counter:
|
||||
return_table = (self.socket_put_and_receive(f"r{read_counter}")).split(",")
|
||||
logger.info(f"Read {read_counter} out of {number_of_positions_planned}")
|
||||
# logger.info(f"{return_table}")
|
||||
read_counter = read_counter + 1
|
||||
|
||||
signals = self._get_signals_from_table(return_table)
|
||||
self.publish_device_data(signals=signals, point_id=int(return_table[0]))
|
||||
|
||||
self.get_device_manager().connector.set(
|
||||
MessageEndpoints.device_status("rt_scan"),
|
||||
messages.DeviceStatusMessage(
|
||||
device="rt_scan", status=0, metadata=self.readout_metadata
|
||||
),
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"LamNI statistics: Average of all standard deviations: x {self.average_stdeviations_x_st_fzp/number_of_samples_to_read}, y {self.average_stdeviations_y_st_fzp/number_of_samples_to_read}, angle {self.average_lamni_angle/number_of_samples_to_read}."
|
||||
)
|
||||
|
||||
def publish_device_data(self, signals, point_id):
|
||||
self.get_device_manager().connector.set_and_publish(
|
||||
MessageEndpoints.device_read("rt_lamni"),
|
||||
messages.DeviceMessage(
|
||||
signals=signals, metadata={"point_id": point_id, **self.readout_metadata}
|
||||
),
|
||||
)
|
||||
|
||||
def feedback_status_angle_lamni(self) -> bool:
|
||||
return_table = (self.socket_put_and_receive(f"J7")).split(",")
|
||||
logger.debug(
|
||||
f"LamNI angle interferomter status {bool(return_table[0])}, position {float(return_table[1])}, signal {float(return_table[2])}"
|
||||
)
|
||||
return bool(return_table[0])
|
||||
|
||||
def feedback_enable_with_reset(self):
|
||||
if not self.feedback_status_angle_lamni():
|
||||
self.feedback_disable_and_even_reset_lamni_angle_interferometer()
|
||||
logger.info(f"LamNI resetting interferometer inclusive angular interferomter.")
|
||||
else:
|
||||
self.feedback_disable()
|
||||
logger.info(
|
||||
f"LamNI resetting interferomter except angular interferometer which is already running."
|
||||
)
|
||||
|
||||
# set these as closed loop target position
|
||||
|
||||
self.socket_put(f"pa0,0")
|
||||
self.get_axis_by_name("rtx").user_setpoint.setpoint = 0
|
||||
self.socket_put(f"pa1,0")
|
||||
self.get_axis_by_name("rty").user_setpoint.setpoint = 0
|
||||
self.socket_put(
|
||||
f"pa2,0"
|
||||
) # we set all three outputs of the traj. gen. although in LamNI case only 0,1 are used
|
||||
self.clear_trajectory_generator()
|
||||
|
||||
self.get_device_manager().devices.lsamrot.obj.move(0, wait=True)
|
||||
|
||||
galil_controller_rt_status = (
|
||||
self.get_device_manager().devices.lsamx.obj.controller.lgalil_is_air_off_and_orchestra_enabled()
|
||||
)
|
||||
|
||||
if galil_controller_rt_status == 0:
|
||||
logger.error(
|
||||
"Cannot enable feedback. The small rotation air is on and/or orchestra disabled by the motor controller."
|
||||
)
|
||||
raise RtLamniError(
|
||||
"Cannot enable feedback. The small rotation air is on and/or orchestra disabled by the motor controller."
|
||||
)
|
||||
|
||||
time.sleep(0.03)
|
||||
|
||||
lsamx_user_params = self.get_device_manager().devices.lsamx.user_parameter
|
||||
if lsamx_user_params is None or lsamx_user_params.get("center") is None:
|
||||
raise RuntimeError("lsamx center is not defined")
|
||||
lsamy_user_params = self.get_device_manager().devices.lsamy.user_parameter
|
||||
if lsamy_user_params is None or lsamy_user_params.get("center") is None:
|
||||
raise RuntimeError("lsamy center is not defined")
|
||||
lsamx_center = lsamx_user_params.get("center")
|
||||
lsamy_center = lsamy_user_params.get("center")
|
||||
self.get_device_manager().devices.lsamx.obj.move(lsamx_center, wait=True)
|
||||
self.get_device_manager().devices.lsamy.obj.move(lsamy_center, wait=True)
|
||||
self.socket_put("J1")
|
||||
|
||||
_waitforfeedbackctr = 0
|
||||
|
||||
interferometer_feedback_not_running = int((self.socket_put_and_receive("J2")).split(",")[0])
|
||||
|
||||
while interferometer_feedback_not_running == 1 and _waitforfeedbackctr < 100:
|
||||
time.sleep(0.01)
|
||||
_waitforfeedbackctr = _waitforfeedbackctr + 1
|
||||
interferometer_feedback_not_running = int(
|
||||
(self.socket_put_and_receive("J2")).split(",")[0]
|
||||
)
|
||||
|
||||
self.set_device_enabled("lsamx", False)
|
||||
self.set_device_enabled("lsamy", False)
|
||||
self.set_device_enabled("loptx", False)
|
||||
self.set_device_enabled("lopty", False)
|
||||
self.set_device_enabled("loptz", False)
|
||||
|
||||
if interferometer_feedback_not_running == 1:
|
||||
logger.error(
|
||||
"Cannot start scan because feedback loop is not running or there is an interferometer error."
|
||||
)
|
||||
raise RtLamniError(
|
||||
"Cannot start scan because feedback loop is not running or there is an interferometer error."
|
||||
)
|
||||
|
||||
time.sleep(0.01)
|
||||
|
||||
# ptychography_alignment_done = 0
|
||||
|
||||
def set_device_enabled(self, device_name: str, enabled: bool) -> None:
|
||||
"""enable / disable a device"""
|
||||
if device_name not in self.get_device_manager().devices:
|
||||
logger.warning(
|
||||
f"Device {device_name} is not configured and cannot be enabled/disabled."
|
||||
)
|
||||
return
|
||||
self.get_device_manager().devices[device_name].read_only = not enabled
|
||||
|
||||
|
||||
class RtLamniSignalBase(SocketSignal):
|
||||
def __init__(self, signal_name, **kwargs):
|
||||
self.signal_name = signal_name
|
||||
super().__init__(**kwargs)
|
||||
self.controller = self.parent.controller
|
||||
self.sock = self.parent.controller.sock
|
||||
|
||||
|
||||
class RtLamniSignalRO(RtLamniSignalBase):
|
||||
def __init__(self, signal_name, **kwargs):
|
||||
super().__init__(signal_name, **kwargs)
|
||||
self._metadata["write_access"] = False
|
||||
|
||||
def _socket_set(self, val):
|
||||
raise ReadOnlyError("Read-only signals cannot be set")
|
||||
|
||||
|
||||
class RtLamniReadbackSignal(RtLamniSignalRO):
|
||||
@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.
|
||||
"""
|
||||
return_table = (self.controller.socket_put_and_receive(f"J4")).split(",")
|
||||
print(return_table)
|
||||
if self.parent.axis_Id_numeric == 0:
|
||||
readback_index = 2
|
||||
elif self.parent.axis_Id_numeric == 1:
|
||||
readback_index = 1
|
||||
else:
|
||||
raise RtLamniError("Currently, only two axes are supported.")
|
||||
|
||||
current_pos = float(return_table[readback_index])
|
||||
|
||||
current_pos *= self.parent.sign
|
||||
return current_pos
|
||||
|
||||
|
||||
class RtLamniSetpointSignal(RtLamniSignalBase):
|
||||
setpoint = 0
|
||||
|
||||
def _socket_get(self) -> float:
|
||||
"""Get command for receiving the setpoint / target value.
|
||||
The value is not pulled from the controller but instead just the last setpoint used.
|
||||
|
||||
Returns:
|
||||
float: setpoint / target value
|
||||
"""
|
||||
return self.setpoint
|
||||
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def _socket_set(self, val: float) -> None:
|
||||
"""Set a new target value / setpoint value. Before submission, the target value is adjusted for the axis' sign.
|
||||
Furthermore, it is ensured that all axes are referenced before a new setpoint is submitted.
|
||||
|
||||
Args:
|
||||
val (float): Target value / setpoint value
|
||||
|
||||
Raises:
|
||||
RtLamniError: Raised if interferometer feedback is disabled.
|
||||
|
||||
"""
|
||||
interferometer_feedback_not_running = int(
|
||||
(self.controller.socket_put_and_receive("J2")).split(",")[0]
|
||||
)
|
||||
if interferometer_feedback_not_running != 0:
|
||||
raise RtLamniError(
|
||||
"The interferometer feedback is not running. Either it is turned off or and interferometer error occured."
|
||||
)
|
||||
self.set_with_feedback_disabled(val)
|
||||
|
||||
def set_with_feedback_disabled(self, val):
|
||||
target_val = val * self.parent.sign
|
||||
self.setpoint = target_val
|
||||
self.controller.socket_put(f"pa{self.parent.axis_Id_numeric},{target_val:.4f}")
|
||||
|
||||
|
||||
class RtLamniMotorIsMoving(RtLamniSignalRO):
|
||||
def _socket_get(self):
|
||||
return self.controller.is_axis_moving(self.parent.axis_Id_numeric)
|
||||
|
||||
def get(self):
|
||||
val = super().get()
|
||||
if val is not None:
|
||||
self._run_subs(sub_type=self.SUB_VALUE, value=val, timestamp=time.time())
|
||||
return val
|
||||
|
||||
|
||||
class RtLamniFeedbackRunning(RtLamniSignalRO):
|
||||
@threadlocked
|
||||
def _socket_get(self):
|
||||
if int((self.controller.socket_put_and_receive("J2")).split(",")[0]) == 0:
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
class RtLamniMotor(Device, PositionerBase):
|
||||
USER_ACCESS = ["controller"]
|
||||
readback = Cpt(RtLamniReadbackSignal, signal_name="readback", kind="hinted")
|
||||
user_setpoint = Cpt(RtLamniSetpointSignal, signal_name="setpoint")
|
||||
|
||||
motor_is_moving = Cpt(RtLamniMotorIsMoving, signal_name="motor_is_moving", kind="normal")
|
||||
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="mpc2680.psi.ch",
|
||||
port=3333,
|
||||
sign=1,
|
||||
socket_cls=SocketIO,
|
||||
device_manager=None,
|
||||
limits=None,
|
||||
**kwargs,
|
||||
):
|
||||
self.axis_Id = axis_Id
|
||||
self.sign = sign
|
||||
self.controller = RtLamniController(socket=socket_cls(host=host, port=port))
|
||||
self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric)
|
||||
self.device_manager = device_manager
|
||||
self.tolerance = kwargs.pop("tolerance", 0.5)
|
||||
|
||||
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():
|
||||
print("motor is moving")
|
||||
val = self.readback.read()
|
||||
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
|
||||
time.sleep(0.01)
|
||||
print("Move finished")
|
||||
self._done_moving()
|
||||
|
||||
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(f"Only single-character axis_Ids are supported.")
|
||||
self._axis_Id_alpha = val
|
||||
self._axis_Id_numeric = ord(val.lower()) - 97
|
||||
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(f"Numeric value exceeds supported range.")
|
||||
self._axis_Id_alpha = val
|
||||
self._axis_Id_numeric = (chr(val + 97)).capitalize()
|
||||
else:
|
||||
raise TypeError(f"Expected value of type int but received {type(val)}")
|
||||
|
||||
def kickoff(self, metadata, **kwargs) -> None:
|
||||
self.controller.kickoff(metadata)
|
||||
|
||||
@property
|
||||
def egu(self):
|
||||
"""The engineering units (EGU) for positions"""
|
||||
return "um"
|
||||
|
||||
# how is this used later?
|
||||
|
||||
def stage(self) -> list[object]:
|
||||
return super().stage()
|
||||
|
||||
def unstage(self) -> list[object]:
|
||||
return super().unstage()
|
||||
|
||||
def stop(self, *, success=False):
|
||||
self.controller.stop_all_axes()
|
||||
return super().stop(success=success)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
mock = False
|
||||
if not mock:
|
||||
rty = RtLamniMotor("B", name="rty", host="mpc2680.psi.ch", port=3333, sign=1)
|
||||
rty.stage()
|
||||
status = rty.move(0, wait=True)
|
||||
status = rty.move(10, wait=True)
|
||||
rty.read()
|
||||
|
||||
rty.get()
|
||||
rty.describe()
|
||||
|
||||
rty.unstage()
|
||||
else:
|
||||
from ophyd_devices.utils.socket import SocketMock
|
||||
|
||||
rtx = RtLamniMotor("A", name="rtx", host="mpc2680.psi.ch", port=3333, socket_cls=SocketMock)
|
||||
rty = RtLamniMotor("B", name="rty", host="mpc2680.psi.ch", port=3333, socket_cls=SocketMock)
|
||||
rtx.stage()
|
||||
# rty.stage()
|
||||
@@ -1,817 +0,0 @@
|
||||
import functools
|
||||
import threading
|
||||
import time
|
||||
from typing import List
|
||||
|
||||
import numpy as np
|
||||
from bec_lib import MessageEndpoints, bec_logger, messages
|
||||
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, ReadOnlyError
|
||||
from ophyd_devices.utils.controller import Controller, threadlocked
|
||||
from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class RtCommunicationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class RtError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BECConfigError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def retry_once(fcn):
|
||||
"""Decorator to rerun a function in case a CommunicationError 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 (RtCommunicationError, RtError):
|
||||
val = fcn(self, *args, **kwargs)
|
||||
return val
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class RtController(Controller):
|
||||
_axes_per_controller = 3
|
||||
USER_ACCESS = [
|
||||
"socket_put_and_receive",
|
||||
"set_rotation_angle",
|
||||
"feedback_disable",
|
||||
"feedback_enable_without_reset",
|
||||
"feedback_disable_and_even_reset_lamni_angle_interferometer",
|
||||
"feedback_enable_with_reset",
|
||||
"add_pos_to_scan",
|
||||
"clear_trajectory_generator",
|
||||
"_set_axis_velocity",
|
||||
"_set_axis_velocity_maximum_speed",
|
||||
"_position_sampling_single_read",
|
||||
"_position_sampling_single_reset_and_start_sampling",
|
||||
]
|
||||
|
||||
def on(self, controller_num=0) -> None:
|
||||
"""Open a new socket connection to the controller"""
|
||||
# if not self.connected:
|
||||
# try:
|
||||
# self.sock.open()
|
||||
# # discuss - after disconnect takes a while for the server to be ready again
|
||||
# max_retries = 10
|
||||
# tries = 0
|
||||
# while not self.connected:
|
||||
# try:
|
||||
# welcome_message = self.sock.receive()
|
||||
# self.connected = True
|
||||
# except ConnectionResetError as conn_reset:
|
||||
# if tries > max_retries:
|
||||
# raise conn_reset
|
||||
# tries += 1
|
||||
# time.sleep(2)
|
||||
# except ConnectionRefusedError as conn_error:
|
||||
# logger.error("Failed to open a connection to RTLamNI.")
|
||||
# raise RtCommunicationError from conn_error
|
||||
|
||||
# else:
|
||||
# logger.info("The connection has already been established.")
|
||||
# # warnings.warn(f"The connection has already been established.", stacklevel=2)
|
||||
super().on()
|
||||
# self._update_flyer_device_info()
|
||||
|
||||
def set_axis(self, axis: Device, axis_nr: int) -> None:
|
||||
"""Assign an axis to a device instance.
|
||||
|
||||
Args:
|
||||
axis (Device): Device instance (e.g. GalilMotor)
|
||||
axis_nr (int): Controller axis number
|
||||
|
||||
"""
|
||||
self._axis[axis_nr] = axis
|
||||
|
||||
@threadlocked
|
||||
def socket_put(self, val: str) -> None:
|
||||
self.sock.put(f"{val}\n".encode())
|
||||
|
||||
@threadlocked
|
||||
def socket_get(self) -> str:
|
||||
return self.sock.receive().decode()
|
||||
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def socket_put_and_receive(self, val: str, remove_trailing_chars=True) -> str:
|
||||
self.socket_put(val)
|
||||
if remove_trailing_chars:
|
||||
return self._remove_trailing_characters(self.sock.receive().decode())
|
||||
return self.socket_get()
|
||||
|
||||
def is_axis_moving(self, axis_Id) -> bool:
|
||||
# this checks that axis is on target
|
||||
axis_is_on_target = bool(float(self.socket_put_and_receive(f"o")))
|
||||
return not axis_is_on_target
|
||||
|
||||
# def is_thread_active(self, thread_id: int) -> bool:
|
||||
# val = float(self.socket_put_and_receive(f"MG_XQ{thread_id}"))
|
||||
# if val == -1:
|
||||
# return False
|
||||
# return True
|
||||
|
||||
def _remove_trailing_characters(self, var) -> str:
|
||||
if len(var) > 1:
|
||||
return var.split("\r\n")[0]
|
||||
return var
|
||||
|
||||
@threadlocked
|
||||
def set_rotation_angle(self, val: float):
|
||||
self.socket_put(f"a{(val-300+30.538)/180*np.pi}")
|
||||
|
||||
@threadlocked
|
||||
def stop_all_axes(self):
|
||||
self.socket_put("sc")
|
||||
|
||||
@threadlocked
|
||||
def feedback_disable(self):
|
||||
self.socket_put("J0")
|
||||
logger.info("LamNI Feedback disabled.")
|
||||
self.set_device_enabled("lsamx", True)
|
||||
self.set_device_enabled("lsamy", True)
|
||||
self.set_device_enabled("loptx", True)
|
||||
self.set_device_enabled("lopty", True)
|
||||
self.set_device_enabled("loptz", True)
|
||||
|
||||
@threadlocked
|
||||
def _set_axis_velocity(self, um_per_s):
|
||||
self.socket_put(f"V{um_per_s}")
|
||||
|
||||
@threadlocked
|
||||
def _set_axis_velocity_maximum_speed(self):
|
||||
self.socket_put(f"V0")
|
||||
|
||||
# for developement of soft continuous scanning
|
||||
@threadlocked
|
||||
def _position_sampling_single_reset_and_start_sampling(self):
|
||||
self.socket_put(f"Ss")
|
||||
|
||||
@threadlocked
|
||||
def _position_sampling_single_read(self):
|
||||
(number_of_samples, sum0, sum0_2, sum1, sum1_2, sum2, sum2_2) = self.socket_put_and_receive(
|
||||
f"Sr"
|
||||
).split(",")
|
||||
avg_x = float(sum1) / int(number_of_samples)
|
||||
avg_y = float(sum0) / int(number_of_samples)
|
||||
stdev_x = np.sqrt(
|
||||
float(sum1_2) / int(number_of_samples)
|
||||
- np.power(float(sum1) / int(number_of_samples), 2)
|
||||
)
|
||||
stdev_y = np.sqrt(
|
||||
float(sum0_2) / int(number_of_samples)
|
||||
- np.power(float(sum0) / int(number_of_samples), 2)
|
||||
)
|
||||
return (avg_x, avg_y, stdev_x, stdev_y)
|
||||
|
||||
@threadlocked
|
||||
def feedback_enable_without_reset(self):
|
||||
# read current interferometer position
|
||||
return_table = (self.socket_put_and_receive(f"J4")).split(",")
|
||||
x_curr = float(return_table[2])
|
||||
y_curr = float(return_table[1])
|
||||
# set these as closed loop target position
|
||||
self.socket_put(f"pa0,{x_curr:.4f}")
|
||||
self.socket_put(f"pa1,{y_curr:.4f}")
|
||||
self.get_device_manager().devices.rtx.obj.user_setpoint.set_with_feedback_disabled(x_curr)
|
||||
self.get_device_manager().devices.rty.obj.user_setpoint.set_with_feedback_disabled(y_curr)
|
||||
self.socket_put("J5")
|
||||
logger.info("LamNI Feedback enabled (without reset).")
|
||||
self.set_device_enabled("lsamx", False)
|
||||
self.set_device_enabled("lsamy", False)
|
||||
self.set_device_enabled("loptx", False)
|
||||
self.set_device_enabled("lopty", False)
|
||||
self.set_device_enabled("loptz", False)
|
||||
|
||||
@threadlocked
|
||||
def feedback_disable_and_even_reset_lamni_angle_interferometer(self):
|
||||
self.socket_put("J6")
|
||||
logger.info("LamNI Feedback disabled including the angular interferometer.")
|
||||
self.set_device_enabled("lsamx", True)
|
||||
self.set_device_enabled("lsamy", True)
|
||||
self.set_device_enabled("loptx", True)
|
||||
self.set_device_enabled("lopty", True)
|
||||
self.set_device_enabled("loptz", True)
|
||||
|
||||
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 get_axis_by_name(self, name):
|
||||
for axis in self._axis:
|
||||
if axis:
|
||||
if axis.name == name:
|
||||
return axis
|
||||
raise RuntimeError(f"Could not find an axis with name {name}")
|
||||
|
||||
@threadlocked
|
||||
def clear_trajectory_generator(self):
|
||||
self.socket_put("sc")
|
||||
logger.info("LamNI scan stopped and deleted, moving to start position")
|
||||
|
||||
def add_pos_to_scan(self, positions) -> None:
|
||||
def send_positions(parent, positions):
|
||||
parent._min_scan_buffer_reached = False
|
||||
for pos_index, pos in enumerate(positions):
|
||||
parent.socket_put_and_receive(f"s{pos[0]},{pos[1]},0")
|
||||
if pos_index > 100:
|
||||
parent._min_scan_buffer_reached = True
|
||||
parent._min_scan_buffer_reached = True
|
||||
|
||||
threading.Thread(target=send_positions, args=(self, positions), daemon=True).start()
|
||||
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def get_scan_status(self):
|
||||
return_table = (self.socket_put_and_receive(f"sr")).split(",")
|
||||
if len(return_table) != 3:
|
||||
raise RtCommunicationError(
|
||||
f"Expected to receive 3 return values. Instead received {return_table}"
|
||||
)
|
||||
mode = int(return_table[0])
|
||||
# mode 0: direct positioning
|
||||
# mode 1: running internal timer (not tested/used anymore)
|
||||
# mode 2: rt point scan running
|
||||
# mode 3: rt point scan starting
|
||||
# mode 5/6: rt continuous scanning (not available in LamNI)
|
||||
number_of_positions_planned = int(return_table[1])
|
||||
current_position_in_scan = int(return_table[2])
|
||||
return (mode, number_of_positions_planned, current_position_in_scan)
|
||||
|
||||
@threadlocked
|
||||
def start_scan(self):
|
||||
interferometer_feedback_not_running = int((self.socket_put_and_receive("J2")).split(",")[0])
|
||||
if interferometer_feedback_not_running == 1:
|
||||
logger.error(
|
||||
"Cannot start scan because feedback loop is not running or there is an interferometer error."
|
||||
)
|
||||
raise RtError(
|
||||
"Cannot start scan because feedback loop is not running or there is an interferometer error."
|
||||
)
|
||||
# here exception
|
||||
(mode, number_of_positions_planned, current_position_in_scan) = self.get_scan_status()
|
||||
|
||||
if number_of_positions_planned == 0:
|
||||
logger.error("Cannot start scan because no target positions are planned.")
|
||||
raise RtError("Cannot start scan because no target positions are planned.")
|
||||
# hier exception
|
||||
# start a point-by-point scan (for cont scan in flomni it would be "sa")
|
||||
self.socket_put_and_receive("sd")
|
||||
|
||||
def start_readout(self):
|
||||
readout = threading.Thread(target=self.read_positions_from_sampler)
|
||||
readout.start()
|
||||
|
||||
def _update_flyer_device_info(self):
|
||||
flyer_info = self._get_flyer_device_info()
|
||||
self.get_device_manager().connector.set(
|
||||
MessageEndpoints.device_info("rt_scan"),
|
||||
messages.DeviceInfoMessage(device="rt_scan", info=flyer_info).dumps(),
|
||||
)
|
||||
|
||||
def _get_flyer_device_info(self) -> dict:
|
||||
return {
|
||||
"device_name": self.name,
|
||||
"device_attr_name": getattr(self, "attr_name", ""),
|
||||
"device_dotted_name": getattr(self, "dotted_name", ""),
|
||||
"device_info": {
|
||||
"device_base_class": "ophydobject",
|
||||
"signals": [],
|
||||
"hints": {"fields": ["average_x_st_fzp", "average_y_st_fzp"]},
|
||||
"describe": {},
|
||||
"describe_configuration": {},
|
||||
"sub_devices": [],
|
||||
"custom_user_access": [],
|
||||
},
|
||||
}
|
||||
|
||||
def kickoff(self, metadata):
|
||||
self.readout_metadata = metadata
|
||||
while not self._min_scan_buffer_reached:
|
||||
time.sleep(0.001)
|
||||
self.start_scan()
|
||||
time.sleep(0.1)
|
||||
self.start_readout()
|
||||
|
||||
def _get_signals_from_table(self, return_table) -> dict:
|
||||
self.average_stdeviations_x_st_fzp += float(return_table[5])
|
||||
self.average_stdeviations_y_st_fzp += float(return_table[8])
|
||||
self.average_lamni_angle += float(return_table[19])
|
||||
signals = {
|
||||
"target_x": {"value": float(return_table[3])},
|
||||
"average_x_st_fzp": {"value": float(return_table[4])},
|
||||
"stdev_x_st_fzp": {"value": float(return_table[5])},
|
||||
"target_y": {"value": float(return_table[6])},
|
||||
"average_y_st_fzp": {"value": float(return_table[7])},
|
||||
"stdev_y_st_fzp": {"value": float(return_table[8])},
|
||||
"average_cap1": {"value": float(return_table[9])},
|
||||
"stdev_cap1": {"value": float(return_table[10])},
|
||||
"average_cap2": {"value": float(return_table[11])},
|
||||
"stdev_cap2": {"value": float(return_table[12])},
|
||||
"average_cap3": {"value": float(return_table[13])},
|
||||
"stdev_cap3": {"value": float(return_table[14])},
|
||||
"average_cap4": {"value": float(return_table[15])},
|
||||
"stdev_cap4": {"value": float(return_table[16])},
|
||||
"average_cap5": {"value": float(return_table[17])},
|
||||
"stdev_cap5": {"value": float(return_table[18])},
|
||||
"average_angle_interf_ST": {"value": float(return_table[19])},
|
||||
"stdev_angle_interf_ST": {"value": float(return_table[20])},
|
||||
"average_stdeviations_x_st_fzp": {
|
||||
"value": self.average_stdeviations_x_st_fzp / (int(return_table[0]) + 1)
|
||||
},
|
||||
"average_stdeviations_y_st_fzp": {
|
||||
"value": self.average_stdeviations_y_st_fzp / (int(return_table[0]) + 1)
|
||||
},
|
||||
"average_lamni_angle": {"value": self.average_lamni_angle / (int(return_table[0]) + 1)},
|
||||
}
|
||||
return signals
|
||||
|
||||
def read_positions_from_sampler(self):
|
||||
# this was for reading after the scan completed
|
||||
number_of_samples_to_read = 1 # self.get_scan_status()[1] #number of valid samples, will be updated upon first data read
|
||||
|
||||
read_counter = 0
|
||||
previous_point_in_scan = 0
|
||||
|
||||
self.average_stdeviations_x_st_fzp = 0
|
||||
self.average_stdeviations_y_st_fzp = 0
|
||||
self.average_lamni_angle = 0
|
||||
|
||||
mode, number_of_positions_planned, current_position_in_scan = self.get_scan_status()
|
||||
|
||||
# if not (mode==2 or mode==3):
|
||||
# error
|
||||
self.get_device_manager().connector.set(
|
||||
MessageEndpoints.device_status("rt_scan"),
|
||||
messages.DeviceStatusMessage(
|
||||
device="rt_scan", status=1, metadata=self.readout_metadata
|
||||
).dumps(),
|
||||
)
|
||||
# while scan is running
|
||||
while mode > 0:
|
||||
# logger.info(f"Current scan position {current_position_in_scan} out of {number_of_positions_planned}")
|
||||
mode, number_of_positions_planned, current_position_in_scan = self.get_scan_status()
|
||||
time.sleep(0.01)
|
||||
if current_position_in_scan > 5:
|
||||
while current_position_in_scan > read_counter + 1:
|
||||
return_table = (self.socket_put_and_receive(f"r{read_counter}")).split(",")
|
||||
# logger.info(f"{return_table}")
|
||||
logger.info(f"Read {read_counter} out of {number_of_positions_planned}")
|
||||
|
||||
read_counter = read_counter + 1
|
||||
|
||||
signals = self._get_signals_from_table(return_table)
|
||||
|
||||
self.publish_device_data(signals=signals, point_id=int(return_table[0]))
|
||||
|
||||
time.sleep(0.05)
|
||||
|
||||
# read the last samples even though scan is finished already
|
||||
while number_of_positions_planned > read_counter:
|
||||
return_table = (self.socket_put_and_receive(f"r{read_counter}")).split(",")
|
||||
logger.info(f"Read {read_counter} out of {number_of_positions_planned}")
|
||||
# logger.info(f"{return_table}")
|
||||
read_counter = read_counter + 1
|
||||
|
||||
signals = self._get_signals_from_table(return_table)
|
||||
self.publish_device_data(signals=signals, point_id=int(return_table[0]))
|
||||
|
||||
self.get_device_manager().connector.set(
|
||||
MessageEndpoints.device_status("rt_scan"),
|
||||
messages.DeviceStatusMessage(
|
||||
device="rt_scan", status=0, metadata=self.readout_metadata
|
||||
).dumps(),
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"LamNI statistics: Average of all standard deviations: x {self.average_stdeviations_x_st_fzp/number_of_samples_to_read}, y {self.average_stdeviations_y_st_fzp/number_of_samples_to_read}, angle {self.average_lamni_angle/number_of_samples_to_read}."
|
||||
)
|
||||
|
||||
def publish_device_data(self, signals, point_id):
|
||||
self.get_device_manager().connector.set_and_publish(
|
||||
MessageEndpoints.device_read("rt_lamni"),
|
||||
messages.DeviceMessage(
|
||||
signals=signals, metadata={"point_id": point_id, **self.readout_metadata}
|
||||
).dumps(),
|
||||
)
|
||||
|
||||
def feedback_status_angle_lamni(self) -> bool:
|
||||
return_table = (self.socket_put_and_receive(f"J7")).split(",")
|
||||
logger.debug(
|
||||
f"LamNI angle interferomter status {bool(return_table[0])}, position {float(return_table[1])}, signal {float(return_table[2])}"
|
||||
)
|
||||
return bool(return_table[0])
|
||||
|
||||
def feedback_enable_with_reset(self):
|
||||
if not self.feedback_status_angle_lamni():
|
||||
self.feedback_disable_and_even_reset_lamni_angle_interferometer()
|
||||
logger.info(f"LamNI resetting interferometer inclusive angular interferomter.")
|
||||
else:
|
||||
self.feedback_disable()
|
||||
logger.info(
|
||||
f"LamNI resetting interferomter except angular interferometer which is already running."
|
||||
)
|
||||
|
||||
# set these as closed loop target position
|
||||
|
||||
self.socket_put(f"pa0,0")
|
||||
self.get_axis_by_name("rtx").user_setpoint.setpoint = 0
|
||||
self.socket_put(f"pa1,0")
|
||||
self.get_axis_by_name("rty").user_setpoint.setpoint = 0
|
||||
self.socket_put(
|
||||
f"pa2,0"
|
||||
) # we set all three outputs of the traj. gen. although in LamNI case only 0,1 are used
|
||||
self.clear_trajectory_generator()
|
||||
|
||||
self.get_device_manager().devices.lsamrot.obj.move(0, wait=True)
|
||||
|
||||
galil_controller_rt_status = (
|
||||
self.get_device_manager().devices.lsamx.obj.controller.lgalil_is_air_off_and_orchestra_enabled()
|
||||
)
|
||||
|
||||
if galil_controller_rt_status == 0:
|
||||
logger.error(
|
||||
"Cannot enable feedback. The small rotation air is on and/or orchestra disabled by the motor controller."
|
||||
)
|
||||
raise RtError(
|
||||
"Cannot enable feedback. The small rotation air is on and/or orchestra disabled by the motor controller."
|
||||
)
|
||||
|
||||
time.sleep(0.03)
|
||||
|
||||
lsamx_user_params = self.get_device_manager().devices.lsamx.user_parameter
|
||||
if lsamx_user_params is None or lsamx_user_params.get("center") is None:
|
||||
raise RuntimeError("lsamx center is not defined")
|
||||
lsamy_user_params = self.get_device_manager().devices.lsamy.user_parameter
|
||||
if lsamy_user_params is None or lsamy_user_params.get("center") is None:
|
||||
raise RuntimeError("lsamy center is not defined")
|
||||
lsamx_center = lsamx_user_params.get("center")
|
||||
lsamy_center = lsamy_user_params.get("center")
|
||||
self.get_device_manager().devices.lsamx.obj.move(lsamx_center, wait=True)
|
||||
self.get_device_manager().devices.lsamy.obj.move(lsamy_center, wait=True)
|
||||
self.socket_put("J1")
|
||||
|
||||
_waitforfeedbackctr = 0
|
||||
|
||||
interferometer_feedback_not_running = int((self.socket_put_and_receive("J2")).split(",")[0])
|
||||
|
||||
while interferometer_feedback_not_running == 1 and _waitforfeedbackctr < 100:
|
||||
time.sleep(0.01)
|
||||
_waitforfeedbackctr = _waitforfeedbackctr + 1
|
||||
interferometer_feedback_not_running = int(
|
||||
(self.socket_put_and_receive("J2")).split(",")[0]
|
||||
)
|
||||
|
||||
self.set_device_enabled("lsamx", False)
|
||||
self.set_device_enabled("lsamy", False)
|
||||
self.set_device_enabled("loptx", False)
|
||||
self.set_device_enabled("lopty", False)
|
||||
self.set_device_enabled("loptz", False)
|
||||
|
||||
if interferometer_feedback_not_running == 1:
|
||||
logger.error(
|
||||
"Cannot start scan because feedback loop is not running or there is an interferometer error."
|
||||
)
|
||||
raise RtError(
|
||||
"Cannot start scan because feedback loop is not running or there is an interferometer error."
|
||||
)
|
||||
|
||||
time.sleep(0.01)
|
||||
|
||||
# ptychography_alignment_done = 0
|
||||
|
||||
def set_device_enabled(self, device_name: str, enabled: bool) -> None:
|
||||
"""enable / disable a device"""
|
||||
if device_name not in self.get_device_manager().devices:
|
||||
logger.warning(
|
||||
f"Device {device_name} is not configured and cannot be enabled/disabled."
|
||||
)
|
||||
return
|
||||
self.get_device_manager().devices[device_name].read_only = not enabled
|
||||
|
||||
|
||||
class RtSignalBase(SocketSignal):
|
||||
def __init__(self, signal_name, **kwargs):
|
||||
self.signal_name = signal_name
|
||||
super().__init__(**kwargs)
|
||||
self.controller = self.parent.controller
|
||||
self.sock = self.parent.controller.sock
|
||||
|
||||
|
||||
class RtSignalRO(RtSignalBase):
|
||||
def __init__(self, signal_name, **kwargs):
|
||||
super().__init__(signal_name, **kwargs)
|
||||
self._metadata["write_access"] = False
|
||||
|
||||
def _socket_set(self, val):
|
||||
raise ReadOnlyError("Read-only signals cannot be set")
|
||||
|
||||
|
||||
class RtReadbackSignal(RtSignalRO):
|
||||
@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.
|
||||
"""
|
||||
return_table = (self.controller.socket_put_and_receive(f"J4")).split(",")
|
||||
print(return_table)
|
||||
if self.parent.axis_Id_numeric == 0:
|
||||
readback_index = 2
|
||||
elif self.parent.axis_Id_numeric == 1:
|
||||
readback_index = 1
|
||||
else:
|
||||
raise RtError("Currently, only two axes are supported.")
|
||||
|
||||
current_pos = float(return_table[readback_index])
|
||||
|
||||
current_pos *= self.parent.sign
|
||||
return current_pos
|
||||
|
||||
|
||||
class RtSetpointSignal(RtSignalBase):
|
||||
setpoint = 0
|
||||
|
||||
def _socket_get(self) -> float:
|
||||
"""Get command for receiving the setpoint / target value.
|
||||
The value is not pulled from the controller but instead just the last setpoint used.
|
||||
|
||||
Returns:
|
||||
float: setpoint / target value
|
||||
"""
|
||||
return self.setpoint
|
||||
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def _socket_set(self, val: float) -> None:
|
||||
"""Set a new target value / setpoint value. Before submission, the target value is adjusted for the axis' sign.
|
||||
Furthermore, it is ensured that all axes are referenced before a new setpoint is submitted.
|
||||
|
||||
Args:
|
||||
val (float): Target value / setpoint value
|
||||
|
||||
Raises:
|
||||
RtError: Raised if interferometer feedback is disabled.
|
||||
|
||||
"""
|
||||
interferometer_feedback_not_running = int(
|
||||
(self.controller.socket_put_and_receive("J2")).split(",")[0]
|
||||
)
|
||||
if interferometer_feedback_not_running != 0:
|
||||
raise RtError(
|
||||
"The interferometer feedback is not running. Either it is turned off or and interferometer error occured."
|
||||
)
|
||||
self.set_with_feedback_disabled(val)
|
||||
|
||||
def set_with_feedback_disabled(self, val):
|
||||
target_val = val * self.parent.sign
|
||||
self.setpoint = target_val
|
||||
self.controller.socket_put(f"pa{self.parent.axis_Id_numeric},{target_val:.4f}")
|
||||
|
||||
|
||||
class RtMotorIsMoving(RtSignalRO):
|
||||
def _socket_get(self):
|
||||
return self.controller.is_axis_moving(self.parent.axis_Id_numeric)
|
||||
|
||||
def get(self):
|
||||
val = super().get()
|
||||
if val is not None:
|
||||
self._run_subs(sub_type=self.SUB_VALUE, value=val, timestamp=time.time())
|
||||
return val
|
||||
|
||||
|
||||
class RtFeedbackRunning(RtSignalRO):
|
||||
@threadlocked
|
||||
def _socket_get(self):
|
||||
if int((self.controller.socket_put_and_receive("J2")).split(",")[0]) == 0:
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
class RtMotor(Device, PositionerBase):
|
||||
USER_ACCESS = ["controller"]
|
||||
readback = Cpt(RtReadbackSignal, signal_name="readback", kind="hinted")
|
||||
user_setpoint = Cpt(RtSetpointSignal, signal_name="setpoint")
|
||||
|
||||
motor_is_moving = Cpt(RtMotorIsMoving, signal_name="motor_is_moving", kind="normal")
|
||||
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="mpc2680.psi.ch",
|
||||
port=3333,
|
||||
sign=1,
|
||||
socket_cls=SocketIO,
|
||||
device_manager=None,
|
||||
limits=None,
|
||||
**kwargs,
|
||||
):
|
||||
self.axis_Id = axis_Id
|
||||
self.sign = sign
|
||||
self.controller = RtController(socket=socket_cls(host=host, port=port))
|
||||
self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric)
|
||||
self.device_manager = device_manager
|
||||
self.tolerance = kwargs.pop("tolerance", 0.5)
|
||||
|
||||
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():
|
||||
print("motor is moving")
|
||||
val = self.readback.read()
|
||||
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
|
||||
time.sleep(0.01)
|
||||
print("Move finished")
|
||||
self._done_moving()
|
||||
|
||||
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(f"Only single-character axis_Ids are supported.")
|
||||
self._axis_Id_alpha = val
|
||||
self._axis_Id_numeric = ord(val.lower()) - 97
|
||||
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(f"Numeric value exceeds supported range.")
|
||||
self._axis_Id_alpha = val
|
||||
self._axis_Id_numeric = (chr(val + 97)).capitalize()
|
||||
else:
|
||||
raise TypeError(f"Expected value of type int but received {type(val)}")
|
||||
|
||||
def kickoff(self, metadata, **kwargs) -> None:
|
||||
self.controller.kickoff(metadata)
|
||||
|
||||
@property
|
||||
def egu(self):
|
||||
"""The engineering units (EGU) for positions"""
|
||||
return "um"
|
||||
|
||||
# how is this used later?
|
||||
|
||||
def stage(self) -> List[object]:
|
||||
return super().stage()
|
||||
|
||||
def unstage(self) -> List[object]:
|
||||
return super().unstage()
|
||||
|
||||
def stop(self, *, success=False):
|
||||
self.controller.stop_all_axes()
|
||||
return super().stop(success=success)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
mock = False
|
||||
if not mock:
|
||||
rty = RtLamniMotor("B", name="rty", host="mpc2680.psi.ch", port=3333, sign=1)
|
||||
rty.stage()
|
||||
status = rty.move(0, wait=True)
|
||||
status = rty.move(10, wait=True)
|
||||
rty.read()
|
||||
|
||||
rty.get()
|
||||
rty.describe()
|
||||
|
||||
rty.unstage()
|
||||
else:
|
||||
from ophyd_devices.utils.socket import SocketMock
|
||||
|
||||
rtx = RtLamniMotor("A", name="rtx", host="mpc2680.psi.ch", port=3333, socket_cls=SocketMock)
|
||||
rty = RtLamniMotor("B", name="rty", host="mpc2680.psi.ch", port=3333, socket_cls=SocketMock)
|
||||
rtx.stage()
|
||||
# rty.stage()
|
||||
@@ -1,9 +0,0 @@
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, EpicsSignal
|
||||
|
||||
|
||||
class Xeye(Device):
|
||||
save_frame = Cpt(EpicsSignal, "XOMNY-XEYE-SAVEFRAME:0")
|
||||
acquisition_done = Cpt(EpicsSignal, "XOMNY-XEYE-ACQDONE:0")
|
||||
acquisition = Cpt(EpicsSignal, "XOMNY-XEYE-ACQ:0")
|
||||
x_width = Cpt(EpicsSignal, "XOMNY-XEYE-XWIDTH_X:0")
|
||||
@@ -1,2 +0,0 @@
|
||||
from .smaract_controller import SmaractController
|
||||
from .smaract_ophyd import SmaractMotor
|
||||
@@ -1,44 +0,0 @@
|
||||
import json
|
||||
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, PositionerBase, Signal
|
||||
from ophyd.ophydobj import OphydObject
|
||||
from ophyd_devices.utils.socket import SocketMock
|
||||
|
||||
|
||||
def get_user_functions(obj) -> list:
|
||||
exclude_list = ["log", "SUB_CONNECTION_CHANGE"]
|
||||
exclude_classes = [Device, OphydObject, PositionerBase, Signal, Cpt]
|
||||
for cls in exclude_classes:
|
||||
exclude_list.extend(dir(cls))
|
||||
access_list = [
|
||||
func for func in dir(obj) if not func.startswith("_") and func not in exclude_list
|
||||
]
|
||||
|
||||
return access_list
|
||||
|
||||
|
||||
def is_serializable(f) -> bool:
|
||||
try:
|
||||
json.dumps(f)
|
||||
return True
|
||||
except (TypeError, OverflowError):
|
||||
return False
|
||||
|
||||
|
||||
def get_user_interface(obj, obj_interface):
|
||||
# user_funcs = get_user_functions(obj)
|
||||
for f in [f for f in dir(obj) if f in obj.USER_ACCESS]:
|
||||
if f == "controller" or f == "on":
|
||||
print(f)
|
||||
m = getattr(obj, f)
|
||||
if not callable(m):
|
||||
if is_serializable(m):
|
||||
obj_interface[f] = {"type": type(m).__name__}
|
||||
elif isinstance(m, SocketMock):
|
||||
pass
|
||||
else:
|
||||
obj_interface[f] = get_user_interface(m, {})
|
||||
else:
|
||||
obj_interface[f] = {"type": "func"}
|
||||
return obj_interface
|
||||
@@ -1,507 +0,0 @@
|
||||
import enum
|
||||
import functools
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
from ophyd_devices.utils.controller import Controller, axis_checked, threadlocked
|
||||
from prettytable import PrettyTable
|
||||
from typeguard import typechecked
|
||||
|
||||
from csaxs_bec.devices.smaract.smaract_errors import SmaractCommunicationError, SmaractErrorCode
|
||||
|
||||
logger = logging.getLogger("smaract_controller")
|
||||
|
||||
|
||||
class SmaractCommunicationMode(enum.Enum):
|
||||
SYNC = 0
|
||||
ASYNC = 1
|
||||
|
||||
|
||||
def retry_once(fcn):
|
||||
"""Decorator to rerun a function in case a SmaractCommunicationError 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 (SmaractCommunicationError, SmaractErrorCode):
|
||||
val = fcn(self, *args, **kwargs)
|
||||
return val
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class SmaractChannelStatus(enum.Enum):
|
||||
STOPPED = 0
|
||||
STEPPING = 1
|
||||
SCANNING = 2
|
||||
HOLDING = 3
|
||||
TARGETING = 4
|
||||
MOVE_DELAY = 5
|
||||
CALIBRATING = 6
|
||||
FINDING_REFERENCE_MARK = 7
|
||||
LOCKED = 9
|
||||
|
||||
|
||||
class SmaractSensorDefinition:
|
||||
def __init__(self, symbol, type_code, positioner_series, comment, reference_type) -> None:
|
||||
self.symbol = symbol
|
||||
self.type_code = type_code
|
||||
self.comment = comment
|
||||
self.positioner_series = positioner_series
|
||||
self.reference_type = reference_type
|
||||
|
||||
|
||||
class SmaractSensors:
|
||||
smaract_sensor_definition_file = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)), "smaract_sensors.json"
|
||||
)
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.avail_sensors = {}
|
||||
|
||||
with open(self.smaract_sensor_definition_file) as json_file:
|
||||
sensor_list = json.load(json_file)
|
||||
for sensor in sensor_list:
|
||||
self.avail_sensors[sensor["type_code"]] = SmaractSensorDefinition(**sensor)
|
||||
|
||||
|
||||
class SmaractController(Controller):
|
||||
_axes_per_controller = 6
|
||||
_initialized = False
|
||||
USER_ACCESS = [
|
||||
"socket_put_and_receive",
|
||||
"smaract_show_all",
|
||||
"move_open_loop_steps",
|
||||
"find_reference_mark",
|
||||
"describe",
|
||||
"axis_is_referenced",
|
||||
"all_axes_referenced",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
name="SmaractController",
|
||||
kind=None,
|
||||
parent=None,
|
||||
socket_cls=None,
|
||||
socket_host=None,
|
||||
socket_port=None,
|
||||
attr_name="",
|
||||
labels=None,
|
||||
):
|
||||
if not self._initialized:
|
||||
super().__init__(
|
||||
name=name,
|
||||
socket_cls=socket_cls,
|
||||
socket_host=socket_host,
|
||||
socket_port=socket_port,
|
||||
attr_name=attr_name,
|
||||
parent=parent,
|
||||
labels=labels,
|
||||
kind=kind,
|
||||
)
|
||||
self._sensors = SmaractSensors()
|
||||
|
||||
@threadlocked
|
||||
def socket_put(self, val: str):
|
||||
self.sock.put(f":{val}\n".encode())
|
||||
|
||||
@threadlocked
|
||||
def socket_get(self):
|
||||
return self.sock.receive().decode()
|
||||
|
||||
@threadlocked
|
||||
def socket_put_and_receive(
|
||||
self, val: str, remove_trailing_chars=True, check_for_errors=True, raise_if_not_status=False
|
||||
) -> str:
|
||||
self.socket_put(val)
|
||||
return_val = ""
|
||||
max_wait_time = 1
|
||||
elapsed_time = 0
|
||||
sleep_time = 0.01
|
||||
while True:
|
||||
ret = self.socket_get()
|
||||
return_val += ret
|
||||
if ret.endswith("\n"):
|
||||
break
|
||||
time.sleep(sleep_time)
|
||||
elapsed_time += sleep_time
|
||||
if elapsed_time > max_wait_time:
|
||||
break
|
||||
if remove_trailing_chars:
|
||||
return_val = self._remove_trailing_characters(return_val)
|
||||
logger.debug(f"Sending {val}; Returned {return_val}")
|
||||
if check_for_errors:
|
||||
self._check_for_error(return_val, raise_if_not_status=raise_if_not_status)
|
||||
return return_val
|
||||
|
||||
@retry_once
|
||||
@axis_checked
|
||||
def get_status(self, axis_Id_numeric: int) -> SmaractChannelStatus:
|
||||
"""Returns the current movement status code of a positioner or end effector.This command can be used to check whether a previously issued movement command has been completed.
|
||||
|
||||
Args:
|
||||
axis_Id_numeric (int): Axis number
|
||||
|
||||
Returns:
|
||||
SmaractChannelStatus: Channel status
|
||||
"""
|
||||
return_val = self.socket_put_and_receive(f"GS{axis_Id_numeric}")
|
||||
if self._message_starts_with(return_val, f":S{axis_Id_numeric}"):
|
||||
return SmaractChannelStatus(int(return_val.split(",")[1]))
|
||||
|
||||
@retry_once
|
||||
@axis_checked
|
||||
def is_axis_moving(self, axis_Id_numeric: int) -> bool:
|
||||
"""Check if axis is moving. Returns true upon open loop move, scanning, closed loop move or reference mark search.
|
||||
|
||||
Args:
|
||||
axis_Id_numeric (int): Axis number.
|
||||
|
||||
Returns:
|
||||
bool: True if axis is moving.
|
||||
"""
|
||||
axis_status = self.get_status(axis_Id_numeric)
|
||||
return axis_status in [
|
||||
SmaractChannelStatus.STEPPING,
|
||||
SmaractChannelStatus.SCANNING,
|
||||
SmaractChannelStatus.TARGETING,
|
||||
SmaractChannelStatus.FINDING_REFERENCE_MARK,
|
||||
]
|
||||
|
||||
@retry_once
|
||||
def stop_all_axes(self):
|
||||
return [
|
||||
self.socket_put_and_receive(f"S{ax.axis_Id_numeric}", raise_if_not_status=True)
|
||||
for ax in self._axis
|
||||
if ax is not None
|
||||
]
|
||||
|
||||
@retry_once
|
||||
@axis_checked
|
||||
def axis_is_referenced(self, axis_Id_numeric: int) -> bool:
|
||||
return_val = self.socket_put_and_receive(f"GPPK{axis_Id_numeric}")
|
||||
if self._message_starts_with(return_val, f":PPK{axis_Id_numeric}"):
|
||||
return bool(int(return_val.split(",")[1]))
|
||||
|
||||
def all_axes_referenced(self) -> bool:
|
||||
return all(
|
||||
self.axis_is_referenced(ax.axis_Id_numeric) for ax in self._axis if ax is not None
|
||||
)
|
||||
|
||||
@retry_once
|
||||
@axis_checked
|
||||
def get_position(self, axis_Id_numeric: int) -> float:
|
||||
"""Returns the current position of a positioner.
|
||||
|
||||
Args:
|
||||
axis_Id_numeric (int): Axis number.
|
||||
|
||||
Returns:
|
||||
float: Position in mm
|
||||
"""
|
||||
return_val = self.socket_put_and_receive(f"GP{axis_Id_numeric}")
|
||||
if self._message_starts_with(return_val, f":P{axis_Id_numeric}"):
|
||||
return float(return_val.split(",")[1]) / 1e6
|
||||
|
||||
@retry_once
|
||||
@axis_checked
|
||||
@typechecked
|
||||
def move_axis_to_absolute_position(
|
||||
self, axis_Id_numeric: int, target_val: float, hold_time: int = 1000
|
||||
) -> None:
|
||||
"""Instructs a positioner to move to a specific position.
|
||||
|
||||
Args:
|
||||
axis_Id_numeric (int): Axis number.
|
||||
target_val (float): Target position in mm.
|
||||
hold_time (int, optional): Specifies how long (in milliseconds) the position is actively held after reaching the target. The valid range is 0..60,000. A 0 deactivates this feature, a value of 60,000 is infinite (until manually stopped, see S command). Defaults to 1000.
|
||||
|
||||
"""
|
||||
self.socket_put_and_receive(
|
||||
f"MPA{axis_Id_numeric},{int(np.round(target_val*1e6))},{hold_time}",
|
||||
raise_if_not_status=True,
|
||||
)
|
||||
|
||||
@retry_once
|
||||
@axis_checked
|
||||
@typechecked
|
||||
def move_axis_to_relative_position(
|
||||
self, axis_Id_numeric: int, target_val: float, hold_time: int = 1000
|
||||
) -> None:
|
||||
"""Instructs a positioner to move to a position relative to its current position.
|
||||
|
||||
Args:
|
||||
axis_Id_numeric (int): Axis number.
|
||||
target_val (float): Relative position to move to in mm.
|
||||
hold_time (int, optional): Specifies how long (in milliseconds) the position is actively held after reaching the target. The valid range is 0..60,000. A 0 deactivates this feature, a value of 60,000 is infinite (until manually stopped, see S command). Defaults to 1000.
|
||||
|
||||
"""
|
||||
self.socket_put_and_receive(
|
||||
f"MPR{axis_Id_numeric},{int(np.round(target_val*1e6))},{hold_time}",
|
||||
raise_if_not_status=True,
|
||||
)
|
||||
|
||||
@retry_once
|
||||
@axis_checked
|
||||
@typechecked
|
||||
def move_open_loop_steps(
|
||||
self, axis_Id_numeric: int, steps: int, amplitude: int = 4000, frequency: int = 2000
|
||||
) -> None:
|
||||
"""Move open loop steps. It performs a burst of steps with the given parameters.
|
||||
|
||||
Args:
|
||||
axis_Id_numeric (int): Axis number.
|
||||
steps (int): Number and direction of steps to perform. The valid range is -30,000..30,000. A value of 0 stops the positioner, but see S command. A value of 30,000 or -30,000 performs an unbounded move. This should be used with caution since the positioner will only stop on an S command.
|
||||
amplitude (int): Amplitude that the steps are performed with. Lower amplitude values result in a smaller step width. The parameter must be given as a 12bit value (range 0..4,095). 0 corresponds to 0V, 4,095 to 100V. Default: 4000
|
||||
frequency (int): Frequency in Hz that the steps are performed with. The valid range is 1..18,500. Default: 2000.
|
||||
"""
|
||||
self.socket_put_and_receive(
|
||||
f"MST{axis_Id_numeric},{steps},{amplitude},{frequency}", raise_if_not_status=True
|
||||
)
|
||||
|
||||
@retry_once
|
||||
def get_communication_mode(self) -> SmaractCommunicationMode:
|
||||
return_val = self.socket_put_and_receive("GCM")
|
||||
if self._message_starts_with(return_val, f":CM"):
|
||||
return SmaractCommunicationMode(int(return_val.strip(":CM")))
|
||||
|
||||
@retry_once
|
||||
@axis_checked
|
||||
def get_channel_type(self, axis_Id_numeric) -> str:
|
||||
return_val = self.socket_put_and_receive(f"GCT{axis_Id_numeric}")
|
||||
if self._message_starts_with(return_val, f":CT{axis_Id_numeric}"):
|
||||
return return_val.split(",")[1]
|
||||
|
||||
@retry_once
|
||||
def get_interface_version(self) -> str:
|
||||
"""This command may be used to retrieve the interface version of the system. It is useful to check if changes
|
||||
have been made to the software interface. An application may check the version in order to ensure that the
|
||||
system behaves as the application expects it to do.
|
||||
|
||||
Returns:
|
||||
str: interface version
|
||||
"""
|
||||
return_val = self.socket_put_and_receive("GIV")
|
||||
if self._message_starts_with(return_val, f":IV"):
|
||||
return return_val.strip(":IV")
|
||||
|
||||
@retry_once
|
||||
def get_number_of_channels(self) -> int:
|
||||
"""This command may be used to determine how many control channels are available on a system. This
|
||||
includes positioner channels and end effector channels. Each channel is of a specific type. Use the GCT
|
||||
command to determine the types of the channels.
|
||||
Note that the number of channels does not represent the number positioners and/or end effectors that are
|
||||
currently connected to the system.
|
||||
The channel indexes throughout the interface are zero based. If your system has N channels then the valid
|
||||
range for a channel index is 0.. N-1.
|
||||
|
||||
Returns:
|
||||
int: number of channels
|
||||
"""
|
||||
return_val = self.socket_put_and_receive("GNC")
|
||||
if self._message_starts_with(return_val, f":N"):
|
||||
return int(return_val.strip(":N"))
|
||||
|
||||
@retry_once
|
||||
def get_system_id(self) -> str:
|
||||
"""This command may be used to physically identify a system connected to the PC. Each system has a unique
|
||||
ID which makes it possible to distinguish one from another.
|
||||
The ID returned is a generic decimal number that uniquely identifies the system.
|
||||
|
||||
"""
|
||||
return_val = self.socket_put_and_receive("GSI")
|
||||
if self._message_starts_with(return_val, f":ID"):
|
||||
return return_val.strip(":ID")
|
||||
|
||||
@retry_once
|
||||
def reset(self) -> None:
|
||||
"""When this command is sent the system will perform a reset. It has the same effect as a power down/power
|
||||
up cycle. The system replies with an acknowledge string before resetting itself.
|
||||
"""
|
||||
self.socket_put_and_receive("R", raise_if_not_status=True)
|
||||
|
||||
@retry_once
|
||||
def set_hcm_mode(self, mode: int):
|
||||
"""If a Hand Control Module (HCM) is connected to the system, this command may be used to enable or
|
||||
disable it in order to avoid interference while the software is in control of the system. There are three possible
|
||||
modes to set:
|
||||
0: In this mode the Hand Control Module is disabled. It may not be used to control positioners.
|
||||
1: This is the default setting where the Hand Control Module may be used to control the positioners.
|
||||
2: In this mode the Hand Control Module cannot be used to control the positioners. However, if there
|
||||
are positioners with sensors attached, their position data will still be displayed.
|
||||
|
||||
Args:
|
||||
mode (int): HCM mode
|
||||
|
||||
"""
|
||||
if mode not in range(3):
|
||||
raise ValueError(f"HCM mode must be 0, 1 or 2. Received: {mode}.")
|
||||
self.socket_put_and_receive(f"SHE{mode}", raise_if_not_status=True)
|
||||
|
||||
@retry_once
|
||||
@axis_checked
|
||||
def get_position_limits(self, axis_Id_numeric: int) -> list:
|
||||
"""May be used to read out the travel range limit that is currently
|
||||
configured for a linear channel.
|
||||
|
||||
Args:
|
||||
axis_Id_numeric (int): Axis
|
||||
|
||||
Returns:
|
||||
list: [low_limit, high_limit] in mm
|
||||
"""
|
||||
return_val = self.socket_put_and_receive(f"GPL{axis_Id_numeric}")
|
||||
if self._message_starts_with(return_val, f":GPL{axis_Id_numeric}"):
|
||||
return [
|
||||
float(limit) / 1e6
|
||||
for limit in return_val.strip(f":GPL{axis_Id_numeric},").split(",")
|
||||
]
|
||||
|
||||
@retry_once
|
||||
@axis_checked
|
||||
def set_position_limits(
|
||||
self, axis_Id_numeric: int, low_limit: float, high_limit: float
|
||||
) -> None:
|
||||
"""For positioners with integrated sensors this command may be used to limit the travel range of a linear
|
||||
positioner by software. By default there is no limit set. If defined the
|
||||
positioner will not move beyond the limit. This affects open-loop as well as closed-loop movements.
|
||||
|
||||
Args:
|
||||
axis_Id_numeric (int): Axis
|
||||
low_limit (float): low limit in mm
|
||||
high_limit (float): high limit in mm
|
||||
|
||||
"""
|
||||
self.socket_put_and_receive(
|
||||
f"SPL{axis_Id_numeric},{np.round(low_limit*1e6)},{np.round(high_limit*1e6)}",
|
||||
raise_if_not_status=True,
|
||||
)
|
||||
|
||||
@retry_once
|
||||
@axis_checked
|
||||
def get_sensor_type(self, axis_Id_numeric: int) -> SmaractSensorDefinition:
|
||||
return_val = self.socket_put_and_receive(f"GST{axis_Id_numeric}")
|
||||
if self._message_starts_with(return_val, f":ST{axis_Id_numeric}"):
|
||||
return self._sensors.avail_sensors.get(int(return_val.strip(f":ST{axis_Id_numeric},")))
|
||||
|
||||
@retry_once
|
||||
@axis_checked
|
||||
def find_reference_mark(
|
||||
self, axis_Id_numeric: int, direction: int, holdTime: int, autoZero: int
|
||||
) -> None:
|
||||
return_val = self.socket_put_and_receive(
|
||||
f"FRM{axis_Id_numeric},{direction},{holdTime},{autoZero}"
|
||||
)
|
||||
|
||||
@retry_once
|
||||
@axis_checked
|
||||
def set_closed_loop_move_speed(self, axis_Id_numeric: int, move_speed: float) -> None:
|
||||
"""This command configures the speed control feature of a channel for closed-loop commands move_axis_to_absolute_position. By default the speed control is inactive. In this state the behavior of closed-loop commands is influenced by the maximum driving frequency. If a movement speed is configured, all following closed-loop commands will be executed with the new speed.
|
||||
|
||||
Args:
|
||||
axis_Id_numeric (int): Axis number.
|
||||
move_speed (float): Movement speed given in mm/s for linear positioners. The valid range is 0 .. 100. A value of 0 (default) deactivates the speed control feature.
|
||||
"""
|
||||
move_speed_in_nm_per_s = int(round(move_speed * 1e6))
|
||||
|
||||
if move_speed_in_nm_per_s > 100e6 or move_speed_in_nm_per_s < 0:
|
||||
raise ValueError("Move speed must be within 0 to 100 mm/s.")
|
||||
|
||||
self.socket_put_and_receive(
|
||||
f"SCLS{axis_Id_numeric},{move_speed_in_nm_per_s}", raise_if_not_status=True
|
||||
)
|
||||
|
||||
@retry_once
|
||||
@axis_checked
|
||||
def get_closed_loop_move_speed(self, axis_Id_numeric: int) -> float:
|
||||
"""Returns the currently configured movement speed that is used for closed-loop commands for a channel.
|
||||
|
||||
Args:
|
||||
axis_Id_numeric (int): Axis number.
|
||||
|
||||
Returns:
|
||||
float: move speed in mm/s. A return value of 0 means that the speed control feature is disabled.
|
||||
"""
|
||||
|
||||
return_val = self.socket_put_and_receive(f"GCLS{axis_Id_numeric}")
|
||||
if self._message_starts_with(return_val, f":CLS{axis_Id_numeric}"):
|
||||
return float(return_val.strip(f":CLS{axis_Id_numeric},")) * 1e6
|
||||
|
||||
def describe(self) -> None:
|
||||
t = PrettyTable()
|
||||
t.title = f"{self.__class__.__name__} on {self.sock.host}:{self.sock.port}"
|
||||
t.field_names = ["Axis", "Name", "Connected", "Referenced", "Closed Loop Speed", "Position"]
|
||||
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.get_closed_loop_move_speed(axis.axis_Id_numeric),
|
||||
axis.readback.read().get(axis.name).get("value"),
|
||||
]
|
||||
)
|
||||
else:
|
||||
t.add_row([None for t in t.field_names])
|
||||
print(t)
|
||||
|
||||
@axis_checked
|
||||
def _error_str(self, axis_Id_numeric: int, error_number: int):
|
||||
return f":E{axis_Id_numeric},{error_number}"
|
||||
|
||||
def _get_error_code_from_msg(self, msg: str) -> int:
|
||||
if msg.startswith(":E"):
|
||||
return int(msg.split(",")[-1])
|
||||
else:
|
||||
return -1
|
||||
|
||||
def _get_axis_from_error_code(self, msg: str) -> int:
|
||||
if msg.startswith(":E"):
|
||||
try:
|
||||
return int(msg.strip(":E").split(",")[0])
|
||||
except ValueError:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
def _check_for_error(self, msg: str, axis_Id_numeric: int = None, raise_if_not_status=False):
|
||||
if msg.startswith(":E"):
|
||||
if axis_Id_numeric is None:
|
||||
axis_Id_numeric = self._get_axis_from_error_code(msg)
|
||||
|
||||
if axis_Id_numeric is None:
|
||||
raise SmaractCommunicationError(
|
||||
"Could not retrieve axis number from error message."
|
||||
)
|
||||
|
||||
if msg != self._error_str(axis_Id_numeric, 0):
|
||||
error_code = self._get_error_code_from_msg(msg)
|
||||
if error_code != 0:
|
||||
raise SmaractErrorCode(error_code)
|
||||
else:
|
||||
if raise_if_not_status:
|
||||
raise SmaractCommunicationError(
|
||||
"Expected error / status message but failed to parse it."
|
||||
)
|
||||
|
||||
def _remove_trailing_characters(self, var: str) -> str:
|
||||
if len(var) > 1:
|
||||
return var.split("\n")[0]
|
||||
return var
|
||||
|
||||
def _message_starts_with(self, msg: str, leading_chars: str) -> bool:
|
||||
if msg.startswith(leading_chars):
|
||||
return True
|
||||
raise SmaractCommunicationError(
|
||||
f"Expected to receive a return message starting with {leading_chars} but instead"
|
||||
f" received '{msg}'"
|
||||
)
|
||||
@@ -1,47 +0,0 @@
|
||||
SMARACT_ERRORS = {
|
||||
1: "Syntax Error: The command could not be processed due to a syntactical error.",
|
||||
2: "Invalid Command Error: The command given is not known to the system.",
|
||||
3: "Overflow Error: This error occurs if a parameter given is too large and therefore cannot be processed.",
|
||||
4: "Parse Error: The command could not be processed due to a parse error.",
|
||||
5: "Too Few Parameters Error: The specified command requires more parameters in order to be executed.",
|
||||
6: "Too Many Parameters Error: There were too many parameters given for the specified command.",
|
||||
7: "Invalid Parameter Error: A parameter given exceeds the valid range. Please see the command description for valid ranges of the parameters.",
|
||||
8: "Wrong Mode Error: This error is generated if the specified command is not available in the current communication mode. For example, the SRC command is not executable in synchronous mode.",
|
||||
129: "No Sensor Present Error: This error occurs if a command was given that requires sensor feedback, but the addressed positioner has none attached.",
|
||||
140: "Sensor Disabled Error: This error occurs if a command was given that requires sensor feedback, but the sensor of the addressed positioner is disabled (see SSE command).",
|
||||
141: "Command Overridden Error: This error is only generated in the asynchronous communication mode. When the software commands a movement which is then interrupted by the Hand Control Module, an error of this type is generated.",
|
||||
142: "End Stop Reached Error: This error is generated in asynchronous mode if the target position of a closed-loop command could not be reached, because a mechanical end stop was detected. After this error the positioner will have a movement status code of 0 (stopped).",
|
||||
143: "Wrong Sensor Type Error: This error occurs if a closed-loop command does not match the sensor type that is currently configured for the addressed channel. For example, issuing a GP command while the targeted channel is configured as rotary will lead to this error.",
|
||||
144: "Could Not Find Reference Mark Error: This error is generated in asynchronous mode (see SCM) if the search for a reference mark was aborted.",
|
||||
145: "Wrong End Effector Type Error: This error occurs if a command does not match the end effector type that is currently configured for the addressed channel. For example, sending GF while the targeted channel is configured for a gripper will lead to this error.",
|
||||
146: "Movement Locked Error: This error occurs if a movement command is issued while the system is in the locked state. ",
|
||||
147: "Range Limit Reached Error: If a range limit is defined (SPL or SAL) and the positioner is about to move beyond this limit, then the positioner will stop and report this error (only in asynchronous mode, see SCM). After this error the positioner will have status code of 0 (stopped).",
|
||||
148: "Physical Position Unknown Error: A range limit is only allowed to be defined if the positioner “knows” its physical position. If this is not the case, the commands SPL and SAL will return this error code.",
|
||||
150: "Command Not Processable Error: This error is generated if a command is sent to a channel when it is in a state where the command cannot be processed. For example, to change the sensor type of a channel the addressed channel must be completely stopped. In this case send a stop command before changing the type.",
|
||||
151: "Waiting For Trigger Error: If there is at least one command queued in the command queue then you may only append more commands (if the queue is not full), but you may not issue movement commands for immediate execution. Doing so will generate this error. See section 2.4.5 “Command Queues“.",
|
||||
152: "Command Not Triggerable Error: After sending a ATC command you are required to issue a movement command that is to be triggered by the given event source. Commands that cannot be triggered will generate this error.",
|
||||
153: "Command Queue Full Error: This error is generated if you attempt to append more commands to the command queue, but the queue cannot hold anymore commands. The queue capacity may be read out with a get channel property command (GCP on p.30).",
|
||||
154: "Invalid Component Error: Indicates that a component (e.g. SCP) was selected that does not exist.",
|
||||
155: "Invalid Sub Component Error: Indicates that a sub component (e.g. SCP) was selected that does not exist.",
|
||||
156: "Invalid Property Error: Indicates that a property (e.g. SCP) was selected that does not exist.",
|
||||
157: "Permission Denied Error: This error is generated when you call a functionality which is not unlocked for the system (e.g. Low Vibration Mode).",
|
||||
}
|
||||
|
||||
|
||||
class SmaractError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SmaractCommunicationError(SmaractError):
|
||||
pass
|
||||
|
||||
|
||||
class SmaractErrorCode(SmaractError):
|
||||
def __init__(self, error_code: int, message=""):
|
||||
self.error_code = error_code
|
||||
self.error_code_message = SMARACT_ERRORS.get(error_code, "UNKNOWN ERROR")
|
||||
self.message = message
|
||||
super().__init__(self.message)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.error_code} / {self.error_code_message}. {self.message}"
|
||||
@@ -1,321 +0,0 @@
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
from bec_lib 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, ReadOnlyError
|
||||
from ophyd_devices.utils.controller import threadlocked
|
||||
from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected
|
||||
|
||||
from csaxs_bec.devices.smaract.smaract_controller import SmaractController
|
||||
from csaxs_bec.devices.smaract.smaract_errors import SmaractCommunicationError, SmaractError
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class SmaractSignalBase(SocketSignal):
|
||||
def __init__(self, signal_name, **kwargs):
|
||||
self.signal_name = signal_name
|
||||
super().__init__(**kwargs)
|
||||
self.controller = self.parent.controller
|
||||
self.sock = self.parent.controller.sock
|
||||
|
||||
|
||||
class SmaractSignalRO(SmaractSignalBase):
|
||||
def __init__(self, signal_name, **kwargs):
|
||||
super().__init__(signal_name, **kwargs)
|
||||
self._metadata["write_access"] = False
|
||||
|
||||
@threadlocked
|
||||
def _socket_set(self, val):
|
||||
raise ReadOnlyError("Read-only signals cannot be set")
|
||||
|
||||
|
||||
class SmaractReadbackSignal(SmaractSignalRO):
|
||||
@threadlocked
|
||||
def _socket_get(self):
|
||||
return self.controller.get_position(self.parent.axis_Id_numeric) * self.parent.sign
|
||||
|
||||
|
||||
class SmaractSetpointSignal(SmaractSignalBase):
|
||||
setpoint = 0
|
||||
|
||||
@threadlocked
|
||||
def _socket_get(self):
|
||||
return self.setpoint
|
||||
|
||||
@threadlocked
|
||||
def _socket_set(self, val):
|
||||
target_val = val * self.parent.sign
|
||||
self.setpoint = target_val
|
||||
|
||||
if self.controller.axis_is_referenced(self.parent.axis_Id_numeric):
|
||||
self.controller.move_axis_to_absolute_position(self.parent.axis_Id_numeric, target_val)
|
||||
# parameters are axis_no,pos_mm*1e6, hold_time_sec*1e3
|
||||
else:
|
||||
raise SmaractError(f"Axis {self.parent.axis_Id_numeric} is not referenced.")
|
||||
|
||||
|
||||
class SmaractMotorIsMoving(SmaractSignalRO):
|
||||
@threadlocked
|
||||
def _socket_get(self):
|
||||
return self.controller.is_axis_moving(self.parent.axis_Id_numeric)
|
||||
|
||||
|
||||
class SmaractAxisReferenced(SmaractSignalRO):
|
||||
@threadlocked
|
||||
def _socket_get(self):
|
||||
return self.parent.controller.axis_is_referenced(self.parent.axis_Id_numeric)
|
||||
|
||||
|
||||
class SmaractAxisLimits(SmaractSignalBase):
|
||||
@threadlocked
|
||||
def _socket_get(self):
|
||||
limits_msg = self.controller.socket_put_and_receive(f"GPL{self.parent.axis_Id_numeric}")
|
||||
if limits_msg.startswith(":PL"):
|
||||
limits = [
|
||||
float(limit)
|
||||
for limit in limits_msg.strip(f":PL{self.parent.axis_Id_numeric},").split(",")
|
||||
]
|
||||
else:
|
||||
raise SmaractCommunicationError("Expected to receive message starting with :PL.")
|
||||
return limits
|
||||
|
||||
# def _socket_set(self, val):
|
||||
|
||||
|
||||
class SmaractMotor(Device, PositionerBase):
|
||||
USER_ACCESS = ["controller"]
|
||||
readback = Cpt(SmaractReadbackSignal, signal_name="readback", kind="hinted")
|
||||
user_setpoint = Cpt(SmaractSetpointSignal, signal_name="setpoint")
|
||||
|
||||
# motor_resolution = Cpt(
|
||||
# SmaractMotorResolution, signal_name="resolution", kind="config"
|
||||
# )
|
||||
|
||||
motor_is_moving = Cpt(SmaractMotorIsMoving, signal_name="motor_is_moving", kind="normal")
|
||||
high_limit_travel = Cpt(Signal, value=0, kind="omitted")
|
||||
low_limit_travel = Cpt(Signal, value=0, kind="omitted")
|
||||
# all_axes_referenced = Cpt(
|
||||
# SmaractAxesReferenced, signal_name="all_axes_referenced", kind="config"
|
||||
# )
|
||||
|
||||
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="mpc2680.psi.ch",
|
||||
port=8085,
|
||||
limits=None,
|
||||
sign=1,
|
||||
socket_cls=SocketIO,
|
||||
**kwargs,
|
||||
):
|
||||
self.controller = SmaractController(
|
||||
socket_cls=socket_cls, socket_host=host, socket_port=port
|
||||
)
|
||||
self.axis_Id = axis_Id
|
||||
self.sign = sign
|
||||
self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric)
|
||||
self.tolerance = kwargs.pop("tolerance", 0.5)
|
||||
|
||||
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()
|
||||
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
|
||||
|
||||
@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", 4)
|
||||
status = super().move(position, timeout=timeout, **kwargs)
|
||||
self.user_setpoint.put(position, wait=False)
|
||||
|
||||
def move_and_finish():
|
||||
while self.motor_is_moving.get():
|
||||
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)
|
||||
self._done_moving(success=success)
|
||||
|
||||
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(f"Only single-character axis_Ids are supported.")
|
||||
self._axis_Id_alpha = val
|
||||
self._axis_Id_numeric = ord(val.lower()) - 97
|
||||
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(f"Numeric value exceeds supported range.")
|
||||
self._axis_Id_alpha = val
|
||||
self._axis_Id_numeric = (chr(val + 97)).capitalize()
|
||||
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 stage(self) -> list[object]:
|
||||
return super().stage()
|
||||
|
||||
def unstage(self) -> list[object]:
|
||||
return super().unstage()
|
||||
|
||||
def stop(self, *, success=False):
|
||||
self.controller.stop_all_axes()
|
||||
return super().stop(success=success)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
mock = False
|
||||
if not mock:
|
||||
lsmarA = SmaractMotor("A", name="lsmarA", host="mpc2680.psi.ch", port=8085, sign=1)
|
||||
lsmarB = SmaractMotor("B", name="lsmarB", host="mpc2680.psi.ch", port=8085, sign=1)
|
||||
|
||||
lsmarA.stage()
|
||||
lsmarB.stage()
|
||||
# status = leyey.move(2, wait=True)
|
||||
# status = leyey.move(2, wait=True)
|
||||
lsmarA.read()
|
||||
lsmarB.read()
|
||||
|
||||
lsmarA.get()
|
||||
lsmarB.get()
|
||||
lsmarA.describe()
|
||||
|
||||
lsmarA.unstage()
|
||||
lsmarA.controller.off()
|
||||
# status = leyey.move(10, wait=False)
|
||||
# print(lSmaract_controller)
|
||||
else:
|
||||
from ophyd_devices.utils.socket import SocketMock
|
||||
|
||||
lsmarA = SmaractMotor(
|
||||
"A", name="lsmarA", host="mpc2680.psi.ch", port=8085, sign=1, socket_cls=SocketMock
|
||||
)
|
||||
lsmarB = SmaractMotor(
|
||||
"B", name="lsmarB", host="mpc2680.psi.ch", port=8085, sign=1, socket_cls=SocketMock
|
||||
)
|
||||
lsmarA.stage()
|
||||
lsmarB.stage()
|
||||
|
||||
lsmarA.read()
|
||||
lsmarB.read()
|
||||
|
||||
lsmarA.get()
|
||||
lsmarB.get()
|
||||
lsmarA.describe()
|
||||
|
||||
lsmarA.unstage()
|
||||
lsmarA.controller.off()
|
||||
# status = leyey.move(10, wait=False)
|
||||
# print(lSmaract_controller)
|
||||
@@ -1,303 +0,0 @@
|
||||
[
|
||||
{
|
||||
"symbol": "S",
|
||||
"type_code": 1,
|
||||
"positioner_series": "SLCxxxxs",
|
||||
"comment": "linear positioner with nano sensor",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "SR",
|
||||
"type_code": 2,
|
||||
"positioner_series": "SR36xxs, SR3511s, SR5714s, SR7021s, SR2812s, SR7012s, SR4513s, SR5018s",
|
||||
"comment": "rotary positioner with nano sensor",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "SP",
|
||||
"type_code": 5,
|
||||
"positioner_series": "SLCxxxxrs",
|
||||
"comment": "linear positioner with nano sensor, large actuator",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "SC",
|
||||
"type_code": 6,
|
||||
"positioner_series": "SLCxxxxsc",
|
||||
"comment": "linear positioner with nano sensor, distance coded reference marks",
|
||||
"reference_type": "mark*"
|
||||
},
|
||||
{
|
||||
"symbol": "SR20",
|
||||
"type_code": 8,
|
||||
"positioner_series": "SR2013s, SR1612s",
|
||||
"comment": "rotary positioner with nano sensor",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "M",
|
||||
"type_code": 9,
|
||||
"positioner_series": "SLCxxxxm",
|
||||
"comment": "linear positioner with micro sensor",
|
||||
"reference_type": "end stop"
|
||||
},
|
||||
{
|
||||
"symbol": "GD",
|
||||
"type_code": 11,
|
||||
"positioner_series": "SGO60.5m",
|
||||
"comment": "goniometer with micro sensor (60.5mm radius)",
|
||||
"reference_type": "end stop"
|
||||
},
|
||||
{
|
||||
"symbol": "GE",
|
||||
"type_code": 12,
|
||||
"positioner_series": "SGO77.5m",
|
||||
"comment": "goniometer with micro sensor (77.5mm radius)",
|
||||
"reference_type": "end stop"
|
||||
},
|
||||
{
|
||||
"symbol": "GF",
|
||||
"type_code": 14,
|
||||
"positioner_series": "SR1209m",
|
||||
"comment": "rotary positioner with micro sensor",
|
||||
"reference_type": "end stop"
|
||||
},
|
||||
{
|
||||
"symbol": "G605S",
|
||||
"type_code": 16,
|
||||
"positioner_series": "SGO60.5s",
|
||||
"comment": "goniometer with nano sensor (60.5mm radius)",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "G775S",
|
||||
"type_code": 17,
|
||||
"positioner_series": "SGO77.5s",
|
||||
"comment": "goniometer with nano sensor (77.5mm radius)",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "SC500",
|
||||
"type_code": 18,
|
||||
"positioner_series": "SLLxxsc",
|
||||
"comment": "linear positioner with nano sensor, distance coded reference marks",
|
||||
"reference_type": "mark*"
|
||||
},
|
||||
{
|
||||
"symbol": "G955S",
|
||||
"type_code": 19,
|
||||
"positioner_series": "SGO95.5s",
|
||||
"comment": "goniometer with nano sensor (95.5mm radius)",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "SR77",
|
||||
"type_code": 20,
|
||||
"positioner_series": "SR77xxs",
|
||||
"comment": "rotary positioner with nano sensor",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "SD",
|
||||
"type_code": 21,
|
||||
"positioner_series": "SLCxxxxds, SLLxxs",
|
||||
"comment": "like S, but with extended scanning Range",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "R20ME",
|
||||
"type_code": 22,
|
||||
"positioner_series": "SR2013sx, SR1410sx",
|
||||
"comment": "rotary positioner with MicroE sensor",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "SR2",
|
||||
"type_code": 23,
|
||||
"positioner_series": "SR36xxs, SR3511s, SR5714s, SR7021s, SR2812s",
|
||||
"comment": "like SR, for high applied masses",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "SCD",
|
||||
"type_code": 24,
|
||||
"positioner_series": "SLCxxxxdsc",
|
||||
"comment": "like SP, but with distance coded reference marks",
|
||||
"reference_type": "mark*"
|
||||
},
|
||||
{
|
||||
"symbol": "SRC",
|
||||
"type_code": 25,
|
||||
"positioner_series": "SR7021sc",
|
||||
"comment": "like SR, but with distance coded reference marks",
|
||||
"reference_type": "mark*"
|
||||
},
|
||||
{
|
||||
"symbol": "SR36M",
|
||||
"type_code": 26,
|
||||
"positioner_series": "SR3610m",
|
||||
"comment": "rotary positioner, no end stops",
|
||||
"reference_type": "none"
|
||||
},
|
||||
{
|
||||
"symbol": "SR36ME",
|
||||
"type_code": 27,
|
||||
"positioner_series": "SR3610m",
|
||||
"comment": "rotary positioner with end stops",
|
||||
"reference_type": "end stop"
|
||||
},
|
||||
{
|
||||
"symbol": "SR50M",
|
||||
"type_code": 28,
|
||||
"positioner_series": "SR5018m",
|
||||
"comment": "rotary positioner, no end stops",
|
||||
"reference_type": "none"
|
||||
},
|
||||
{
|
||||
"symbol": "SR50ME",
|
||||
"type_code": 29,
|
||||
"positioner_series": "SR5018m",
|
||||
"comment": "rotary positioner with end stops",
|
||||
"reference_type": "end stop"
|
||||
},
|
||||
{
|
||||
"symbol": "G1045S",
|
||||
"type_code": 30,
|
||||
"positioner_series": "SGO104.5s",
|
||||
"comment": "goniometer with nano sensor (104.5mm radius)",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "G1395S",
|
||||
"type_code": 31,
|
||||
"positioner_series": "SGO139.5s",
|
||||
"comment": "goniometer with nano sensor (139.5mm radius)",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "MD",
|
||||
"type_code": 32,
|
||||
"positioner_series": "SLCxxxxdme",
|
||||
"comment": "like M, but with large actuator",
|
||||
"reference_type": "end stop"
|
||||
},
|
||||
{
|
||||
"symbol": "G935M",
|
||||
"type_code": 33,
|
||||
"positioner_series": "SGO93.5me",
|
||||
"comment": "goniometer with micro sensor (93.5mm radius)",
|
||||
"reference_type": "end stop"
|
||||
},
|
||||
{
|
||||
"symbol": "SHL20",
|
||||
"type_code": 34,
|
||||
"positioner_series": "SHL-20",
|
||||
"comment": "high load vertical positioner",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "SCT",
|
||||
"type_code": 35,
|
||||
"positioner_series": "SLCxxxxscu",
|
||||
"comment": "like SCD, but with even larger actuator",
|
||||
"reference_type": "mark*"
|
||||
},
|
||||
{
|
||||
"symbol": "SR77T",
|
||||
"type_code": 36,
|
||||
"positioner_series": "SR7021s",
|
||||
"comment": "like SR77, but with larger actuator",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "SR120",
|
||||
"type_code": 37,
|
||||
"positioner_series": "SR120xxs",
|
||||
"comment": "large rotary positioner",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "LC",
|
||||
"type_code": 38,
|
||||
"positioner_series": "SLCxxxxl",
|
||||
"comment": "linear positioner with improved micro sensor",
|
||||
"reference_type": "mark*"
|
||||
},
|
||||
{
|
||||
"symbol": "LR",
|
||||
"type_code": 39,
|
||||
"positioner_series": "SRxxxxl",
|
||||
"comment": "rotary positioner with improved micro sensor",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "LCD",
|
||||
"type_code": 40,
|
||||
"positioner_series": "SLCxxxxdl",
|
||||
"comment": "like LC, but with large actuator",
|
||||
"reference_type": "mark*"
|
||||
},
|
||||
{
|
||||
"symbol": "L",
|
||||
"type_code": 41,
|
||||
"positioner_series": "SLCxxxxl",
|
||||
"comment": "linear positioner with improved micro sensor",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "LD",
|
||||
"type_code": 42,
|
||||
"positioner_series": "SLCxxxxdl",
|
||||
"comment": "like L, but with large actuator",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "LE",
|
||||
"type_code": 43,
|
||||
"positioner_series": "SLCxxxxl",
|
||||
"comment": "Linear positioner with improved micro sensor",
|
||||
"reference_type": "end stop"
|
||||
},
|
||||
{
|
||||
"symbol": "LED",
|
||||
"type_code": 44,
|
||||
"positioner_series": "SLCxxxxdl",
|
||||
"comment": "Like L, but with large actuator",
|
||||
"reference_type": "end stop"
|
||||
},
|
||||
{
|
||||
"symbol": "GDD",
|
||||
"type_code": 45,
|
||||
"positioner_series": "SGO60.5md",
|
||||
"comment": "goniometer with micro sensor (60.5mm radius), large actuator",
|
||||
"reference_type": "end stop"
|
||||
},
|
||||
{
|
||||
"symbol": "GED",
|
||||
"type_code": 46,
|
||||
"positioner_series": "SGO77.5md",
|
||||
"comment": "goniometer with micro sensor (77.5mm radius), large actuator",
|
||||
"reference_type": "end stop"
|
||||
},
|
||||
{
|
||||
"symbol": "G935S",
|
||||
"type_code": 47,
|
||||
"positioner_series": "SGO96.5s",
|
||||
"comment": "goniometer with nano sensor (93.5mm radius)",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "G605DS",
|
||||
"type_code": 48,
|
||||
"positioner_series": "SGO60.5ds",
|
||||
"comment": "goniometer with nano sensor (60.5mm radius), large actuator",
|
||||
"reference_type": "mark"
|
||||
},
|
||||
{
|
||||
"symbol": "G775DS",
|
||||
"type_code": 49,
|
||||
"positioner_series": "SGO77.5ds",
|
||||
"comment": "goniometer with nano sensor (77.5mm radius), large actuator",
|
||||
"reference_type": "mark"
|
||||
}
|
||||
]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user