Compare commits
42 Commits
refactor/g
...
tomcat_con
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a2df919fb | ||
| 4c342de5c4 | |||
|
|
f688cfca0f | ||
|
|
5d5eef11f6 | ||
|
|
b3e0a64bf1 | ||
|
|
672cb60e72 | ||
|
|
2d23c5e071 | ||
|
|
9a878db49a | ||
|
|
3689ec0677 | ||
|
|
f3961322e3 | ||
|
|
4437bb13b8 | ||
| bea5f95503 | |||
|
|
56f4a6a61e | ||
|
|
6dd03d24a0 | ||
|
|
38fe391654 | ||
|
|
faaccafec6 | ||
|
|
5f5bf291a1 | ||
|
|
84bc0d692f | ||
|
|
8a2d9c3569 | ||
|
|
5726b4349a | ||
|
|
f64d603cff | ||
|
|
eff10e1386 | ||
|
|
bb5c2316e1 | ||
|
|
045f348322 | ||
|
|
32b976f9d6 | ||
|
|
49e8c64433 | ||
|
|
20dcd1849a | ||
|
|
c9a2ce0dc5 | ||
|
|
9051e1a9ee | ||
|
|
6c0405ec7a | ||
|
|
1c5e4a691a | ||
|
|
ab72fa3ffa | ||
|
|
e02bb5892e | ||
|
|
4266798e30 | ||
|
|
f15fd00712 | ||
|
|
76cf6ac447 | ||
|
|
0bc3778d3f | ||
|
|
9d104173bd | ||
|
|
f6fecfdc3f | ||
|
|
e8b3aedb10 | ||
|
|
c3cf12b8e2 | ||
|
|
d1e072b8d9 |
9
.copier-answers.yml
Normal file
9
.copier-answers.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
# Do not edit this file!
|
||||
# It is needed to track the repo template version, and editing may break things.
|
||||
# This file will be overwritten by copier on template updates.
|
||||
|
||||
_commit: v1.0.0
|
||||
_src_path: https://gitea.psi.ch/bec/bec_plugin_copier_template.git
|
||||
make_commit: false
|
||||
project_name: tomcat_bec
|
||||
widget_plugins_input: []
|
||||
5
LICENSE
5
LICENSE
@@ -1,6 +1,7 @@
|
||||
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2024, Paul Scherrer Institute
|
||||
Copyright (c) 2025, Paul Scherrer Institute
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
@@ -25,4 +26,4 @@ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
1
bin/.gitignore
vendored
Normal file
1
bin/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
# Add anything you don't want to check in to git, e.g. very large files
|
||||
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
|
||||
[project]
|
||||
name = "tomcat_bec"
|
||||
version = "0.0.0"
|
||||
description = "Custom device implementations based on the ophyd hardware abstraction layer"
|
||||
description = "The TOMCAT plugin repository for BEC"
|
||||
requires-python = ">=3.10"
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
@@ -24,6 +24,7 @@ dependencies = [
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"black",
|
||||
"copier",
|
||||
"isort",
|
||||
"coverage",
|
||||
"pylint",
|
||||
@@ -46,12 +47,15 @@ plugin_file_writer = "tomcat_bec.file_writer"
|
||||
[project.entry-points."bec.scans"]
|
||||
plugin_scans = "tomcat_bec.scans"
|
||||
|
||||
[project.entry-points."bec.scans.metadata_schema"]
|
||||
plugin_metadata_schema = "tomcat_bec.scans.metadata_schema"
|
||||
|
||||
[project.entry-points."bec.ipython_client_startup"]
|
||||
plugin_ipython_client_pre = "tomcat_bec.bec_ipython_client.startup.pre_startup"
|
||||
plugin_ipython_client_post = "tomcat_bec.bec_ipython_client.startup"
|
||||
|
||||
[project.entry-points."bec.widgets.auto_updates"]
|
||||
plugin_widgets_update = "tomcat_bec.bec_widgets.auto_updates:PlotUpdate"
|
||||
plugin_widgets_update = "tomcat_bec.bec_widgets.auto_updates"
|
||||
|
||||
[project.entry-points."bec.widgets.user_widgets"]
|
||||
plugin_widgets = "tomcat_bec.bec_widgets.widgets"
|
||||
|
||||
@@ -1,31 +1,34 @@
|
||||
# Getting Started with Testing using pytest
|
||||
|
||||
BEC is using the [pytest](https://docs.pytest.org/en/8.0.x/) framework.
|
||||
It can be install via
|
||||
``` bash
|
||||
BEC is using the [pytest](https://docs.pytest.org/en/latest/) framework.
|
||||
It can be installed via
|
||||
|
||||
```bash
|
||||
pip install pytest
|
||||
```
|
||||
in your *python environment*.
|
||||
|
||||
in your _python environment_.
|
||||
We note that pytest is part of the optional-dependencies `[dev]` of the plugin package.
|
||||
|
||||
## Introduction
|
||||
|
||||
Tests in this package should be stored in the `tests` directory.
|
||||
We suggest to sort tests of different submodules, i.e. `scans` or `devices` in the respective folder structure, and to folow a naming convention of `<test_module_name.py>`.
|
||||
It is mandatory for test files to begin with `test_` for pytest to discover them.
|
||||
|
||||
To run all tests, navigate to the directory of the plugin from the command line, and run the command
|
||||
To run all tests, navigate to the directory of the plugin from the command line, and run the command
|
||||
|
||||
``` bash
|
||||
```bash
|
||||
pytest -v --random-order ./tests
|
||||
```
|
||||
|
||||
Note, the python environment needs to be active.
|
||||
The additional arg `-v` allows pytest to run in verbose mode which provides more detailed information about the tests being run.
|
||||
The argument `--random-order` instructs pytest to run the tests in random order, which is the default in the CI pipelines.
|
||||
The argument `--random-order` instructs pytest to run the tests in random order, which is the default in the CI pipelines.
|
||||
|
||||
## Test examples
|
||||
|
||||
Writing tests can be quite specific for the given function.
|
||||
Writing tests can be quite specific for the given function.
|
||||
We recommend writing tests as isolated as possible, i.e. try to test single functions instead of full classes.
|
||||
A very useful class to enable isolated testing is [MagicMock](https://docs.python.org/3/library/unittest.mock.html).
|
||||
In addition, we also recommend to take a look at the [How-to guides from pytest](https://docs.pytest.org/en/8.0.x/how-to/index.html).
|
||||
|
||||
|
||||
@@ -1,31 +1,34 @@
|
||||
# Getting Started with Testing using pytest
|
||||
|
||||
BEC is using the [pytest](https://docs.pytest.org/en/8.0.x/) framework.
|
||||
It can be install via
|
||||
``` bash
|
||||
BEC is using the [pytest](https://docs.pytest.org/en/latest/) framework.
|
||||
It can be installed via
|
||||
|
||||
```bash
|
||||
pip install pytest
|
||||
```
|
||||
in your *python environment*.
|
||||
|
||||
in your _python environment_.
|
||||
We note that pytest is part of the optional-dependencies `[dev]` of the plugin package.
|
||||
|
||||
## Introduction
|
||||
|
||||
Tests in this package should be stored in the `tests` directory.
|
||||
We suggest to sort tests of different submodules, i.e. `scans` or `devices` in the respective folder structure, and to folow a naming convention of `<test_module_name.py>`.
|
||||
It is mandatory for test files to begin with `test_` for pytest to discover them.
|
||||
|
||||
To run all tests, navigate to the directory of the plugin from the command line, and run the command
|
||||
To run all tests, navigate to the directory of the plugin from the command line, and run the command
|
||||
|
||||
``` bash
|
||||
```bash
|
||||
pytest -v --random-order ./tests
|
||||
```
|
||||
|
||||
Note, the python environment needs to be active.
|
||||
The additional arg `-v` allows pytest to run in verbose mode which provides more detailed information about the tests being run.
|
||||
The argument `--random-order` instructs pytest to run the tests in random order, which is the default in the CI pipelines.
|
||||
The argument `--random-order` instructs pytest to run the tests in random order, which is the default in the CI pipelines.
|
||||
|
||||
## Test examples
|
||||
|
||||
Writing tests can be quite specific for the given function.
|
||||
Writing tests can be quite specific for the given function.
|
||||
We recommend writing tests as isolated as possible, i.e. try to test single functions instead of full classes.
|
||||
A very useful class to enable isolated testing is [MagicMock](https://docs.python.org/3/library/unittest.mock.html).
|
||||
In addition, we also recommend to take a look at the [How-to guides from pytest](https://docs.pytest.org/en/8.0.x/how-to/index.html).
|
||||
|
||||
|
||||
34
tests/tests_dap_services/README.md
Normal file
34
tests/tests_dap_services/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Getting Started with Testing using pytest
|
||||
|
||||
BEC is using the [pytest](https://docs.pytest.org/en/latest/) framework.
|
||||
It can be installed via
|
||||
|
||||
```bash
|
||||
pip install pytest
|
||||
```
|
||||
|
||||
in your _python environment_.
|
||||
We note that pytest is part of the optional-dependencies `[dev]` of the plugin package.
|
||||
|
||||
## Introduction
|
||||
|
||||
Tests in this package should be stored in the `tests` directory.
|
||||
We suggest to sort tests of different submodules, i.e. `scans` or `devices` in the respective folder structure, and to folow a naming convention of `<test_module_name.py>`.
|
||||
It is mandatory for test files to begin with `test_` for pytest to discover them.
|
||||
|
||||
To run all tests, navigate to the directory of the plugin from the command line, and run the command
|
||||
|
||||
```bash
|
||||
pytest -v --random-order ./tests
|
||||
```
|
||||
|
||||
Note, the python environment needs to be active.
|
||||
The additional arg `-v` allows pytest to run in verbose mode which provides more detailed information about the tests being run.
|
||||
The argument `--random-order` instructs pytest to run the tests in random order, which is the default in the CI pipelines.
|
||||
|
||||
## Test examples
|
||||
|
||||
Writing tests can be quite specific for the given function.
|
||||
We recommend writing tests as isolated as possible, i.e. try to test single functions instead of full classes.
|
||||
A very useful class to enable isolated testing is [MagicMock](https://docs.python.org/3/library/unittest.mock.html).
|
||||
In addition, we also recommend to take a look at the [How-to guides from pytest](https://docs.pytest.org/en/8.0.x/how-to/index.html).
|
||||
@@ -1,31 +1,34 @@
|
||||
# Getting Started with Testing using pytest
|
||||
|
||||
BEC is using the [pytest](https://docs.pytest.org/en/8.0.x/) framework.
|
||||
It can be install via
|
||||
``` bash
|
||||
BEC is using the [pytest](https://docs.pytest.org/en/latest/) framework.
|
||||
It can be installed via
|
||||
|
||||
```bash
|
||||
pip install pytest
|
||||
```
|
||||
in your *python environment*.
|
||||
|
||||
in your _python environment_.
|
||||
We note that pytest is part of the optional-dependencies `[dev]` of the plugin package.
|
||||
|
||||
## Introduction
|
||||
|
||||
Tests in this package should be stored in the `tests` directory.
|
||||
We suggest to sort tests of different submodules, i.e. `scans` or `devices` in the respective folder structure, and to folow a naming convention of `<test_module_name.py>`.
|
||||
It is mandatory for test files to begin with `test_` for pytest to discover them.
|
||||
|
||||
To run all tests, navigate to the directory of the plugin from the command line, and run the command
|
||||
To run all tests, navigate to the directory of the plugin from the command line, and run the command
|
||||
|
||||
``` bash
|
||||
```bash
|
||||
pytest -v --random-order ./tests
|
||||
```
|
||||
|
||||
Note, the python environment needs to be active.
|
||||
The additional arg `-v` allows pytest to run in verbose mode which provides more detailed information about the tests being run.
|
||||
The argument `--random-order` instructs pytest to run the tests in random order, which is the default in the CI pipelines.
|
||||
The argument `--random-order` instructs pytest to run the tests in random order, which is the default in the CI pipelines.
|
||||
|
||||
## Test examples
|
||||
|
||||
Writing tests can be quite specific for the given function.
|
||||
Writing tests can be quite specific for the given function.
|
||||
We recommend writing tests as isolated as possible, i.e. try to test single functions instead of full classes.
|
||||
A very useful class to enable isolated testing is [MagicMock](https://docs.python.org/3/library/unittest.mock.html).
|
||||
In addition, we also recommend to take a look at the [How-to guides from pytest](https://docs.pytest.org/en/8.0.x/how-to/index.html).
|
||||
|
||||
|
||||
@@ -1,358 +1,353 @@
|
||||
import json
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
import requests_mock
|
||||
import typeguard
|
||||
from ophyd import StatusBase
|
||||
from websockets import WebSocketException
|
||||
|
||||
from tomcat_bec.devices.gigafrost.std_daq_client import (
|
||||
StdDaqClient,
|
||||
StdDaqConfig,
|
||||
StdDaqError,
|
||||
StdDaqStatus,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
parent_device = mock.MagicMock()
|
||||
_client = StdDaqClient(
|
||||
parent=parent_device, ws_url="http://localhost:5000", rest_url="http://localhost:5000"
|
||||
)
|
||||
yield _client
|
||||
_client.shutdown()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def full_config():
|
||||
full_config = StdDaqConfig(
|
||||
detector_name="tomcat-gf",
|
||||
detector_type="gigafrost",
|
||||
n_modules=8,
|
||||
bit_depth=16,
|
||||
image_pixel_height=2016,
|
||||
image_pixel_width=2016,
|
||||
start_udp_port=2000,
|
||||
writer_user_id=18600,
|
||||
max_number_of_forwarders_spawned=8,
|
||||
use_all_forwarders=True,
|
||||
module_sync_queue_size=4096,
|
||||
number_of_writers=12,
|
||||
module_positions={},
|
||||
ram_buffer_gb=150,
|
||||
delay_filter_timeout=10,
|
||||
live_stream_configs={
|
||||
"tcp://129.129.95.111:20000": {"type": "periodic", "config": [1, 5]},
|
||||
"tcp://129.129.95.111:20001": {"type": "periodic", "config": [1, 5]},
|
||||
"tcp://129.129.95.38:20000": {"type": "periodic", "config": [1, 1]},
|
||||
},
|
||||
)
|
||||
return full_config
|
||||
|
||||
|
||||
def test_stddaq_client(client):
|
||||
assert client is not None
|
||||
|
||||
|
||||
def test_stddaq_client_get_daq_config(client, full_config):
|
||||
with requests_mock.Mocker() as m:
|
||||
response = full_config
|
||||
m.get("http://localhost:5000/api/config/get?user=ioc", json=response.model_dump())
|
||||
out = client.get_config()
|
||||
|
||||
# Check that the response is simply the json response
|
||||
assert out == response.model_dump()
|
||||
|
||||
assert client._config == response
|
||||
|
||||
|
||||
def test_stddaq_client_set_config_pydantic(client, full_config):
|
||||
"""Test setting configurations through the StdDAQ client"""
|
||||
with requests_mock.Mocker() as m:
|
||||
m.post("http://localhost:5000/api/config/set?user=ioc")
|
||||
|
||||
# Test with StdDaqConfig object
|
||||
config = full_config
|
||||
with mock.patch.object(client, "_pre_restart"), mock.patch.object(client, "_post_restart"):
|
||||
client.set_config(config)
|
||||
|
||||
# Verify the last request
|
||||
assert m.last_request.json() == full_config.model_dump()
|
||||
|
||||
|
||||
def test_std_daq_client_set_config_dict(client, full_config):
|
||||
"""
|
||||
Test setting configurations through the StdDAQ client with a dictionary input.
|
||||
"""
|
||||
|
||||
with requests_mock.Mocker() as m:
|
||||
m.post("http://localhost:5000/api/config/set?user=ioc")
|
||||
|
||||
# Test with dictionary input
|
||||
config_dict = full_config.model_dump()
|
||||
with mock.patch.object(client, "_pre_restart"), mock.patch.object(client, "_post_restart"):
|
||||
client.set_config(config_dict)
|
||||
assert m.last_request.json() == full_config.model_dump()
|
||||
|
||||
|
||||
def test_stddaq_client_set_config_ignores_extra_keys(client, full_config):
|
||||
"""
|
||||
Test that the set_config method ignores extra keys in the input dictionary.
|
||||
"""
|
||||
|
||||
with requests_mock.Mocker() as m:
|
||||
m.post("http://localhost:5000/api/config/set?user=ioc")
|
||||
# import json
|
||||
# from unittest import mock
|
||||
|
||||
# import pytest
|
||||
# import requests
|
||||
# import requests_mock
|
||||
# import typeguard
|
||||
# from ophyd import StatusBase
|
||||
# from websockets import WebSocketException
|
||||
|
||||
# from tomcat_bec.devices.gigafrost.std_daq_client import StdDaqClient, StdDaqError, StdDaqStatus
|
||||
|
||||
|
||||
# @pytest.fixture
|
||||
# def client():
|
||||
# parent_device = mock.MagicMock()
|
||||
# _client = StdDaqClient(
|
||||
# parent=parent_device, ws_url="ws://localhost:5001", rest_url="http://localhost:5000"
|
||||
# )
|
||||
# yield _client
|
||||
# _client.shutdown()
|
||||
|
||||
|
||||
# @pytest.fixture
|
||||
# def full_config():
|
||||
# full_config = dict(
|
||||
# detector_name="tomcat-gf",
|
||||
# detector_type="gigafrost",
|
||||
# n_modules=8,
|
||||
# bit_depth=16,
|
||||
# image_pixel_height=2016,
|
||||
# image_pixel_width=2016,
|
||||
# start_udp_port=2000,
|
||||
# writer_user_id=18600,
|
||||
# max_number_of_forwarders_spawned=8,
|
||||
# use_all_forwarders=True,
|
||||
# module_sync_queue_size=4096,
|
||||
# number_of_writers=12,
|
||||
# module_positions={},
|
||||
# ram_buffer_gb=150,
|
||||
# delay_filter_timeout=10,
|
||||
# live_stream_configs={
|
||||
# "tcp://129.129.95.111:20000": {"type": "periodic", "config": [1, 5]},
|
||||
# "tcp://129.129.95.111:20001": {"type": "periodic", "config": [1, 5]},
|
||||
# "tcp://129.129.95.38:20000": {"type": "periodic", "config": [1, 1]},
|
||||
# },
|
||||
# )
|
||||
# return full_config
|
||||
|
||||
|
||||
# def test_stddaq_client(client):
|
||||
# assert client is not None
|
||||
|
||||
|
||||
# def test_stddaq_client_get_daq_config(client, full_config):
|
||||
# with requests_mock.Mocker() as m:
|
||||
# response = full_config
|
||||
# m.get("http://localhost:5000/api/config/get?user=ioc", json=response.model_dump())
|
||||
# out = client.get_config()
|
||||
|
||||
# # Check that the response is simply the json response
|
||||
# assert out == response.model_dump()
|
||||
|
||||
# assert client._config == response
|
||||
|
||||
|
||||
# def test_stddaq_client_set_config_pydantic(client, full_config):
|
||||
# """Test setting configurations through the StdDAQ client"""
|
||||
# with requests_mock.Mocker() as m:
|
||||
# m.post("http://localhost:5000/api/config/set?user=ioc")
|
||||
|
||||
# # Test with StdDaqConfig object
|
||||
# config = full_config
|
||||
# with mock.patch.object(client, "_pre_restart"), mock.patch.object(client, "_post_restart"):
|
||||
# client.set_config(config)
|
||||
|
||||
# # Verify the last request
|
||||
# assert m.last_request.json() == full_config.model_dump()
|
||||
|
||||
|
||||
# def test_std_daq_client_set_config_dict(client, full_config):
|
||||
# """
|
||||
# Test setting configurations through the StdDAQ client with a dictionary input.
|
||||
# """
|
||||
|
||||
# Test with dictionary input
|
||||
config_dict = full_config.model_dump()
|
||||
config_dict["extra_key"] = "extra_value"
|
||||
with mock.patch.object(client, "_pre_restart"), mock.patch.object(client, "_post_restart"):
|
||||
client.set_config(config_dict)
|
||||
assert m.last_request.json() == full_config.model_dump()
|
||||
# with requests_mock.Mocker() as m:
|
||||
# m.post("http://localhost:5000/api/config/set?user=ioc")
|
||||
|
||||
# # Test with dictionary input
|
||||
# config_dict = full_config.model_dump()
|
||||
# with mock.patch.object(client, "_pre_restart"), mock.patch.object(client, "_post_restart"):
|
||||
# client.set_config(config_dict)
|
||||
# assert m.last_request.json() == full_config.model_dump()
|
||||
|
||||
|
||||
def test_stddaq_client_set_config_error(client, full_config):
|
||||
"""
|
||||
Test error handling in the set_config method.
|
||||
"""
|
||||
with requests_mock.Mocker() as m:
|
||||
config = full_config
|
||||
m.post("http://localhost:5000/api/config/set?user=ioc", status_code=500)
|
||||
with mock.patch.object(client, "_pre_restart"), mock.patch.object(client, "_post_restart"):
|
||||
with pytest.raises(requests.exceptions.HTTPError):
|
||||
client.set_config(config)
|
||||
# def test_stddaq_client_set_config_ignores_extra_keys(client, full_config):
|
||||
# """
|
||||
# Test that the set_config method ignores extra keys in the input dictionary.
|
||||
# """
|
||||
|
||||
# with requests_mock.Mocker() as m:
|
||||
# m.post("http://localhost:5000/api/config/set?user=ioc")
|
||||
|
||||
# # Test with dictionary input
|
||||
# config_dict = full_config.model_dump()
|
||||
# config_dict["extra_key"] = "extra_value"
|
||||
# with mock.patch.object(client, "_pre_restart"), mock.patch.object(client, "_post_restart"):
|
||||
# client.set_config(config_dict)
|
||||
# assert m.last_request.json() == full_config.model_dump()
|
||||
|
||||
def test_stddaq_client_get_config_cached(client, full_config):
|
||||
"""
|
||||
Test that the client returns the cached configuration if it is available.
|
||||
"""
|
||||
|
||||
# Set the cached configuration
|
||||
config = full_config
|
||||
client._config = config
|
||||
# def test_stddaq_client_set_config_error(client, full_config):
|
||||
# """
|
||||
# Test error handling in the set_config method.
|
||||
# """
|
||||
# with requests_mock.Mocker() as m:
|
||||
# config = full_config
|
||||
# m.post("http://localhost:5000/api/config/set?user=ioc", status_code=500)
|
||||
# with mock.patch.object(client, "_pre_restart"), mock.patch.object(client, "_post_restart"):
|
||||
# with pytest.raises(requests.exceptions.HTTPError):
|
||||
# client.set_config(config)
|
||||
|
||||
# Test that the client returns the cached configuration
|
||||
assert client.get_config(cached=True) == config
|
||||
|
||||
# def test_stddaq_client_get_config_cached(client, full_config):
|
||||
# """
|
||||
# Test that the client returns the cached configuration if it is available.
|
||||
# """
|
||||
|
||||
def test_stddaq_client_status(client):
|
||||
client._status = StdDaqStatus.FILE_CREATED
|
||||
assert client.status == StdDaqStatus.FILE_CREATED
|
||||
# # Set the cached configuration
|
||||
# config = full_config
|
||||
# client._config = config
|
||||
|
||||
# # Test that the client returns the cached configuration
|
||||
# assert client.get_config(cached=True) == config
|
||||
|
||||
def test_stddaq_client_start(client):
|
||||
|
||||
with mock.patch("tomcat_bec.devices.gigafrost.std_daq_client.StatusBase") as StatusBase:
|
||||
client.start(file_path="test_file_path", file_prefix="test_file_prefix", num_images=10)
|
||||
out = client._send_queue.get()
|
||||
assert out == {
|
||||
"command": "start",
|
||||
"path": "test_file_path",
|
||||
"file_prefix": "test_file_prefix",
|
||||
"n_image": 10,
|
||||
}
|
||||
StatusBase().wait.assert_called_once()
|
||||
# def test_stddaq_client_status(client):
|
||||
# client._status = StdDaqStatus.FILE_CREATED
|
||||
# assert client.status == StdDaqStatus.FILE_CREATED
|
||||
|
||||
|
||||
def test_stddaq_client_start_type_error(client):
|
||||
with pytest.raises(typeguard.TypeCheckError):
|
||||
client.start(file_path="test_file_path", file_prefix="test_file_prefix", num_images="10")
|
||||
# def test_stddaq_client_start(client):
|
||||
|
||||
# with mock.patch("tomcat_bec.devices.gigafrost.std_daq_client.StatusBase") as StatusBase:
|
||||
# client.start(file_path="test_file_path", file_prefix="test_file_prefix", num_images=10)
|
||||
# out = client._send_queue.get()
|
||||
# assert out == {
|
||||
# "command": "start",
|
||||
# "path": "test_file_path",
|
||||
# "file_prefix": "test_file_prefix",
|
||||
# "n_image": 10,
|
||||
# }
|
||||
# StatusBase().wait.assert_called_once()
|
||||
|
||||
def test_stddaq_client_stop(client):
|
||||
"""
|
||||
Check that the stop method puts the stop command in the send queue.
|
||||
"""
|
||||
client.stop()
|
||||
client._send_queue.get() == {"command": "stop"}
|
||||
|
||||
# def test_stddaq_client_start_type_error(client):
|
||||
# with pytest.raises(typeguard.TypeCheckError):
|
||||
# client.start(file_path="test_file_path", file_prefix="test_file_prefix", num_images="10")
|
||||
|
||||
def test_stddaq_client_update_config(client, full_config):
|
||||
"""
|
||||
Test that the update_config method updates the configuration with the provided dictionary.
|
||||
"""
|
||||
|
||||
config = full_config
|
||||
with requests_mock.Mocker() as m:
|
||||
m.get("http://localhost:5000/api/config/get?user=ioc", json=config.model_dump())
|
||||
# def test_stddaq_client_stop(client):
|
||||
# """
|
||||
# Check that the stop method puts the stop command in the send queue.
|
||||
# """
|
||||
# client.stop()
|
||||
# client._send_queue.get() == {"command": "stop"}
|
||||
|
||||
# Update the configuration
|
||||
update_dict = {"detector_name": "new_name"}
|
||||
with mock.patch.object(client, "set_config") as set_config:
|
||||
client.update_config(update_dict)
|
||||
|
||||
assert set_config.call_count == 1
|
||||
# def test_stddaq_client_update_config(client, full_config):
|
||||
# """
|
||||
# Test that the update_config method updates the configuration with the provided dictionary.
|
||||
# """
|
||||
|
||||
# config = full_config
|
||||
# with requests_mock.Mocker() as m:
|
||||
# m.get("http://localhost:5000/api/config/get?user=ioc", json=config.model_dump())
|
||||
|
||||
def test_stddaq_client_updates_only_changed_configs(client, full_config):
|
||||
"""
|
||||
Test that the update_config method only updates the configuration if the config has changed.
|
||||
"""
|
||||
|
||||
config = full_config
|
||||
with requests_mock.Mocker() as m:
|
||||
m.get("http://localhost:5000/api/config/get?user=ioc", json=config.model_dump())
|
||||
|
||||
# Update the configuration
|
||||
update_dict = {"detector_name": "tomcat-gf"}
|
||||
with mock.patch.object(client, "set_config") as set_config:
|
||||
client.update_config(update_dict)
|
||||
|
||||
assert set_config.call_count == 0
|
||||
|
||||
|
||||
def test_stddaq_client_updates_only_changed_configs_empty(client, full_config):
|
||||
"""
|
||||
Test that the update_config method only updates the configuration if the config has changed.
|
||||
"""
|
||||
|
||||
config = full_config
|
||||
with requests_mock.Mocker() as m:
|
||||
m.get("http://localhost:5000/api/config/get?user=ioc", json=config.model_dump())
|
||||
|
||||
# Update the configuration
|
||||
update_dict = {}
|
||||
with mock.patch.object(client, "set_config") as set_config:
|
||||
client.update_config(update_dict)
|
||||
|
||||
assert set_config.call_count == 0
|
||||
|
||||
|
||||
def test_stddaq_client_pre_restart(client):
|
||||
"""
|
||||
Test that the pre_restart method sets the status to RESTARTING.
|
||||
"""
|
||||
# let's assume the websocket loop is already idle
|
||||
client._ws_idle_event.set()
|
||||
client.ws_client = mock.MagicMock()
|
||||
client._pre_restart()
|
||||
client.ws_client.close.assert_called_once()
|
||||
|
||||
|
||||
def test_stddaq_client_post_restart(client):
|
||||
"""
|
||||
Test that the post_restart method sets the status to IDLE.
|
||||
"""
|
||||
with mock.patch.object(client, "wait_for_connection") as wait_for_connection:
|
||||
client._post_restart()
|
||||
wait_for_connection.assert_called_once()
|
||||
assert client._daq_is_running.is_set()
|
||||
|
||||
|
||||
def test_stddaq_client_reset(client):
|
||||
"""
|
||||
Test that the reset method calls get_config and set_config.
|
||||
"""
|
||||
with (
|
||||
mock.patch.object(client, "get_config") as get_config,
|
||||
mock.patch.object(client, "set_config") as set_config,
|
||||
):
|
||||
client.reset()
|
||||
get_config.assert_called_once()
|
||||
set_config.assert_called_once()
|
||||
|
||||
|
||||
def test_stddaq_client_run_status_callbacks(client):
|
||||
"""
|
||||
Test that the run_status_callback method runs the status callback.
|
||||
"""
|
||||
status = StatusBase()
|
||||
client.add_status_callback(status, success=[StdDaqStatus.FILE_CREATED], error=[])
|
||||
client._status = StdDaqStatus.FILE_CREATED
|
||||
client._run_status_callbacks()
|
||||
status.wait()
|
||||
|
||||
assert len(status._callbacks) == 0
|
||||
|
||||
|
||||
def test_stddaq_client_run_status_callbacks_error(client):
|
||||
"""
|
||||
Test that the run_status_callback method runs the status callback.
|
||||
"""
|
||||
status = StatusBase()
|
||||
client.add_status_callback(status, success=[], error=[StdDaqStatus.FILE_CREATED])
|
||||
client._status = StdDaqStatus.FILE_CREATED
|
||||
client._run_status_callbacks()
|
||||
with pytest.raises(StdDaqError):
|
||||
status.wait()
|
||||
|
||||
assert len(status._callbacks) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"msg, updated",
|
||||
[({"status": "IDLE"}, False), (json.dumps({"status": "waiting_for_first_image"}), True)],
|
||||
)
|
||||
def test_stddaq_client_on_received_ws_message(client, msg, updated):
|
||||
"""
|
||||
Test that the on_received_ws_message method runs the status callback.
|
||||
"""
|
||||
client._status = None
|
||||
with mock.patch.object(client, "_run_status_callbacks") as run_status_callbacks:
|
||||
client._on_received_ws_message(msg)
|
||||
if updated:
|
||||
run_status_callbacks.assert_called_once()
|
||||
assert client._status == StdDaqStatus.WAITING_FOR_FIRST_IMAGE
|
||||
else:
|
||||
run_status_callbacks.assert_not_called()
|
||||
assert client._status is None
|
||||
|
||||
|
||||
def test_stddaq_client_ws_send_and_receive(client):
|
||||
|
||||
client.ws_client = mock.MagicMock()
|
||||
client._send_queue.put({"command": "test"})
|
||||
client._ws_send_and_receive()
|
||||
# queue is not empty, so we should send the message
|
||||
client.ws_client.send.assert_called_once()
|
||||
client.ws_client.recv.assert_called_once()
|
||||
|
||||
client.ws_client.reset_mock()
|
||||
client._ws_send_and_receive()
|
||||
# queue is empty, so we should not send the message
|
||||
client.ws_client.send.assert_not_called()
|
||||
client.ws_client.recv.assert_called_once()
|
||||
|
||||
|
||||
def test_stddaq_client_ws_send_and_receive_websocket_error(client):
|
||||
"""
|
||||
Test that the ws_send_and_receive method handles websocket errors.
|
||||
"""
|
||||
client.ws_client = mock.MagicMock()
|
||||
client.ws_client.send.side_effect = WebSocketException()
|
||||
client._send_queue.put({"command": "test"})
|
||||
with mock.patch.object(client, "wait_for_connection") as wait_for_connection:
|
||||
client._ws_send_and_receive()
|
||||
wait_for_connection.assert_called_once()
|
||||
|
||||
|
||||
def test_stddaq_client_ws_send_and_receive_timeout_error(client):
|
||||
"""
|
||||
Test that the ws_send_and_receive method handles timeout errors.
|
||||
"""
|
||||
client.ws_client = mock.MagicMock()
|
||||
client.ws_client.recv.side_effect = TimeoutError()
|
||||
client._send_queue.put({"command": "test"})
|
||||
with mock.patch.object(client, "wait_for_connection") as wait_for_connection:
|
||||
client._ws_send_and_receive()
|
||||
wait_for_connection.assert_not_called()
|
||||
|
||||
|
||||
def test_stddaq_client_ws_update_loop(client):
|
||||
"""
|
||||
Test that the ws_update_loop method runs the status callback.
|
||||
"""
|
||||
client._shutdown_event = mock.MagicMock()
|
||||
client._shutdown_event.is_set.side_effect = [False, True]
|
||||
with (
|
||||
mock.patch.object(client, "_ws_send_and_receive") as ws_send_and_receive,
|
||||
mock.patch.object(client, "_wait_for_server_running") as wait_for_server_running,
|
||||
):
|
||||
client._ws_update_loop()
|
||||
|
||||
ws_send_and_receive.assert_called_once()
|
||||
wait_for_server_running.assert_called_once()
|
||||
# # Update the configuration
|
||||
# update_dict = {"detector_name": "new_name"}
|
||||
# with mock.patch.object(client, "set_config") as set_config:
|
||||
# client.update_config(update_dict)
|
||||
|
||||
# assert set_config.call_count == 1
|
||||
|
||||
|
||||
# def test_stddaq_client_updates_only_changed_configs(client, full_config):
|
||||
# """
|
||||
# Test that the update_config method only updates the configuration if the config has changed.
|
||||
# """
|
||||
|
||||
# config = full_config
|
||||
# with requests_mock.Mocker() as m:
|
||||
# m.get("http://localhost:5000/api/config/get?user=ioc", json=config.model_dump())
|
||||
|
||||
# # Update the configuration
|
||||
# update_dict = {"detector_name": "tomcat-gf"}
|
||||
# with mock.patch.object(client, "set_config") as set_config:
|
||||
# client.update_config(update_dict)
|
||||
|
||||
# assert set_config.call_count == 0
|
||||
|
||||
|
||||
# def test_stddaq_client_updates_only_changed_configs_empty(client, full_config):
|
||||
# """
|
||||
# Test that the update_config method only updates the configuration if the config has changed.
|
||||
# """
|
||||
|
||||
# config = full_config
|
||||
# with requests_mock.Mocker() as m:
|
||||
# m.get("http://localhost:5000/api/config/get?user=ioc", json=config.model_dump())
|
||||
|
||||
# # Update the configuration
|
||||
# update_dict = {}
|
||||
# with mock.patch.object(client, "set_config") as set_config:
|
||||
# client.update_config(update_dict)
|
||||
|
||||
# assert set_config.call_count == 0
|
||||
|
||||
|
||||
# def test_stddaq_client_pre_restart(client):
|
||||
# """
|
||||
# Test that the pre_restart method sets the status to RESTARTING.
|
||||
# """
|
||||
# # let's assume the websocket loop is already idle
|
||||
# client._ws_idle_event.set()
|
||||
# client.ws_client = mock.MagicMock()
|
||||
# client._pre_restart()
|
||||
# client.ws_client.close.assert_called_once()
|
||||
|
||||
|
||||
# def test_stddaq_client_post_restart(client):
|
||||
# """
|
||||
# Test that the post_restart method sets the status to IDLE.
|
||||
# """
|
||||
# with mock.patch.object(client, "wait_for_connection") as wait_for_connection:
|
||||
# client._post_restart()
|
||||
# wait_for_connection.assert_called_once()
|
||||
# assert client._daq_is_running.is_set()
|
||||
|
||||
|
||||
# def test_stddaq_client_reset(client):
|
||||
# """
|
||||
# Test that the reset method calls get_config and set_config.
|
||||
# """
|
||||
# with (
|
||||
# mock.patch.object(client, "get_config") as get_config,
|
||||
# mock.patch.object(client, "set_config") as set_config,
|
||||
# ):
|
||||
# client.reset()
|
||||
# get_config.assert_called_once()
|
||||
# set_config.assert_called_once()
|
||||
|
||||
|
||||
# def test_stddaq_client_run_status_callbacks(client):
|
||||
# """
|
||||
# Test that the run_status_callback method runs the status callback.
|
||||
# """
|
||||
# status = StatusBase()
|
||||
# client.add_status_callback(status, success=[StdDaqStatus.FILE_CREATED], error=[])
|
||||
# client._status = StdDaqStatus.FILE_CREATED
|
||||
# client._run_status_callbacks()
|
||||
# status.wait()
|
||||
|
||||
# assert len(status._callbacks) == 0
|
||||
|
||||
|
||||
# def test_stddaq_client_run_status_callbacks_error(client):
|
||||
# """
|
||||
# Test that the run_status_callback method runs the status callback.
|
||||
# """
|
||||
# status = StatusBase()
|
||||
# client.add_status_callback(status, success=[], error=[StdDaqStatus.FILE_CREATED])
|
||||
# client._status = StdDaqStatus.FILE_CREATED
|
||||
# client._run_status_callbacks()
|
||||
# with pytest.raises(StdDaqError):
|
||||
# status.wait()
|
||||
|
||||
# assert len(status._callbacks) == 0
|
||||
|
||||
|
||||
# @pytest.mark.parametrize(
|
||||
# "msg, updated",
|
||||
# [({"status": "IDLE"}, False), (json.dumps({"status": "waiting_for_first_image"}), True)],
|
||||
# )
|
||||
# def test_stddaq_client_on_received_ws_message(client, msg, updated):
|
||||
# """
|
||||
# Test that the on_received_ws_message method runs the status callback.
|
||||
# """
|
||||
# client._status = None
|
||||
# with mock.patch.object(client, "_run_status_callbacks") as run_status_callbacks:
|
||||
# client._on_received_ws_message(msg)
|
||||
# if updated:
|
||||
# run_status_callbacks.assert_called_once()
|
||||
# assert client._status == StdDaqStatus.WAITING_FOR_FIRST_IMAGE
|
||||
# else:
|
||||
# run_status_callbacks.assert_not_called()
|
||||
# assert client._status is None
|
||||
|
||||
|
||||
# def test_stddaq_client_ws_send_and_receive(client):
|
||||
|
||||
# client.ws_client = mock.MagicMock()
|
||||
# client._send_queue.put({"command": "test"})
|
||||
# client._ws_send_and_receive()
|
||||
# # queue is not empty, so we should send the message
|
||||
# client.ws_client.send.assert_called_once()
|
||||
# client.ws_client.recv.assert_called_once()
|
||||
|
||||
# client.ws_client.reset_mock()
|
||||
# client._ws_send_and_receive()
|
||||
# # queue is empty, so we should not send the message
|
||||
# client.ws_client.send.assert_not_called()
|
||||
# client.ws_client.recv.assert_called_once()
|
||||
|
||||
|
||||
# def test_stddaq_client_ws_send_and_receive_websocket_error(client):
|
||||
# """
|
||||
# Test that the ws_send_and_receive method handles websocket errors.
|
||||
# """
|
||||
# client.ws_client = mock.MagicMock()
|
||||
# client.ws_client.send.side_effect = WebSocketException()
|
||||
# client._send_queue.put({"command": "test"})
|
||||
# with mock.patch.object(client, "wait_for_connection") as wait_for_connection:
|
||||
# client._ws_send_and_receive()
|
||||
# wait_for_connection.assert_called_once()
|
||||
|
||||
|
||||
# def test_stddaq_client_ws_send_and_receive_timeout_error(client):
|
||||
# """
|
||||
# Test that the ws_send_and_receive method handles timeout errors.
|
||||
# """
|
||||
# client.ws_client = mock.MagicMock()
|
||||
# client.ws_client.recv.side_effect = TimeoutError()
|
||||
# client._send_queue.put({"command": "test"})
|
||||
# with mock.patch.object(client, "wait_for_connection") as wait_for_connection:
|
||||
# client._ws_send_and_receive()
|
||||
# wait_for_connection.assert_not_called()
|
||||
|
||||
|
||||
# def test_stddaq_client_ws_update_loop(client):
|
||||
# """
|
||||
# Test that the ws_update_loop method runs the status callback.
|
||||
# """
|
||||
# client._shutdown_event = mock.MagicMock()
|
||||
# client._shutdown_event.is_set.side_effect = [False, True]
|
||||
# with (
|
||||
# mock.patch.object(client, "_ws_send_and_receive") as ws_send_and_receive,
|
||||
# mock.patch.object(client, "_wait_for_server_running") as wait_for_server_running,
|
||||
# ):
|
||||
# client._ws_update_loop()
|
||||
|
||||
# ws_send_and_receive.assert_called_once()
|
||||
# wait_for_server_running.assert_called_once()
|
||||
|
||||
@@ -1,31 +1,34 @@
|
||||
# Getting Started with Testing using pytest
|
||||
|
||||
BEC is using the [pytest](https://docs.pytest.org/en/8.0.x/) framework.
|
||||
It can be install via
|
||||
``` bash
|
||||
BEC is using the [pytest](https://docs.pytest.org/en/latest/) framework.
|
||||
It can be installed via
|
||||
|
||||
```bash
|
||||
pip install pytest
|
||||
```
|
||||
in your *python environment*.
|
||||
|
||||
in your _python environment_.
|
||||
We note that pytest is part of the optional-dependencies `[dev]` of the plugin package.
|
||||
|
||||
## Introduction
|
||||
|
||||
Tests in this package should be stored in the `tests` directory.
|
||||
We suggest to sort tests of different submodules, i.e. `scans` or `devices` in the respective folder structure, and to folow a naming convention of `<test_module_name.py>`.
|
||||
It is mandatory for test files to begin with `test_` for pytest to discover them.
|
||||
|
||||
To run all tests, navigate to the directory of the plugin from the command line, and run the command
|
||||
To run all tests, navigate to the directory of the plugin from the command line, and run the command
|
||||
|
||||
``` bash
|
||||
```bash
|
||||
pytest -v --random-order ./tests
|
||||
```
|
||||
|
||||
Note, the python environment needs to be active.
|
||||
The additional arg `-v` allows pytest to run in verbose mode which provides more detailed information about the tests being run.
|
||||
The argument `--random-order` instructs pytest to run the tests in random order, which is the default in the CI pipelines.
|
||||
The argument `--random-order` instructs pytest to run the tests in random order, which is the default in the CI pipelines.
|
||||
|
||||
## Test examples
|
||||
|
||||
Writing tests can be quite specific for the given function.
|
||||
Writing tests can be quite specific for the given function.
|
||||
We recommend writing tests as isolated as possible, i.e. try to test single functions instead of full classes.
|
||||
A very useful class to enable isolated testing is [MagicMock](https://docs.python.org/3/library/unittest.mock.html).
|
||||
In addition, we also recommend to take a look at the [How-to guides from pytest](https://docs.pytest.org/en/8.0.x/how-to/index.html).
|
||||
|
||||
|
||||
@@ -1,31 +1,34 @@
|
||||
# Getting Started with Testing using pytest
|
||||
|
||||
BEC is using the [pytest](https://docs.pytest.org/en/8.0.x/) framework.
|
||||
It can be install via
|
||||
``` bash
|
||||
BEC is using the [pytest](https://docs.pytest.org/en/latest/) framework.
|
||||
It can be installed via
|
||||
|
||||
```bash
|
||||
pip install pytest
|
||||
```
|
||||
in your *python environment*.
|
||||
|
||||
in your _python environment_.
|
||||
We note that pytest is part of the optional-dependencies `[dev]` of the plugin package.
|
||||
|
||||
## Introduction
|
||||
|
||||
Tests in this package should be stored in the `tests` directory.
|
||||
We suggest to sort tests of different submodules, i.e. `scans` or `devices` in the respective folder structure, and to folow a naming convention of `<test_module_name.py>`.
|
||||
It is mandatory for test files to begin with `test_` for pytest to discover them.
|
||||
|
||||
To run all tests, navigate to the directory of the plugin from the command line, and run the command
|
||||
To run all tests, navigate to the directory of the plugin from the command line, and run the command
|
||||
|
||||
``` bash
|
||||
```bash
|
||||
pytest -v --random-order ./tests
|
||||
```
|
||||
|
||||
Note, the python environment needs to be active.
|
||||
The additional arg `-v` allows pytest to run in verbose mode which provides more detailed information about the tests being run.
|
||||
The argument `--random-order` instructs pytest to run the tests in random order, which is the default in the CI pipelines.
|
||||
The argument `--random-order` instructs pytest to run the tests in random order, which is the default in the CI pipelines.
|
||||
|
||||
## Test examples
|
||||
|
||||
Writing tests can be quite specific for the given function.
|
||||
Writing tests can be quite specific for the given function.
|
||||
We recommend writing tests as isolated as possible, i.e. try to test single functions instead of full classes.
|
||||
A very useful class to enable isolated testing is [MagicMock](https://docs.python.org/3/library/unittest.mock.html).
|
||||
In addition, we also recommend to take a look at the [How-to guides from pytest](https://docs.pytest.org/en/8.0.x/how-to/index.html).
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ While command-line arguments have to be set in the pre-startup script, the
|
||||
post-startup script can be used to load beamline specific information and
|
||||
to setup the prompts.
|
||||
|
||||
from bec_lib import bec_logger
|
||||
from bec_lib.logger import bec_logger
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""
|
||||
Pre-startup script for BEC client. This script is executed before the BEC client
|
||||
is started. It can be used to add additional command line arguments.
|
||||
is started. It can be used to add additional command line arguments.
|
||||
"""
|
||||
|
||||
from bec_lib.service_config import ServiceConfig
|
||||
|
||||
0
tomcat_bec/bec_widgets/auto_updates/__init__.py
Normal file
0
tomcat_bec/bec_widgets/auto_updates/__init__.py
Normal file
144
tomcat_bec/device_configs/S-TOMCAT_config.yml
Normal file
144
tomcat_bec/device_configs/S-TOMCAT_config.yml
Normal file
@@ -0,0 +1,144 @@
|
||||
fe_sldi_centerx_mm:
|
||||
description: FE slit horizontal center position in mm
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
deviceConfig:
|
||||
prefix: X02DA-FE-SLDI:CENTERX
|
||||
deviceTags:
|
||||
- ??
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
softwareTrigger: false
|
||||
fe_sldi_centerx_mrad:
|
||||
description: FE slit horizontal center position in mrad
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
deviceConfig:
|
||||
prefix: X02DA-FE-SLDI:CENTERX_MRAD
|
||||
deviceTags:
|
||||
- ??
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
softwareTrigger: false
|
||||
fe_sldi_centery_mm:
|
||||
description: FE slit vertical center position in mm
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
deviceConfig:
|
||||
prefix: X02DA-FE-SLDI:CENTERY
|
||||
deviceTags:
|
||||
- ??
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
softwareTrigger: false
|
||||
fe_sldi_centery_mrad:
|
||||
description: FE slit vertical center position in mrad
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
deviceConfig:
|
||||
prefix: X02DA-FE-SLDI:CENTERY_MRAD
|
||||
deviceTags:
|
||||
- ??
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
softwareTrigger: false
|
||||
fe_sldi_sizex_mm:
|
||||
description: FE slit horizontal size in mm
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
deviceConfig:
|
||||
prefix: X02DA-FE-SLDI:SIZEX
|
||||
deviceTags:
|
||||
- ??
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
softwareTrigger: false
|
||||
fe_sldi_sizex_mrad:
|
||||
description: FE slit horizontal size in mrad
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
deviceConfig:
|
||||
prefix: X02DA-FE-SLDI:SIZEX_MRAD
|
||||
deviceTags:
|
||||
- ??
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
softwareTrigger: false
|
||||
fe_sldi_sizey_mm:
|
||||
description: FE slit vertical size in mm
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
deviceConfig:
|
||||
prefix: X02DA-FE-SLDI:SIZEY
|
||||
deviceTags:
|
||||
- ??
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
softwareTrigger: false
|
||||
fe_sldi_sizey_mrad:
|
||||
description: FE slit vertical size in mrad
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
deviceConfig:
|
||||
prefix: X02DA-FE-SLDI:SIZEY_MRAD
|
||||
deviceTags:
|
||||
- ??
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
softwareTrigger: false
|
||||
fe_sldi_trxr:
|
||||
description: Ring FE slit X motion
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
deviceConfig:
|
||||
prefix: X02DA-FE-SLDI:TRXR
|
||||
deviceTags:
|
||||
- ??
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
softwareTrigger: false
|
||||
fe_sldi_trxw:
|
||||
description: Wall FE slit X motion
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
deviceConfig:
|
||||
prefix: X02DA-FE-SLDI:TRXW
|
||||
deviceTags:
|
||||
- ??
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
softwareTrigger: false
|
||||
fe_sldi_tryb:
|
||||
description: Bottom FE slit Y motion
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
deviceConfig:
|
||||
prefix: X02DA-FE-SLDI:TRYB
|
||||
deviceTags:
|
||||
- ??
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
softwareTrigger: false
|
||||
fe_sldi_tryt:
|
||||
description: Top FE slit Y motion
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
deviceConfig:
|
||||
prefix: X02DA-FE-SLDI:TRYT
|
||||
deviceTags:
|
||||
- ??
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
softwareTrigger: false
|
||||
@@ -51,59 +51,72 @@ es1_roty:
|
||||
readOnly: false
|
||||
softwareTrigger: false
|
||||
|
||||
# es1_ismc:
|
||||
# description: 'Automation1 iSMC interface'
|
||||
# deviceClass: tomcat_bec.devices.aa1Controller
|
||||
# deviceConfig:
|
||||
# prefix: 'X02DA-ES1-SMP1:CTRL:'
|
||||
# deviceTags:
|
||||
# - es1
|
||||
# enabled: true
|
||||
# onFailure: buffer
|
||||
# readOnly: false
|
||||
# readoutPriority: monitored
|
||||
# softwareTrigger: false
|
||||
es1_trx:
|
||||
readoutPriority: monitored
|
||||
description: 'Test translation stage'
|
||||
deviceClass: ophyd.EpicsMotor
|
||||
deviceConfig:
|
||||
prefix: X02DA-ES1-SMP1:TRX
|
||||
deviceTags:
|
||||
- es1-sam
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readOnly: false
|
||||
softwareTrigger: false
|
||||
|
||||
# es1_tasks:
|
||||
# description: 'Automation1 task management interface'
|
||||
# deviceClass: tomcat_bec.devices.aa1Tasks
|
||||
# deviceConfig:
|
||||
# prefix: 'X02DA-ES1-SMP1:TASK:'
|
||||
# deviceTags:
|
||||
# - es1
|
||||
# enabled: true
|
||||
# onFailure: buffer
|
||||
# readOnly: false
|
||||
# readoutPriority: monitored
|
||||
# softwareTrigger: false
|
||||
es1_ismc:
|
||||
description: 'Automation1 iSMC interface'
|
||||
deviceClass: tomcat_bec.devices.aa1Controller
|
||||
deviceConfig:
|
||||
prefix: 'X02DA-ES1-SMP1:CTRL:'
|
||||
deviceTags:
|
||||
- es1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: monitored
|
||||
softwareTrigger: false
|
||||
|
||||
es1_tasks:
|
||||
description: 'Automation1 task management interface'
|
||||
deviceClass: tomcat_bec.devices.aa1Tasks
|
||||
deviceConfig:
|
||||
prefix: 'X02DA-ES1-SMP1:TASK:'
|
||||
deviceTags:
|
||||
- es1
|
||||
enabled: false
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: monitored
|
||||
softwareTrigger: false
|
||||
|
||||
|
||||
# es1_psod:
|
||||
# description: 'AA1 PSO output interface (trigger)'
|
||||
# deviceClass: tomcat_bec.devices.aa1AxisPsoDistance
|
||||
# deviceConfig:
|
||||
# prefix: 'X02DA-ES1-SMP1:ROTY:PSO:'
|
||||
# deviceTags:
|
||||
# - es1
|
||||
# enabled: true
|
||||
# onFailure: buffer
|
||||
# readOnly: false
|
||||
# readoutPriority: monitored
|
||||
# softwareTrigger: true
|
||||
es1_psod:
|
||||
description: 'AA1 PSO output interface (trigger)'
|
||||
deviceClass: tomcat_bec.devices.aa1AxisPsoDistance
|
||||
deviceConfig:
|
||||
prefix: 'X02DA-ES1-SMP1:ROTY:PSO:'
|
||||
deviceTags:
|
||||
- es1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: monitored
|
||||
softwareTrigger: false
|
||||
|
||||
|
||||
# es1_ddaq:
|
||||
# description: 'Automation1 position recording interface'
|
||||
# deviceClass: tomcat_bec.devices.aa1AxisDriveDataCollection
|
||||
# deviceConfig:
|
||||
# prefix: 'X02DA-ES1-SMP1:ROTY:DDC:'
|
||||
# deviceTags:
|
||||
# - es1
|
||||
# enabled: true
|
||||
# onFailure: buffer
|
||||
# readOnly: false
|
||||
# readoutPriority: monitored
|
||||
# softwareTrigger: false
|
||||
es1_ddaq:
|
||||
description: 'Automation1 position recording interface'
|
||||
deviceClass: tomcat_bec.devices.aa1AxisDriveDataCollection
|
||||
deviceConfig:
|
||||
prefix: 'X02DA-ES1-SMP1:ROTY:DDC:'
|
||||
deviceTags:
|
||||
- es1
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: monitored
|
||||
softwareTrigger: false
|
||||
|
||||
|
||||
#camera:
|
||||
@@ -119,6 +132,25 @@ es1_roty:
|
||||
# readoutPriority: monitored
|
||||
# softwareTrigger: true
|
||||
|
||||
|
||||
# gfcam:
|
||||
# description: GigaFrost camera client
|
||||
# deviceClass: tomcat_bec.devices.GigaFrostCamera
|
||||
# deviceConfig:
|
||||
# prefix: 'X02DA-CAM-GF2:'
|
||||
# backend_url: 'http://sls-daq-001:8080'
|
||||
# auto_soft_enable: true
|
||||
# deviceTags:
|
||||
# - camera
|
||||
# - trigger
|
||||
# - gfcam
|
||||
# enabled: true
|
||||
# onFailure: buffer
|
||||
# readOnly: false
|
||||
# readoutPriority: monitored
|
||||
# softwareTrigger: true
|
||||
|
||||
|
||||
gfcam:
|
||||
description: GigaFrost camera client
|
||||
deviceClass: tomcat_bec.devices.GigaFrostCamera
|
||||
@@ -126,6 +158,9 @@ gfcam:
|
||||
prefix: 'X02DA-CAM-GF2:'
|
||||
backend_url: 'http://sls-daq-001:8080'
|
||||
auto_soft_enable: true
|
||||
std_daq_live: 'tcp://129.129.95.111:20000'
|
||||
std_daq_ws: 'ws://129.129.95.111:8080'
|
||||
std_daq_rest: 'http://129.129.95.111:5000'
|
||||
deviceTags:
|
||||
- camera
|
||||
- trigger
|
||||
@@ -136,49 +171,51 @@ gfcam:
|
||||
readoutPriority: monitored
|
||||
softwareTrigger: true
|
||||
|
||||
gfdaq:
|
||||
description: GigaFrost stdDAQ client
|
||||
deviceClass: tomcat_bec.devices.StdDaqClient
|
||||
deviceConfig:
|
||||
ws_url: 'ws://129.129.95.111:8080'
|
||||
rest_url: 'http://129.129.95.111:5000'
|
||||
data_source_name: 'gfcam'
|
||||
deviceTags:
|
||||
- std-daq
|
||||
- gfcam
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: monitored
|
||||
softwareTrigger: false
|
||||
# gfdaq:
|
||||
# description: GigaFrost stdDAQ client
|
||||
# deviceClass: tomcat_bec.devices.StdDaqClient
|
||||
# deviceConfig:
|
||||
# ws_url: 'ws://129.129.95.111:8080'
|
||||
# rest_url: 'http://129.129.95.111:5000'
|
||||
# data_source_name: 'gfcam'
|
||||
# deviceTags:
|
||||
# - std-daq
|
||||
# - gfcam
|
||||
# enabled: false
|
||||
# onFailure: buffer
|
||||
# readOnly: false
|
||||
# readoutPriority: monitored
|
||||
# softwareTrigger: false
|
||||
|
||||
daq_stream0:
|
||||
description: stdDAQ preview (2 every 555)
|
||||
deviceClass: tomcat_bec.devices.StdDaqPreviewDetector
|
||||
deviceConfig:
|
||||
url: 'tcp://129.129.95.111:20000'
|
||||
deviceTags:
|
||||
- std-daq
|
||||
- gfcam
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: monitored
|
||||
softwareTrigger: false
|
||||
# gf_stream0:
|
||||
# description: stdDAQ preview (2 every 555)
|
||||
# deviceClass: tomcat_bec.devices.StdDaqPreviewDetector
|
||||
# deviceConfig:
|
||||
# url: 'tcp://129.129.95.111:20000'
|
||||
# deviceTags:
|
||||
# - std-daq
|
||||
# - gfcam
|
||||
# enabled: false
|
||||
# onFailure: buffer
|
||||
# readOnly: false
|
||||
# readoutPriority: monitored
|
||||
# softwareTrigger: false
|
||||
|
||||
daq_stream1:
|
||||
description: stdDAQ preview (1 at 5 Hz)
|
||||
deviceClass: tomcat_bec.devices.StdDaqPreviewDetector
|
||||
deviceConfig:
|
||||
url: 'tcp://129.129.95.111:20001'
|
||||
deviceTags:
|
||||
- std-daq
|
||||
- gfcam
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: monitored
|
||||
softwareTrigger: false
|
||||
|
||||
# pcocam:
|
||||
# description: PCO.edge camera client
|
||||
# deviceClass: tomcat_bec.devices.PcoEdge5M
|
||||
# deviceConfig:
|
||||
# prefix: 'X02DA-CCDCAM2:'
|
||||
# deviceTags:
|
||||
# - camera
|
||||
# - trigger
|
||||
# - pcocam
|
||||
# enabled: true
|
||||
# onFailure: buffer
|
||||
# readOnly: false
|
||||
# readoutPriority: monitored
|
||||
# softwareTrigger: true
|
||||
|
||||
|
||||
pcocam:
|
||||
@@ -186,6 +223,9 @@ pcocam:
|
||||
deviceClass: tomcat_bec.devices.PcoEdge5M
|
||||
deviceConfig:
|
||||
prefix: 'X02DA-CCDCAM2:'
|
||||
std_daq_live: 'tcp://129.129.95.111:20010'
|
||||
std_daq_ws: 'ws://129.129.95.111:8081'
|
||||
std_daq_rest: 'http://129.129.95.111:5010'
|
||||
deviceTags:
|
||||
- camera
|
||||
- trigger
|
||||
@@ -196,31 +236,31 @@ pcocam:
|
||||
readoutPriority: monitored
|
||||
softwareTrigger: true
|
||||
|
||||
pcodaq:
|
||||
description: GigaFrost stdDAQ client
|
||||
deviceClass: tomcat_bec.devices.StdDaqClient
|
||||
deviceConfig:
|
||||
ws_url: 'ws://129.129.95.111:8081'
|
||||
rest_url: 'http://129.129.95.111:5010'
|
||||
deviceTags:
|
||||
- std-daq
|
||||
- pcocam
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: monitored
|
||||
softwareTrigger: false
|
||||
# pcodaq:
|
||||
# description: GigaFrost stdDAQ client
|
||||
# deviceClass: tomcat_bec.devices.StdDaqClient
|
||||
# deviceConfig:
|
||||
# ws_url: 'ws://129.129.95.111:8081'
|
||||
# rest_url: 'http://129.129.95.111:5010'
|
||||
# deviceTags:
|
||||
# - std-daq
|
||||
# - pcocam
|
||||
# enabled: true
|
||||
# onFailure: buffer
|
||||
# readOnly: false
|
||||
# readoutPriority: monitored
|
||||
# softwareTrigger: false
|
||||
|
||||
pco_stream0:
|
||||
description: stdDAQ preview (2 every 555)
|
||||
deviceClass: tomcat_bec.devices.StdDaqPreviewDetector
|
||||
deviceConfig:
|
||||
url: 'tcp://129.129.95.111:20010'
|
||||
deviceTags:
|
||||
- std-daq
|
||||
- pcocam
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: monitored
|
||||
softwareTrigger: false
|
||||
# pco_stream0:
|
||||
# description: stdDAQ preview (2 every 555)
|
||||
# deviceClass: tomcat_bec.devices.StdDaqPreviewDetector
|
||||
# deviceConfig:
|
||||
# url: 'tcp://129.129.95.111:20010'
|
||||
# deviceTags:
|
||||
# - std-daq
|
||||
# - pcocam
|
||||
# enabled: true
|
||||
# onFailure: buffer
|
||||
# readOnly: false
|
||||
# readoutPriority: monitored
|
||||
# softwareTrigger: false
|
||||
|
||||
@@ -57,7 +57,7 @@ es1_roty:
|
||||
deviceTags:
|
||||
- es1-sam
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
enabled: false
|
||||
readOnly: false
|
||||
softwareTrigger: false
|
||||
|
||||
@@ -81,7 +81,7 @@ es1_tasks:
|
||||
prefix: 'X02DA-ES1-SMP1:TASK:'
|
||||
deviceTags:
|
||||
- es1
|
||||
enabled: true
|
||||
enabled: false
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: monitored
|
||||
@@ -109,7 +109,7 @@ es1_ddaq:
|
||||
prefix: 'X02DA-ES1-SMP1:ROTY:DDC:'
|
||||
deviceTags:
|
||||
- es1
|
||||
enabled: true
|
||||
enabled: false
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: monitored
|
||||
|
||||
@@ -5,13 +5,10 @@ from .aerotech import (
|
||||
aa1GlobalVariableBindings,
|
||||
aa1GlobalVariables,
|
||||
aa1Tasks,
|
||||
aa1Controller
|
||||
)
|
||||
from .grashopper_tomcat import GrashopperTOMCAT
|
||||
from .psimotor import EpicsMotorMR, EpicsMotorEC
|
||||
|
||||
from .gigafrost.gigafrostcamera import GigaFrostCamera
|
||||
from .gigafrost.pcoedgecamera import PcoEdge5M
|
||||
|
||||
from .gigafrost.stddaq_client import StdDaqClient
|
||||
from .gigafrost.stddaq_preview import StdDaqPreviewDetector
|
||||
from .grashopper_tomcat import GrashopperTOMCAT
|
||||
from .psimotor import EpicsMotorEC, EpicsMotorMR
|
||||
|
||||
@@ -8,68 +8,17 @@ drive data collection (DDC) interface.
|
||||
import time
|
||||
from collections import OrderedDict
|
||||
|
||||
from ophyd import Component, EpicsSignal, EpicsSignalRO, Kind
|
||||
from ophyd import Device, Component, EpicsSignal, EpicsSignalRO, Kind
|
||||
from ophyd.status import SubscriptionStatus
|
||||
|
||||
from ophyd_devices.interfaces.base_classes.psi_detector_base import PSIDetectorBase as PSIDeviceBase
|
||||
from ophyd_devices.interfaces.base_classes.psi_detector_base import (
|
||||
CustomDetectorMixin as CustomDeviceMixin,
|
||||
)
|
||||
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
|
||||
|
||||
from bec_lib import bec_logger
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class AerotechDriveDataCollectionMixin(CustomDeviceMixin):
|
||||
"""Mixin class for self-configuration and staging
|
||||
|
||||
NOTE: scripted scans start drive data collection internally
|
||||
"""
|
||||
# parent : aa1Tasks
|
||||
def on_stage(self) -> None:
|
||||
"""Configuration and staging"""
|
||||
|
||||
# Fish out configuration from scaninfo (does not need to be full configuration)
|
||||
d = {}
|
||||
if "kwargs" in self.parent.scaninfo.scan_msg.info:
|
||||
scanargs = self.parent.scaninfo.scan_msg.info["kwargs"]
|
||||
# NOTE: Scans don't have to fully configure the device
|
||||
if "ddc_trigger" in scanargs:
|
||||
d["ddc_trigger"] = scanargs["ddc_trigger"]
|
||||
if "ddc_num_points" in scanargs:
|
||||
d["num_points_total"] = scanargs["ddc_num_points"]
|
||||
else:
|
||||
# Try to figure out number of points
|
||||
num_points = 1
|
||||
points_valid = False
|
||||
if "steps" in scanargs and scanargs['steps'] is not None:
|
||||
num_points *= scanargs["steps"]
|
||||
points_valid = True
|
||||
elif "exp_burst" in scanargs and scanargs['exp_burst'] is not None:
|
||||
num_points *= scanargs["exp_burst"]
|
||||
points_valid = True
|
||||
elif "repeats" in scanargs and scanargs['repeats'] is not None:
|
||||
num_points *= scanargs["repeats"]
|
||||
points_valid = True
|
||||
if points_valid:
|
||||
d["num_points_total"] = num_points
|
||||
|
||||
# Perform bluesky-style configuration
|
||||
if len(d) > 0:
|
||||
logger.warning(f"[{self.parent.name}] Configuring with:\n{d}")
|
||||
self.parent.configure(d=d)
|
||||
|
||||
# Stage the data collection if not in internally launced mode
|
||||
# NOTE: Scripted scans start acquiring from the scrits
|
||||
if self.parent.scaninfo.scan_type not in ("script", "scripted"):
|
||||
self.parent.bluestage()
|
||||
|
||||
def on_unstage(self):
|
||||
"""Standard bluesky unstage"""
|
||||
self.parent._switch.set("Stop", settle_time=0.2).wait()
|
||||
|
||||
|
||||
class aa1AxisDriveDataCollection(PSIDeviceBase):
|
||||
class aa1AxisDriveDataCollection(PSIDeviceBase, Device):
|
||||
"""Axis data collection
|
||||
|
||||
This class provides convenience wrappers around the Aerotech API's axis
|
||||
@@ -88,9 +37,10 @@ class aa1AxisDriveDataCollection(PSIDeviceBase):
|
||||
...
|
||||
ret = yield from ddc.collect()
|
||||
|
||||
NOTE: scripted scans start drive data collection internally
|
||||
|
||||
NOTE: Expected behavior is that the device is disabled when not in use,
|
||||
i.e. there's avtive enable/disable management.
|
||||
i.e. there's active enable/disable management.
|
||||
"""
|
||||
|
||||
# ########################################################################
|
||||
@@ -111,8 +61,32 @@ class aa1AxisDriveDataCollection(PSIDeviceBase):
|
||||
_buffer0 = Component(EpicsSignalRO, "BUFFER0", auto_monitor=True, kind=Kind.normal)
|
||||
_buffer1 = Component(EpicsSignalRO, "BUFFER1", auto_monitor=True, kind=Kind.normal)
|
||||
|
||||
custom_prepare_cls = AerotechDriveDataCollectionMixin
|
||||
USER_ACCESS = ["configure", "reset"]
|
||||
USER_ACCESS = ["configure", "reset", "arm", "disarm"]
|
||||
|
||||
# pylint: disable=duplicate-code, too-many-arguments
|
||||
def __init__(
|
||||
self,
|
||||
prefix="",
|
||||
*,
|
||||
name,
|
||||
kind=None,
|
||||
read_attrs=None,
|
||||
configuration_attrs=None,
|
||||
parent=None,
|
||||
scan_info=None,
|
||||
**kwargs,
|
||||
):
|
||||
# Need to call super() to call the mixin class
|
||||
super().__init__(
|
||||
prefix=prefix,
|
||||
name=name,
|
||||
kind=kind,
|
||||
read_attrs=read_attrs,
|
||||
configuration_attrs=configuration_attrs,
|
||||
parent=parent,
|
||||
scan_info=scan_info,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def configure(self, d: dict = None) -> tuple:
|
||||
"""Configure data capture
|
||||
@@ -128,21 +102,75 @@ class aa1AxisDriveDataCollection(PSIDeviceBase):
|
||||
if "num_points_total" in d:
|
||||
self.npoints.set(d["num_points_total"]).wait()
|
||||
if "ddc_trigger" in d:
|
||||
self._trigger.set(d['ddc_trigger']).wait()
|
||||
self._trigger.set(d["ddc_trigger"]).wait()
|
||||
if "ddc_source0" in d:
|
||||
self._input0.set(d['ddc_source0']).wait()
|
||||
self._input0.set(d["ddc_source0"]).wait()
|
||||
if "ddc_source1" in d:
|
||||
self._input1.set(d['ddc_source1']).wait()
|
||||
self._input1.set(d["ddc_source1"]).wait()
|
||||
|
||||
# Reset incremental readback
|
||||
self._switch.set("ResetRB", settle_time=0.1).wait()
|
||||
new = self.read_configuration()
|
||||
return (old, new)
|
||||
|
||||
def bluestage(self) -> None:
|
||||
def on_stage(self) -> None:
|
||||
"""Configuration and staging"""
|
||||
# Fish out configuration from scaninfo (does not need to be full configuration)
|
||||
|
||||
d = {}
|
||||
scan_args = {
|
||||
**self.scan_info.msg.request_inputs["inputs"],
|
||||
**self.scan_info.msg.request_inputs["kwargs"],
|
||||
**self.scan_info.msg.scan_parameters,
|
||||
}
|
||||
# NOTE: Scans don't have to fully configure the device
|
||||
if "ddc_trigger" in scan_args:
|
||||
d["ddc_trigger"] = scan_args["ddc_trigger"]
|
||||
if "ddc_num_points" in scan_args:
|
||||
d["num_points_total"] = scan_args["ddc_num_points"]
|
||||
else:
|
||||
# Try to figure out number of points
|
||||
num_points = 1
|
||||
points_valid = False
|
||||
if "steps" in scan_args and scan_args["steps"] is not None:
|
||||
num_points *= scan_args["steps"]
|
||||
points_valid = True
|
||||
if "exp_burst" in scan_args and scan_args["exp_burst"] is not None:
|
||||
num_points *= scan_args["exp_burst"]
|
||||
points_valid = True
|
||||
if "repeats" in scan_args and scan_args["repeats"] is not None:
|
||||
num_points *= scan_args["repeats"]
|
||||
points_valid = True
|
||||
if "burst_at_each_point" in scan_args and scan_args["burst_at_each_point"] is not None:
|
||||
num_points *= scan_args["burst_at_each_point"]
|
||||
points_valid = True
|
||||
if points_valid:
|
||||
d["num_points_total"] = num_points
|
||||
|
||||
# Perform bluesky-style configuration
|
||||
if d:
|
||||
self.configure(d=d)
|
||||
|
||||
# Stage the data collection if not in internally launced mode
|
||||
# NOTE: Scripted scans start acquiring from the scrits
|
||||
if "scan_type" in scan_args and scan_args["scan_type"] in ("scripted", "script"):
|
||||
self.arm()
|
||||
# Reset readback
|
||||
self.reset()
|
||||
|
||||
def on_unstage(self):
|
||||
"""Standard bluesky unstage"""
|
||||
self.disarm()
|
||||
|
||||
def arm(self) -> None:
|
||||
"""Bluesky-style stage"""
|
||||
self._switch.set("ResetRB", settle_time=0.1).wait()
|
||||
self._switch.set("Start", settle_time=0.2).wait()
|
||||
|
||||
def disarm(self):
|
||||
"""Standard bluesky unstage"""
|
||||
self._switch.set("Stop", settle_time=0.2).wait()
|
||||
|
||||
def reset(self):
|
||||
"""Reset incremental readback"""
|
||||
self._switch.set("ResetRB", settle_time=0.1).wait()
|
||||
@@ -164,20 +192,22 @@ class aa1AxisDriveDataCollection(PSIDeviceBase):
|
||||
timestamp_ = timestamp
|
||||
return result
|
||||
|
||||
status = None
|
||||
if index == 0:
|
||||
status = SubscriptionStatus(self._readstatus0, neg_edge, settle_time=0.5)
|
||||
self._readback0.set(1).wait()
|
||||
elif index == 1:
|
||||
status = SubscriptionStatus(self._readstatus1, neg_edge, settle_time=0.5)
|
||||
self._readback1.set(1).wait()
|
||||
else:
|
||||
raise RuntimeError(f"Unsupported drive data collection channel: {index}")
|
||||
|
||||
# Start asynchronous readback
|
||||
status.wait()
|
||||
return status
|
||||
|
||||
def describe_collect(self) -> OrderedDict:
|
||||
"""Describes collected array format according to JSONschema
|
||||
"""
|
||||
"""Describes collected array format according to JSONschema"""
|
||||
ret = OrderedDict()
|
||||
ret["buffer0"] = {
|
||||
"source": "internal",
|
||||
|
||||
@@ -7,78 +7,17 @@ synchronized output (PSO) interface.
|
||||
"""
|
||||
from time import sleep
|
||||
import numpy as np
|
||||
from ophyd import Component, EpicsSignal, EpicsSignalRO, Kind
|
||||
from ophyd import Device, Component, EpicsSignal, EpicsSignalRO, Kind
|
||||
from ophyd.status import DeviceStatus
|
||||
|
||||
from ophyd_devices.interfaces.base_classes.psi_detector_base import PSIDetectorBase as PSIDeviceBase
|
||||
from ophyd_devices.interfaces.base_classes.psi_detector_base import (
|
||||
CustomDetectorMixin as CustomDeviceMixin,
|
||||
)
|
||||
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
|
||||
|
||||
from bec_lib import bec_logger
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class AerotechPsoDistanceMixin(CustomDeviceMixin):
|
||||
"""Mixin class for self-configuration and staging
|
||||
"""
|
||||
# parent : aa1Tasks
|
||||
def on_stage(self) -> None:
|
||||
"""Configuration and staging
|
||||
|
||||
NOTE: Scans don't have to fully configure the device, that can be done
|
||||
manually outside. However we expect that the device is disabled
|
||||
when not in use. I.e. this method is not expected to be called when
|
||||
PSO is not needed or when it'd conflict with other devices.
|
||||
"""
|
||||
# Fish out configuration from scaninfo (does not need to be full configuration)
|
||||
d = {}
|
||||
if "kwargs" in self.parent.scaninfo.scan_msg.info:
|
||||
scanargs = self.parent.scaninfo.scan_msg.info["kwargs"]
|
||||
if "pso_distance" in scanargs:
|
||||
d["pso_distance"] = scanargs["pso_distance"]
|
||||
if "pso_wavemode" in scanargs:
|
||||
d["pso_wavemode"] = scanargs["pso_wavemode"]
|
||||
if "pso_w_pulse" in scanargs:
|
||||
d["pso_w_pulse"] = scanargs["pso_w_pulse"]
|
||||
if "pso_t_pulse" in scanargs:
|
||||
d["pso_t_pulse"] = scanargs["pso_t_pulse"]
|
||||
if "pso_n_pulse" in scanargs:
|
||||
d["pso_n_pulse"] = scanargs["pso_n_pulse"]
|
||||
|
||||
# Perform bluesky-style configuration
|
||||
if len(d) > 0:
|
||||
logger.info(f"[{self.parent.name}] Configuring with:\n{d}")
|
||||
self.parent.configure(d=d)
|
||||
|
||||
# Stage the PSO distance module
|
||||
self.parent.bluestage()
|
||||
|
||||
def on_unstage(self):
|
||||
"""Standard bluesky unstage"""
|
||||
# Ensure output is set to low
|
||||
# if self.parent.output.value:
|
||||
# self.parent.toggle()
|
||||
# Turn off window mode
|
||||
self.parent.winOutput.set("Off").wait()
|
||||
self.parent.winEvents.set("Off").wait()
|
||||
# Turn off distance mode
|
||||
self.parent.dstEventsEna.set("Off").wait()
|
||||
self.parent.dstCounterEna.set("Off").wait()
|
||||
# Disable output
|
||||
self.parent.outSource.set("None").wait()
|
||||
# Sleep for one poll period
|
||||
sleep(0.2)
|
||||
|
||||
def on_trigger(self) -> None | DeviceStatus:
|
||||
"""Fire a single PSO event (i.e. manual software trigger)"""
|
||||
# Only trigger if distance was set to invalid
|
||||
logger.warning(f"[{self.parent.name}] Triggerin...")
|
||||
if self.parent.dstDistanceVal.get() == 0:
|
||||
status = self.parent._eventSingle.set(1, settle_time=0.1)
|
||||
return status
|
||||
|
||||
|
||||
class aa1AxisPsoBase(PSIDeviceBase):
|
||||
class AerotechPsoBase(PSIDeviceBase, Device):
|
||||
"""Position Sensitive Output - Base class
|
||||
|
||||
This class provides convenience wrappers around the Aerotech IOC's PSO
|
||||
@@ -152,8 +91,35 @@ class aa1AxisPsoBase(PSIDeviceBase):
|
||||
outPin = Component(EpicsSignalRO, "PIN", auto_monitor=True, kind=Kind.config)
|
||||
outSource = Component(EpicsSignal, "SOURCE", put_complete=True, kind=Kind.config)
|
||||
|
||||
def trigger(self, settle_time=0.1) -> DeviceStatus:
|
||||
# pylint: disable=duplicate-code, too-many-arguments
|
||||
def __init__(
|
||||
self,
|
||||
prefix="",
|
||||
*,
|
||||
name,
|
||||
kind=None,
|
||||
read_attrs=None,
|
||||
configuration_attrs=None,
|
||||
parent=None,
|
||||
scan_info=None,
|
||||
**kwargs,
|
||||
):
|
||||
# Need to call super() to call the mixin class
|
||||
super().__init__(
|
||||
prefix=prefix,
|
||||
name=name,
|
||||
kind=kind,
|
||||
read_attrs=read_attrs,
|
||||
configuration_attrs=configuration_attrs,
|
||||
parent=parent,
|
||||
scan_info=scan_info,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def fire(self, settle_time=0.1) -> None | DeviceStatus:
|
||||
"""Fire a single PSO event (i.e. manual software trigger)"""
|
||||
# Only trigger if distance was set to invalid
|
||||
logger.warning(f"[{self.name}] Triggerin...")
|
||||
self._eventSingle.set(1, settle_time=settle_time).wait()
|
||||
status = DeviceStatus(self)
|
||||
status.set_finished()
|
||||
@@ -163,7 +129,7 @@ class aa1AxisPsoBase(PSIDeviceBase):
|
||||
"""Toggle waveform outup"""
|
||||
orig_wave_mode = self.waveMode.get()
|
||||
self.waveMode.set("Toggle").wait()
|
||||
self.trigger(0.1)
|
||||
self.fire(0.1)
|
||||
self.waveMode.set(orig_wave_mode).wait()
|
||||
|
||||
def configure(self, d: dict):
|
||||
@@ -203,7 +169,7 @@ class aa1AxisPsoBase(PSIDeviceBase):
|
||||
self.outSource.set("Window").wait()
|
||||
|
||||
|
||||
class aa1AxisPsoDistance(aa1AxisPsoBase):
|
||||
class aa1AxisPsoDistance(AerotechPsoBase):
|
||||
"""Position Sensitive Output - Distance mode
|
||||
|
||||
This class provides convenience wrappers around the Aerotech API's PSO functionality in
|
||||
@@ -232,13 +198,12 @@ class aa1AxisPsoDistance(aa1AxisPsoBase):
|
||||
```
|
||||
"""
|
||||
|
||||
custom_prepare_cls = AerotechPsoDistanceMixin
|
||||
USER_ACCESS = ["configure", "prepare", "toggle"]
|
||||
USER_ACCESS = ["configure", "fire", "toggle", "arm", "disarm"]
|
||||
_distance_value = None
|
||||
|
||||
# ########################################################################
|
||||
# PSO high level interface
|
||||
def configure(self, d: dict = {}) -> tuple:
|
||||
def configure(self, d: dict = None) -> tuple:
|
||||
"""Simplified configuration interface to access the most common
|
||||
functionality for distance mode PSO.
|
||||
|
||||
@@ -282,7 +247,51 @@ class aa1AxisPsoDistance(aa1AxisPsoBase):
|
||||
logger.info(f"[{self.name}] PSO configured to {pso_wavemode} mode")
|
||||
return (old, new)
|
||||
|
||||
def bluestage(self) -> None:
|
||||
def on_stage(self) -> None:
|
||||
"""Configuration and staging
|
||||
|
||||
NOTE: Scans don't have to fully configure the device, that can be done
|
||||
manually outside. However we expect that the device is disabled
|
||||
when not in use. I.e. this method is not expected to be called when
|
||||
PSO is not needed or when it'd conflict with other devices.
|
||||
"""
|
||||
# Fish out configuration from scaninfo (does not need to be full configuration)
|
||||
d = {}
|
||||
scan_args = {
|
||||
**self.scan_info.msg.request_inputs["inputs"],
|
||||
**self.scan_info.msg.request_inputs["kwargs"],
|
||||
**self.scan_info.msg.scan_parameters,
|
||||
}
|
||||
if "pso_distance" in scan_args:
|
||||
d["pso_distance"] = scan_args["pso_distance"]
|
||||
if "pso_wavemode" in scan_args:
|
||||
d["pso_wavemode"] = scan_args["pso_wavemode"]
|
||||
if "pso_w_pulse" in scan_args:
|
||||
d["pso_w_pulse"] = scan_args["pso_w_pulse"]
|
||||
if "pso_t_pulse" in scan_args:
|
||||
d["pso_t_pulse"] = scan_args["pso_t_pulse"]
|
||||
if "pso_n_pulse" in scan_args:
|
||||
d["pso_n_pulse"] = scan_args["pso_n_pulse"]
|
||||
|
||||
# Perform bluesky-style configuration
|
||||
if d:
|
||||
self.configure(d=d)
|
||||
|
||||
# Stage the PSO distance module
|
||||
self.arm()
|
||||
|
||||
def on_unstage(self):
|
||||
"""Turn off the PSO module"""
|
||||
self.disarm()
|
||||
|
||||
def on_trigger(self, settle_time=0.1) -> None | DeviceStatus:
|
||||
"""Fire a single PSO event (i.e. manual software trigger)"""
|
||||
# Only trigger if distance was set to invalid
|
||||
# if self.dstDistanceVal.get() == 0:
|
||||
logger.warning(f"[{self.name}] Triggerin...")
|
||||
return self.fire(settle_time)
|
||||
|
||||
def arm(self) -> None:
|
||||
"""Bluesky style stage"""
|
||||
# Stage the PSO distance module and zero counter
|
||||
if isinstance(self._distance_value, (np.ndarray, list, tuple)):
|
||||
@@ -293,3 +302,19 @@ class aa1AxisPsoDistance(aa1AxisPsoBase):
|
||||
if self.dstDistanceVal.get() > 0:
|
||||
self.dstEventsEna.set("On").wait()
|
||||
self.dstCounterEna.set("On").wait()
|
||||
|
||||
def disarm(self):
|
||||
"""Standard bluesky unstage"""
|
||||
# Ensure output is set to low
|
||||
# if self.output.value:
|
||||
# self.toggle()
|
||||
# Turn off window mode
|
||||
self.winOutput.set("Off").wait()
|
||||
self.winEvents.set("Off").wait()
|
||||
# Turn off distance mode
|
||||
self.dstEventsEna.set("Off").wait()
|
||||
self.dstCounterEna.set("Off").wait()
|
||||
# Disable output
|
||||
self.outSource.set("None").wait()
|
||||
# Sleep for one poll period
|
||||
sleep(0.2)
|
||||
|
||||
@@ -34,7 +34,8 @@ program
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// Internal parameters - dont use
|
||||
var $axis as axis = ROTY
|
||||
var $ii as integer
|
||||
var $ii as integer
|
||||
var $axisFaults as integer = 0
|
||||
var $iDdcSafeSpace as integer = 4096
|
||||
|
||||
// Set acceleration
|
||||
@@ -126,7 +127,12 @@ program
|
||||
if $eScanType == ScanType.POS || $eScanType == ScanType.NEG
|
||||
PsoDistanceConfigureArrayDistances($axis, $iPsoArrayPosAddr, $iPsoArrayPosSize, 0)
|
||||
MoveAbsolute($axis, $fPosEnd, $fVelScan)
|
||||
WaitForInPosition($axis)
|
||||
WaitForMotionDone($axis)
|
||||
|
||||
$axisFaults = StatusGetAxisItem($axis, AxisDataSignal.AxisFault)
|
||||
if $axisFaults
|
||||
TaskSetError(TaskGetIndex(), "AxisFault on axis ROTY")
|
||||
end
|
||||
elseif $eScanType == ScanType.POSNEG || $eScanType == ScanType.NEGPOS
|
||||
for $ii = 0 to ($iNumRepeat-1)
|
||||
// Feedback on progress
|
||||
@@ -134,11 +140,15 @@ program
|
||||
if ($ii % 2) == 0
|
||||
PsoDistanceConfigureArrayDistances($axis, $iPsoArrayPosAddr, $iPsoArrayPosSize, 0)
|
||||
MoveAbsolute($axis, $fPosEnd, $fVelScan)
|
||||
WaitForInPosition($axis)
|
||||
elseif ($ii % 2) == 1
|
||||
PsoDistanceConfigureArrayDistances($axis, $iPsoArrayNegAddr, $iPsoArrayNegSize, 0)
|
||||
MoveAbsolute($axis, $fPosStart, $fVelScan)
|
||||
WaitForInPosition($axis)
|
||||
end
|
||||
WaitForMotionDone($axis)
|
||||
|
||||
$axisFaults = StatusGetAxisItem($axis, AxisDataSignal.AxisFault)
|
||||
if $axisFaults
|
||||
TaskSetError(TaskGetIndex(), "AxisFault on axis ROTY")
|
||||
end
|
||||
Dwell(0.2)
|
||||
end
|
||||
|
||||
@@ -78,6 +78,7 @@ program
|
||||
///////////////////////////////////////////////////////////
|
||||
$iglobal[2] = $iNumSteps
|
||||
for $ii = 0 to ($iNumSteps-1)
|
||||
$rglobal[4] = $ii
|
||||
MoveAbsolute($axis, $fStartPosition + $ii * $fStepSize, $fVelScan)
|
||||
WaitForMotionDone($axis)
|
||||
|
||||
|
||||
@@ -5,67 +5,17 @@ interface.
|
||||
|
||||
@author: mohacsi_i
|
||||
"""
|
||||
from time import sleep
|
||||
from ophyd import Component, EpicsSignal, EpicsSignalRO, Kind
|
||||
from ophyd.status import DeviceStatus, SubscriptionStatus
|
||||
from ophyd import Device, Component, EpicsSignal, EpicsSignalRO, Kind
|
||||
from ophyd.status import SubscriptionStatus
|
||||
|
||||
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
|
||||
|
||||
from ophyd_devices.interfaces.base_classes.psi_detector_base import PSIDetectorBase as PSIDeviceBase
|
||||
from ophyd_devices.interfaces.base_classes.psi_detector_base import (
|
||||
CustomDetectorMixin as CustomDeviceMixin,
|
||||
)
|
||||
from bec_lib import bec_logger
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class AerotechTasksMixin(CustomDeviceMixin):
|
||||
"""Mixin class for self-configuration and staging
|
||||
"""
|
||||
# parent : aa1Tasks
|
||||
def on_stage(self) -> None:
|
||||
"""Configuration and staging
|
||||
|
||||
In the BEC model ophyd devices must fish out their own configuration from the 'scaninfo'.
|
||||
I.e. they need to know which parameters are relevant for them at each scan.
|
||||
|
||||
NOTE: Scans don't have to fully configure the device, that can be done
|
||||
manually outside. However we expect that the device is disabled
|
||||
when not in use. I.e. this method is not expected to be called when
|
||||
PSO is not needed or when it'd conflict with other devices.
|
||||
"""
|
||||
# logger.warning(self.parent.scaninfo.scan_msg.info['kwargs'].keys())
|
||||
|
||||
# Fish out our configuration from scaninfo (via explicit or generic addressing)
|
||||
d = {}
|
||||
if "kwargs" in self.parent.scaninfo.scan_msg.info:
|
||||
scanargs = self.parent.scaninfo.scan_msg.info["kwargs"]
|
||||
if self.parent.scaninfo.scan_type in ("script", "scripted"):
|
||||
# NOTE: Scans don't have to fully configure the device
|
||||
if "script_text" in scanargs and scanargs["script_text"] is not None:
|
||||
d["script_text"] = scanargs["script_text"]
|
||||
if "script_file" in scanargs and scanargs["script_file"] is not None:
|
||||
d["script_file"] = scanargs["script_file"]
|
||||
if "script_task" in scanargs and scanargs["script_task"] is not None:
|
||||
d["script_task"] = scanargs["script_task"]
|
||||
|
||||
# Perform bluesky-style configuration
|
||||
if len(d) > 0:
|
||||
logger.warning(f"[{self.parent.name}] Configuring with:\n{d}")
|
||||
self.parent.configure(d=d)
|
||||
|
||||
# The actual staging
|
||||
self.parent.bluestage()
|
||||
|
||||
def on_unstage(self):
|
||||
"""Stop the currently selected task"""
|
||||
self.parent.switch.set("Stop").wait()
|
||||
|
||||
def on_stop(self):
|
||||
"""Stop the currently selected task"""
|
||||
self.parent.switch.set("Stop").wait()
|
||||
|
||||
|
||||
class aa1Tasks(PSIDeviceBase):
|
||||
class aa1Tasks(PSIDeviceBase, Device):
|
||||
"""Task management API
|
||||
|
||||
The place to manage tasks and AeroScript user files on the controller.
|
||||
@@ -96,7 +46,7 @@ class aa1Tasks(PSIDeviceBase):
|
||||
|
||||
"""
|
||||
|
||||
custom_prepare_cls = AerotechTasksMixin
|
||||
USER_ACCESS = ["arm", "disarm", "launch", "kickoff"]
|
||||
|
||||
_failure = Component(EpicsSignalRO, "FAILURE", auto_monitor=True, kind=Kind.normal)
|
||||
errStatus = Component(EpicsSignalRO, "ERRW", auto_monitor=True, kind=Kind.normal)
|
||||
@@ -109,27 +59,55 @@ class aa1Tasks(PSIDeviceBase):
|
||||
_executeReply = Component(EpicsSignalRO, "EXECUTE_RBV", string=True, auto_monitor=True)
|
||||
|
||||
fileName = Component(EpicsSignal, "FILENAME", string=True, kind=Kind.omitted, put_complete=True)
|
||||
# _fileRead = Component(EpicsPassiveRO, "FILEREAD", string=True, kind=Kind.omitted)
|
||||
_fileWrite = Component(
|
||||
EpicsSignal, "FILEWRITE", string=True, kind=Kind.omitted, put_complete=True
|
||||
)
|
||||
|
||||
# pylint: disable=duplicate-code, too-many-arguments
|
||||
def __init__(
|
||||
self,
|
||||
prefix="",
|
||||
*,
|
||||
name,
|
||||
kind=None,
|
||||
read_attrs=None,
|
||||
configuration_attrs=None,
|
||||
parent=None,
|
||||
scan_info=None,
|
||||
**kwargs,
|
||||
):
|
||||
# Need to call super() to call the mixin class
|
||||
super().__init__(
|
||||
prefix=prefix,
|
||||
name=name,
|
||||
kind=kind,
|
||||
read_attrs=read_attrs,
|
||||
configuration_attrs=configuration_attrs,
|
||||
parent=parent,
|
||||
scan_info=scan_info,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def configure(self, d: dict) -> tuple:
|
||||
"""Configuration interface for flying"""
|
||||
"""Configure the scripting interface
|
||||
|
||||
Handles AeroScript loading and the launching of existing script files
|
||||
on the Automation1 iSMC. The interface is meant to be used for flying.
|
||||
"""
|
||||
# Common operations
|
||||
old = self.read_configuration()
|
||||
self.switch.set("Reset").wait()
|
||||
# Check what we got
|
||||
if "script_task" in d:
|
||||
if d['script_task'] < 3 or d['script_task'] > 21:
|
||||
if d["script_task"] < 3 or d["script_task"] > 21:
|
||||
raise RuntimeError(f"Invalid task index: {d['script_task']}")
|
||||
self.taskIndex.set(d['script_task']).wait()
|
||||
self.taskIndex.set(d["script_task"]).wait()
|
||||
if "script_file" in d:
|
||||
self.fileName.set(d["script_file"]).wait()
|
||||
if "script_text" in d:
|
||||
# Compile text for syntax checking
|
||||
# NOTE: This will load to 'script_file'
|
||||
self._fileWrite.set(d['script_text'], settle_time=0.2).wait()
|
||||
self._fileWrite.set(d["script_text"], settle_time=0.2).wait()
|
||||
self.switch.set("Load").wait()
|
||||
# Check the result of load
|
||||
if self._failure.value:
|
||||
@@ -138,25 +116,85 @@ class aa1Tasks(PSIDeviceBase):
|
||||
new = self.read_configuration()
|
||||
return (old, new)
|
||||
|
||||
def bluestage(self) -> None:
|
||||
"""Bluesky style stage"""
|
||||
def on_stage(self) -> None:
|
||||
"""Configuration and staging
|
||||
|
||||
In the BEC model ophyd devices must fish out their own configuration from the 'scaninfo'.
|
||||
I.e. they need to know which parameters are relevant for them at each scan.
|
||||
|
||||
NOTE: Scans don't have to fully configure the device, that can be done
|
||||
manually outside. However we expect that the device is disabled
|
||||
when not in use. I.e. this method is not expected to be called when
|
||||
PSO is not needed or when it'd conflict with other devices.
|
||||
"""
|
||||
# Fish out our configuration from scaninfo (via explicit or generic addressing)
|
||||
d = {}
|
||||
scan_args = {
|
||||
**self.scan_info.msg.request_inputs["inputs"],
|
||||
**self.scan_info.msg.request_inputs["kwargs"],
|
||||
**self.scan_info.msg.scan_parameters,
|
||||
}
|
||||
# if self.scan_info.scan_type in ("script", "scripted"):
|
||||
# NOTE: Scans don't have to fully configure the device
|
||||
if "script_text" in scan_args and scan_args["script_text"] is not None:
|
||||
d["script_text"] = scan_args["script_text"]
|
||||
if "script_file" in scan_args and scan_args["script_file"] is not None:
|
||||
d["script_file"] = scan_args["script_file"]
|
||||
if "script_task" in scan_args and scan_args["script_task"] is not None:
|
||||
d["script_task"] = scan_args["script_task"]
|
||||
|
||||
# FIXME: The above should be exchanged with:
|
||||
# d = self.scan_info.scan_msg.scan_parameters.get("aerotech_config")
|
||||
|
||||
# Perform bluesky-style configuration
|
||||
if d:
|
||||
self.configure(d=d)
|
||||
|
||||
# The actual staging
|
||||
self.arm()
|
||||
|
||||
def on_unstage(self):
|
||||
"""Stop the currently selected task"""
|
||||
self.disarm()
|
||||
|
||||
def on_stop(self):
|
||||
"""Stop the currently selected task"""
|
||||
self.unstage()
|
||||
|
||||
def on_kickoff(self):
|
||||
"""Start execution of the selected task"""
|
||||
self.launch()
|
||||
|
||||
def arm(self) -> None:
|
||||
"""Bluesky style stage, prepare, but does not execute"""
|
||||
if self.taskIndex.get() in (0, 1, 2):
|
||||
logger.error(f"[{self.name}] Launching AeroScript on system task. Daring today are we?")
|
||||
# Launch and check success
|
||||
status = self.switch.set("Run", settle_time=0.2)
|
||||
logger.error(f"[{self.name}] Loading AeroScript on system task. Daring today are we?")
|
||||
# Load and check success
|
||||
status = self.switch.set("Load", settle_time=0.2)
|
||||
status.wait()
|
||||
if self._failure.value:
|
||||
raise RuntimeError("Failed to kick off task, please check the Aerotech IOC")
|
||||
raise RuntimeError("Failed to load task, please check the Aerotech IOC")
|
||||
return status
|
||||
|
||||
##########################################################################
|
||||
# Bluesky flyer interface
|
||||
def complete(self) -> DeviceStatus:
|
||||
def disarm(self):
|
||||
"""Bluesky style unstage, stops execution"""
|
||||
self.switch.set("Stop").wait()
|
||||
|
||||
def launch(self):
|
||||
"""Bluesky style kickoff"""
|
||||
# Launch and check success
|
||||
status = self.switch.set("Start", settle_time=0.2)
|
||||
status.wait()
|
||||
if self._failure.value:
|
||||
raise RuntimeError("Failed to load task, please check the Aerotech IOC")
|
||||
return status
|
||||
|
||||
def complete(self) -> SubscriptionStatus:
|
||||
"""Wait for a RUNNING task"""
|
||||
timestamp_ = 0
|
||||
task_idx = int(self.taskIndex.get())
|
||||
|
||||
def not_running(*args, value, timestamp, **kwargs):
|
||||
def not_running(*, value, timestamp, **_):
|
||||
nonlocal timestamp_
|
||||
result = value[task_idx] not in ["Running", 4]
|
||||
timestamp_ = timestamp
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
from .AerotechTasks import aa1Tasks
|
||||
from .AerotechPso import aa1AxisPsoDistance
|
||||
from .AerotechDriveDataCollection import aa1AxisDriveDataCollection
|
||||
from .AerotechAutomation1 import aa1Controller, aa1GlobalVariables, aa1GlobalVariableBindings, aa1AxisIo
|
||||
from .AerotechAutomation1 import (
|
||||
aa1Controller,
|
||||
aa1GlobalVariables,
|
||||
aa1GlobalVariableBindings,
|
||||
aa1AxisIo,
|
||||
)
|
||||
|
||||
@@ -7,57 +7,7 @@ from ophyd import Component as Cpt
|
||||
from ophyd import Device, DynamicDeviceComponent, EpicsSignal, EpicsSignalRO, Kind, Signal
|
||||
|
||||
import tomcat_bec.devices.gigafrost.gfconstants as const
|
||||
|
||||
|
||||
class GigaFrostSignalWithValidation(EpicsSignal):
|
||||
"""
|
||||
Custom EpicsSignal class that validates the value with the specified validator
|
||||
before setting the value.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
read_pv,
|
||||
write_pv=None,
|
||||
*,
|
||||
put_complete=False,
|
||||
string=False,
|
||||
limits=False,
|
||||
name=None,
|
||||
validator=None,
|
||||
**kwargs,
|
||||
):
|
||||
self._validator = validator
|
||||
super().__init__(
|
||||
read_pv,
|
||||
write_pv,
|
||||
put_complete=put_complete,
|
||||
string=string,
|
||||
limits=limits,
|
||||
name=name,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def check_value(self, value):
|
||||
if self._validator is not None:
|
||||
self._validator(value)
|
||||
return super().check_value(value)
|
||||
|
||||
|
||||
def check_image_width(value):
|
||||
"""
|
||||
The Gigafrost camera requires the image width to be a multiple of 48.
|
||||
"""
|
||||
if value % 48 != 0:
|
||||
raise ValueError("Image width must be a multiple of 48")
|
||||
|
||||
|
||||
def check_image_height(value):
|
||||
"""
|
||||
The Gigafrost camera requires the image height to be a multiple of 16.
|
||||
"""
|
||||
if value % 16 != 0:
|
||||
raise ValueError("Image height must be a multiple of 16")
|
||||
from tomcat_bec.devices.gigafrost.gfutils import extend_header_table
|
||||
|
||||
|
||||
class GigaFrostBase(Device):
|
||||
@@ -86,50 +36,39 @@ class GigaFrostBase(Device):
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
|
||||
busy_stat = Cpt(EpicsSignalRO, "BUSY_STAT", auto_monitor=True)
|
||||
sync_flag = Cpt(EpicsSignalRO, "SYNC_FLAG", auto_monitor=True)
|
||||
sync_swhw = Cpt(EpicsSignal, "SYNC_SWHW.PROC", put_complete=True, kind=Kind.omitted)
|
||||
start_cam = Cpt(EpicsSignal, "START_CAM", put_complete=True, kind=Kind.omitted)
|
||||
set_param = Cpt(EpicsSignal, "SET_PARAM.PROC", put_complete=True, kind=Kind.omitted)
|
||||
acqmode = Cpt(EpicsSignal, "ACQMODE", put_complete=True, kind=Kind.config)
|
||||
|
||||
# Standard camera configs
|
||||
acquire = Cpt(EpicsSignal, "START_CAM", put_complete=True, kind=Kind.omitted)
|
||||
acquire_time = Cpt(
|
||||
EpicsSignal, "EXPOSURE", put_complete=True, auto_monitor=True, kind=Kind.config
|
||||
)
|
||||
acquire_period = Cpt(
|
||||
EpicsSignal, "FRAMERATE", put_complete=True, auto_monitor=True, kind=Kind.config
|
||||
)
|
||||
num_exposures = Cpt(
|
||||
EpicsSignal, "CNT_NUM", put_complete=True, auto_monitor=True, kind=Kind.config
|
||||
)
|
||||
array_size = DynamicDeviceComponent(
|
||||
{
|
||||
"array_size_x": (EpicsSignalRO, "ROIX", {"auto_monitor": True}),
|
||||
"array_size_y": (EpicsSignalRO, "ROIY", {"auto_monitor": True}),
|
||||
"array_size_x": (EpicsSignal, "ROIX", {"auto_monitor": True, "put_complete": True}),
|
||||
"array_size_y": (EpicsSignal, "ROIY", {"auto_monitor": True, "put_complete": True}),
|
||||
},
|
||||
doc="Size of the array in the XY dimensions",
|
||||
)
|
||||
|
||||
# UDP header
|
||||
ports = Cpt(EpicsSignal, "PORTS", put_complete=True, kind=Kind.config)
|
||||
framenum = Cpt(EpicsSignal, "FRAMENUM", put_complete=True, kind=Kind.config)
|
||||
ht_offset = Cpt(EpicsSignal, "HT_OFFSET", put_complete=True, kind=Kind.config)
|
||||
write_srv = Cpt(EpicsSignal, "WRITE_SRV.PROC", put_complete=True, kind=Kind.omitted)
|
||||
# DAQ parameters
|
||||
file_path = Cpt(Signal, kind=Kind.config, value="/gpfs/test/test-beamline")
|
||||
file_prefix = Cpt(Signal, kind=Kind.config, value="scan_")
|
||||
num_images = Cpt(Signal, kind=Kind.config, value=1000)
|
||||
num_images_counter = Cpt(Signal, kind=Kind.hinted, value=0)
|
||||
|
||||
# Standard camera configs
|
||||
exposure = Cpt(EpicsSignal, "EXPOSURE", put_complete=True, auto_monitor=True, kind=Kind.config)
|
||||
framerate = Cpt(
|
||||
EpicsSignal, "FRAMERATE", put_complete=True, auto_monitor=True, kind=Kind.config
|
||||
)
|
||||
roix = Cpt(
|
||||
GigaFrostSignalWithValidation,
|
||||
"ROIX",
|
||||
put_complete=True,
|
||||
auto_monitor=True,
|
||||
kind=Kind.config,
|
||||
validator=check_image_width,
|
||||
)
|
||||
roiy = Cpt(
|
||||
GigaFrostSignalWithValidation,
|
||||
"ROIY",
|
||||
put_complete=True,
|
||||
auto_monitor=True,
|
||||
kind=Kind.config,
|
||||
validator=check_image_height,
|
||||
)
|
||||
# GF specific interface
|
||||
acquire_block = Cpt(Signal, kind=Kind.config, value=0)
|
||||
busy_stat = Cpt(EpicsSignalRO, "BUSY_STAT", auto_monitor=True)
|
||||
sync_flag = Cpt(EpicsSignalRO, "SYNC_FLAG", auto_monitor=True)
|
||||
sync_swhw = Cpt(EpicsSignal, "SYNC_SWHW.PROC", put_complete=True, kind=Kind.omitted)
|
||||
set_param = Cpt(EpicsSignal, "SET_PARAM.PROC", put_complete=True, kind=Kind.omitted)
|
||||
acqmode = Cpt(EpicsSignal, "ACQMODE", put_complete=True, kind=Kind.config)
|
||||
scan_id = Cpt(EpicsSignal, "SCAN_ID", put_complete=True, auto_monitor=True, kind=Kind.config)
|
||||
cnt_num = Cpt(EpicsSignal, "CNT_NUM", put_complete=True, auto_monitor=True, kind=Kind.config)
|
||||
corr_mode = Cpt(
|
||||
EpicsSignal, "CORR_MODE", put_complete=True, auto_monitor=True, kind=Kind.config
|
||||
)
|
||||
@@ -139,6 +78,10 @@ class GigaFrostBase(Device):
|
||||
soft_trig = Cpt(EpicsSignal, "SOFT_TRIG.PROC", put_complete=True, kind=Kind.omitted)
|
||||
soft_exp = Cpt(EpicsSignal, "SOFT_EXP", put_complete=True)
|
||||
|
||||
###############################################################################################
|
||||
# Automatically set modes on camera init
|
||||
auto_soft_enable = Cpt(Signal, kind=Kind.config, metadata={"write_access": False})
|
||||
|
||||
###############################################################################################
|
||||
# Enable schemes
|
||||
# NOTE: 0 physical, 1 virtual (i.e. always running, but logs enable signal)
|
||||
@@ -242,13 +185,6 @@ class GigaFrostBase(Device):
|
||||
EpicsSignal, "CNT_ENDBIT_RBV", write_pv="CNT_ENDBIT", put_complete=True, kind=Kind.config
|
||||
)
|
||||
|
||||
# Line swap selection
|
||||
ls_sw = Cpt(EpicsSignal, "LS_SW", put_complete=True, kind=Kind.config)
|
||||
ls_nw = Cpt(EpicsSignal, "LS_NW", put_complete=True, kind=Kind.config)
|
||||
ls_se = Cpt(EpicsSignal, "LS_SE", put_complete=True, kind=Kind.config)
|
||||
ls_ne = Cpt(EpicsSignal, "LS_NE", put_complete=True, kind=Kind.config)
|
||||
conn_parm = Cpt(EpicsSignal, "CONN_PARM", string=True, put_complete=True, kind=Kind.config)
|
||||
|
||||
# HW settings as read only
|
||||
pixrate = Cpt(EpicsSignalRO, "PIXRATE", auto_monitor=True, kind=Kind.config)
|
||||
trig_delay = Cpt(EpicsSignalRO, "TRIG_DELAY", auto_monitor=True, kind=Kind.config)
|
||||
@@ -261,32 +197,121 @@ class GigaFrostBase(Device):
|
||||
bnc5_rbv = Cpt(EpicsSignalRO, "BNC5_RBV", auto_monitor=True, kind=Kind.config)
|
||||
t_board = Cpt(EpicsSignalRO, "T_BOARD", auto_monitor=True)
|
||||
|
||||
auto_soft_enable = Cpt(Signal, kind=Kind.config)
|
||||
backend_url = Cpt(Signal, kind=Kind.config)
|
||||
### HW configuration parameters
|
||||
# TODO: Only used at INIT, signals not needed
|
||||
# UDP header configuration parameters
|
||||
mac_north = Cpt(Signal, kind=Kind.config)
|
||||
mac_south = Cpt(Signal, kind=Kind.config)
|
||||
ip_north = Cpt(Signal, kind=Kind.config)
|
||||
ip_south = Cpt(Signal, kind=Kind.config)
|
||||
udp_backend_url = Cpt(Signal, kind=Kind.config, metadata={"write_access": False})
|
||||
udp_ports = Cpt(EpicsSignal, "PORTS", put_complete=True, kind=Kind.config)
|
||||
udp_framenum = Cpt(EpicsSignal, "FRAMENUM", put_complete=True, kind=Kind.config)
|
||||
udp_ht_offset = Cpt(EpicsSignal, "HT_OFFSET", put_complete=True, kind=Kind.config)
|
||||
udp_write_srv = Cpt(EpicsSignal, "WRITE_SRV.PROC", put_complete=True, kind=Kind.omitted)
|
||||
conn_parm = Cpt(EpicsSignal, "CONN_PARM", string=True, put_complete=True, kind=Kind.config)
|
||||
|
||||
file_path = Cpt(Signal, kind=Kind.config, value="")
|
||||
file_prefix = Cpt(Signal, kind=Kind.config, value="")
|
||||
num_images = Cpt(Signal, kind=Kind.config, value=1)
|
||||
# Line swap selection
|
||||
ls_sw = Cpt(EpicsSignal, "LS_SW", put_complete=True, kind=Kind.config)
|
||||
ls_nw = Cpt(EpicsSignal, "LS_NW", put_complete=True, kind=Kind.config)
|
||||
ls_se = Cpt(EpicsSignal, "LS_SE", put_complete=True, kind=Kind.config)
|
||||
ls_ne = Cpt(EpicsSignal, "LS_NE", put_complete=True, kind=Kind.config)
|
||||
|
||||
# pylint: disable=protected-access
|
||||
def _define_backend_ip(self):
|
||||
"""Select backend IP address for UDP stream"""
|
||||
if self.backend_url.get() == const.BE3_DAFL_CLIENT: # xbl-daq-33
|
||||
if self.udp_backend_url.get() == const.BE3_DAFL_CLIENT: # xbl-daq-33
|
||||
return const.BE3_NORTH_IP, const.BE3_SOUTH_IP
|
||||
if self.backend_url.get() == const.BE999_DAFL_CLIENT:
|
||||
if self.udp_backend_url.get() == const.BE999_DAFL_CLIENT:
|
||||
return const.BE999_NORTH_IP, const.BE999_SOUTH_IP
|
||||
|
||||
raise RuntimeError(f"Backend {self.backend_url.get()} not recognized.")
|
||||
raise RuntimeError(f"Backend {self.udp_backend_url.get()} not recognized.")
|
||||
|
||||
def _define_backend_mac(self):
|
||||
"""Select backend MAC address for UDP stream"""
|
||||
if self.backend_url.get() == const.BE3_DAFL_CLIENT: # xbl-daq-33
|
||||
if self.udp_backend_url.get() == const.BE3_DAFL_CLIENT: # xbl-daq-33
|
||||
return const.BE3_NORTH_MAC, const.BE3_SOUTH_MAC
|
||||
if self.backend_url.get() == const.BE999_DAFL_CLIENT:
|
||||
if self.udp_backend_url.get() == const.BE999_DAFL_CLIENT:
|
||||
return const.BE999_NORTH_MAC, const.BE999_SOUTH_MAC
|
||||
|
||||
raise RuntimeError(f"Backend {self.backend_url.get()} not recognized.")
|
||||
raise RuntimeError(f"Backend {self.udp_backend_url.get()} not recognized.")
|
||||
|
||||
def _build_udp_header_table(self):
|
||||
"""Build the header table for the UDP communication"""
|
||||
udp_header_table = []
|
||||
|
||||
for i in range(0, 64, 1):
|
||||
for j in range(0, 8, 1):
|
||||
dest_port = 2000 + 8 * i + j
|
||||
source_port = 3000 + j
|
||||
if j < 4:
|
||||
extend_header_table(
|
||||
udp_header_table,
|
||||
self.mac_south.get(),
|
||||
self.ip_south.get(),
|
||||
dest_port,
|
||||
source_port,
|
||||
)
|
||||
else:
|
||||
extend_header_table(
|
||||
udp_header_table,
|
||||
self.mac_north.get(),
|
||||
self.ip_north.get(),
|
||||
dest_port,
|
||||
source_port,
|
||||
)
|
||||
return udp_header_table
|
||||
|
||||
def initialize_gigafrost(self) -> None:
|
||||
"""Initialize the camera, set channel values"""
|
||||
# Stop acquisition
|
||||
self.acquire.set(0).wait()
|
||||
|
||||
# set entry to UDP table
|
||||
# number of UDP ports to use
|
||||
self.udp_ports.set(2).wait()
|
||||
# number of images to send to each UDP port before switching to next
|
||||
self.udp_framenum.set(5).wait()
|
||||
# offset in UDP table - where to find the first entry
|
||||
self.udp_ht_offset.set(0).wait()
|
||||
# activate changes
|
||||
self.udp_write_srv.set(1).wait()
|
||||
|
||||
# Configure triggering if needed
|
||||
if self.auto_soft_enable.get():
|
||||
# Set modes
|
||||
# self.fix_nframes_mode = "start"
|
||||
self.cnt_startbit.set(1).wait()
|
||||
self.cnt_endbit.set(0).wait()
|
||||
# self.enable_mode = "soft"
|
||||
self.mode_enbl_ext.set(0).wait()
|
||||
self.mode_endbl_soft.set(1).wait()
|
||||
self.mode_enbl_auto.set(0).wait()
|
||||
# self.trigger_mode = "auto"
|
||||
self.mode_trig_auto.set(1).wait()
|
||||
self.mode_trig_soft.set(0).wait()
|
||||
self.mode_trig_timer.set(0).wait()
|
||||
self.mode_trig_ext.set(0).wait()
|
||||
# self.exposure_mode = "timer"
|
||||
self.mode_exp_ext.set(0).wait()
|
||||
self.mode_exp_soft.set(0).wait()
|
||||
self.mode_exp_timer.set(1).wait()
|
||||
|
||||
# line swap - on for west, off for east
|
||||
self.ls_sw.set(1).wait()
|
||||
self.ls_nw.set(1).wait()
|
||||
self.ls_se.set(0).wait()
|
||||
self.ls_ne.set(0).wait()
|
||||
|
||||
# Commit parameters
|
||||
self.set_param.set(1).wait()
|
||||
|
||||
# Initialize data backend
|
||||
n, s = self._define_backend_ip()
|
||||
self.ip_north.put(n, force=True)
|
||||
self.ip_south.put(s, force=True)
|
||||
n, s = self._define_backend_mac()
|
||||
self.mac_north.put(n, force=True)
|
||||
self.mac_south.put(s, force=True)
|
||||
# Set udp header table (data communication parameters)
|
||||
self.conn_parm.set(self._build_udp_header_table()).wait()
|
||||
|
||||
@@ -6,23 +6,17 @@ Created on Thu Jun 27 17:28:43 2024
|
||||
|
||||
@author: mohacsi_i
|
||||
"""
|
||||
from time import sleep
|
||||
from typing import Literal
|
||||
from time import sleep, time
|
||||
import numpy as np
|
||||
from bec_lib.logger import bec_logger
|
||||
from ophyd import DeviceStatus
|
||||
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
|
||||
|
||||
import tomcat_bec.devices.gigafrost.gfconstants as const
|
||||
from tomcat_bec.devices.gigafrost.gfutils import extend_header_table
|
||||
from tomcat_bec.devices.gigafrost.gigafrost_base import GigaFrostBase
|
||||
from tomcat_bec.devices.gigafrost.std_daq_client import (
|
||||
StdDaqClient,
|
||||
StdDaqConfigPartial,
|
||||
StdDaqStatus,
|
||||
)
|
||||
|
||||
from tomcat_bec.devices.gigafrost.std_daq_preview import StdDaqPreview
|
||||
from tomcat_bec.devices.gigafrost.std_daq_client import StdDaqClient, StdDaqStatus
|
||||
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
@@ -63,15 +57,21 @@ class GigaFrostCamera(PSIDeviceBase, GigaFrostBase):
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
USER_ACCESS = [
|
||||
"complete",
|
||||
"exposure_mode",
|
||||
"fix_nframes_mode",
|
||||
"trigger_mode",
|
||||
"enable_mode",
|
||||
"backend",
|
||||
"acq_done",
|
||||
"live_preview"
|
||||
"live_preview",
|
||||
"arm",
|
||||
"disarm",
|
||||
]
|
||||
_initialized = False
|
||||
|
||||
# Placeholders for stdDAQ and livestream clients
|
||||
backend = None
|
||||
live_preview = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -90,11 +90,6 @@ class GigaFrostCamera(PSIDeviceBase, GigaFrostBase):
|
||||
std_daq_live: str | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
# Ugly hack to pass values before on_init()
|
||||
self._signals_to_be_set = {}
|
||||
self._signals_to_be_set["auto_soft_enable"] = auto_soft_enable
|
||||
self._signals_to_be_set["backend_url"] = backend_url
|
||||
|
||||
# super() will call the mixin class
|
||||
super().__init__(
|
||||
prefix=prefix,
|
||||
@@ -106,76 +101,94 @@ class GigaFrostCamera(PSIDeviceBase, GigaFrostBase):
|
||||
scan_info=scan_info,
|
||||
**kwargs,
|
||||
)
|
||||
# Configure the stdDAQ client
|
||||
if std_daq_rest is None or std_daq_ws is None:
|
||||
raise ValueError("Both std_daq_rest and std_daq_ws must be provided")
|
||||
self.backend = StdDaqClient(parent=self, ws_url=std_daq_ws, rest_url=std_daq_rest)
|
||||
self.live_preview = None
|
||||
# raise ValueError("Both std_daq_rest and std_daq_ws must be provided")
|
||||
logger.error("No stdDAQ address provided, launching without data backend!")
|
||||
else:
|
||||
self.backend = StdDaqClient(parent=self, ws_url=std_daq_ws, rest_url=std_daq_rest)
|
||||
# Configure image preview
|
||||
if std_daq_live is not None:
|
||||
self.live_preview = StdDaqPreview(url=std_daq_live, cb=self._on_preview_update)
|
||||
|
||||
else:
|
||||
logger.error("No stdDAQ stream address provided, launching without preview!")
|
||||
# Configure camera backend
|
||||
self.auto_soft_enable.put(auto_soft_enable, force=True)
|
||||
self.udp_backend_url.put(backend_url, force=True)
|
||||
|
||||
def configure(self, d: dict = None):
|
||||
"""Configure the next scan with the GigaFRoST camera
|
||||
|
||||
Parameters as 'd' dictionary
|
||||
----------------------------
|
||||
num_images : int, optional
|
||||
Number of images to be taken during each scan. Set to -1 for an
|
||||
unlimited number of images (limited by the ringbuffer size and
|
||||
backend speed). (default = 10)
|
||||
num_exposures : int, optional
|
||||
Number of images to be taken during each scan. Set to -1 for unlimited
|
||||
number of images (limited by the ringbuffer size and backend speed).
|
||||
exposure_time_ms : float, optional
|
||||
Exposure time [ms]. (default = 0.2)
|
||||
Exposure time [ms].
|
||||
exposure_period_ms : float, optional
|
||||
Exposure period [ms], ignored in soft trigger mode. (default = 1.0)
|
||||
Exposure period [ms], ignored in soft trigger mode.
|
||||
image_width : int, optional
|
||||
ROI size in the x-direction [pixels] (default = 2016)
|
||||
ROI size in the x-direction [pixels] (max. 2016)
|
||||
image_height : int, optional
|
||||
ROI size in the y-direction [pixels] (default = 2016)
|
||||
ROI size in the y-direction [pixels] (max. 2016)
|
||||
scanid : int, optional
|
||||
Scan identification number to be associated with the scan data
|
||||
(default = 0)
|
||||
correction_mode : int, optional
|
||||
The correction to be applied to the imaging data. The following
|
||||
modes are available (default = 5):
|
||||
|
||||
* 0: Bypass. No corrections are applied to the data.
|
||||
* 1: Send correction factor A instead of pixel values
|
||||
* 2: Send correction factor B instead of pixel values
|
||||
* 3: Send correction factor C instead of pixel values
|
||||
* 4: Invert pixel values, but do not apply any linearity correction
|
||||
* 5: Apply the full linearity correction
|
||||
acq_mode : str, optional
|
||||
Select one of the pre-configured trigger behavior
|
||||
"""
|
||||
# Stop acquisition
|
||||
self.set_idle()
|
||||
self.disarm()
|
||||
|
||||
backend_config = StdDaqConfigPartial(**d)
|
||||
self.backend.update_config(backend_config)
|
||||
# If Bluesky style configure
|
||||
if d:
|
||||
# Commonly changed settings
|
||||
if "exposure_num_burst" in d:
|
||||
self.num_exposures.set(d["exposure_num_burst"]).wait()
|
||||
if "num_exposures" in d:
|
||||
self.num_exposures.set(d["num_exposures"]).wait()
|
||||
if "exposure_time_ms" in d:
|
||||
self.acquire_time.set(d["exposure_time_ms"]).wait()
|
||||
if "exposure_period_ms" in d:
|
||||
self.acquire_period.set(d["exposure_period_ms"]).wait()
|
||||
if "image_width" in d:
|
||||
if d["image_width"] % 48 != 0:
|
||||
raise RuntimeError(f"[{self.name}] image_width must be divisible by 48")
|
||||
self.array_size.array_size_x.set(d["image_width"]).wait()
|
||||
if "image_height" in d:
|
||||
if d["image_height"] % 16 != 0:
|
||||
raise RuntimeError(f"[{self.name}] image_height must be divisible by 16")
|
||||
self.array_size.array_size_y.set(d["image_height"]).wait()
|
||||
|
||||
# Update all specified ophyd signals
|
||||
config = {}
|
||||
for key in self.component_names:
|
||||
val = d.get(key)
|
||||
if val is not None:
|
||||
config[key] = val
|
||||
self.corr_mode.set(d.get("corr_mode", 5)).wait()
|
||||
self.scan_id.set(d.get("scan_id", 0)).wait()
|
||||
|
||||
if d.get("exp_time", 0) > 0:
|
||||
config["exposure"] = d["exp_time"] * 1000 # exposure time in ms
|
||||
# If a pre-configured acquisition mode is specified, set it
|
||||
if "acq_mode" in d:
|
||||
self.set_acquisition_mode(d["acq_mode"])
|
||||
|
||||
if "corr_mode" not in config:
|
||||
config["corr_mode"] = 5
|
||||
if "scan_id" not in config:
|
||||
config["scan_id"] = 0
|
||||
super().configure(config)
|
||||
|
||||
# If the acquisition mode is specified, set it
|
||||
if "acq_mode" in d:
|
||||
self.set_acquisition_mode(config["acq_mode"])
|
||||
|
||||
# Commit parameters
|
||||
# Commit parameters to GigaFrost
|
||||
self.set_param.set(1).wait()
|
||||
|
||||
# Backend stdDAQ configuration
|
||||
if d and self.backend is not None:
|
||||
daq_update = {}
|
||||
if "image_height" in d:
|
||||
daq_update["image_pixel_height"] = d["image_height"]
|
||||
if "image_width" in d:
|
||||
daq_update["image_pixel_width"] = d["image_width"]
|
||||
if "bit_depth" in d:
|
||||
daq_update["bit_depth"] = d["bit_depth"]
|
||||
if "number_of_writers" in d:
|
||||
daq_update["number_of_writers"] = d["number_of_writers"]
|
||||
|
||||
if daq_update:
|
||||
self.backend.set_config(daq_update, force=False)
|
||||
|
||||
def set_acquisition_mode(self, acq_mode):
|
||||
"""Set acquisition mode
|
||||
|
||||
@@ -186,11 +199,10 @@ class GigaFrostCamera(PSIDeviceBase, GigaFrostBase):
|
||||
supplied signal. Use external enable instead, that works!
|
||||
"""
|
||||
|
||||
if acq_mode == "default":
|
||||
# NOTE: Trigger using software events via softEnable (actually works)
|
||||
if acq_mode in ["default", "step"]:
|
||||
# NOTE: Software trigger via softEnable (actually works)
|
||||
# Trigger parameters
|
||||
self.fix_nframes_mode = "start"
|
||||
|
||||
# Switch to physical enable signal
|
||||
self.mode_enbl_exp.set(0).wait()
|
||||
|
||||
@@ -236,7 +248,7 @@ class GigaFrostCamera(PSIDeviceBase, GigaFrostBase):
|
||||
raise RuntimeError(f"Unsupported acquisition mode: {acq_mode}")
|
||||
|
||||
@property
|
||||
def exposure_mode(self):
|
||||
def exposure_mode(self) -> str | None:
|
||||
"""Returns the current exposure mode of the GigaFRost camera.
|
||||
|
||||
Returns
|
||||
@@ -258,35 +270,34 @@ class GigaFrostCamera(PSIDeviceBase, GigaFrostBase):
|
||||
return None
|
||||
|
||||
@exposure_mode.setter
|
||||
def exposure_mode(self, exp_mode):
|
||||
def exposure_mode(self, mode):
|
||||
"""Apply the exposure mode for the GigaFRoST camera.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
exp_mode : {'external', 'timer', 'soft'}
|
||||
mode : {'external', 'timer', 'soft'}
|
||||
The exposure mode to be set.
|
||||
"""
|
||||
|
||||
modes = {
|
||||
"external": self.mode_exp_ext,
|
||||
"timer": self.mode_exp_timer,
|
||||
"soft": self.mode_exp_soft,
|
||||
}
|
||||
|
||||
if exp_mode not in const.gf_valid_exposure_modes:
|
||||
raise ValueError(
|
||||
f"Invalid exposure mode! Valid modes are:\n{const.gf_valid_exposure_modes}"
|
||||
)
|
||||
|
||||
for key, attr in modes.items():
|
||||
# set the desired mode to 1, all others to 0
|
||||
attr.set(int(key == exp_mode)).wait()
|
||||
if mode == "external":
|
||||
self.mode_exp_ext.set(1).wait()
|
||||
self.mode_exp_soft.set(0).wait()
|
||||
self.mode_exp_timer.set(0).wait()
|
||||
elif mode == "timer":
|
||||
self.mode_exp_ext.set(0).wait()
|
||||
self.mode_exp_soft.set(0).wait()
|
||||
self.mode_exp_timer.set(1).wait()
|
||||
elif mode == "soft":
|
||||
self.mode_exp_ext.set(0).wait()
|
||||
self.mode_exp_soft.set(1).wait()
|
||||
self.mode_exp_timer.set(0).wait()
|
||||
else:
|
||||
raise ValueError(f"Invalid exposure mode: {mode}!")
|
||||
|
||||
# Commit parameters
|
||||
self.set_param.set(1).wait()
|
||||
|
||||
@property
|
||||
def fix_nframes_mode(self) -> Literal["off", "start", "end", "start+end"] | None:
|
||||
def fix_nframes_mode(self) -> str | None:
|
||||
"""Return the current fixed number of frames mode of the GigaFRoST camera.
|
||||
|
||||
Returns
|
||||
@@ -309,7 +320,7 @@ class GigaFrostCamera(PSIDeviceBase, GigaFrostBase):
|
||||
return None
|
||||
|
||||
@fix_nframes_mode.setter
|
||||
def fix_nframes_mode(self, mode: Literal["off", "start", "end", "start+end"]):
|
||||
def fix_nframes_mode(self, mode: str):
|
||||
"""Apply the fixed number of frames settings to the GigaFRoST camera.
|
||||
|
||||
Parameters
|
||||
@@ -317,29 +328,26 @@ class GigaFrostCamera(PSIDeviceBase, GigaFrostBase):
|
||||
mode : {'off', 'start', 'end', 'start+end'}
|
||||
The fixed number of frames mode to be applied.
|
||||
"""
|
||||
self._fix_nframes_mode = mode
|
||||
if self._fix_nframes_mode == "off":
|
||||
if mode == "off":
|
||||
self.cnt_startbit.set(0).wait()
|
||||
self.cnt_endbit.set(0).wait()
|
||||
elif self._fix_nframes_mode == "start":
|
||||
elif mode == "start":
|
||||
self.cnt_startbit.set(1).wait()
|
||||
self.cnt_endbit.set(0).wait()
|
||||
elif self._fix_nframes_mode == "end":
|
||||
elif mode == "end":
|
||||
self.cnt_startbit.set(0).wait()
|
||||
self.cnt_endbit.set(1).wait()
|
||||
elif self._fix_nframes_mode == "start+end":
|
||||
elif mode == "start+end":
|
||||
self.cnt_startbit.set(1).wait()
|
||||
self.cnt_endbit.set(1).wait()
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Invalid fixed frame number mode! Valid modes are: {const.gf_valid_fix_nframe_modes}"
|
||||
)
|
||||
raise ValueError(f"Invalid fixed frame number mode: {mode}!")
|
||||
|
||||
# Commit parameters
|
||||
self.set_param.set(1).wait()
|
||||
|
||||
@property
|
||||
def trigger_mode(self) -> Literal["auto", "external", "timer", "soft"] | None:
|
||||
def trigger_mode(self) -> str | None:
|
||||
"""Method to detect the current trigger mode set in the GigaFRost camera.
|
||||
|
||||
Returns
|
||||
@@ -364,34 +372,43 @@ class GigaFrostCamera(PSIDeviceBase, GigaFrostBase):
|
||||
return None
|
||||
|
||||
@trigger_mode.setter
|
||||
def trigger_mode(self, mode: Literal["auto", "external", "timer", "soft"]):
|
||||
def trigger_mode(self, mode: str):
|
||||
"""
|
||||
Set the trigger mode for the GigaFRoST camera.
|
||||
|
||||
Args:
|
||||
mode(str): The trigger mode to be set. Valid arguments are: ['auto', 'external', 'timer', 'soft']
|
||||
Parameters
|
||||
----------
|
||||
mode : {'auto', 'external', 'timer', 'soft'}
|
||||
The trigger mode to be set.
|
||||
"""
|
||||
modes = {
|
||||
"auto": self.mode_trig_auto,
|
||||
"soft": self.mode_trig_soft,
|
||||
"timer": self.mode_trig_timer,
|
||||
"external": self.mode_trig_ext,
|
||||
}
|
||||
|
||||
if mode not in modes:
|
||||
raise ValueError(
|
||||
"Invalid trigger mode! Valid modes are: ['auto', 'external', 'timer', 'soft']"
|
||||
)
|
||||
|
||||
for key, attr in modes.items():
|
||||
# set the desired mode to 1, all others to 0
|
||||
attr.set(int(key == mode)).wait()
|
||||
if mode == "auto":
|
||||
self.mode_trig_auto.set(1).wait()
|
||||
self.mode_trig_soft.set(0).wait()
|
||||
self.mode_trig_timer.set(0).wait()
|
||||
self.mode_trig_ext.set(0).wait()
|
||||
elif mode == "soft":
|
||||
self.mode_trig_auto.set(0).wait()
|
||||
self.mode_trig_soft.set(1).wait()
|
||||
self.mode_trig_timer.set(0).wait()
|
||||
self.mode_trig_ext.set(0).wait()
|
||||
elif mode == "timer":
|
||||
self.mode_trig_auto.set(0).wait()
|
||||
self.mode_trig_soft.set(0).wait()
|
||||
self.mode_trig_timer.set(1).wait()
|
||||
self.mode_trig_ext.set(0).wait()
|
||||
elif mode == "external":
|
||||
self.mode_trig_auto.set(0).wait()
|
||||
self.mode_trig_soft.set(0).wait()
|
||||
self.mode_trig_timer.set(0).wait()
|
||||
self.mode_trig_ext.set(1).wait()
|
||||
else:
|
||||
raise ValueError(f"Invalid trigger mode: {mode}!")
|
||||
|
||||
# Commit parameters
|
||||
self.set_param.set(1).wait()
|
||||
|
||||
@property
|
||||
def enable_mode(self) -> Literal["soft", "external", "soft+ext", "always"] | None:
|
||||
def enable_mode(self) -> str | None:
|
||||
"""Return the enable mode set in the GigaFRoST camera.
|
||||
|
||||
Returns
|
||||
@@ -413,7 +430,7 @@ class GigaFrostCamera(PSIDeviceBase, GigaFrostBase):
|
||||
return None
|
||||
|
||||
@enable_mode.setter
|
||||
def enable_mode(self, mode: Literal["soft", "external", "soft+ext", "always"]):
|
||||
def enable_mode(self, mode: str):
|
||||
"""
|
||||
Set the enable mode for the GigaFRoST camera.
|
||||
|
||||
@@ -425,27 +442,17 @@ class GigaFrostCamera(PSIDeviceBase, GigaFrostBase):
|
||||
The GigaFRoST enable mode. Valid arguments are:
|
||||
|
||||
* 'soft':
|
||||
The GigaFRoST enable signal is supplied through a software
|
||||
signal
|
||||
The GigaFRoST enable signal is supplied through a software signal
|
||||
* 'external':
|
||||
The GigaFRoST enable signal is supplied through an external TTL
|
||||
gating signal from the rotaiton stage or some other control
|
||||
unit
|
||||
The GigaFRoST enable signal is supplied through an external TTL gating
|
||||
signal from the rotaiton stage or some other control unit
|
||||
* 'soft+ext':
|
||||
The GigaFRoST enable signal can be supplied either via the
|
||||
software signal or externally. The two signals are combined
|
||||
with a logical OR gate.
|
||||
The GigaFRoST enable signal can be supplied either via the software signal
|
||||
or externally. The two signals are combined with a logical OR gate.
|
||||
* 'always':
|
||||
The GigaFRoST is always enabled.
|
||||
CAUTION: This mode is not compatible with the fixed number of
|
||||
frames modes!
|
||||
CAUTION: This mode is not compatible with the fixed number of frames modes!
|
||||
"""
|
||||
|
||||
if mode not in const.gf_valid_enable_modes:
|
||||
raise ValueError(
|
||||
f"Invalid enable mode {mode}! Valid modes are:\n{const.gf_valid_enable_modes}"
|
||||
)
|
||||
|
||||
if mode == "soft":
|
||||
self.mode_enbl_ext.set(0).wait()
|
||||
self.mode_endbl_soft.set(1).wait()
|
||||
@@ -462,96 +469,29 @@ class GigaFrostCamera(PSIDeviceBase, GigaFrostBase):
|
||||
self.mode_enbl_ext.set(0).wait()
|
||||
self.mode_endbl_soft.set(0).wait()
|
||||
self.mode_enbl_auto.set(1).wait()
|
||||
else:
|
||||
raise ValueError(f"Invalid enable mode {mode}!")
|
||||
# Commit parameters
|
||||
self.set_param.set(1).wait()
|
||||
|
||||
def set_idle(self):
|
||||
def arm(self) -> None:
|
||||
"""Prepare the camera to accept triggers"""
|
||||
self.acquire.set(1).wait()
|
||||
|
||||
def disarm(self):
|
||||
"""Set the camera to idle state"""
|
||||
self.start_cam.set(0).wait()
|
||||
self.acquire.set(0).wait()
|
||||
if self.auto_soft_enable.get():
|
||||
self.soft_enable.set(0).wait()
|
||||
|
||||
def initialize_gigafrost(self) -> None:
|
||||
"""Initialize the camera, set channel values"""
|
||||
# Stop acquisition
|
||||
self.start_cam.set(0).wait()
|
||||
|
||||
# set entry to UDP table
|
||||
# number of UDP ports to use
|
||||
self.ports.set(2).wait()
|
||||
# number of images to send to each UDP port before switching to next
|
||||
self.framenum.set(5).wait()
|
||||
# offset in UDP table - where to find the first entry
|
||||
self.ht_offset.set(0).wait()
|
||||
# activate changes
|
||||
self.write_srv.set(1).wait()
|
||||
|
||||
# Configure software triggering if needed
|
||||
if self.auto_soft_enable.get():
|
||||
# trigger modes
|
||||
self.cnt_startbit.set(1).wait()
|
||||
self.cnt_endbit.set(0).wait()
|
||||
|
||||
# set modes
|
||||
self.enable_mode = "soft"
|
||||
self.trigger_mode = "auto"
|
||||
self.exposure_mode = "timer"
|
||||
|
||||
# line swap - on for west, off for east
|
||||
self.ls_sw.set(1).wait()
|
||||
self.ls_nw.set(1).wait()
|
||||
self.ls_se.set(0).wait()
|
||||
self.ls_ne.set(0).wait()
|
||||
|
||||
# Commit parameters
|
||||
self.set_param.set(1).wait()
|
||||
|
||||
# Initialize data backend
|
||||
n, s = self._define_backend_ip()
|
||||
self.ip_north.put(n, force=True)
|
||||
self.ip_south.put(s, force=True)
|
||||
n, s = self._define_backend_mac()
|
||||
self.mac_north.put(n, force=True)
|
||||
self.mac_south.put(s, force=True)
|
||||
# Set udp header table
|
||||
self.set_udp_header_table()
|
||||
|
||||
def set_udp_header_table(self):
|
||||
"""Set the communication parameters for the camera module"""
|
||||
self.conn_parm.set(self._build_udp_header_table()).wait()
|
||||
|
||||
def destroy(self):
|
||||
self.backend.shutdown()
|
||||
if self.backend is not None:
|
||||
self.backend.shutdown()
|
||||
super().destroy()
|
||||
|
||||
def _build_udp_header_table(self):
|
||||
"""Build the header table for the UDP communication"""
|
||||
udp_header_table = []
|
||||
|
||||
for i in range(0, 64, 1):
|
||||
for j in range(0, 8, 1):
|
||||
dest_port = 2000 + 8 * i + j
|
||||
source_port = 3000 + j
|
||||
if j < 4:
|
||||
extend_header_table(
|
||||
udp_header_table,
|
||||
self.mac_south.get(),
|
||||
self.ip_south.get(),
|
||||
dest_port,
|
||||
source_port,
|
||||
)
|
||||
else:
|
||||
extend_header_table(
|
||||
udp_header_table,
|
||||
self.mac_north.get(),
|
||||
self.ip_north.get(),
|
||||
dest_port,
|
||||
source_port,
|
||||
)
|
||||
|
||||
return udp_header_table
|
||||
|
||||
def _on_preview_update(self, img:np.ndarray):
|
||||
def _on_preview_update(self, img: np.ndarray, header: dict):
|
||||
"""Send preview stream and update frame index counter"""
|
||||
self.num_images_counter.put(header["frame"], force=True)
|
||||
self._run_subs(sub_type=self.SUB_DEVICE_MONITOR_2D, obj=self, value=img)
|
||||
|
||||
def acq_done(self) -> DeviceStatus:
|
||||
@@ -564,12 +504,14 @@ class GigaFrostCamera(PSIDeviceBase, GigaFrostBase):
|
||||
DeviceStatus: The status of the acquisition
|
||||
"""
|
||||
status = DeviceStatus(self)
|
||||
|
||||
self.backend.add_status_callback(
|
||||
status,
|
||||
success=[StdDaqStatus.IDLE, StdDaqStatus.FILE_SAVED],
|
||||
error=[StdDaqStatus.REJECTED, StdDaqStatus.ERROR],
|
||||
)
|
||||
if self.backend is not None:
|
||||
self.backend.add_status_callback(
|
||||
status,
|
||||
success=[StdDaqStatus.IDLE, StdDaqStatus.FILE_SAVED],
|
||||
error=[StdDaqStatus.REJECTED, StdDaqStatus.ERROR],
|
||||
)
|
||||
else:
|
||||
status.set_finished()
|
||||
return status
|
||||
|
||||
########################################
|
||||
@@ -589,18 +531,9 @@ class GigaFrostCamera(PSIDeviceBase, GigaFrostBase):
|
||||
Called after the device is connected and its signals are connected.
|
||||
Default values for signals should be set here.
|
||||
"""
|
||||
|
||||
# TODO: check if this can be moved to the config file
|
||||
# pylint: disable=protected-access
|
||||
self.auto_soft_enable._metadata["write_access"] = False
|
||||
self.backend_url._metadata["write_access"] = False
|
||||
self.auto_soft_enable.put(self._signals_to_be_set["auto_soft_enable"], force=True)
|
||||
self.backend_url.put(self._signals_to_be_set["backend_url"], force=True)
|
||||
|
||||
# Perform a full initialization of the GigaFrost
|
||||
self.initialize_gigafrost()
|
||||
|
||||
self.backend.connect()
|
||||
|
||||
def on_stage(self) -> DeviceStatus | None:
|
||||
"""
|
||||
Called while staging the device.
|
||||
@@ -613,43 +546,76 @@ class GigaFrostCamera(PSIDeviceBase, GigaFrostBase):
|
||||
self.unstage()
|
||||
sleep(0.5)
|
||||
|
||||
scan_msg = self.scan_info.msg
|
||||
# FIXME: I don't care about how we fish out config parameters from scan info
|
||||
scan_args = {
|
||||
**scan_msg.request_inputs["inputs"],
|
||||
**scan_msg.request_inputs["kwargs"],
|
||||
**scan_msg.scan_parameters,
|
||||
**self.scan_info.msg.request_inputs["inputs"],
|
||||
**self.scan_info.msg.request_inputs["kwargs"],
|
||||
**self.scan_info.msg.scan_parameters,
|
||||
}
|
||||
|
||||
self.configure(scan_args)
|
||||
d = {}
|
||||
if "image_width" in scan_args and scan_args["image_width"] is not None:
|
||||
d["image_width"] = scan_args["image_width"]
|
||||
if "image_height" in scan_args and scan_args["image_height"] is not None:
|
||||
d["image_height"] = scan_args["image_height"]
|
||||
if "exp_time" in scan_args and scan_args["exp_time"] is not None:
|
||||
d["exposure_time_ms"] = scan_args["exp_time"]
|
||||
if "exp_period" in scan_args and scan_args["exp_period"] is not None:
|
||||
d["exposure_period_ms"] = scan_args["exp_period"]
|
||||
if "acq_time" in scan_args and scan_args["acq_time"] is not None:
|
||||
d["exposure_time_ms"] = scan_args["acq_time"]
|
||||
if "acq_period" in scan_args and scan_args["acq_period"] is not None:
|
||||
d["exposure_period_ms"] = scan_args["acq_period"]
|
||||
if "exp_burst" in scan_args and scan_args["exp_burst"] is not None:
|
||||
d["exposure_num_burst"] = scan_args["exp_burst"]
|
||||
if "acq_mode" in scan_args and scan_args["acq_mode"] is not None:
|
||||
d["acq_mode"] = scan_args["acq_mode"]
|
||||
|
||||
if d:
|
||||
self.configure(d)
|
||||
|
||||
# Sync if out of sync
|
||||
if self.sync_flag.value == 0:
|
||||
self.sync_swhw.set(1).wait()
|
||||
|
||||
# stdDAQ backend parameters
|
||||
num_points = (
|
||||
1
|
||||
* scan_args.get("steps", 1)
|
||||
* scan_args.get("exp_burst", 1)
|
||||
* scan_args.get("repeats", 1)
|
||||
* scan_args.get("burst_at_each_point", 1)
|
||||
)
|
||||
self.num_images.set(num_points).wait()
|
||||
if "daq_file_path" in scan_args and scan_args["daq_file_path"] is not None:
|
||||
self.file_path.set(scan_args["daq_file_path"]).wait()
|
||||
if "daq_file_prefix" in scan_args and scan_args["daq_file_prefix"] is not None:
|
||||
self.file_prefix.set(scan_args["daq_file_prefix"]).wait()
|
||||
if "daq_num_images" in scan_args and scan_args["daq_num_images"] is not None:
|
||||
self.num_images.set(scan_args["daq_num_images"]).wait()
|
||||
# Start stdDAQ preview
|
||||
if self.live_preview is not None:
|
||||
self.live_preview.start()
|
||||
|
||||
def on_unstage(self) -> DeviceStatus | None:
|
||||
"""Called while unstaging the device."""
|
||||
# Switch to idle
|
||||
self.set_idle()
|
||||
logger.info(f"StdDaq status on unstage: {self.backend.status}")
|
||||
self.backend.stop()
|
||||
self.disarm()
|
||||
if self.backend is not None:
|
||||
logger.info(f"StdDaq status before unstage: {self.backend.status}")
|
||||
self.backend.stop()
|
||||
|
||||
def on_pre_scan(self) -> DeviceStatus | None:
|
||||
"""Called right before the scan starts on all devices automatically."""
|
||||
# Switch to acquiring
|
||||
self.backend.start(
|
||||
file_path=self.file_path.get(),
|
||||
file_prefix=self.file_prefix.get(),
|
||||
num_images=self.num_images.get(),
|
||||
)
|
||||
self.start_cam.set(1).wait()
|
||||
# First start the stdDAQ
|
||||
if self.backend is not None:
|
||||
self.backend.start(
|
||||
file_path=self.file_path.get(),
|
||||
file_prefix=self.file_prefix.get(),
|
||||
num_images=self.num_images.get(),
|
||||
)
|
||||
# Then start the camera
|
||||
self.arm()
|
||||
|
||||
def on_trigger(self) -> DeviceStatus | None:
|
||||
"""Called when the device is triggered."""
|
||||
@@ -667,6 +633,13 @@ class GigaFrostCamera(PSIDeviceBase, GigaFrostBase):
|
||||
# BEC teststand operation mode: posedge of SoftEnable if Started
|
||||
self.soft_enable.set(0).wait()
|
||||
self.soft_enable.set(1).wait()
|
||||
|
||||
if self.acquire_block.get() or self.backend is None:
|
||||
wait_time = 0.2 + 0.001 * self.num_exposures.value * max(
|
||||
self.acquire_time.value, self.acquire_period.value
|
||||
)
|
||||
logger.info(f"[{self.name}] Triggering set to block for {wait_time} seconds")
|
||||
return DeviceStatus(self, done=True, success=True, settle_time=wait_time)
|
||||
else:
|
||||
self.soft_trig.set(1).wait()
|
||||
|
||||
@@ -685,6 +658,12 @@ class GigaFrostCamera(PSIDeviceBase, GigaFrostBase):
|
||||
# Automatically connect to MicroSAXS testbench if directly invoked
|
||||
if __name__ == "__main__":
|
||||
gf = GigaFrostCamera(
|
||||
"X02DA-CAM-GF2:", name="gf2", backend_url="http://xbl-daq-28:8080", auto_soft_enable=True
|
||||
"X02DA-CAM-GF2:",
|
||||
name="gf2",
|
||||
backend_url="http://xbl-daq-28:8080",
|
||||
auto_soft_enable=True,
|
||||
std_daq_ws="ws://129.129.95.111:8080",
|
||||
std_daq_rest="http://129.129.95.111:5000",
|
||||
std_daq_live="tcp://129.129.95.111:20000",
|
||||
)
|
||||
gf.wait_for_connection()
|
||||
|
||||
158
tomcat_bec/devices/gigafrost/pco_consumer.py
Normal file
158
tomcat_bec/devices/gigafrost/pco_consumer.py
Normal file
@@ -0,0 +1,158 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Standard DAQ preview image stream module
|
||||
|
||||
Created on Thu Jun 27 17:28:43 2024
|
||||
|
||||
@author: mohacsi_i
|
||||
"""
|
||||
from time import sleep, time
|
||||
import threading
|
||||
import zmq
|
||||
import json
|
||||
|
||||
ZMQ_TOPIC_FILTER = b""
|
||||
|
||||
|
||||
|
||||
class PcoTestConsumer:
|
||||
"""Detector wrapper class around the StdDaq preview image stream.
|
||||
|
||||
This was meant to provide live image stream directly from the StdDAQ.
|
||||
Note that the preview stream must be already throtled in order to cope
|
||||
with the incoming data and the python class might throttle it further.
|
||||
|
||||
You can add a preview widget to the dock by:
|
||||
cam_widget = gui.add_dock('cam_dock1').add_widget('BECFigure').image('daq_stream1')
|
||||
"""
|
||||
|
||||
# Subscriptions for plotting image
|
||||
_shutdown_event = threading.Event()
|
||||
_monitor_mutex = threading.Lock()
|
||||
_monitor_thread = None
|
||||
|
||||
# Status attributes
|
||||
_url = None
|
||||
_image = None
|
||||
_frame = None
|
||||
_socket = None
|
||||
|
||||
def __init__(self, url: str = "tcp://129.129.95.38:20000") -> None:
|
||||
super().__init__()
|
||||
self._url = url
|
||||
|
||||
def connect(self):
|
||||
"""Connect to te StDAQs PUB-SUB streaming interface"""
|
||||
# Socket to talk to server
|
||||
context = zmq.Context()
|
||||
self._socket = context.socket(zmq.PULL)
|
||||
try:
|
||||
self._socket.connect(self.url)
|
||||
except ConnectionRefusedError:
|
||||
sleep(1)
|
||||
self._socket.connect(self.url)
|
||||
|
||||
def disconnect(self):
|
||||
"""Disconnect"""
|
||||
try:
|
||||
if self._socket is not None:
|
||||
self._socket.disconnect(self.url)
|
||||
except zmq.ZMQError:
|
||||
pass
|
||||
finally:
|
||||
self._socket = None
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return self._url
|
||||
|
||||
@property
|
||||
def image(self):
|
||||
return self._image
|
||||
|
||||
@property
|
||||
def frame(self):
|
||||
return self._frame
|
||||
|
||||
# pylint: disable=protected-access
|
||||
def start(self):
|
||||
"""Start listening for preview data stream"""
|
||||
if self._monitor_mutex.locked():
|
||||
raise RuntimeError("Only one consumer permitted")
|
||||
|
||||
self.connect()
|
||||
self._mon = threading.Thread(target=self.poll, daemon=True)
|
||||
self._mon.start()
|
||||
|
||||
def stop(self):
|
||||
"""Stop a running preview"""
|
||||
self._shutdown_event.set()
|
||||
if self._mon is not None:
|
||||
self._stop_polling = True
|
||||
# Might hang on recv_multipart
|
||||
self._mon.join(timeout=1)
|
||||
# So also disconnect the socket
|
||||
self.disconnect()
|
||||
self._shutdown_event.clear()
|
||||
|
||||
def poll(self):
|
||||
"""Collect streamed updates"""
|
||||
try:
|
||||
t_last = time()
|
||||
print("Starting monitor")
|
||||
with self._monitor_mutex:
|
||||
while not self._shutdown_event.is_set():
|
||||
try:
|
||||
# pylint: disable=no-member
|
||||
r = self._socket.recv_multipart(flags=zmq.NOBLOCK)
|
||||
|
||||
# Length and throtling checks
|
||||
t_curr = time()
|
||||
t_elapsed = t_curr - t_last
|
||||
if t_elapsed < self.parent.throttle.get():
|
||||
continue
|
||||
# # Unpack the Array V1 reply to metadata and array data
|
||||
meta, data = r
|
||||
|
||||
# Update image and update subscribers
|
||||
header = json.loads(meta)
|
||||
self.header = header
|
||||
# if header["type"] == "uint16":
|
||||
# image = np.frombuffer(data, dtype=np.uint16)
|
||||
# if image.size != np.prod(header['shape']):
|
||||
# err = f"Unexpected array size of {image.size} for header: {header}"
|
||||
# raise ValueError(err)
|
||||
# image = image.reshape(header['shape'])
|
||||
|
||||
# # Update image and update subscribers
|
||||
# self._frame = header['frame']
|
||||
# self._image = image
|
||||
t_last = t_curr
|
||||
# print(
|
||||
# f"[{self.name}] Updated frame {header['frame']}\t"
|
||||
# f"Shape: {header['shape']}\tMean: {np.mean(image):.3f}"
|
||||
# )
|
||||
except ValueError:
|
||||
# Happens when ZMQ partially delivers the multipart message
|
||||
pass
|
||||
except zmq.error.Again:
|
||||
# Happens when receive queue is empty
|
||||
sleep(0.1)
|
||||
except Exception as ex:
|
||||
print(f"{str(ex)}")
|
||||
raise
|
||||
finally:
|
||||
try:
|
||||
self._socket.disconnect(self.url)
|
||||
except RuntimeError:
|
||||
pass
|
||||
self._monitor_thread = None
|
||||
print(f"Detaching monitor")
|
||||
|
||||
|
||||
# Automatically connect to MicroSAXS testbench if directly invoked
|
||||
if __name__ == "__main__":
|
||||
daq = PcoTestConsumer(url="tcp://10.4.0.82:8080")
|
||||
daq.start()
|
||||
sleep(500)
|
||||
daq.stop()
|
||||
@@ -9,6 +9,7 @@ Created on Thu Jun 27 17:28:43 2024
|
||||
from time import sleep, time
|
||||
from threading import Thread
|
||||
import zmq
|
||||
import json
|
||||
from ophyd import Device, Signal, Component, Kind
|
||||
from ophyd_devices.interfaces.base_classes.psi_detector_base import (
|
||||
CustomDetectorMixin,
|
||||
@@ -55,6 +56,7 @@ class PcoTestConsumerMixin(CustomDetectorMixin):
|
||||
"""Collect streamed updates"""
|
||||
try:
|
||||
t_last = time()
|
||||
print("Starting monitor")
|
||||
while True:
|
||||
try:
|
||||
# Exit loop and finish monitoring
|
||||
@@ -63,7 +65,7 @@ class PcoTestConsumerMixin(CustomDetectorMixin):
|
||||
break
|
||||
|
||||
# pylint: disable=no-member
|
||||
r = self.parent._socket.recv()
|
||||
r = self.parent._socket.recv_multipart(flags=zmq.NOBLOCK)
|
||||
|
||||
# Length and throtling checks
|
||||
t_curr = time()
|
||||
@@ -71,11 +73,11 @@ class PcoTestConsumerMixin(CustomDetectorMixin):
|
||||
if t_elapsed < self.parent.throttle.get():
|
||||
continue
|
||||
# # Unpack the Array V1 reply to metadata and array data
|
||||
# meta, data = r
|
||||
# print(meta)
|
||||
meta, data = r
|
||||
|
||||
# # Update image and update subscribers
|
||||
# header = json.loads(meta)
|
||||
# Update image and update subscribers
|
||||
header = json.loads(meta)
|
||||
self.parent.header = header
|
||||
# if header["type"] == "uint16":
|
||||
# image = np.frombuffer(data, dtype=np.uint16)
|
||||
# if image.size != np.prod(header['shape']):
|
||||
@@ -94,6 +96,7 @@ class PcoTestConsumerMixin(CustomDetectorMixin):
|
||||
# f"[{self.parent.name}] Updated frame {header['frame']}\t"
|
||||
# f"Shape: {header['shape']}\tMean: {np.mean(image):.3f}"
|
||||
# )
|
||||
print(f"[{self.parent.name}] Updated frame {header['frame']}\t")
|
||||
except ValueError:
|
||||
# Happens when ZMQ partially delivers the multipart message
|
||||
pass
|
||||
@@ -105,7 +108,7 @@ class PcoTestConsumerMixin(CustomDetectorMixin):
|
||||
raise
|
||||
finally:
|
||||
try:
|
||||
self.parent._socket.disconnect()
|
||||
self.parent._socket.disconnect(self.parent.url.get())
|
||||
except RuntimeError:
|
||||
pass
|
||||
self.parent._mon = None
|
||||
@@ -128,6 +131,8 @@ class PcoTestConsumer(PSIDetectorBase):
|
||||
SUB_MONITOR = "device_monitor_2d"
|
||||
_default_sub = SUB_MONITOR
|
||||
|
||||
header = None
|
||||
|
||||
custom_prepare_cls = PcoTestConsumerMixin
|
||||
|
||||
# Status attributes
|
||||
|
||||
176
tomcat_bec/devices/gigafrost/pcoedge_base.py
Normal file
176
tomcat_bec/devices/gigafrost/pcoedge_base.py
Normal file
@@ -0,0 +1,176 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created on Wed Dec 6 11:33:54 2023
|
||||
|
||||
@author: mohacsi_i
|
||||
"""
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, DynamicDeviceComponent, EpicsSignal, EpicsSignalRO, Kind, Signal
|
||||
|
||||
|
||||
class PcoEdgeBase(Device):
|
||||
"""Ophyd baseclass for Helge camera IOCs
|
||||
|
||||
This class provides wrappers for Helge's camera IOCs around SwissFEL and
|
||||
for high performance SLS 2.0 cameras. The IOC's operation is a bit arcane
|
||||
and there are different versions and cameras all around. So this device
|
||||
only covers the absolute basics.
|
||||
|
||||
Probably the most important part is the configuration state machine. As
|
||||
the SET_PARAMS takes care of buffer allocations it might take some time,
|
||||
as well as a full re-configuration is required every time we change the
|
||||
binning, roi, etc... This is automatically performed upon starting an
|
||||
exposure (if it heven't been done before).
|
||||
|
||||
The status flag state machine during re-configuration is:
|
||||
BUSY low, SET low -> BUSY high, SET low -> BUSY low, SET high -> BUSY low, SET low
|
||||
|
||||
|
||||
UPDATE: Data sending operation modes
|
||||
- Switch to ZMQ streaming by setting FILEFORMAT to ZEROMQ
|
||||
- Set SAVESTART and SAVESTOP to select a ROI of image indices
|
||||
- Start file transfer with FTRANSFER.
|
||||
The ZMQ connection operates in PUSH-PULL mode, i.e. it needs incoming connection.
|
||||
|
||||
STOREMODE sets the acquisition mode:
|
||||
if STOREMODE == Recorder
|
||||
Fills up the buffer with images. Here SAVESTART and SAVESTOP selects a ROI
|
||||
of image indices to be streamed out (i.e. maximum buffer_size number of images)
|
||||
|
||||
if STOREMODE == FIFO buffer
|
||||
Continously streams out data using the buffer as a FIFO queue.
|
||||
Here SAVESTART and SAVESTOP selects a ROI of image indices to be streamed continously
|
||||
(i.e. a large SAVESTOP streams indefinitely). Note that in FIFO mode buffer reads are
|
||||
destructive. to prevent this, we don't have EPICS preview
|
||||
"""
|
||||
|
||||
# ########################################################################
|
||||
# General hardware info (in AD nomenclature)
|
||||
manufacturer = Cpt(EpicsSignalRO, "QUERY", kind=Kind.config, doc="Camera manufacturer info")
|
||||
model = Cpt(EpicsSignalRO, "BOARD", kind=Kind.omitted, doc="Camera board info")
|
||||
|
||||
# ########################################################################
|
||||
# Acquisition configuration (in AD nomenclature)
|
||||
acquire = Cpt(EpicsSignal, "CAMERASTATUS", put_complete=True, kind=Kind.omitted)
|
||||
acquire_time = Cpt(
|
||||
EpicsSignal, "EXPOSURE", put_complete=True, auto_monitor=True, kind=Kind.config
|
||||
)
|
||||
acquire_delay = Cpt(
|
||||
EpicsSignal, "DELAY", put_complete=True, auto_monitor=True, kind=Kind.config
|
||||
)
|
||||
trigger_mode = Cpt(
|
||||
EpicsSignal, "TRIGGER", put_complete=True, auto_monitor=True, kind=Kind.config
|
||||
)
|
||||
# num_exposures = Cpt(
|
||||
# EpicsSignal, "CNT_NUM", put_complete=True, auto_monitor=True, kind=Kind.config
|
||||
# )
|
||||
|
||||
array_size = DynamicDeviceComponent(
|
||||
{
|
||||
"array_size_x": (EpicsSignal, "WIDTH", {"auto_monitor": True, "put_complete": True}),
|
||||
"array_size_y": (EpicsSignal, "HEIGHT", {"auto_monitor": True, "put_complete": True}),
|
||||
},
|
||||
doc="Size of the array in the XY dimensions",
|
||||
)
|
||||
|
||||
# DAQ parameters
|
||||
file_path = Cpt(Signal, kind=Kind.config, value="/gpfs/test/test-beamline")
|
||||
file_prefix = Cpt(Signal, kind=Kind.config, value="scan_")
|
||||
num_images = Cpt(Signal, kind=Kind.config, value=1000)
|
||||
num_images_counter = Cpt(Signal, kind=Kind.hinted, value=0)
|
||||
|
||||
# GF specific interface
|
||||
acquire_block = Cpt(Signal, kind=Kind.config, value=0)
|
||||
|
||||
# ########################################################################
|
||||
# Image size configuration (in AD nomenclature)
|
||||
bin_x = Cpt(EpicsSignal, "BINX", put_complete=True, auto_monitor=True, kind=Kind.config)
|
||||
bin_y = Cpt(EpicsSignal, "BINY", put_complete=True, auto_monitor=True, kind=Kind.config)
|
||||
|
||||
# ########################################################################
|
||||
# Additional status info
|
||||
busy = Cpt(EpicsSignalRO, "BUSY", auto_monitor=True, kind=Kind.config)
|
||||
camState = Cpt(EpicsSignalRO, "SS_CAMERA", auto_monitor=True, kind=Kind.config)
|
||||
camProgress = Cpt(EpicsSignalRO, "CAMPROGRESS", auto_monitor=True, kind=Kind.config)
|
||||
|
||||
# ########################################################################
|
||||
# Configuration state maschine with separate transition states
|
||||
set_param = Cpt(
|
||||
EpicsSignal,
|
||||
"BUSY_SET_PARAM",
|
||||
write_pv="SET_PARAM",
|
||||
put_complete=True,
|
||||
auto_monitor=True,
|
||||
kind=Kind.config,
|
||||
)
|
||||
|
||||
camera_statuscode = Cpt(EpicsSignalRO, "STATUSCODE", auto_monitor=True, kind=Kind.config)
|
||||
camera_init = Cpt(EpicsSignalRO, "INIT", auto_monitor=True, kind=Kind.config)
|
||||
camera_init_busy = Cpt(EpicsSignalRO, "BUSY_INIT", auto_monitor=True, kind=Kind.config)
|
||||
# camCamera = Cpt(EpicsSignalRO, "CAMERA", auto_monitor=True, kind=Kind.config)
|
||||
# camCameraBusy = Component(EpicsSignalRO, "BUSY_CAMERA", auto_monitor=True, kind=Kind.config)
|
||||
|
||||
# ########################################################################
|
||||
# Acquisition configuration
|
||||
acquire_mode = Cpt(EpicsSignalRO, "ACQMODE", auto_monitor=True, kind=Kind.config)
|
||||
acquire_trigger = Cpt(EpicsSignalRO, "TRIGGER", auto_monitor=True, kind=Kind.config)
|
||||
# acqTriggerSource = Component(
|
||||
# EpicsSignalRO, "TRIGGERSOURCE", auto_monitor=True, kind=Kind.config)
|
||||
# acqTriggerEdge = Component(EpicsSignalRO, "TRIGGEREDGE", auto_monitor=True, kind=Kind.config)
|
||||
|
||||
# ########################################################################
|
||||
# Buffer configuration
|
||||
bufferRecMode = Cpt(EpicsSignalRO, "RECMODE", auto_monitor=True, kind=Kind.config)
|
||||
bufferStoreMode = Cpt(EpicsSignal, "STOREMODE", auto_monitor=True, kind=Kind.config)
|
||||
fileRecMode = Cpt(EpicsSignalRO, "RECMODE", auto_monitor=True, kind=Kind.config)
|
||||
|
||||
buffer_used = Cpt(EpicsSignalRO, "PIC_BUFFER", auto_monitor=True, kind=Kind.normal)
|
||||
buffer_size = Cpt(EpicsSignalRO, "PIC_MAX", auto_monitor=True, kind=Kind.normal)
|
||||
buffer_clear = Cpt(EpicsSignal, "CLEARMEM", put_complete=True, kind=Kind.omitted)
|
||||
|
||||
# ########################################################################
|
||||
# File saving/streaming interface
|
||||
cam_data_rate = Cpt(EpicsSignalRO, "CAMRATE", auto_monitor=True, kind=Kind.normal)
|
||||
file_data_rate = Cpt(EpicsSignalRO, "FILERATE", auto_monitor=True, kind=Kind.normal)
|
||||
file_savestart = Cpt(EpicsSignal, "SAVESTART", put_complete=True, kind=Kind.config)
|
||||
file_savestop = Cpt(EpicsSignal, "SAVESTOP", put_complete=True, kind=Kind.config)
|
||||
file_format = Cpt(EpicsSignal, "FILEFORMAT", put_complete=True, kind=Kind.config)
|
||||
file_transfer = Cpt(EpicsSignal, "FTRANSFER", put_complete=True, kind=Kind.config)
|
||||
file_savebusy = Cpt(EpicsSignalRO, "FILESAVEBUSY", auto_monitor=True, kind=Kind.normal)
|
||||
|
||||
# ########################################################################
|
||||
# Throtled image preview
|
||||
image = Cpt(EpicsSignalRO, "FPICTURE", kind=Kind.omitted, doc="Throttled image preview")
|
||||
|
||||
# ########################################################################
|
||||
# General hardware info
|
||||
camError = Cpt(EpicsSignalRO, "ERRCODE", auto_monitor=True, kind=Kind.config)
|
||||
camWarning = Cpt(EpicsSignalRO, "WARNCODE", auto_monitor=True, kind=Kind.config)
|
||||
|
||||
@property
|
||||
def state(self) -> str:
|
||||
"""Single word camera state"""
|
||||
if self.set_param.value:
|
||||
return "BUSY"
|
||||
if self.camera_statuscode.value == 2 and self.camera_init.value == 1:
|
||||
return "IDLE"
|
||||
if self.camera_statuscode.value == 6 and self.camera_init.value == 1:
|
||||
return "RUNNING"
|
||||
# if self.camRemoval.value==0 and self.camInit.value==0:
|
||||
if self.camera_init.value == 0:
|
||||
return "OFFLINE"
|
||||
# if self.camRemoval.value:
|
||||
# return "REMOVED"
|
||||
return "UNKNOWN"
|
||||
|
||||
@state.setter
|
||||
def state(self):
|
||||
raise RuntimeError("State is a ReadOnly property")
|
||||
|
||||
|
||||
# Automatically connect to test camera if directly invoked
|
||||
if __name__ == "__main__":
|
||||
|
||||
# Drive data collection
|
||||
cam = PcoEdgeBase("X02DA-CCDCAM2:", name="mcpcam")
|
||||
cam.wait_for_connection()
|
||||
@@ -5,141 +5,22 @@ Created on Wed Dec 6 11:33:54 2023
|
||||
@author: mohacsi_i
|
||||
"""
|
||||
import time
|
||||
from ophyd import Component, EpicsSignal, EpicsSignalRO, Kind
|
||||
import numpy as np
|
||||
from ophyd.status import SubscriptionStatus, DeviceStatus
|
||||
from ophyd_devices import BECDeviceBase
|
||||
from ophyd_devices.interfaces.base_classes.psi_detector_base import (
|
||||
CustomDetectorMixin as CustomPrepare,
|
||||
)
|
||||
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
|
||||
|
||||
try:
|
||||
from bec_lib import bec_logger
|
||||
from bec_lib.logger import bec_logger
|
||||
|
||||
logger = bec_logger.logger
|
||||
except ModuleNotFoundError:
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("PcoEdgeCam")
|
||||
from tomcat_bec.devices.gigafrost.pcoedge_base import PcoEdgeBase
|
||||
from tomcat_bec.devices.gigafrost.std_daq_preview import StdDaqPreview
|
||||
from tomcat_bec.devices.gigafrost.std_daq_client import StdDaqClient, StdDaqStatus
|
||||
|
||||
|
||||
class PcoEdgeCameraMixin(CustomPrepare):
|
||||
"""Mixin class to setup the Helge camera bae class.
|
||||
|
||||
This class will be called by the custom_prepare_cls attribute of the detector class.
|
||||
"""
|
||||
# pylint: disable=protected-access
|
||||
def on_stage(self) -> None:
|
||||
"""Configure and arm PCO.Edge camera for acquisition"""
|
||||
|
||||
# PCO can finish a run without explicit unstaging
|
||||
if self.parent.state not in ("IDLE"):
|
||||
logger.warning(
|
||||
f"Trying to stage the camera from state {self.parent.state}, unstaging it first!"
|
||||
)
|
||||
self.parent.unstage()
|
||||
time.sleep(0.5)
|
||||
|
||||
# Fish out our configuration from scaninfo (via explicit or generic addressing)
|
||||
scanparam = self.parent.scaninfo.scan_msg.info
|
||||
d = {}
|
||||
if "kwargs" in scanparam:
|
||||
scanargs = scanparam["kwargs"]
|
||||
if "exp_burst" in scanargs and scanargs["exp_burst"] is not None:
|
||||
d["exposure_num_burst"] = scanargs["exp_burst"]
|
||||
if "image_width" in scanargs and scanargs["image_width"] is not None:
|
||||
d["image_width"] = scanargs["image_width"]
|
||||
if "image_height" in scanargs and scanargs["image_height"] is not None:
|
||||
d["image_height"] = scanargs["image_height"]
|
||||
if "exp_time" in scanargs and scanargs["exp_time"] is not None:
|
||||
d["exposure_time_ms"] = scanargs["exp_time"]
|
||||
if "exp_period" in scanargs and scanargs["exp_period"] is not None:
|
||||
d["exposure_period_ms"] = scanargs["exp_period"]
|
||||
# if 'exp_burst' in scanargs and scanargs['exp_burst'] is not None:
|
||||
# d['exposure_num_burst'] = scanargs['exp_burst']
|
||||
# if 'acq_mode' in scanargs and scanargs['acq_mode'] is not None:
|
||||
# d['acq_mode'] = scanargs['acq_mode']
|
||||
# elif self.parent.scaninfo.scan_type == "step":
|
||||
# d['acq_mode'] = "default"
|
||||
if "pco_store_mode" in scanargs and scanargs["pco_store_mode"] is not None:
|
||||
d["store_mode"] = scanargs["pco_store_mode"]
|
||||
if "pco_data_format" in scanargs and scanargs["pco_data_format"] is not None:
|
||||
d["data_format"] = scanargs["pco_data_format"]
|
||||
|
||||
# Perform bluesky-style configuration
|
||||
if len(d) > 0:
|
||||
logger.warning(f"[{self.parent.name}] Configuring with:\n{d}")
|
||||
self.parent.configure(d=d)
|
||||
|
||||
# ARM the camera
|
||||
self.parent.bluestage()
|
||||
|
||||
def on_unstage(self) -> None:
|
||||
"""Disarm the PCO.Edge camera"""
|
||||
self.parent.blueunstage()
|
||||
|
||||
def on_stop(self) -> None:
|
||||
"""Stop the PCO.Edge camera"""
|
||||
self.parent.blueunstage()
|
||||
|
||||
def on_trigger(self) -> None | DeviceStatus:
|
||||
"""Trigger mode operation
|
||||
|
||||
Use it to repeatedly record a fixed number of frames and send it to stdDAQ. The method waits
|
||||
for the acquisition and data transfer to complete.
|
||||
|
||||
NOTE: Maciej confirmed that sparse data is no problem to the stdDAQ.
|
||||
TODO: Optimize data transfer to launch at end and check completion at the beginning.
|
||||
"""
|
||||
# Ensure that previous data transfer finished
|
||||
# def sentIt(*args, value, timestamp, **kwargs):
|
||||
# return value==0
|
||||
# status = SubscriptionStatus(self.parent.file_savebusy, sentIt, timeout=120)
|
||||
# status.wait()
|
||||
|
||||
# Not sure if it always sends the first batch of images or the newest
|
||||
def wait_bufferreset(*, old_value, value, timestamp, **_):
|
||||
return (value < old_value) or (value == 0)
|
||||
|
||||
self.parent.buffer_clear.set(1).wait()
|
||||
status = SubscriptionStatus(self.parent.buffer_used, wait_bufferreset, timeout=5)
|
||||
status.wait()
|
||||
|
||||
t_expected = (
|
||||
self.parent.acquire_time.get() + self.parent.acquire_delay.get()
|
||||
) * self.parent.file_savestop.get()
|
||||
|
||||
# Wait until the buffer fills up with enough images
|
||||
def wait_acquisition(*, value, timestamp, **_):
|
||||
num_target = self.parent.file_savestop.get()
|
||||
# logger.warning(f"{value} of {num_target}")
|
||||
return bool(value >= num_target)
|
||||
max_wait = max(5, 5 * t_expected)
|
||||
status = SubscriptionStatus(
|
||||
self.parent.buffer_used, wait_acquisition, timeout=max_wait, settle_time=0.2
|
||||
)
|
||||
status.wait()
|
||||
|
||||
# Then start file transfer (need to get the save busy flag update)
|
||||
# self.parent.file_transfer.set(1, settle_time=0.2).wait()
|
||||
self.parent.file_transfer.set(1).wait()
|
||||
|
||||
# And wait until the images have been sent
|
||||
# NOTE: this does not wait for new value, the first check will be
|
||||
# against values from the previous cycle, i.e. pass automatically.
|
||||
t_start = time.time()
|
||||
|
||||
def wait_sending(*args, old_value, value, timestamp, **kwargs):
|
||||
t_elapsed = timestamp - t_start
|
||||
# logger.warning(f"{old_value}\t{value}\t{t_elapsed}")
|
||||
return old_value == 1 and value == 0 and t_elapsed > 0
|
||||
|
||||
status = SubscriptionStatus(
|
||||
self.parent.file_savebusy, wait_sending, timeout=120, settle_time=0.2
|
||||
)
|
||||
status.wait()
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class HelgeCameraBase(BECDeviceBase):
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class PcoEdge5M(PSIDeviceBase, PcoEdgeBase):
|
||||
"""Ophyd baseclass for Helge camera IOCs
|
||||
|
||||
This class provides wrappers for Helge's camera IOCs around SwissFEL and
|
||||
@@ -175,107 +56,52 @@ class HelgeCameraBase(BECDeviceBase):
|
||||
destructive. to prevent this, we don't have EPICS preview
|
||||
"""
|
||||
|
||||
# ########################################################################
|
||||
# General hardware info (in AD nomenclature)
|
||||
manufacturer = Component(EpicsSignalRO, "QUERY", kind=Kind.config, doc="Camera model info")
|
||||
model = Component(EpicsSignalRO, "BOARD", kind=Kind.omitted, doc="Camera board info")
|
||||
USER_ACCESS = ["complete", "backend", "live_preview", "arm", "disarm"]
|
||||
|
||||
# ########################################################################
|
||||
# Acquisition commands
|
||||
camStatusCmd = Component(EpicsSignal, "CAMERASTATUS", put_complete=True, kind=Kind.config)
|
||||
# Placeholders for stdDAQ and livestream clients
|
||||
backend = None
|
||||
live_preview = None
|
||||
|
||||
# ########################################################################
|
||||
# Acquisition configuration (in AD nomenclature)
|
||||
acquire_time = Component(
|
||||
EpicsSignal, "EXPOSURE", put_complete=True, auto_monitor=True, kind=Kind.config
|
||||
)
|
||||
acquire_delay = Component(
|
||||
EpicsSignal, "DELAY", put_complete=True, auto_monitor=True, kind=Kind.config
|
||||
)
|
||||
trigger_mode = Component(
|
||||
EpicsSignal, "TRIGGER", put_complete=True, auto_monitor=True, kind=Kind.config
|
||||
)
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(
|
||||
self,
|
||||
prefix="",
|
||||
*,
|
||||
name,
|
||||
kind=None,
|
||||
read_attrs=None,
|
||||
configuration_attrs=None,
|
||||
parent=None,
|
||||
scan_info=None,
|
||||
std_daq_rest: str | None = None,
|
||||
std_daq_ws: str | None = None,
|
||||
std_daq_live: str | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
# super() will call the mixin class
|
||||
super().__init__(
|
||||
prefix=prefix,
|
||||
name=name,
|
||||
kind=kind,
|
||||
read_attrs=read_attrs,
|
||||
configuration_attrs=configuration_attrs,
|
||||
parent=parent,
|
||||
scan_info=scan_info,
|
||||
**kwargs,
|
||||
)
|
||||
# Configure the stdDAQ client
|
||||
if std_daq_rest is None or std_daq_ws is None:
|
||||
# raise ValueError("Both std_daq_rest and std_daq_ws must be provided")
|
||||
logger.error("No stdDAQ address provided, launching without data backend!")
|
||||
else:
|
||||
self.backend = StdDaqClient(parent=self, ws_url=std_daq_ws, rest_url=std_daq_rest)
|
||||
# Configure image preview
|
||||
if std_daq_live is not None:
|
||||
self.live_preview = StdDaqPreview(url=std_daq_live, cb=self._on_preview_update)
|
||||
else:
|
||||
logger.error("No stdDAQ stream address provided, launching without preview!")
|
||||
|
||||
# ########################################################################
|
||||
# Image size configuration (in AD nomenclature)
|
||||
bin_x = Component(EpicsSignal, "BINX", put_complete=True, auto_monitor=True, kind=Kind.config)
|
||||
bin_y = Component(EpicsSignal, "BINY", put_complete=True, auto_monitor=True, kind=Kind.config)
|
||||
array_size_x = Component(
|
||||
EpicsSignalRO, "WIDTH", auto_monitor=True, kind=Kind.config, doc="Final image width"
|
||||
)
|
||||
array_size_y = Component(
|
||||
EpicsSignalRO, "HEIGHT", auto_monitor=True, kind=Kind.config, doc="Final image height"
|
||||
)
|
||||
|
||||
# ########################################################################
|
||||
# General hardware info
|
||||
camError = Component(EpicsSignalRO, "ERRCODE", auto_monitor=True, kind=Kind.config)
|
||||
camWarning = Component(EpicsSignalRO, "WARNCODE", auto_monitor=True, kind=Kind.config)
|
||||
|
||||
# ########################################################################
|
||||
# Buffer configuration
|
||||
bufferRecMode = Component(EpicsSignalRO, "RECMODE", auto_monitor=True, kind=Kind.config)
|
||||
bufferStoreMode = Component(EpicsSignal, "STOREMODE", auto_monitor=True, kind=Kind.config)
|
||||
fileRecMode = Component(EpicsSignalRO, "RECMODE", auto_monitor=True, kind=Kind.config)
|
||||
|
||||
buffer_used = Component(EpicsSignalRO, "PIC_BUFFER", auto_monitor=True, kind=Kind.normal)
|
||||
buffer_size = Component(EpicsSignalRO, "PIC_MAX", auto_monitor=True, kind=Kind.normal)
|
||||
buffer_clear = Component(EpicsSignal, "CLEARMEM", put_complete=True, kind=Kind.omitted)
|
||||
|
||||
# ########################################################################
|
||||
# File saving interface
|
||||
cam_data_rate = Component(EpicsSignalRO, "CAMRATE", auto_monitor=True, kind=Kind.normal)
|
||||
file_data_rate = Component(EpicsSignalRO, "FILERATE", auto_monitor=True, kind=Kind.normal)
|
||||
file_savestart = Component(EpicsSignal, "SAVESTART", put_complete=True, kind=Kind.config)
|
||||
file_savestop = Component(EpicsSignal, "SAVESTOP", put_complete=True, kind=Kind.config)
|
||||
file_format = Component(EpicsSignal, "FILEFORMAT", put_complete=True, kind=Kind.config)
|
||||
file_transfer = Component(EpicsSignal, "FTRANSFER", put_complete=True, kind=Kind.config)
|
||||
file_savebusy = Component(EpicsSignalRO, "FILESAVEBUSY", auto_monitor=True, kind=Kind.normal)
|
||||
|
||||
# ########################################################################
|
||||
# Configuration state maschine with separate transition states
|
||||
camStatusCode = Component(EpicsSignalRO, "STATUSCODE", auto_monitor=True, kind=Kind.config)
|
||||
camSetParam = Component(EpicsSignal, "SET_PARAM", auto_monitor=True, kind=Kind.config)
|
||||
camSetParamBusy = Component(
|
||||
EpicsSignalRO, "BUSY_SET_PARAM", auto_monitor=True, kind=Kind.config
|
||||
)
|
||||
camCamera = Component(EpicsSignalRO, "CAMERA", auto_monitor=True, kind=Kind.config)
|
||||
camCameraBusy = Component(EpicsSignalRO, "BUSY_CAMERA", auto_monitor=True, kind=Kind.config)
|
||||
camInit = Component(EpicsSignalRO, "INIT", auto_monitor=True, kind=Kind.config)
|
||||
camInitBusy = Component(EpicsSignalRO, "BUSY_INIT", auto_monitor=True, kind=Kind.config)
|
||||
|
||||
# ########################################################################
|
||||
# Throtled image preview
|
||||
image = Component(EpicsSignalRO, "FPICTURE", kind=Kind.omitted, doc="Throttled image preview")
|
||||
|
||||
# ########################################################################
|
||||
# Misc PVs
|
||||
# camRemoval = Component(EpicsSignalRO, "REMOVAL", auto_monitor=True, kind=Kind.config)
|
||||
camStateString = Component(
|
||||
EpicsSignalRO, "SS_CAMERA", string=True, auto_monitor=True, kind=Kind.config
|
||||
)
|
||||
|
||||
@property
|
||||
def state(self) -> str:
|
||||
"""Single word camera state"""
|
||||
if self.camSetParamBusy.value:
|
||||
return "BUSY"
|
||||
if self.camStatusCode.value == 2 and self.camInit.value == 1:
|
||||
return "IDLE"
|
||||
if self.camStatusCode.value == 6 and self.camInit.value == 1:
|
||||
return "RUNNING"
|
||||
# if self.camRemoval.value==0 and self.camInit.value==0:
|
||||
if self.camInit.value == 0:
|
||||
return "OFFLINE"
|
||||
# if self.camRemoval.value:
|
||||
# return "REMOVED"
|
||||
return "UNKNOWN"
|
||||
|
||||
@state.setter
|
||||
def state(self):
|
||||
raise RuntimeError("State is a ReadOnly property")
|
||||
|
||||
def configure(self, d: dict = {}) -> tuple:
|
||||
def configure(self, d: dict = None) -> tuple:
|
||||
"""Configure the base Helge camera device
|
||||
|
||||
Parameters as 'd' dictionary
|
||||
@@ -293,6 +119,8 @@ class HelgeCameraBase(BECDeviceBase):
|
||||
*'FIFO buffer' for continous streaming
|
||||
data_format : str
|
||||
Usually set to 'ZEROMQ'
|
||||
acq_mode : str
|
||||
Store mode and data format according to preconfigured settings
|
||||
"""
|
||||
if self.state not in ("IDLE"):
|
||||
raise RuntimeError(f"Can't change configuration from state {self.state}")
|
||||
@@ -308,11 +136,19 @@ class HelgeCameraBase(BECDeviceBase):
|
||||
self.acquire_delay.set(d["exposure_period_ms"]).wait()
|
||||
if "exposure_period_ms" in d:
|
||||
self.acquire_delay.set(d["exposure_period_ms"]).wait()
|
||||
if "image_width" in d:
|
||||
self.array_size.array_size_x.set(d["image_width"]).wait()
|
||||
if "image_height" in d:
|
||||
self.array_size.array_size_y.set(d["image_height"]).wait()
|
||||
if "store_mode" in d:
|
||||
self.bufferStoreMode.set(d["store_mode"]).wait()
|
||||
if "data_format" in d:
|
||||
self.file_format.set(d["data_format"]).wait()
|
||||
|
||||
# If a pre-configured acquisition mode is specified, set it
|
||||
if "acq_mode" in d:
|
||||
self.set_acquisition_mode(d["acq_mode"])
|
||||
|
||||
# State machine
|
||||
# Initial: BUSY and SET both low
|
||||
# 0. Write 1 to SET_PARAM
|
||||
@@ -320,16 +156,31 @@ class HelgeCameraBase(BECDeviceBase):
|
||||
# 2. BUSY goes low, SET goes high
|
||||
# 3. BUSY stays low, SET goes low
|
||||
# So we need a 'negedge' on SET_PARAM
|
||||
self.camSetParam.set(1).wait()
|
||||
|
||||
def negedge(*, old_value, value, timestamp, **_):
|
||||
return bool(old_value and not value)
|
||||
|
||||
# Subscribe and wait for update
|
||||
status = SubscriptionStatus(self.camSetParam, negedge, timeout=5, settle_time=0.5)
|
||||
status = SubscriptionStatus(self.set_param, negedge, timeout=5, settle_time=0.5)
|
||||
|
||||
self.set_param.set(1).wait()
|
||||
status.wait()
|
||||
|
||||
def bluestage(self):
|
||||
def set_acquisition_mode(self, acq_mode):
|
||||
"""Set acquisition mode
|
||||
|
||||
Utility function to quickly select between pre-configured and tested
|
||||
acquisition modes.
|
||||
"""
|
||||
if acq_mode in ["default", "step"]:
|
||||
# NOTE: Trigger duration requires a consumer
|
||||
self.bufferStoreMode.set("FIFO Buffer").wait()
|
||||
if acq_mode in ["stream"]:
|
||||
# NOTE: Trigger duration requires a consumer
|
||||
self.bufferStoreMode.set("FIFO Buffer").wait()
|
||||
else:
|
||||
raise RuntimeError(f"Unsupported acquisition mode: {acq_mode}")
|
||||
|
||||
def arm(self):
|
||||
"""Bluesky style stage: arm the detector"""
|
||||
logger.warning("Staging PCO")
|
||||
# Acquisition is only allowed when the IOC is not busy
|
||||
@@ -345,129 +196,220 @@ class HelgeCameraBase(BECDeviceBase):
|
||||
)
|
||||
|
||||
# Start the acquisition (this sets parameers and starts acquisition)
|
||||
self.camStatusCmd.set("Running").wait()
|
||||
self.acquire.set("Running").wait()
|
||||
|
||||
# Subscribe and wait for update
|
||||
def is_running(*, value, timestamp, **_):
|
||||
return bool(value == 6)
|
||||
|
||||
status = SubscriptionStatus(self.camStatusCode, is_running, timeout=5, settle_time=0.2)
|
||||
status = SubscriptionStatus(self.camera_statuscode, is_running, timeout=5, settle_time=0.2)
|
||||
status.wait()
|
||||
|
||||
def blueunstage(self):
|
||||
def disarm(self):
|
||||
"""Bluesky style unstage: stop the detector"""
|
||||
self.camStatusCmd.set("Idle").wait()
|
||||
self.acquire.set("Idle").wait()
|
||||
|
||||
# Data streaming is stopped by setting the max index to 0
|
||||
# FIXME: This might interrupt data transfer
|
||||
# FIXME: This will interrupt data transfer
|
||||
self.file_savestop.set(0).wait()
|
||||
|
||||
def bluekickoff(self):
|
||||
def destroy(self):
|
||||
logger.warning("Destroy called")
|
||||
if self.backend is not None:
|
||||
self.backend.shutdown()
|
||||
super().destroy()
|
||||
|
||||
def _on_preview_update(self, img: np.ndarray, header: dict):
|
||||
"""Send preview stream and update frame index counter"""
|
||||
# FIXME: There's also a recorded images counter provided by the stdDAQ writer
|
||||
self.num_images_counter.put(header["frame"], force=True)
|
||||
self._run_subs(sub_type=self.SUB_DEVICE_MONITOR_2D, obj=self, value=img)
|
||||
|
||||
def acq_done(self) -> DeviceStatus:
|
||||
"""
|
||||
Check if the acquisition is done. For the GigaFrost camera, this is
|
||||
done by checking the status of the backend as the camera does not
|
||||
provide any feedback about its internal state.
|
||||
|
||||
Returns:
|
||||
DeviceStatus: The status of the acquisition
|
||||
"""
|
||||
status = DeviceStatus(self)
|
||||
if self.backend is not None:
|
||||
self.backend.add_status_callback(
|
||||
status,
|
||||
success=[StdDaqStatus.IDLE, StdDaqStatus.FILE_SAVED],
|
||||
error=[StdDaqStatus.REJECTED, StdDaqStatus.ERROR],
|
||||
)
|
||||
return status
|
||||
|
||||
########################################
|
||||
# Beamline Specific Implementations #
|
||||
########################################
|
||||
|
||||
# pylint: disable=protected-access
|
||||
def on_stage(self) -> None:
|
||||
"""Configure and arm PCO.Edge camera for acquisition"""
|
||||
|
||||
# PCO can finish a run without explicit unstaging
|
||||
if self.state not in ("IDLE"):
|
||||
logger.warning(
|
||||
f"Trying to stage the camera from state {self.state}, unstaging it first!"
|
||||
)
|
||||
self.unstage()
|
||||
time.sleep(0.5)
|
||||
|
||||
# Fish out our configuration from scaninfo (via explicit or generic addressing)
|
||||
scan_args = {
|
||||
**self.scan_info.msg.request_inputs["inputs"],
|
||||
**self.scan_info.msg.request_inputs["kwargs"],
|
||||
**self.scan_info.msg.scan_parameters,
|
||||
}
|
||||
|
||||
d = {}
|
||||
if "image_width" in scan_args and scan_args["image_width"] is not None:
|
||||
d["image_width"] = scan_args["image_width"]
|
||||
if "image_height" in scan_args and scan_args["image_height"] is not None:
|
||||
d["image_height"] = scan_args["image_height"]
|
||||
if "exp_time" in scan_args and scan_args["exp_time"] is not None:
|
||||
d["exposure_time_ms"] = scan_args["exp_time"]
|
||||
if "exp_period" in scan_args and scan_args["exp_period"] is not None:
|
||||
d["exposure_period_ms"] = scan_args["exp_period"]
|
||||
if "exp_burst" in scan_args and scan_args["exp_burst"] is not None:
|
||||
d["exposure_num_burst"] = scan_args["exp_burst"]
|
||||
if "acq_time" in scan_args and scan_args["acq_time"] is not None:
|
||||
d["exposure_time_ms"] = scan_args["acq_time"]
|
||||
if "acq_period" in scan_args and scan_args["acq_period"] is not None:
|
||||
d["exposure_period_ms"] = scan_args["acq_period"]
|
||||
if "acq_burst" in scan_args and scan_args["acq_burst"] is not None:
|
||||
d["exposure_num_burst"] = scan_args["acq_burst"]
|
||||
if "acq_mode" in scan_args and scan_args["acq_mode"] is not None:
|
||||
d["acq_mode"] = scan_args["acq_mode"]
|
||||
# elif self.scaninfo.scan_type == "step":
|
||||
# d['acq_mode'] = "default"
|
||||
if "pco_store_mode" in scan_args and scan_args["pco_store_mode"] is not None:
|
||||
d["store_mode"] = scan_args["pco_store_mode"]
|
||||
if "pco_data_format" in scan_args and scan_args["pco_data_format"] is not None:
|
||||
d["data_format"] = scan_args["pco_data_format"]
|
||||
|
||||
# Perform bluesky-style configuration
|
||||
if d:
|
||||
logger.warning(f"[{self.name}] Configuring with:\n{d}")
|
||||
self.configure(d=d)
|
||||
|
||||
# stdDAQ backend parameters
|
||||
num_points = (
|
||||
1
|
||||
* scan_args.get("steps", 1)
|
||||
* scan_args.get("exp_burst", 1)
|
||||
* scan_args.get("repeats", 1)
|
||||
* scan_args.get("burst_at_each_point", 1)
|
||||
)
|
||||
self.num_images.set(num_points).wait()
|
||||
if "daq_file_path" in scan_args and scan_args["daq_file_path"] is not None:
|
||||
self.file_path.set(scan_args["daq_file_path"]).wait()
|
||||
if "daq_file_prefix" in scan_args and scan_args["daq_file_prefix"] is not None:
|
||||
self.file_prefix.set(scan_args["daq_file_prefix"]).wait()
|
||||
if "daq_num_images" in scan_args and scan_args["daq_num_images"] is not None:
|
||||
self.num_images.set(scan_args["daq_num_images"]).wait()
|
||||
# Start stdDAQ preview
|
||||
if self.live_preview is not None:
|
||||
self.live_preview.start()
|
||||
|
||||
def on_unstage(self) -> None:
|
||||
"""Disarm the PCO.Edge camera"""
|
||||
self.disarm()
|
||||
if self.backend is not None:
|
||||
logger.info(f"StdDaq status before unstage: {self.backend.status}")
|
||||
self.backend.stop()
|
||||
|
||||
def on_pre_scan(self) -> DeviceStatus | None:
|
||||
"""Called right before the scan starts on all devices automatically."""
|
||||
logger.warning("Called op_prescan on PCO camera")
|
||||
# First start the stdDAQ
|
||||
if self.backend is not None:
|
||||
self.backend.start(
|
||||
file_path=self.file_path.get(),
|
||||
file_prefix=self.file_prefix.get(),
|
||||
num_images=self.num_images.get(),
|
||||
)
|
||||
# Then start the camera
|
||||
self.arm()
|
||||
|
||||
def on_trigger(self) -> None | DeviceStatus:
|
||||
"""Trigger mode operation
|
||||
|
||||
Use it to repeatedly record a fixed number of frames and send it to stdDAQ. The method waits
|
||||
for the acquisition and data transfer to complete.
|
||||
|
||||
NOTE: Maciej confirmed that sparse data is no problem to the stdDAQ.
|
||||
TODO: Optimize data transfer to launch at end and check completion at the beginning.
|
||||
"""
|
||||
# Ensure that previous data transfer finished
|
||||
# def sentIt(*args, value, timestamp, **kwargs):
|
||||
# return value==0
|
||||
# status = SubscriptionStatus(self.file_savebusy, sentIt, timeout=120)
|
||||
# status.wait()
|
||||
|
||||
# Not sure if it always sends the first batch of images or the newest
|
||||
self.buffer_clear.set(1, settle_time=0.1).wait()
|
||||
|
||||
# Wait until the buffer fills up with enough images
|
||||
t_expected = (self.acquire_time.get() + self.acquire_delay.get()) * self.file_savestop.get()
|
||||
|
||||
def wait_acquisition(*, value, timestamp, **_):
|
||||
num_target = self.file_savestop.get()
|
||||
# logger.warning(f"{value} of {num_target}")
|
||||
return bool(value >= num_target)
|
||||
|
||||
max_wait = max(5, 5 * t_expected)
|
||||
status = SubscriptionStatus(
|
||||
self.buffer_used, wait_acquisition, timeout=max_wait, settle_time=0.2
|
||||
)
|
||||
status.wait()
|
||||
|
||||
# Then start file transfer (need to get the save busy flag update)
|
||||
# self.file_transfer.set(1, settle_time=0.2).wait()
|
||||
self.file_transfer.set(1).wait()
|
||||
|
||||
# And wait until the images have been sent
|
||||
# NOTE: this does not wait for new value, the first check will be
|
||||
# against values from the previous cycle, i.e. pass automatically.
|
||||
t_start = time.time()
|
||||
|
||||
def wait_sending(*, old_value, value, timestamp, **_):
|
||||
t_elapsed = timestamp - t_start
|
||||
# logger.warning(f"{old_value}\t{value}\t{t_elapsed}")
|
||||
return old_value == 1 and value == 0 and t_elapsed > 0
|
||||
|
||||
status = SubscriptionStatus(self.file_savebusy, wait_sending, timeout=120, settle_time=0.2)
|
||||
status.wait()
|
||||
|
||||
def on_complete(self) -> DeviceStatus | None:
|
||||
"""Called to inquire if a device has completed a scans."""
|
||||
return self.acq_done()
|
||||
|
||||
def on_kickoff(self) -> DeviceStatus | None:
|
||||
"""Start data transfer
|
||||
|
||||
TODO: Need to revisit this once triggering is complete
|
||||
"""
|
||||
self.file_transfer.set(1).wait()
|
||||
|
||||
|
||||
class PcoEdge5M(HelgeCameraBase):
|
||||
"""Ophyd baseclass for PCO.Edge cameras
|
||||
|
||||
This class provides wrappers for Helge's camera IOCs around SwissFEL and
|
||||
for high performance SLS 2.0 cameras. Theese are mostly PCO cameras running
|
||||
on a special Windows IOC host with lots of RAM and CPU power.
|
||||
"""
|
||||
|
||||
custom_prepare_cls = PcoEdgeCameraMixin
|
||||
USER_ACCESS = ["bluestage", "blueunstage", "bluekickoff"]
|
||||
|
||||
# ########################################################################
|
||||
# Additional status info
|
||||
busy = Component(EpicsSignalRO, "BUSY", auto_monitor=True, kind=Kind.config)
|
||||
camState = Component(EpicsSignalRO, "SS_CAMERA", auto_monitor=True, kind=Kind.config)
|
||||
camProgress = Component(EpicsSignalRO, "CAMPROGRESS", auto_monitor=True, kind=Kind.config)
|
||||
camRate = Component(EpicsSignalRO, "CAMRATE", auto_monitor=True, kind=Kind.config)
|
||||
|
||||
# ########################################################################
|
||||
# Acquisition configuration
|
||||
acqMode = Component(EpicsSignalRO, "ACQMODE", auto_monitor=True, kind=Kind.config)
|
||||
acqDelay = Component(EpicsSignalRO, "DELAY", auto_monitor=True, kind=Kind.config)
|
||||
acqTriggerEna = Component(EpicsSignalRO, "TRIGGER", auto_monitor=True, kind=Kind.config)
|
||||
# acqTriggerSource = Component(
|
||||
# EpicsSignalRO, "TRIGGERSOURCE", auto_monitor=True, kind=Kind.config)
|
||||
# acqTriggerEdge = Component(EpicsSignalRO, "TRIGGEREDGE", auto_monitor=True, kind=Kind.config)
|
||||
|
||||
# ########################################################################
|
||||
# Image size settings
|
||||
# Priority is: binning -> roi -> final size
|
||||
pxRoiX_lo = Component(
|
||||
EpicsSignal, "REGIONX_START", put_complete=True, auto_monitor=True, kind=Kind.config
|
||||
)
|
||||
pxRoiX_hi = Component(
|
||||
EpicsSignal, "REGIONX_END", put_complete=True, auto_monitor=True, kind=Kind.config
|
||||
)
|
||||
pxRoiY_lo = Component(
|
||||
EpicsSignal, "REGIONY_START", put_complete=True, auto_monitor=True, kind=Kind.config
|
||||
)
|
||||
pxRoiY_hi = Component(
|
||||
EpicsSignal, "REGIONY_END", put_complete=True, auto_monitor=True, kind=Kind.config
|
||||
)
|
||||
|
||||
def configure(self, d: dict = {}) -> tuple:
|
||||
"""
|
||||
Camera configuration instructions:
|
||||
After setting the corresponding PVs, one needs to process SET_PARAM and wait until
|
||||
BUSY_SET_PARAM goes high and low, followed by SET_PARAM goes high and low. This will
|
||||
both send the settings to the camera and allocate the necessary buffers in the correct
|
||||
size and shape (that takes time). Starting the exposure with CAMERASTATUS will also
|
||||
call SET_PARAM, but it might take long.
|
||||
|
||||
NOTE:
|
||||
The camera IOC will automatically round up RoiX coordinates to the
|
||||
next multiple of 160. This means that configure can only change image
|
||||
width in steps of 320 pixels (or manually of 160). Roi
|
||||
|
||||
Parameters as 'd' dictionary
|
||||
----------------------------
|
||||
exposure_time_ms : float, optional
|
||||
Exposure time [ms].
|
||||
exposure_period_ms : float, optional
|
||||
Exposure period [ms], ignored in soft trigger mode.
|
||||
image_width : int, optional
|
||||
ROI size in the x-direction, multiple of 320 [pixels]
|
||||
image_height : int, optional
|
||||
ROI size in the y-direction, multiple of 2 [pixels]
|
||||
image_binx : int optional
|
||||
Binning along image width [pixels]
|
||||
image_biny: int, optional
|
||||
Binning along image height [pixels]
|
||||
acq_mode : str, not yet implemented
|
||||
Select one of the pre-configured trigger behavior
|
||||
"""
|
||||
if d is not None:
|
||||
# Need to be smart how we set the ROI....
|
||||
# Image sensor is 2560x2160 (X and Y)
|
||||
# Values are rounded to multiples of 16
|
||||
if "image_width" in d and d["image_width"] is not None:
|
||||
width = d["image_width"]
|
||||
self.pxRoiX_lo.set(2560 / 2 - width / 2).wait()
|
||||
self.pxRoiX_hi.set(2560 / 2 + width / 2).wait()
|
||||
if "image_height" in d and d["image_height"] is not None:
|
||||
height = d["image_height"]
|
||||
self.pxRoiY_lo.set(2160 / 2 - height / 2).wait()
|
||||
self.pxRoiY_hi.set(2160 / 2 + height / 2).wait()
|
||||
if "image_binx" in d and d["image_binx"] is not None:
|
||||
self.bin_x.set(d["image_binx"]).wait()
|
||||
if "image_biny" in d and d["image_biny"] is not None:
|
||||
self.bin_y.set(d["image_biny"]).wait()
|
||||
|
||||
# Call super() to commit the changes
|
||||
super().configure(d)
|
||||
def on_stop(self) -> None:
|
||||
"""Called when the device is stopped."""
|
||||
return self.on_unstage()
|
||||
|
||||
|
||||
# Automatically connect to test camera if directly invoked
|
||||
if __name__ == "__main__":
|
||||
|
||||
# Drive data collection
|
||||
cam = PcoEdge5M("X02DA-CCDCAM2:", name="mcpcam")
|
||||
cam = PcoEdge5M(
|
||||
"X02DA-CCDCAM2:",
|
||||
name="mcpcam",
|
||||
std_daq_ws="ws://129.129.95.111:8081",
|
||||
std_daq_rest="http://129.129.95.111:5010",
|
||||
std_daq_live="tcp://129.129.95.111:20010",
|
||||
)
|
||||
cam.wait_for_connection()
|
||||
|
||||
@@ -3,20 +3,18 @@ from __future__ import annotations
|
||||
import copy
|
||||
import enum
|
||||
import json
|
||||
import queue
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
from typing import TYPE_CHECKING, Callable, Literal
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import requests
|
||||
from bec_lib.logger import bec_logger
|
||||
from ophyd import StatusBase
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
from typeguard import typechecked
|
||||
from websockets import State
|
||||
from websockets.exceptions import WebSocketException
|
||||
from websockets.sync.client import ClientConnection, connect
|
||||
import websockets.sync.client as ws
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from ophyd import Device, DeviceStatus
|
||||
@@ -48,96 +46,38 @@ class StdDaqStatus(str, enum.Enum):
|
||||
WAITING_FOR_FIRST_IMAGE = "waiting_for_first_image"
|
||||
|
||||
|
||||
class StdDaqConfig(BaseModel):
|
||||
"""
|
||||
Configuration for the StdDAQ
|
||||
"""
|
||||
|
||||
detector_name: str
|
||||
detector_type: str
|
||||
n_modules: int
|
||||
bit_depth: int
|
||||
image_pixel_height: int
|
||||
image_pixel_width: int
|
||||
start_udp_port: int
|
||||
writer_user_id: int
|
||||
max_number_of_forwarders_spawned: int
|
||||
use_all_forwarders: bool
|
||||
module_sync_queue_size: int
|
||||
number_of_writers: int
|
||||
module_positions: dict
|
||||
ram_buffer_gb: float
|
||||
delay_filter_timeout: float
|
||||
live_stream_configs: dict[str, dict[Literal["type", "config"], str | list]]
|
||||
|
||||
model_config = ConfigDict(extra="ignore")
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def resolve_aliases(cls, values):
|
||||
if "roix" in values:
|
||||
values["image_pixel_height"] = values.pop("roiy")
|
||||
if "roiy" in values:
|
||||
values["image_pixel_width"] = values.pop("roix")
|
||||
return values
|
||||
|
||||
|
||||
class StdDaqConfigPartial(BaseModel):
|
||||
"""
|
||||
Partial configuration for the StdDAQ.
|
||||
"""
|
||||
|
||||
detector_name: str | None = None
|
||||
detector_type: str | None = None
|
||||
n_modules: int | None = None
|
||||
bit_depth: int | None = None
|
||||
image_pixel_height: int | None = Field(default=None, alias="roiy")
|
||||
image_pixel_width: int | None = Field(default=None, alias="roix")
|
||||
start_udp_port: int | None = None
|
||||
writer_user_id: int | None = None
|
||||
max_number_of_forwarders_spawned: int | None = None
|
||||
use_all_forwarders: bool | None = None
|
||||
module_sync_queue_size: int | None = None
|
||||
number_of_writers: int | None = None
|
||||
module_positions: dict | None = None
|
||||
ram_buffer_gb: float | None = None
|
||||
delay_filter_timeout: float | None = None
|
||||
live_stream_configs: dict[str, dict[Literal["type", "config"], str | list]] | None = None
|
||||
|
||||
model_config = ConfigDict(extra="ignore")
|
||||
|
||||
|
||||
class StdDaqWsResponse(BaseModel):
|
||||
"""
|
||||
Response from the StdDAQ websocket
|
||||
"""
|
||||
|
||||
status: StdDaqStatus
|
||||
reason: str | None = None
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class StdDaqClient:
|
||||
"""Standalone stdDAQ client class"""
|
||||
|
||||
USER_ACCESS = ["status", "start", "stop", "get_config", "set_config", "reset"]
|
||||
USER_ACCESS = ["status", "count", "start", "stop", "get_config", "set_config", "reset"]
|
||||
|
||||
_ws_client: ws.ClientConnection | None = None
|
||||
_count: int = 0
|
||||
_status: str = "undefined"
|
||||
_status_timestamp: float | None = None
|
||||
_ws_monitor_thread: threading.Thread | None = None
|
||||
_config: dict | None = None
|
||||
_status_callbacks: dict[str, tuple[DeviceStatus, list[StdDaqStatus], list[StdDaqStatus]]] = {}
|
||||
|
||||
def __init__(self, parent: Device, ws_url: str, rest_url: str):
|
||||
self.parent = parent
|
||||
self.ws_url = ws_url
|
||||
self.rest_url = rest_url
|
||||
self.ws_client: ClientConnection | None = None
|
||||
self._status: StdDaqStatus = StdDaqStatus.UNDEFINED
|
||||
self._ws_update_thread: threading.Thread | None = None
|
||||
self.name = self.parent.name if self.parent is not None else "None"
|
||||
# Must be here otherwise they're static (shared between class instances)
|
||||
self._ws_recv_mutex = threading.Lock()
|
||||
self._shutdown_event = threading.Event()
|
||||
self._ws_idle_event = threading.Event()
|
||||
self._daq_is_running = threading.Event()
|
||||
self._config: StdDaqConfig | None = None
|
||||
self._status_callbacks: dict[
|
||||
str, tuple[DeviceStatus, list[StdDaqStatus], list[StdDaqStatus]]
|
||||
] = {}
|
||||
self._send_queue = queue.Queue()
|
||||
|
||||
# Connect to WS interface and start status monitoring
|
||||
self.wait_for_connection()
|
||||
self._daq_is_running.set()
|
||||
self._ws_monitor_thread = threading.Thread(
|
||||
target=self._ws_monitor_loop, name=f"{self.name}_ws_monitor", daemon=True
|
||||
)
|
||||
self._ws_monitor_thread.start()
|
||||
|
||||
@property
|
||||
def status(self) -> StdDaqStatus:
|
||||
@@ -146,13 +86,19 @@ class StdDaqClient:
|
||||
"""
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def count(self) -> int:
|
||||
"""Get the recorded frame count"""
|
||||
return self._count
|
||||
|
||||
def add_status_callback(
|
||||
self, status: DeviceStatus, success: list[StdDaqStatus], error: list[StdDaqStatus]
|
||||
):
|
||||
"""
|
||||
Add a DeviceStatus callback for the StdDAQ. The status will be updated when the StdDAQ status changes and
|
||||
set to finished when the status matches one of the specified success statuses and to exception when the status
|
||||
matches one of the specified error statuses.
|
||||
Add a DeviceStatus callback for the StdDAQ. The status will be updated
|
||||
when the StdDAQ status changes and set to finished when the status
|
||||
matches one of the specified success statuses and to exception when the
|
||||
status matches one of the specified error statuses.
|
||||
|
||||
Args:
|
||||
status (DeviceStatus): DeviceStatus object
|
||||
@@ -163,63 +109,90 @@ class StdDaqClient:
|
||||
|
||||
@typechecked
|
||||
def start(
|
||||
self, file_path: str, file_prefix: str, num_images: int, timeout: float = 20, wait=True
|
||||
) -> StatusBase:
|
||||
"""
|
||||
Start acquisition on the StdDAQ.
|
||||
self,
|
||||
file_path: str,
|
||||
file_prefix: str,
|
||||
num_images: int,
|
||||
timeout: float = 20,
|
||||
wait: bool = True,
|
||||
) -> StatusBase | None:
|
||||
"""Start acquisition on the StdDAQ.
|
||||
|
||||
Args:
|
||||
file_path (str): path to save the files
|
||||
file_prefix (str): prefix of the files
|
||||
num_images (int): number of images to acquire
|
||||
timeout (float): timeout for the request
|
||||
Returns:
|
||||
status (StatusBase): Ophyd status object with attached monitor
|
||||
"""
|
||||
logger.info(f"Starting StdDaq backend. Current status: {self.status}")
|
||||
# Ensure connection
|
||||
self.wait_for_connection()
|
||||
|
||||
status = StatusBase()
|
||||
self.add_status_callback(status, success=["waiting_for_first_image"], error=[])
|
||||
# NOTE: CREATING_FILE --> IDLE is a known error, the exact cause is unknown,
|
||||
# Might be botched overwrite protection (solved by changing file_prefix)
|
||||
# In previous versions there was also a mutex ownership problem
|
||||
self.add_status_callback(
|
||||
status, success=["waiting_for_first_image"], error=["rejected", "idle"]
|
||||
)
|
||||
message = {
|
||||
"command": "start",
|
||||
"path": file_path,
|
||||
"file_prefix": file_prefix,
|
||||
"n_image": num_images,
|
||||
}
|
||||
self._send_queue.put(message)
|
||||
logger.info(f"Starting StdDaq backend. Current status: {self.status}. Message: {message}")
|
||||
self._ws_client.send(json.dumps(message))
|
||||
if wait:
|
||||
return status.wait(timeout=timeout)
|
||||
|
||||
status.wait(timeout=timeout)
|
||||
return None
|
||||
return status
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stop acquisition on the StdDAQ.
|
||||
@typechecked
|
||||
def stop(self, timeout: float = 5, wait=True, stop_cmd="stop") -> StatusBase | None:
|
||||
"""Stop acquisition on the StdDAQ.
|
||||
|
||||
Args:
|
||||
timeout (float): timeout for the request
|
||||
"""
|
||||
message = {"command": "stop"}
|
||||
return self._send_queue.put(message)
|
||||
|
||||
def get_config(self, cached=False, timeout: float = 2) -> dict:
|
||||
"""
|
||||
Get the current configuration of the StdDAQ.
|
||||
|
||||
Args:
|
||||
cached (bool): whether to use the cached configuration
|
||||
timeout (float): timeout for the request
|
||||
|
||||
Returns:
|
||||
StdDaqConfig: configuration of the StdDAQ
|
||||
status (StatusBase): Ophyd status object with attached monitor
|
||||
"""
|
||||
if cached and self._config is not None:
|
||||
# Ensure connection
|
||||
self.wait_for_connection()
|
||||
|
||||
logger.info(f"Stopping StdDaq backend. Current status: {self.status}")
|
||||
status = StatusBase()
|
||||
self.add_status_callback(status, success=["idle"], error=["error"])
|
||||
message = {"command": stop_cmd}
|
||||
|
||||
self._ws_client.send(json.dumps(message))
|
||||
if wait:
|
||||
status.wait(timeout=timeout)
|
||||
return None
|
||||
return status
|
||||
|
||||
def get_config(self, timeout: float = 2, cached=False) -> dict:
|
||||
"""Get the current configuration of the StdDAQ.
|
||||
|
||||
Args:
|
||||
timeout (float): timeout for the request
|
||||
Returns:
|
||||
config (dict): configuration of the StdDAQ
|
||||
"""
|
||||
if cached:
|
||||
return self._config
|
||||
|
||||
response = requests.get(
|
||||
self.rest_url + "/api/config/get", params={"user": "ioc"}, timeout=timeout
|
||||
)
|
||||
response.raise_for_status()
|
||||
self._config = StdDaqConfig(**response.json())
|
||||
return self._config.model_dump()
|
||||
self._config = response.json()
|
||||
return self._config
|
||||
|
||||
def set_config(self, config: StdDaqConfig | dict, timeout: float = 2) -> None:
|
||||
def set_config(
|
||||
self, config: dict, timeout: float = 2, update: bool = True, force: bool = True
|
||||
) -> None:
|
||||
"""
|
||||
Set the configuration of the StdDAQ. This will overwrite the current configuration.
|
||||
|
||||
@@ -227,20 +200,29 @@ class StdDaqClient:
|
||||
config (StdDaqConfig | dict): configuration to set
|
||||
timeout (float): timeout for the request
|
||||
"""
|
||||
if not isinstance(config, StdDaqConfig):
|
||||
config = StdDaqConfig(**config)
|
||||
old_config = self.get_config()
|
||||
if update:
|
||||
cfg = copy.deepcopy(self._config)
|
||||
cfg.update(config)
|
||||
new_config = cfg
|
||||
else:
|
||||
new_config = config
|
||||
|
||||
out = config.model_dump(exclude_none=True)
|
||||
if not out:
|
||||
logger.info(
|
||||
"The provided config does not contain relevant values for the StdDaq. Skipping set_config."
|
||||
)
|
||||
# Escape unnecesary restarts
|
||||
if not force and new_config == old_config:
|
||||
return
|
||||
if not new_config:
|
||||
return
|
||||
|
||||
self._pre_restart()
|
||||
|
||||
# new_jason = json.dumps(new_config)
|
||||
logger.warning(new_config)
|
||||
response = requests.post(
|
||||
self.rest_url + "/api/config/set", params={"user": "ioc"}, json=out, timeout=timeout
|
||||
self.rest_url + "/api/config/set",
|
||||
params={"user": "ioc"},
|
||||
json=new_config,
|
||||
timeout=timeout,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
@@ -248,38 +230,18 @@ class StdDaqClient:
|
||||
self._post_restart()
|
||||
|
||||
def _pre_restart(self):
|
||||
"""Stop monitor before restart"""
|
||||
self._daq_is_running.clear()
|
||||
self._ws_idle_event.wait()
|
||||
if self.ws_client is not None:
|
||||
self.ws_client.close()
|
||||
if self._ws_client is not None:
|
||||
self._ws_client.close()
|
||||
|
||||
def _post_restart(self):
|
||||
"""Start monitor after a restart"""
|
||||
time.sleep(2)
|
||||
self.wait_for_connection()
|
||||
self._daq_is_running.set()
|
||||
|
||||
def update_config(self, config: StdDaqConfigPartial | dict, timeout: float = 2) -> None:
|
||||
"""
|
||||
Update the configuration of the StdDAQ. This will update the current configuration.
|
||||
|
||||
Args:
|
||||
config (StdDaqConfigPartial | dict): configuration to update
|
||||
timeout (float): timeout for the request
|
||||
"""
|
||||
if not isinstance(config, StdDaqConfigPartial):
|
||||
config = StdDaqConfigPartial(**config)
|
||||
|
||||
patch_config_dict = config.model_dump(exclude_none=True)
|
||||
if not patch_config_dict:
|
||||
return
|
||||
|
||||
current_config = copy.deepcopy(self.get_config())
|
||||
new_config = copy.deepcopy(current_config)
|
||||
new_config.update(patch_config_dict)
|
||||
if current_config == new_config:
|
||||
return
|
||||
|
||||
self.set_config(StdDaqConfig(**new_config), timeout=timeout)
|
||||
|
||||
def reset(self, min_wait: float = 5) -> None:
|
||||
"""
|
||||
Reset the StdDAQ.
|
||||
@@ -299,10 +261,10 @@ class StdDaqClient:
|
||||
"""
|
||||
start_time = time.time()
|
||||
while True:
|
||||
if self.ws_client is not None and self.ws_client.state == State.OPEN:
|
||||
if self._ws_client is not None and self._ws_client.state == State.OPEN:
|
||||
return
|
||||
try:
|
||||
self.ws_client = connect(self.ws_url)
|
||||
self._ws_client = ws.connect(self.ws_url)
|
||||
break
|
||||
except ConnectionRefusedError as exc:
|
||||
if time.time() - start_time > timeout:
|
||||
@@ -335,25 +297,19 @@ class StdDaqClient:
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
def connect(self):
|
||||
"""
|
||||
Connect to the StdDAQ. This method should be called after the client is created. It will
|
||||
launch a background thread to exchange data with the StdDAQ.
|
||||
"""
|
||||
self._ws_update_thread = threading.Thread(
|
||||
target=self._ws_update_loop, name=f"{self.parent.name}_stddaq_ws_loop", daemon=True
|
||||
)
|
||||
self._ws_update_thread.start()
|
||||
|
||||
def shutdown(self):
|
||||
"""
|
||||
Shutdown the StdDAQ client.
|
||||
"""
|
||||
if self._ws_update_thread is not None:
|
||||
self._ws_update_thread.join()
|
||||
if self.ws_client is not None:
|
||||
self.ws_client.close()
|
||||
self.ws_client = None
|
||||
logger.warning("Shutting down sdtDAQ monitor")
|
||||
self._shutdown_event.set()
|
||||
if self._ws_monitor_thread is not None:
|
||||
self._ws_monitor_thread.join()
|
||||
logger.warning("Thread joined")
|
||||
if self._ws_client is not None:
|
||||
self._ws_client.close()
|
||||
self._ws_client = None
|
||||
logger.warning("Shutdown complete")
|
||||
|
||||
def _wait_for_server_running(self):
|
||||
"""
|
||||
@@ -366,55 +322,48 @@ class StdDaqClient:
|
||||
break
|
||||
self._ws_idle_event.set()
|
||||
|
||||
def _ws_send_and_receive(self):
|
||||
if not self.ws_client:
|
||||
self.wait_for_connection()
|
||||
try:
|
||||
try:
|
||||
msg = self._send_queue.get(block=False)
|
||||
logger.trace(f"Sending to stddaq ws: {msg}")
|
||||
self.ws_client.send(json.dumps(msg))
|
||||
logger.trace(f"Sent to stddaq ws: {msg}")
|
||||
except queue.Empty:
|
||||
pass
|
||||
try:
|
||||
recv_msgs = self.ws_client.recv(timeout=0.1)
|
||||
except TimeoutError:
|
||||
return
|
||||
logger.trace(f"Received from stddaq ws: {recv_msgs}")
|
||||
if recv_msgs is not None:
|
||||
self._on_received_ws_message(recv_msgs)
|
||||
except WebSocketException:
|
||||
content = traceback.format_exc()
|
||||
logger.warning(f"Websocket connection closed unexpectedly: {content}")
|
||||
self.wait_for_connection()
|
||||
def _ws_monitor_loop(self):
|
||||
"""Loop to update the status property of the StdDAQ.
|
||||
|
||||
def _ws_update_loop(self):
|
||||
This is a persistent monitor that updates the status and calls attached
|
||||
callbacks. It also handles stdDAQ restarts and reconnection by itself.
|
||||
"""
|
||||
Loop to update the status property of the StdDAQ.
|
||||
"""
|
||||
while not self._shutdown_event.is_set():
|
||||
self._wait_for_server_running()
|
||||
self._ws_send_and_receive()
|
||||
|
||||
def _on_received_ws_message(self, msg: str):
|
||||
"""
|
||||
Handle a message received from the StdDAQ.
|
||||
"""
|
||||
try:
|
||||
data = StdDaqWsResponse(**json.loads(msg))
|
||||
except Exception:
|
||||
content = traceback.format_exc()
|
||||
logger.warning(f"Failed to decode websocket message: {content}")
|
||||
if self._ws_recv_mutex.locked():
|
||||
logger.warning(f"[{self.name}] stdDAQ WS monitor loop already locked")
|
||||
return
|
||||
self._status = data.status
|
||||
self._run_status_callbacks()
|
||||
|
||||
with self._ws_recv_mutex:
|
||||
while not self._shutdown_event.is_set():
|
||||
self._wait_for_server_running()
|
||||
try:
|
||||
msg = self._ws_client.recv(timeout=0.1)
|
||||
msg_timestamp = time.time()
|
||||
except TimeoutError:
|
||||
continue
|
||||
except WebSocketException:
|
||||
content = traceback.format_exc()
|
||||
# TODO: this is expected to happen on every reconfiguration
|
||||
logger.warning(f"Websocket connection closed unexpectedly: {content}")
|
||||
self.wait_for_connection()
|
||||
continue
|
||||
msg = json.loads(msg)
|
||||
if self._status != msg["status"]:
|
||||
logger.warning(
|
||||
f"[{self.name}] stdDAQ state transition: {self._status} --> {msg['status']}"
|
||||
)
|
||||
if msg["status"] == "recording":
|
||||
self._count = msg.get("count", 0)
|
||||
# Update status and run callbacks
|
||||
self._status = msg["status"]
|
||||
self._status_timestamp = msg_timestamp
|
||||
self._run_status_callbacks()
|
||||
|
||||
def _run_status_callbacks(self):
|
||||
"""
|
||||
Update the DeviceStatus objects based on the current status of the StdDAQ.
|
||||
If the status matches one of the success or error statuses, the DeviceStatus object will be set to finished
|
||||
or exception, respectively and removed from the list of callbacks.
|
||||
If the status matches one of the success or error statuses, the DeviceStatus
|
||||
object will be set to finished or exception, respectively and removed from
|
||||
the list of callbacks.
|
||||
"""
|
||||
|
||||
status = self._status
|
||||
@@ -431,3 +380,16 @@ class StdDaqClient:
|
||||
|
||||
for cb in completed_callbacks:
|
||||
self._status_callbacks.pop(id(cb))
|
||||
|
||||
|
||||
# Automatically connect to microXAS testbench if directly invoked
|
||||
if __name__ == "__main__":
|
||||
# pylint: disable=disallowed-name,too-few-public-methods
|
||||
class foo:
|
||||
"""Dummy"""
|
||||
|
||||
name = "bar"
|
||||
|
||||
daq = StdDaqClient(
|
||||
parent=foo(), ws_url="ws://129.129.95.111:8080", rest_url="http://129.129.95.111:5000"
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ import json
|
||||
import threading
|
||||
import time
|
||||
from typing import Callable
|
||||
import traceback
|
||||
|
||||
import numpy as np
|
||||
import zmq
|
||||
@@ -13,14 +14,21 @@ ZMQ_TOPIC_FILTER = b""
|
||||
|
||||
|
||||
class StdDaqPreview:
|
||||
USER_ACCESS = ["start", "stop"]
|
||||
"""Standalone stdDAQ preview class"""
|
||||
|
||||
USER_ACCESS = ["start", "stop", "image", "frameno"]
|
||||
_socket = None
|
||||
_zmq_thread = None
|
||||
_throttle = 0.2
|
||||
image = None
|
||||
frameno = None
|
||||
|
||||
def __init__(self, url: str, cb: Callable):
|
||||
self.url = url
|
||||
self._socket = None
|
||||
self._shutdown_event = threading.Event()
|
||||
self._zmq_thread = None
|
||||
self._on_update_callback = cb
|
||||
# Must be here otherwise they're static (shared between class instances)
|
||||
self._monitor_mutex = threading.Lock()
|
||||
self._shutdown_event = threading.Event()
|
||||
|
||||
def connect(self):
|
||||
"""Connect to te StDAQs PUB-SUB streaming interface
|
||||
@@ -40,56 +48,65 @@ class StdDaqPreview:
|
||||
self._socket.connect(self.url)
|
||||
|
||||
def start(self):
|
||||
"""Start the preview thread"""
|
||||
# Only one consumer thread
|
||||
if self._zmq_thread:
|
||||
self.stop()
|
||||
|
||||
self._shutdown_event.clear()
|
||||
self._zmq_thread = threading.Thread(
|
||||
target=self._zmq_update_loop, daemon=True, name="StdDaq_live_preview"
|
||||
target=self._zmq_monitor, daemon=True, name="StdDaq_live_preview"
|
||||
)
|
||||
self._zmq_thread.start()
|
||||
|
||||
def stop(self):
|
||||
"""Stop the preview and disconnect from ZMQ stream"""
|
||||
self._shutdown_event.set()
|
||||
if self._zmq_thread:
|
||||
self._zmq_thread.join()
|
||||
self._zmq_thread = None
|
||||
|
||||
def _zmq_update_loop(self):
|
||||
while not self._shutdown_event.is_set():
|
||||
if self._socket is None:
|
||||
self.connect()
|
||||
try:
|
||||
self._poll()
|
||||
except ValueError:
|
||||
# Happens when ZMQ partially delivers the multipart message
|
||||
pass
|
||||
except zmq.error.Again:
|
||||
# Happens when receive queue is empty
|
||||
time.sleep(0.1)
|
||||
def _zmq_monitor(self):
|
||||
"""ZMQ stream monitor"""
|
||||
|
||||
def _poll(self):
|
||||
"""
|
||||
Poll the ZMQ socket for new data. It will throttle the data update and
|
||||
only subscribe to the topic for a single update. This is not very nice
|
||||
but it seems like there is currently no option to set the update rate on
|
||||
the backend.
|
||||
"""
|
||||
|
||||
if self._shutdown_event.wait(0.2):
|
||||
# Exit if another monitor is running
|
||||
if self._monitor_mutex.locked():
|
||||
return
|
||||
|
||||
try:
|
||||
# subscribe to the topic
|
||||
self._socket.setsockopt(zmq.SUBSCRIBE, ZMQ_TOPIC_FILTER)
|
||||
with self._monitor_mutex:
|
||||
# Open a new connection
|
||||
self.connect()
|
||||
|
||||
# pylint: disable=no-member
|
||||
r = self._socket.recv_multipart(flags=zmq.NOBLOCK)
|
||||
self._parse_data(r)
|
||||
try:
|
||||
# Run the monitor loop
|
||||
t_last = time.time()
|
||||
while not self._shutdown_event.is_set():
|
||||
try:
|
||||
# pylint: disable=no-member
|
||||
r = self._socket.recv_multipart(flags=zmq.NOBLOCK)
|
||||
|
||||
finally:
|
||||
# Unsubscribe from the topic
|
||||
self._socket.setsockopt(zmq.UNSUBSCRIBE, ZMQ_TOPIC_FILTER)
|
||||
# Throttle parsing and callbacks
|
||||
t_curr = time.time()
|
||||
if t_curr - t_last > self._throttle:
|
||||
self._parse_data(r)
|
||||
t_last = t_curr
|
||||
except ValueError:
|
||||
# Happens when ZMQ partially delivers the multipart message
|
||||
content = traceback.format_exc()
|
||||
logger.warning(f"Websocket connection closed unexpectedly: {content}")
|
||||
continue
|
||||
except zmq.error.Again:
|
||||
# Happens when receive queue is empty
|
||||
time.sleep(0.1)
|
||||
finally:
|
||||
# Stop receiving incoming data
|
||||
self._socket.close()
|
||||
logger.warning("Detached live_preview monitoring")
|
||||
|
||||
def _parse_data(self, data):
|
||||
# Length and throtling checks
|
||||
if len(data) != 2:
|
||||
logger.warning(f"Received malformed array of length {len(data)}")
|
||||
logger.warning(f"Received incomplete ZMQ message of length {len(data)}")
|
||||
|
||||
# Unpack the Array V1 reply to metadata and array data
|
||||
meta, img_data = data
|
||||
@@ -98,11 +115,19 @@ class StdDaqPreview:
|
||||
header = json.loads(meta)
|
||||
if header["type"] == "uint16":
|
||||
image = np.frombuffer(img_data, dtype=np.uint16)
|
||||
elif header["type"] == "uint8":
|
||||
image = np.frombuffer(img_data, dtype=np.uint8)
|
||||
else:
|
||||
raise ValueError(f"Unexpected type {header['type']}")
|
||||
if image.size != np.prod(header["shape"]):
|
||||
err = f"Unexpected array size of {image.size} for header: {header}"
|
||||
raise ValueError(err)
|
||||
image = image.reshape(header["shape"])
|
||||
logger.info(f"Live update: frame {header['frame']}")
|
||||
self._on_update_callback(image)
|
||||
# Print diadnostics and run callback
|
||||
logger.info(
|
||||
f"Live update: frame {header['frame']}\tShape: {header['shape']}\t"
|
||||
f"Mean: {np.mean(image):.3f}"
|
||||
)
|
||||
self.image = image
|
||||
self.frameno = header["frame"]
|
||||
self._on_update_callback(image, header)
|
||||
|
||||
@@ -1,2 +1,8 @@
|
||||
from .tutorial_fly_scan import AcquireDark, AcquireWhite, AcquireRefs, AcquireProjections, TutorialFlyScanContLine
|
||||
from .tomcat_scans import TomcatSnapNStep, TomcatSimpleSequence
|
||||
from .tomcat_scans import TomcatSimpleSequence, TomcatSnapNStep
|
||||
from .tutorial_fly_scan import (
|
||||
AcquireDark,
|
||||
AcquireProjections,
|
||||
AcquireRefs,
|
||||
AcquireWhite,
|
||||
TutorialFlyScanContLine,
|
||||
)
|
||||
|
||||
410
tomcat_bec/scans/advanced_scans.py
Normal file
410
tomcat_bec/scans/advanced_scans.py
Normal file
@@ -0,0 +1,410 @@
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
from bec_lib.device import DeviceBase
|
||||
from bec_server.scan_server.scans import Acquire, AsyncFlyScanBase
|
||||
|
||||
from bec_lib import bec_logger
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class Shutter:
|
||||
"""Shutter status"""
|
||||
|
||||
CLOSED = 0
|
||||
OPEN = 1
|
||||
|
||||
|
||||
class AcquireDarkV2(Acquire):
|
||||
scan_name = "acquire_dark_v2"
|
||||
required_kwargs = ["exp_burst"]
|
||||
gui_config = {"Acquisition parameters": ["exp_burst"]}
|
||||
|
||||
def __init__(self, exp_burst: int, file_prefix="", **kwargs):
|
||||
"""
|
||||
Acquire dark images. This scan is used to acquire dark images. Dark images are images taken with the shutter
|
||||
closed and no beam on the sample. Dark images are used to correct the data images for dark current.
|
||||
|
||||
NOTE: this scan has a special operation mode that does not call
|
||||
|
||||
Args:
|
||||
exp_burst : int
|
||||
Number of dark images to acquire (no default)
|
||||
file_prefix : str
|
||||
File prefix
|
||||
|
||||
Examples:
|
||||
>>> scans.acquire_dark(5)
|
||||
|
||||
"""
|
||||
super().__init__(exp_burst=exp_burst, file_prefix="", **kwargs)
|
||||
self.burst_at_each_point = 1 # At each point, how many times I want to individually trigger
|
||||
self.exp_burst = exp_burst
|
||||
self.file_prefix = file_prefix
|
||||
|
||||
def pre_scan(self):
|
||||
"""Close the shutter before scan"""
|
||||
yield from self.stubs.set(device=["eyex"], value=[Shutter.CLOSED])
|
||||
return super().pre_scan()
|
||||
|
||||
def direct(self):
|
||||
"""Direct scan procedure call"""
|
||||
# Collect relevant devices
|
||||
self.cams = [
|
||||
cam.name
|
||||
for cam in self.device_manager.devices.get_devices_with_tags("camera")
|
||||
if cam.enabled
|
||||
]
|
||||
self.prev = [
|
||||
pre.name
|
||||
for pre in self.device_manager.devices.get_devices_with_tags("preview")
|
||||
if pre.enabled
|
||||
]
|
||||
self.daqs = [
|
||||
daq.name
|
||||
for daq in self.device_manager.devices.get_devices_with_tags("daq")
|
||||
if daq.enabled
|
||||
]
|
||||
|
||||
# Do not call stage, as there's no ScanInfo emitted for direct call
|
||||
for daq in self.daqs:
|
||||
cam = yield from self.stubs.send_rpc_and_wait(daq, "datasource.get")
|
||||
prefix = f"{self.file_prefix}_{cam}_dark"
|
||||
yield from self.stubs.send_rpc_and_wait(daq, "file_prefix.set", prefix)
|
||||
yield from self.stubs.send_rpc_and_wait(daq, "num_images.set", self.exp_burst)
|
||||
yield from self.stubs.send_rpc_and_wait(daq, "arm")
|
||||
for prev in self.prev:
|
||||
yield from self.stubs.send_rpc_and_wait(prev, "arm")
|
||||
for cam in self.cams:
|
||||
yield from self.stubs.send_rpc_and_wait(
|
||||
cam, "configure", {"exposure_num_burst": self.exp_burst}
|
||||
)
|
||||
yield from self.stubs.send_rpc_and_wait(cam, "arm")
|
||||
|
||||
yield from self.pre_scan()
|
||||
yield from self.scan_core()
|
||||
yield from self.finalize()
|
||||
yield from self.unstage()
|
||||
yield from self.cleanup()
|
||||
|
||||
|
||||
class AcquireWhiteV2(Acquire):
|
||||
scan_name = "acquire_white_v2"
|
||||
gui_config = {"Acquisition parameters": ["exp_burst"]}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
motor: DeviceBase,
|
||||
exp_burst: int,
|
||||
sample_position_out: float,
|
||||
sample_angle_out: float,
|
||||
file_prefix: str = "",
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Acquire flat field images. This scan is used to acquire flat field images. The flat field
|
||||
image is an image taken with the shutter open but the sample out of the beam. Flat field
|
||||
images are used to correct the data images for non-uniformity in the detector.
|
||||
|
||||
Args:
|
||||
motor : DeviceBase
|
||||
Motor to be moved to move the sample out of beam
|
||||
exp_burst : int
|
||||
Number of flat field images to acquire (no default)
|
||||
sample_position_out : float
|
||||
Position to move the sample stage out of beam and take flat field images
|
||||
sample_angle_out : float
|
||||
Angular position where to take the flat field images
|
||||
|
||||
Examples:
|
||||
>>> scans.acquire_white(dev.samx, 5, 20)
|
||||
|
||||
"""
|
||||
super().__init__(exp_burst=exp_burst, **kwargs)
|
||||
self.exp_burst = exp_burst
|
||||
self.file_prefix = file_prefix
|
||||
self.burst_at_each_point = 1
|
||||
|
||||
self.scan_motors = [motor, "eyez"]
|
||||
# self.scan_motors = [motor, "es1_roty"]
|
||||
self.out_position = [sample_position_out, sample_angle_out]
|
||||
|
||||
def pre_scan(self):
|
||||
"""Open the shutter before scan"""
|
||||
# Move sample out
|
||||
yield from self._move_scan_motors_and_wait(self.out_position)
|
||||
# Open the main shutter (TODO change to the correct shutter device)
|
||||
yield from self.stubs.set(device=["eyex"], value=[Shutter.OPEN])
|
||||
|
||||
return super().pre_scan()
|
||||
|
||||
def cleanup(self):
|
||||
"""Close the shutter after scan"""
|
||||
# Close fast shutter
|
||||
yield from self.stubs.set(device=["eyex"], value=[Shutter.CLOSED])
|
||||
return super().cleanup()
|
||||
|
||||
def direct(self):
|
||||
"""Direct scan procedure call"""
|
||||
# Collect relevant devices
|
||||
self.cams = [
|
||||
cam.name
|
||||
for cam in self.device_manager.devices.get_devices_with_tags("camera")
|
||||
if cam.enabled
|
||||
]
|
||||
self.prev = [
|
||||
pre.name
|
||||
for pre in self.device_manager.devices.get_devices_with_tags("preview")
|
||||
if pre.enabled
|
||||
]
|
||||
self.daqs = [
|
||||
daq.name
|
||||
for daq in self.device_manager.devices.get_devices_with_tags("daq")
|
||||
if daq.enabled
|
||||
]
|
||||
|
||||
# Do not call stage, as there's no ScanInfo emitted for direct call
|
||||
for daq in self.daqs:
|
||||
cam = yield from self.stubs.send_rpc_and_wait(daq, "datasource.get")
|
||||
prefix = f"{self.file_prefix}_{cam}_white"
|
||||
yield from self.stubs.send_rpc_and_wait(daq, "file_prefix.set", prefix)
|
||||
yield from self.stubs.send_rpc_and_wait(daq, "num_images.set", self.exp_burst)
|
||||
yield from self.stubs.send_rpc_and_wait(daq, "arm")
|
||||
for prev in self.prev:
|
||||
yield from self.stubs.send_rpc_and_wait(prev, "arm")
|
||||
for cam in self.cams:
|
||||
yield from self.stubs.send_rpc_and_wait(
|
||||
cam, "configure", {"exposure_num_burst": self.exp_burst}
|
||||
)
|
||||
yield from self.stubs.send_rpc_and_wait(cam, "arm")
|
||||
|
||||
yield from self.pre_scan()
|
||||
yield from self.scan_core()
|
||||
yield from self.finalize()
|
||||
yield from self.unstage()
|
||||
yield from self.cleanup()
|
||||
|
||||
|
||||
# class AcquireProjections(AsyncFlyScanBase):
|
||||
# scan_name = "acquire_projections"
|
||||
# gui_config = {
|
||||
# "Motor": ["motor"],
|
||||
# "Acquisition parameters": ["sample_position_in", "start_angle", "angular_range" ],
|
||||
# "Camera": ["exp_time", "exp_burst"]
|
||||
# }
|
||||
|
||||
# def __init__(self,
|
||||
# motor: DeviceBase,
|
||||
# exp_burst: int,
|
||||
# sample_position_in: float,
|
||||
# start_angle: float,
|
||||
# angular_range: float,
|
||||
# exp_time:float,
|
||||
# **kwargs):
|
||||
# """
|
||||
# Acquire projection images.
|
||||
|
||||
# Args:
|
||||
# motor : DeviceBase
|
||||
# Motor to move continuously from start to stop position
|
||||
# exp_burst : int
|
||||
# Number of flat field images to acquire (no default)
|
||||
# sample_position_in : float
|
||||
# Position to move the sample stage to position the sample in the beam
|
||||
# start_angle : float
|
||||
# Angular start position for the scan
|
||||
# angular_range : float
|
||||
# Angular range
|
||||
# exp_time : float, optional
|
||||
# Exposure time [ms]. If not specified, the currently configured value on the camera will be used
|
||||
# exp_period : float, optional
|
||||
# Exposure period [ms]. If not specified, the currently configured value on the camera will be used
|
||||
# image_width : int, optional
|
||||
# ROI size in the x-direction [pixels]. If not specified, the currently configured value on the camera will be used
|
||||
# image_height : int, optional
|
||||
# ROI size in the y-direction [pixels]. If not specified, the currently configured value on the camera will be used
|
||||
# acq_mode : str, optional
|
||||
# Predefined acquisition mode (default= 'default')
|
||||
# file_path : str, optional
|
||||
# File path for standard daq
|
||||
# ddc_trigger : int, optional
|
||||
# Drive Data Capture Trigger
|
||||
# ddc_source0 : int, optional
|
||||
# Drive Data capture Input0
|
||||
|
||||
# Returns:
|
||||
# ScanReport
|
||||
|
||||
# Examples:
|
||||
# >>> scans.acquire_projections()
|
||||
|
||||
# """
|
||||
# self.motor = motor
|
||||
# super().__init__(exp_time=exp_time,**kwargs)
|
||||
|
||||
# self.burst_at_each_point = 1
|
||||
# self.sample_position_in = sample_position_in
|
||||
# self.start_angle = start_angle
|
||||
# self.angular_range = angular_range
|
||||
|
||||
# self.dark_shutter_pos_out = 1 ### change with a variable
|
||||
# self.dark_shutter_pos_in = 0 ### change with a variable
|
||||
|
||||
# def update_scan_motors(self):
|
||||
# return [self.motor]
|
||||
|
||||
# def prepare_positions(self):
|
||||
# self.positions = np.array([[self.start_angle], [self.start_angle+self.angular_range]])
|
||||
# self.num_pos = None
|
||||
# yield from self._set_position_offset()
|
||||
|
||||
# def scan_core(self):
|
||||
|
||||
# # move to in position and go to start angular position
|
||||
# yield from self.stubs.set(device=["eyez", self.motor], value=[self.sample_position_in, self.positions[0][0]])
|
||||
|
||||
# # open the shutter
|
||||
# yield from self.stubs.set(device="eyex", value=self.dark_shutter_pos_out)
|
||||
# # TODO add opening of fast shutter
|
||||
|
||||
# # start the flyer
|
||||
# flyer_request = yield from self.stubs.set(
|
||||
# device=self.motor, value=self.positions[1][0], wait=False
|
||||
# )
|
||||
|
||||
# self.connector.send_client_info(
|
||||
# "Starting the scan", show_asap=True, rid=self.metadata.get("RID")
|
||||
# )
|
||||
|
||||
# yield from self.stubs.trigger()
|
||||
|
||||
# while not flyer_request.done:
|
||||
|
||||
# yield from self.stubs.read(
|
||||
# group="monitored", point_id=self.point_id
|
||||
# )
|
||||
# time.sleep(1)
|
||||
|
||||
# # increase the point id
|
||||
# self.point_id += 1
|
||||
|
||||
# self.num_pos = self.point_id
|
||||
|
||||
|
||||
class AcquireRefsV2(Acquire):
|
||||
scan_name = "acquire_refs_v2"
|
||||
gui_config = {}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
motor: DeviceBase,
|
||||
num_darks: int = 0,
|
||||
num_flats: int = 0,
|
||||
sample_angle_out: float = 0,
|
||||
sample_position_in: float = 0,
|
||||
sample_position_out: float = 1,
|
||||
file_prefix_dark: str = "tmp_dark",
|
||||
file_prefix_white: str = "tmp_white",
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Acquire reference images (darks + whites) and return to beam position.
|
||||
|
||||
Reference images are acquired automatically in an optimized sequence and
|
||||
the sample is returned to the sample_in_position afterwards.
|
||||
|
||||
Args:
|
||||
motor : DeviceBase
|
||||
Motor to be moved to move the sample out of beam
|
||||
num_darks : int , optional
|
||||
Number of dark field images to acquire
|
||||
num_flats : int , optional
|
||||
Number of white field images to acquire
|
||||
sample_angle_out : float , optional
|
||||
Angular position where to take the flat field images
|
||||
sample_position_in : float , optional
|
||||
Sample stage X position for sample in beam [um]
|
||||
sample_position_out : float ,optional
|
||||
Sample stage X position for sample out of the beam [um]
|
||||
exp_time : float, optional
|
||||
Exposure time [ms]. If not specified, the currently configured value
|
||||
on the camera will be used
|
||||
exp_period : float, optional
|
||||
Exposure period [ms]. If not specified, the currently configured value
|
||||
on the camera will be used
|
||||
image_width : int, optional
|
||||
ROI size in the x-direction [pixels]. If not specified, the currently
|
||||
configured value on the camera will be used
|
||||
image_height : int, optional
|
||||
ROI size in the y-direction [pixels]. If not specified, the currently
|
||||
configured value on the camera will be used
|
||||
acq_mode : str, optional
|
||||
Predefined acquisition mode (default= 'default')
|
||||
file_path : str, optional
|
||||
File path for standard daq
|
||||
|
||||
Returns:
|
||||
ScanReport
|
||||
|
||||
Examples:
|
||||
>>> scans.acquire_refs(sample_angle_out=90, sample_position_in=10, num_darks=5, num_flats=5, exp_time=0.1)
|
||||
|
||||
"""
|
||||
self.motor = motor
|
||||
super().__init__(**kwargs)
|
||||
self.sample_position_in = sample_position_in
|
||||
self.sample_position_out = sample_position_out
|
||||
self.sample_angle_out = sample_angle_out
|
||||
self.num_darks = num_darks
|
||||
self.num_flats = num_flats
|
||||
self.file_prefix_dark = file_prefix_dark
|
||||
self.file_prefix_white = file_prefix_white
|
||||
self.scan_parameters["std_daq_params"] = {"reconnect": False}
|
||||
|
||||
def stage(self):
|
||||
"""Wrapped scan doesn't need staging"""
|
||||
yield None
|
||||
|
||||
def scan_core(self):
|
||||
|
||||
if self.num_darks:
|
||||
msg = f"Acquiring {self.num_darks} dark images"
|
||||
logger.warning(msg)
|
||||
self.connector.send_client_info(msg, show_asap=True, rid=self.metadata.get("RID"))
|
||||
|
||||
darks = AcquireDarkV2(
|
||||
exp_burst=self.num_darks,
|
||||
# file_prefix=self.file_prefix_dark,
|
||||
device_manager=self.device_manager,
|
||||
metadata=self.metadata,
|
||||
instruction_handler=self.stubs._instruction_handler,
|
||||
**self.caller_kwargs,
|
||||
)
|
||||
|
||||
yield from darks.direct()
|
||||
self.point_id = darks.point_id
|
||||
|
||||
if self.num_flats:
|
||||
msg = f"Acquiring {self.num_flats} flat field images"
|
||||
logger.warning(msg)
|
||||
self.connector.send_client_info(msg, show_asap=True, rid=self.metadata.get("RID"))
|
||||
logger.warning("Calling AcquireWhite")
|
||||
|
||||
flats = AcquireWhiteV2(
|
||||
motor=self.motor,
|
||||
exp_burst=self.num_flats,
|
||||
sample_position_out=self.sample_position_out,
|
||||
# sample_angle_out=self.sample_angle_out,
|
||||
device_manager=self.device_manager,
|
||||
metadata=self.metadata,
|
||||
instruction_handler=self.stubs._instruction_handler,
|
||||
**self.caller_kwargs,
|
||||
)
|
||||
|
||||
flats.point_id = self.point_id
|
||||
yield from flats.direct()
|
||||
self.point_id = flats.point_id
|
||||
## TODO move sample in beam and do not wait
|
||||
## TODO move rotation to angle and do not wait
|
||||
logger.warning("[AcquireRefsV2] Done with scan_core")
|
||||
0
tomcat_bec/scans/metadata_schema/__init__.py
Normal file
0
tomcat_bec/scans/metadata_schema/__init__.py
Normal file
12
tomcat_bec/scans/metadata_schema/metadata_schema_registry.py
Normal file
12
tomcat_bec/scans/metadata_schema/metadata_schema_registry.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# from .metadata_schema_template import ExampleSchema
|
||||
|
||||
METADATA_SCHEMA_REGISTRY = {
|
||||
# Add models which should be used to validate scan metadata here.
|
||||
# Make a model according to the template, and import it as above
|
||||
# Then associate it with a scan like so:
|
||||
# "example_scan": ExampleSchema
|
||||
}
|
||||
|
||||
# Define a default schema type which should be used as the fallback for everything:
|
||||
|
||||
DEFAULT_SCHEMA = None
|
||||
34
tomcat_bec/scans/metadata_schema/metadata_schema_template.py
Normal file
34
tomcat_bec/scans/metadata_schema/metadata_schema_template.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# # By inheriting from BasicScanMetadata you can define a schema by which metadata
|
||||
# # supplied to a scan must be validated.
|
||||
# # This schema is a Pydantic model: https://docs.pydantic.dev/latest/concepts/models/
|
||||
# # but by default it will still allow you to add any arbitrary information to it.
|
||||
# # That is to say, when you run a scan with which such a model has been associated in the
|
||||
# # metadata_schema_registry, you can supply any python dictionary with strings as keys
|
||||
# # and built-in python types (strings, integers, floats) as values, and these will be
|
||||
# # added to the experiment metadata, but it *must* contain the keys and values of the
|
||||
# # types defined in the schema class.
|
||||
# #
|
||||
# #
|
||||
# # For example, say that you would like to enforce recording information about sample
|
||||
# # pretreatment, you could define the following:
|
||||
# #
|
||||
#
|
||||
# from bec_lib.metadata_schema import BasicScanMetadata
|
||||
#
|
||||
#
|
||||
# class ExampleSchema(BasicScanMetadata):
|
||||
# treatment_description: str
|
||||
# treatment_temperature_k: int
|
||||
#
|
||||
#
|
||||
# # If this was used according to the example in metadata_schema_registry.py,
|
||||
# # then when calling the scan, the user would need to write something like:
|
||||
# >>> scans.example_scan(
|
||||
# >>> motor,
|
||||
# >>> 1,
|
||||
# >>> 2,
|
||||
# >>> 3,
|
||||
# >>> metadata={"treatment_description": "oven overnight", "treatment_temperature_k": 575},
|
||||
# >>> )
|
||||
#
|
||||
# # And the additional metadata would be saved in the HDF5 file created for the scan.
|
||||
0
tomcat_bec/scans/simple_scans.py
Normal file
0
tomcat_bec/scans/simple_scans.py
Normal file
@@ -18,95 +18,11 @@ import os
|
||||
import time
|
||||
|
||||
from bec_lib import bec_logger
|
||||
from bec_server.scan_server.scans import AsyncFlyScanBase, ScanBase, ScanArgType
|
||||
from bec_server.scan_server.scans import AsyncFlyScanBase, ScanArgType
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class TomcatStepScan(ScanBase):
|
||||
"""Simple software step scan for Tomcat
|
||||
|
||||
Example class for simple BEC-based step scan using the low-level API. It's just a standard
|
||||
'line_scan' with the only difference that overrides burst behavior to use camera burst instead
|
||||
of individual software triggers.
|
||||
|
||||
NOTE: As decided by Tomcat, the scans should not manage the scope of devices
|
||||
- All enabled devices are expected to be configured for acquisition by the end of stage
|
||||
- Some devices can configure themselves from mandatory scan parameters (steps, burst)
|
||||
- Other devices can be optionally configured by keyword arguments
|
||||
- Devices will try to stage using whatever was set on them before
|
||||
|
||||
Example:
|
||||
--------
|
||||
>>> scans.tomcatstepscan(scan_start=-25, scan_end=155, steps=180, exp_time=0.005, exp_burst=5)
|
||||
|
||||
Common keyword arguments:
|
||||
-------------------------
|
||||
image_width : int
|
||||
image_height : int
|
||||
ddc_trigger : str
|
||||
"""
|
||||
|
||||
scan_name = "tomcatstepscan"
|
||||
scan_type = "step"
|
||||
required_kwargs = ["scan_start", "scan_end", "steps"]
|
||||
gui_config = {
|
||||
"Movement parameters": ["steps"],
|
||||
"Acquisition parameters": ["exp_time", "exp_burst"],
|
||||
}
|
||||
|
||||
def update_scan_motors(self):
|
||||
self.scan_motors = ["es1_roty"]
|
||||
|
||||
def _get_scan_motors(self):
|
||||
self.scan_motors = ["es1_roty"]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
scan_start: float,
|
||||
scan_end: float,
|
||||
steps: int,
|
||||
exp_time: float=0.005,
|
||||
settling_time: float=0.2,
|
||||
exp_burst: int=1,
|
||||
**kwargs,
|
||||
):
|
||||
# Converting generic kwargs to tomcat device configuration parameters
|
||||
super().__init__(
|
||||
exp_time=exp_time,
|
||||
settling_time=settling_time,
|
||||
burst_at_each_point=1,
|
||||
optim_trajectory=None,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# For position calculation
|
||||
self.motor = "es1_roty"
|
||||
self.scan_start = scan_start
|
||||
self.scan_end = scan_end
|
||||
self.scan_steps = steps
|
||||
self.scan_stepsize = (scan_end - scan_start) / steps
|
||||
|
||||
def _calculate_positions(self) -> None:
|
||||
"""Pre-calculate scan positions"""
|
||||
for ii in range(self.scan_steps + 1):
|
||||
self.positions.append(self.scan_start + ii * self.scan_stepsize)
|
||||
|
||||
def _at_each_point(self, ind=None, pos=None):
|
||||
""" Overriden at_each_point, using detector burst instaead of manual triggering"""
|
||||
yield from self._move_scan_motors_and_wait(pos)
|
||||
time.sleep(self.settling_time)
|
||||
|
||||
trigger_time = 0.001*self.exp_time * self.burst_at_each_point
|
||||
yield from self.stubs.trigger(min_wait=trigger_time)
|
||||
# yield from self.stubs.trigger(group='trigger', point_id=self.point_id)
|
||||
# time.sleep(trigger_time)
|
||||
|
||||
yield from self.stubs.read(group="monitored", point_id=self.point_id)
|
||||
# yield from self.stubs.read(group="monitored", point_id=self.point_id, wait_group=None)
|
||||
self.point_id += 1
|
||||
|
||||
|
||||
class TomcatSnapNStep(AsyncFlyScanBase):
|
||||
"""Simple software step scan forTomcat
|
||||
|
||||
@@ -115,21 +31,21 @@ class TomcatSnapNStep(AsyncFlyScanBase):
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> scans.tomcatsnapnstepscan(scan_start=-25, scan_end=155, steps=180, exp_time=0.005, exp_burst=5)
|
||||
>>> scans.tomcatsnapnstepscan(scan_start=-25, scan_end=155, steps=180, acq_time=5, exp_burst=5)
|
||||
"""
|
||||
|
||||
scan_name = "tomcatsnapnstepscan"
|
||||
scan_type = "scripted"
|
||||
# scan_type = "scripted"
|
||||
# arg_input = {"camera" : ScanArgType.DEVICE,
|
||||
# "exp_time" : ScanArgType.FLOAT}
|
||||
# arg_bundle_size= {"bundle": len(arg_input), "min": 1, "max": None}
|
||||
required_kwargs = ["scan_start", "scan_end", "steps"]
|
||||
gui_config = {
|
||||
"Movement parameters": ["steps"],
|
||||
"Acquisition parameters": ["exp_time", "exp_burst"],
|
||||
"Acquisition parameters": ["acq_time", "exp_burst"],
|
||||
}
|
||||
|
||||
def _get_scan_motors(self):
|
||||
def _update_scan_motors(self):
|
||||
self.scan_motors = ["es1_roty"]
|
||||
|
||||
def __init__(
|
||||
@@ -137,8 +53,8 @@ class TomcatSnapNStep(AsyncFlyScanBase):
|
||||
scan_start: float,
|
||||
scan_end: float,
|
||||
steps: int,
|
||||
exp_time:float=0.005,
|
||||
settling_time:float=0.2,
|
||||
acq_time:float=5.0,
|
||||
exp_burst:int=1,
|
||||
sync:str="event",
|
||||
**kwargs,
|
||||
@@ -147,25 +63,30 @@ class TomcatSnapNStep(AsyncFlyScanBase):
|
||||
self.scan_start = scan_start
|
||||
self.scan_end = scan_end
|
||||
self.scan_steps = steps
|
||||
self.scan_stepsize = (scan_end - scan_start) / steps
|
||||
self.scan_ntotal = exp_burst * (steps + 1)
|
||||
self.exp_time = exp_time
|
||||
self.exp_time = acq_time
|
||||
self.exp_burst = exp_burst
|
||||
self.settling_time = settling_time
|
||||
self.scan_sync = sync
|
||||
|
||||
# General device configuration
|
||||
# Gigafrost trigger mode
|
||||
kwargs["parameter"]["kwargs"]["acq_mode"] = "ext_enable"
|
||||
|
||||
# Used for Aeroscript file substitutions for the task interface
|
||||
filename = "AerotechSnapAndStepTemplate.ascript"
|
||||
filesubs = self.get_filesubs()
|
||||
filetext = self.render_file(filename, filesubs)
|
||||
kwargs["parameter"]["kwargs"]["script_text"] = filetext
|
||||
kwargs["parameter"]["kwargs"]["script_file"] = "bec.ascript"
|
||||
self.scan_parameters["aerotech_config"] = {
|
||||
"script_text":filetext,
|
||||
"script_file":"bec.ascript",
|
||||
"script_task": 4,
|
||||
}
|
||||
# self.scan_parameters["script_file"] = "bec.ascript"
|
||||
# kwargs["parameter"]["kwargs"]["script_text"] = filetext
|
||||
# kwargs["parameter"]["kwargs"]["script_file"] = "bec.ascript"
|
||||
|
||||
super().__init__(
|
||||
exp_time=exp_time,
|
||||
acq_time=acq_time,
|
||||
exp_burst=exp_burst,
|
||||
settling_time=settling_time,
|
||||
burst_at_each_point=1,
|
||||
optim_trajectory=None,
|
||||
@@ -182,12 +103,12 @@ class TomcatSnapNStep(AsyncFlyScanBase):
|
||||
}
|
||||
filesubs = {
|
||||
"startpos": self.scan_start,
|
||||
"stepsize": self.scan_stepsize,
|
||||
"stepsize": (self.scan_end - self.scan_start) / self.scan_steps,
|
||||
"numsteps": self.scan_steps,
|
||||
"exptime": 2 * self.exp_time * self.exp_burst,
|
||||
"exptime": 0.002 * self.exp_time * self.exp_burst,
|
||||
"settling": self.settling_time,
|
||||
"psotrigger": p_modes[self.scan_sync],
|
||||
"npoints": self.scan_ntotal,
|
||||
"npoints": self.exp_burst * (self.scan_steps + 1),
|
||||
}
|
||||
return filesubs
|
||||
|
||||
@@ -196,7 +117,7 @@ class TomcatSnapNStep(AsyncFlyScanBase):
|
||||
# Load the test file
|
||||
filename = os.path.join(os.path.dirname(__file__), "../devices/aerotech/" + filename)
|
||||
logger.info(f"Attempting to load file {filename}")
|
||||
with open(filename) as f:
|
||||
with open(filename, "r", encoding="utf-8") as f:
|
||||
templatetext = f.read()
|
||||
|
||||
# Substitute jinja template
|
||||
@@ -208,12 +129,15 @@ class TomcatSnapNStep(AsyncFlyScanBase):
|
||||
"""The actual scan routine"""
|
||||
print("TOMCAT Running scripted scan (via Jinjad AeroScript)")
|
||||
t_start = time.time()
|
||||
# Kickoff
|
||||
yield from self.stubs.send_rpc_and_wait("es1_tasks", "kickoff")
|
||||
|
||||
# Complete
|
||||
# FIXME: this will swallow errors
|
||||
# yield from self.stubs.complete(device="es1_tasks")
|
||||
st = yield from self.stubs.send_rpc_and_wait("es1_tasks", "complete")
|
||||
# st.wait()
|
||||
yield from self.stubs.send_rpc_and_wait("es1_tasks", "complete")
|
||||
|
||||
# Check final state
|
||||
task_states = yield from self.stubs.send_rpc_and_wait("es1_tasks", "taskStates.get")
|
||||
if task_states[4] == 8:
|
||||
raise RuntimeError(f"Task {4} finished in ERROR state")
|
||||
@@ -243,11 +167,11 @@ class TomcatSimpleSequence(AsyncFlyScanBase):
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> scans.tomcatsimplesequencescan(33, 180, 180, exp_time=0.005, exp_burst=1800, repeats=10)
|
||||
>>> scans.tomcatsimplesequencescan(33, 180, 180, acq_time=5, exp_burst=1800, repeats=10)
|
||||
"""
|
||||
|
||||
scan_name = "tomcatsimplesequencescan"
|
||||
scan_type = "scripted"
|
||||
# scan_type = "scripted"
|
||||
scan_report_hint = "table"
|
||||
required_kwargs = ["scan_start", "gate_high", "gate_low"]
|
||||
gui_config = {
|
||||
@@ -255,7 +179,7 @@ class TomcatSimpleSequence(AsyncFlyScanBase):
|
||||
"Acquisition parameters": [
|
||||
"gate_high",
|
||||
"gate_low",
|
||||
"exp_time",
|
||||
"acq_time",
|
||||
"exp_burst",
|
||||
"sync",
|
||||
],
|
||||
@@ -271,7 +195,7 @@ class TomcatSimpleSequence(AsyncFlyScanBase):
|
||||
gate_low: float,
|
||||
repeats: int = 1,
|
||||
repmode: str = "PosNeg",
|
||||
exp_time: float = 0.005,
|
||||
acq_time: float = 5,
|
||||
exp_burst: float = 180,
|
||||
sync: str = "pso",
|
||||
**kwargs,
|
||||
@@ -282,13 +206,13 @@ class TomcatSimpleSequence(AsyncFlyScanBase):
|
||||
self.gate_low = gate_low
|
||||
self.scan_repnum = repeats
|
||||
self.scan_repmode = repmode.upper()
|
||||
self.exp_time = exp_time
|
||||
self.exp_time = acq_time
|
||||
self.exp_burst = exp_burst
|
||||
self.scan_sync = sync
|
||||
|
||||
# Synthetic values
|
||||
self.scan_ntotal = exp_burst * repeats
|
||||
self.scan_velocity = gate_high / (exp_time * exp_burst)
|
||||
self.scan_velocity = gate_high / (0.001*acq_time * exp_burst)
|
||||
self.scan_acceleration = 500
|
||||
self.scan_safedistance = 10
|
||||
self.scan_accdistance = (
|
||||
@@ -314,7 +238,7 @@ class TomcatSimpleSequence(AsyncFlyScanBase):
|
||||
kwargs["parameter"]["kwargs"]["script_file"] = "bec.ascript"
|
||||
|
||||
super().__init__(
|
||||
exp_time=exp_time,
|
||||
acq_time=acq_time,
|
||||
settling_time=0.5,
|
||||
relative=False,
|
||||
burst_at_each_point=1,
|
||||
@@ -327,7 +251,7 @@ class TomcatSimpleSequence(AsyncFlyScanBase):
|
||||
# Load the test file
|
||||
filename = os.path.join(os.path.dirname(__file__), "../devices/aerotech/" + filename)
|
||||
logger.info(f"Attempting to load file {filename}")
|
||||
with open(filename) as f:
|
||||
with open(filename, 'r', encoding="utf-8") as f:
|
||||
templatetext = f.read()
|
||||
|
||||
# Substitute jinja template
|
||||
@@ -339,12 +263,14 @@ class TomcatSimpleSequence(AsyncFlyScanBase):
|
||||
"""The actual scan routine"""
|
||||
print("TOMCAT Running scripted scan (via Jinjad AeroScript)")
|
||||
t_start = time.time()
|
||||
# Kickoff
|
||||
yield from self.stubs.send_rpc_and_wait("es1_tasks", "kickoff")
|
||||
|
||||
# Complete
|
||||
# FIXME: this will swallow errors
|
||||
# yield from self.stubs.complete(device="es1_tasks")
|
||||
st = yield from self.stubs.send_rpc_and_wait("es1_tasks", "complete")
|
||||
# st.wait()
|
||||
yield from self.stubs.send_rpc_and_wait("es1_tasks", "complete")
|
||||
|
||||
task_states = yield from self.stubs.send_rpc_and_wait("es1_tasks", "taskStates.get")
|
||||
if task_states[4] == 8:
|
||||
raise RuntimeError(f"Task {4} finished in ERROR state")
|
||||
|
||||
@@ -340,7 +340,7 @@ class AcquireRefs(Acquire):
|
||||
# to set signals on a device
|
||||
darks = AcquireDark(
|
||||
exp_burst=self.num_darks,
|
||||
file_prefix=self.file_prefix_dark,
|
||||
# file_prefix=self.file_prefix_dark,
|
||||
device_manager=self.device_manager,
|
||||
metadata=self.metadata,
|
||||
instruction_handler=self.stubs._instruction_handler,
|
||||
@@ -389,6 +389,7 @@ class AcquireRefs(Acquire):
|
||||
self.point_id = flats.point_id
|
||||
## TODO move sample in beam and do not wait
|
||||
## TODO move rotation to angle and do not wait
|
||||
logger.warning("Done with scan_core")
|
||||
|
||||
|
||||
class TutorialFlyScanContLine(AsyncFlyScanBase):
|
||||
|
||||
@@ -16,6 +16,7 @@ class Measurement:
|
||||
self.nimages_white = 100
|
||||
|
||||
self.start_angle = 0
|
||||
self.angular_range = 180
|
||||
self.sample_angle_out = 0
|
||||
self.sample_position_in = 0
|
||||
self.sample_position_out = 1
|
||||
@@ -43,6 +44,10 @@ class Measurement:
|
||||
self.exposure_period = self.det.cfgFramerate.get()
|
||||
self.roix = self.det.cfgRoiX.get()
|
||||
self.roiy = self.det.cfgRoiY.get()
|
||||
|
||||
self.get_position_rb()
|
||||
#self.position_rb = False
|
||||
#self.disable_position_rb_device()
|
||||
|
||||
|
||||
def build_filename(self, acquisition_type='data'):
|
||||
@@ -74,7 +79,8 @@ class Measurement:
|
||||
exposure_period=None, roix=None, roiy=None,nimages=None,
|
||||
nimages_dark=None, nimages_white=None,
|
||||
start_angle=None, sample_angle_out=None,
|
||||
sample_position_in=None, sample_position_out=None):
|
||||
sample_position_in=None, sample_position_out=None,
|
||||
position_rb=None):
|
||||
"""
|
||||
Reconfigure the measurement with any number of new parameter
|
||||
|
||||
@@ -110,6 +116,8 @@ class Measurement:
|
||||
sample_position_out : float, optional
|
||||
Sample stage X position for sample out of the beam [um]
|
||||
(default = None)
|
||||
position_rb : bool, optional
|
||||
Enable position readback (default = None)
|
||||
"""
|
||||
|
||||
if sample_name != None:
|
||||
@@ -138,6 +146,13 @@ class Measurement:
|
||||
self.sample_position_in = sample_position_in
|
||||
if sample_position_out != None:
|
||||
self.sample_position_out = sample_position_out
|
||||
if position_rb != None:
|
||||
if position_rb == True:
|
||||
self.enable_position_rb_device()
|
||||
elif position_rb == False:
|
||||
self.disable_position_rb_device()
|
||||
else:
|
||||
print("WARNING! Position readback should be either True, False or None")
|
||||
|
||||
self.build_filename()
|
||||
|
||||
@@ -168,6 +183,12 @@ class Measurement:
|
||||
self.device_name = self.enabled_detectors[0].name
|
||||
self.build_filename()
|
||||
|
||||
def disable_position_rb_device(self):
|
||||
|
||||
"Disable position readback device"
|
||||
|
||||
dev.es1_ddaq.enabled= False
|
||||
self.position_rb = False
|
||||
|
||||
def enable_detector(self, detector_name):
|
||||
"""
|
||||
@@ -197,7 +218,12 @@ class Measurement:
|
||||
self.device_name = self.enabled_detectors[0].name
|
||||
self.build_filename()
|
||||
|
||||
def enable_position_rb_device(self):
|
||||
|
||||
"Enable position readback device"
|
||||
|
||||
dev.es1_ddaq.enabled= True
|
||||
self.position_rb = True
|
||||
|
||||
def get_available_detectors(self):
|
||||
"""
|
||||
@@ -215,6 +241,14 @@ class Measurement:
|
||||
"""
|
||||
self.enabled_detectors = [obj for obj in dev.get_devices_with_tags('camera') if obj.enabled]
|
||||
|
||||
def get_position_rb(self):
|
||||
"""
|
||||
Get position rb
|
||||
"""
|
||||
if dev.es1_ddaq.enabled == True:
|
||||
self.position_rb = True
|
||||
else:
|
||||
self.position_rb = False
|
||||
|
||||
def show_available_detectors(self):
|
||||
"""
|
||||
@@ -242,39 +276,41 @@ class Measurement:
|
||||
TODO: make it work for multiple devices
|
||||
"""
|
||||
|
||||
print("Sample name: " + self.sample_name)
|
||||
print("Data path: " + self.data_path)
|
||||
print("Number of images: " + str(self.nimages))
|
||||
print("Number of darks: " + str(self.nimages_dark))
|
||||
print("Number of flats: " + str(self.nimages_white))
|
||||
print("Sample name (sample_name): " + self.sample_name)
|
||||
print("Data path (data_path): " + self.data_path)
|
||||
print("Number of images (nimages): " + str(self.nimages))
|
||||
print("Number of darks (nimages_dark): " + str(self.nimages_dark))
|
||||
print("Number of flats (nimages_flat): " + str(self.nimages_white))
|
||||
if self.exposure_time == None:
|
||||
print("Exposure time: " + str(self.det.cfgExposure.get()))
|
||||
print("Exposure time (exposure_time): " + str(self.det.cfgExposure.get()))
|
||||
self.exposure_time = self.det.cfgExposure.get()
|
||||
else:
|
||||
print("Exposure time: " + str(self.exposure_time))
|
||||
print("Exposure time (exposure_time): " + str(self.exposure_time))
|
||||
if self.exposure_period == None:
|
||||
print("Exposure period: " + str(self.det.cfgFramerate.get()))
|
||||
print("Exposure period (exposure_period): " + str(self.det.cfgFramerate.get()))
|
||||
self.exposure_period = self.det.cfgFramerate.get()
|
||||
else:
|
||||
print("Exposure period: " + str(self.exposure_period))
|
||||
print("Exposure period (exposure_period): " + str(self.exposure_period))
|
||||
if self.roix == None:
|
||||
print("Roix: " + str(self.det.cfgRoiX.get()))
|
||||
print("Roix (roix): " + str(self.det.cfgRoiX.get()))
|
||||
self.roix = self.det.cfgRoiX.get()
|
||||
else:
|
||||
print("Roix: " + str(self.roix))
|
||||
print("Roix (roix): " + str(self.roix))
|
||||
if self.roiy == None:
|
||||
print("Roiy: " + str(self.det.cfgRoiY.get()))
|
||||
print("Roiy (roiy): " + str(self.det.cfgRoiY.get()))
|
||||
self.roiy = self.det.cfgRoiY.get()
|
||||
else:
|
||||
print("Roiy: " + str(self.roiy))
|
||||
print("Start angle: " + str(self.start_angle))
|
||||
print("Sample angle out: " + str(self.sample_angle_out))
|
||||
print("Sample position in: " + str(self.sample_position_in))
|
||||
print("Sample position out: " + str(self.sample_position_out))
|
||||
print("Roiy (roiy): " + str(self.roiy))
|
||||
print("Start angle (start_angle): " + str(self.start_angle))
|
||||
print("Angular range (angular_range): " + str(self.angular_range))
|
||||
print("Sample angle out (sample_angle_out): " + str(self.sample_angle_out))
|
||||
print("Sample position in (sample_position_in): " + str(self.sample_position_in))
|
||||
print("Sample position out (sample_position_out): " + str(self.sample_position_out))
|
||||
print("Position readback (position_rb): " + str(self.position_rb))
|
||||
|
||||
|
||||
def acquire_darks(self,nimages_dark=None, exposure_time=None, exposure_period=None,
|
||||
roix=None, roiy=None, acq_mode=None):
|
||||
roix=None, roiy=None, acq_mode=None, **kwargs):
|
||||
"""
|
||||
Acquire a set of dark images with shutters closed.
|
||||
|
||||
@@ -315,17 +351,19 @@ class Measurement:
|
||||
print("Handing over to 'scans.acquire_dark")
|
||||
scans.acquire_dark(exp_burst=self.nimages_dark, exp_time=self.exposure_time, exp_period=self.exposure_period, image_width=self.roix,
|
||||
image_height=self.roiy, acq_mode=acq_mode, file_path=self.file_path, nr_writers=2, base_path=self.base_path,
|
||||
file_prefix=self.file_prefix)
|
||||
file_prefix=self.file_prefix, ddc_trigger=4, ddc_source0=1, **kwargs)
|
||||
|
||||
|
||||
def acquire_whites(self,nimages_white=None, sample_angle_out=None, sample_position_out=None,
|
||||
def acquire_whites(self,motor="eyez", nimages_white=None, sample_angle_out=None, sample_position_out=None,
|
||||
exposure_time=None, exposure_period=None,
|
||||
roix=None, roiy=None, acq_mode=None):
|
||||
roix=None, roiy=None, acq_mode=None, **kwargs):
|
||||
"""
|
||||
Acquire a set of whites images with shutters open and sample out of beam.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
motor : DeviceBase
|
||||
Motor to be moved to move the sample out of beam
|
||||
nimages_whites : int, optional
|
||||
Number of white images to acquire (no default)
|
||||
sample_angle_out : float, optional
|
||||
@@ -348,6 +386,7 @@ class Measurement:
|
||||
m.acquire_whites(nimages_whites=100, exposure_time=5)
|
||||
"""
|
||||
|
||||
self.motor_sample = motor
|
||||
if nimages_white != None:
|
||||
self.nimages_white = nimages_white
|
||||
if sample_angle_out != None:
|
||||
@@ -367,16 +406,76 @@ class Measurement:
|
||||
|
||||
### TODO: camera reset
|
||||
print("Handing over to 'scans.acquire_whites")
|
||||
scans.acquire_white(exp_burst=self.nimages_white, sample_angle_out=self.sample_angle_out, sample_position_out= self.sample_position_out,
|
||||
scans.acquire_white(motor=self.motor_sample, exp_burst=self.nimages_white, sample_angle_out=self.sample_angle_out, sample_position_out= self.sample_position_out,
|
||||
exp_time=self.exposure_time, exp_period=self.exposure_period, image_width=self.roix,
|
||||
image_height=self.roiy, acq_mode=acq_mode, file_path=self.file_path, nr_writers=2, base_path=self.base_path,
|
||||
file_prefix=self.file_prefix)
|
||||
file_prefix=self.file_prefix, ddc_trigger=4, ddc_source0=1, **kwargs)
|
||||
|
||||
def acquire_projections(self, nimages=None, sample_position_in=None,
|
||||
start_angle=None, angular_range=None,
|
||||
exposure_time=None, exposure_period=None,
|
||||
roix=None, roiy=None, acq_mode=None, **kwargs):
|
||||
"""
|
||||
Acquire a set of whites images with shutters open and sample out of beam.
|
||||
|
||||
def acquire_refs(self,nimages_dark=None, nimages_white=None, sample_angle_out=None,
|
||||
Parameters
|
||||
----------
|
||||
nimages : int, optional
|
||||
Number of projection images to acquire (no default)
|
||||
sample_position_in : float, optional
|
||||
Sample stage X position for sample in the beam [um]
|
||||
start_angle : float, optional
|
||||
Starting angular position [deg]
|
||||
angular_range : float, optional
|
||||
Angular range [deg]
|
||||
exposure_time : float, optional
|
||||
Exposure time [ms]. If not specified, the currently configured value on the camera will be used
|
||||
exposure_period : float, optional
|
||||
Exposure period [ms]
|
||||
roix : int, optional
|
||||
ROI size in the x-direction [pixels]
|
||||
roiy : int, optional
|
||||
ROI size in the y-direction [pixels]
|
||||
acq_mode : str, optional
|
||||
Predefined acquisition mode (default=None)
|
||||
|
||||
Example:
|
||||
--------
|
||||
m.acquire_projections(nimages_projections=100, exposure_time=5)
|
||||
"""
|
||||
|
||||
if nimages != None:
|
||||
self.nimages = nimages
|
||||
if sample_position_in != None:
|
||||
self.sample_position_in = sample_position_in
|
||||
if start_angle != None:
|
||||
self.start_angle = start_angle
|
||||
if angular_range != None:
|
||||
self.angular_range = angular_range
|
||||
if exposure_time != None:
|
||||
self.exposure_time = exposure_time
|
||||
if exposure_period != None:
|
||||
self.exposure_period = exposure_period
|
||||
if roix != None:
|
||||
self.roix = roix
|
||||
if roiy != None:
|
||||
self.roiy = roiy
|
||||
|
||||
self.build_filename(acquisition_type='data')
|
||||
|
||||
### TODO: camera reset
|
||||
print("Handing over to 'scans.acquire_projections")
|
||||
scans.acquire_projections(motor="es1_roty", exp_burst=self.nimages, sample_position_in= self.sample_position_in,
|
||||
start_angle = self.start_angle, angular_range = self.angular_range,
|
||||
exp_time=self.exposure_time, exp_period=self.exposure_period, image_width=self.roix,
|
||||
image_height=self.roiy, acq_mode=acq_mode, file_path=self.file_path, nr_writers=2,
|
||||
base_path=self.base_path,file_prefix=self.file_prefix, ddc_trigger=4, ddc_source0=1, **kwargs)
|
||||
|
||||
|
||||
def acquire_refs(self, motor="eyez", nimages_dark=None, nimages_white=None, sample_angle_out=None,
|
||||
sample_position_in=None, sample_position_out=None,
|
||||
exposure_time=None, exposure_period=None,
|
||||
roix=None, roiy=None, acq_mode=None):
|
||||
roix=None, roiy=None, acq_mode=None, **kwargs):
|
||||
"""
|
||||
Acquire reference images (darks + whites) and return to beam position.
|
||||
|
||||
@@ -385,6 +484,8 @@ class Measurement:
|
||||
|
||||
Parameters
|
||||
----------
|
||||
motor : DeviceBase
|
||||
Motor to be moved to move the sample out of beam
|
||||
darks : int, optional
|
||||
Number of dark images to acquire (no default)
|
||||
nimages_whites : int, optional
|
||||
@@ -435,10 +536,30 @@ class Measurement:
|
||||
self.build_filename(acquisition_type='white')
|
||||
file_prefix_white = self.file_prefix
|
||||
|
||||
print(file_prefix_dark)
|
||||
print(file_prefix_white)
|
||||
|
||||
### TODO: camera reset
|
||||
print("Handing over to 'scans.acquire_refs")
|
||||
scans.acquire_refs(num_darks=self.nimages_dark, num_flats=self.nimages_white, sample_angle_out=self.sample_angle_out,
|
||||
scans.acquire_refs(motor=motor, num_darks=self.nimages_dark, num_flats=self.nimages_white, sample_angle_out=self.sample_angle_out,
|
||||
sample_position_in=self.sample_position_in, sample_position_out=self.sample_position_out,
|
||||
exp_time=self.exposure_time, exp_period=self.exposure_period, image_width=self.roix,
|
||||
image_height=self.roiy, acq_mode='default', file_path=self.file_path, nr_writers=2, base_path=self.base_path,
|
||||
file_prefix_dark=file_prefix_dark, file_prefix_white=file_prefix_white)
|
||||
file_prefix_dark=file_prefix_dark, file_prefix_white=file_prefix_white,
|
||||
ddc_trigger=4, ddc_source0=1, **kwargs)
|
||||
|
||||
|
||||
def start_preview(self, exposure_time=None, exposure_period=None,
|
||||
preview_strategy='', preview_paramters=200, **kwargs):
|
||||
"""
|
||||
Start the camera in preview mode, no data will be written.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
exposure_time : float, optional
|
||||
"""
|
||||
|
||||
if exposure_time is None:
|
||||
exposure_time = self.exposure_time
|
||||
if exposure_period is None:
|
||||
exposure_period = 50 # no need to go faster for a preview
|
||||
Reference in New Issue
Block a user