nbs-bl 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nbs_bl/__init__.py +15 -0
- nbs_bl/beamline.py +450 -0
- nbs_bl/configuration.py +838 -0
- nbs_bl/detectors.py +89 -0
- nbs_bl/devices/__init__.py +12 -0
- nbs_bl/devices/detectors.py +154 -0
- nbs_bl/devices/motors.py +242 -0
- nbs_bl/devices/sampleholders.py +360 -0
- nbs_bl/devices/shutters.py +120 -0
- nbs_bl/devices/slits.py +51 -0
- nbs_bl/gGrEqns.py +171 -0
- nbs_bl/geometry/__init__.py +0 -0
- nbs_bl/geometry/affine.py +197 -0
- nbs_bl/geometry/bars.py +189 -0
- nbs_bl/geometry/frames.py +534 -0
- nbs_bl/geometry/linalg.py +138 -0
- nbs_bl/geometry/polygons.py +56 -0
- nbs_bl/help.py +126 -0
- nbs_bl/hw.py +270 -0
- nbs_bl/load.py +113 -0
- nbs_bl/motors.py +19 -0
- nbs_bl/planStatus.py +5 -0
- nbs_bl/plans/__init__.py +8 -0
- nbs_bl/plans/batches.py +174 -0
- nbs_bl/plans/conditions.py +77 -0
- nbs_bl/plans/flyscan_base.py +180 -0
- nbs_bl/plans/groups.py +55 -0
- nbs_bl/plans/maximizers.py +423 -0
- nbs_bl/plans/metaplans.py +179 -0
- nbs_bl/plans/plan_stubs.py +246 -0
- nbs_bl/plans/preprocessors.py +160 -0
- nbs_bl/plans/scan_base.py +58 -0
- nbs_bl/plans/scan_decorators.py +524 -0
- nbs_bl/plans/scans.py +145 -0
- nbs_bl/plans/suspenders.py +87 -0
- nbs_bl/plans/time_estimation.py +168 -0
- nbs_bl/plans/xas.py +123 -0
- nbs_bl/printing.py +221 -0
- nbs_bl/qt/models/beamline.py +11 -0
- nbs_bl/qt/models/energy.py +53 -0
- nbs_bl/qt/widgets/energy.py +225 -0
- nbs_bl/queueserver.py +249 -0
- nbs_bl/redisDevice.py +96 -0
- nbs_bl/run_engine.py +63 -0
- nbs_bl/samples.py +130 -0
- nbs_bl/settings.py +68 -0
- nbs_bl/shutters.py +39 -0
- nbs_bl/sim/__init__.py +2 -0
- nbs_bl/sim/config/polphase.nc +0 -0
- nbs_bl/sim/energy.py +403 -0
- nbs_bl/sim/manipulator.py +14 -0
- nbs_bl/sim/utils.py +36 -0
- nbs_bl/startup.py +27 -0
- nbs_bl/status.py +114 -0
- nbs_bl/tests/__init__.py +0 -0
- nbs_bl/tests/modify_regions.py +160 -0
- nbs_bl/tests/test_frames.py +99 -0
- nbs_bl/tests/test_panels.py +69 -0
- nbs_bl/utils.py +235 -0
- nbs_bl-0.2.0.dist-info/METADATA +71 -0
- nbs_bl-0.2.0.dist-info/RECORD +64 -0
- nbs_bl-0.2.0.dist-info/WHEEL +4 -0
- nbs_bl-0.2.0.dist-info/entry_points.txt +2 -0
- nbs_bl-0.2.0.dist-info/licenses/LICENSE +13 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
from qtpy.QtWidgets import (
|
|
2
|
+
QGroupBox,
|
|
3
|
+
QVBoxLayout,
|
|
4
|
+
QHBoxLayout,
|
|
5
|
+
QComboBox,
|
|
6
|
+
QPushButton,
|
|
7
|
+
QMessageBox,
|
|
8
|
+
QDialog,
|
|
9
|
+
)
|
|
10
|
+
from nbs_gui.views.views import AutoControl, AutoMonitor
|
|
11
|
+
from nbs_gui.views.motor import MotorMonitor
|
|
12
|
+
from bluesky_queueserver_api import BPlan
|
|
13
|
+
from nbs_gui.settings import get_top_level_model
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class EnergyMonitor(QGroupBox):
|
|
17
|
+
"""Display widget for monitoring energy-related components.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
energy : object
|
|
22
|
+
Energy model containing energy, gap, and phase attributes
|
|
23
|
+
parent_model : object
|
|
24
|
+
Parent model containing beamline configuration
|
|
25
|
+
orientation : str, optional
|
|
26
|
+
Layout orientation
|
|
27
|
+
*args, **kwargs
|
|
28
|
+
Additional arguments passed to QGroupBox
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, energy, parent_model, *args, orientation=None, **kwargs):
|
|
32
|
+
print("Initializing EnergyMonitor")
|
|
33
|
+
super().__init__("Energy Monitor", *args, **kwargs)
|
|
34
|
+
self.model = energy
|
|
35
|
+
self.top_level_model = get_top_level_model()
|
|
36
|
+
self.beamline_model = self.top_level_model.beamline
|
|
37
|
+
vbox1 = QVBoxLayout()
|
|
38
|
+
|
|
39
|
+
print("Adding energy monitor")
|
|
40
|
+
vbox1.addWidget(AutoMonitor(self.model.energy, parent_model))
|
|
41
|
+
|
|
42
|
+
has_slits = (
|
|
43
|
+
hasattr(self.beamline_model, "slits")
|
|
44
|
+
and self.beamline_model.slits is not None
|
|
45
|
+
)
|
|
46
|
+
if has_slits:
|
|
47
|
+
print("Adding slits monitor")
|
|
48
|
+
vbox1.addWidget(AutoMonitor(self.beamline_model.slits, parent_model))
|
|
49
|
+
|
|
50
|
+
print("Adding CFF monitor")
|
|
51
|
+
vbox1.addWidget(AutoMonitor(self.model.cff, parent_model))
|
|
52
|
+
|
|
53
|
+
print("Adding grating motor monitor")
|
|
54
|
+
if hasattr(self.model, "grating_motor"):
|
|
55
|
+
vbox1.addWidget(MotorMonitor(self.model.grating_motor, parent_model))
|
|
56
|
+
|
|
57
|
+
self.setLayout(vbox1)
|
|
58
|
+
print("EnergyMonitor initialization complete")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class EnergyControl(QGroupBox):
|
|
62
|
+
"""Widget for controlling energy-related components.
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
energy : object
|
|
67
|
+
Energy model containing energy, gap, and phase attributes
|
|
68
|
+
parent_model : object
|
|
69
|
+
Parent model containing beamline configuration
|
|
70
|
+
orientation : str, optional
|
|
71
|
+
Layout orientation
|
|
72
|
+
*args, **kwargs
|
|
73
|
+
Additional arguments passed to QGroupBox
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(self, energy, parent_model, *args, orientation=None, **kwargs):
|
|
77
|
+
print("Initializing EnergyControl")
|
|
78
|
+
super().__init__("Energy Control", *args, **kwargs)
|
|
79
|
+
|
|
80
|
+
self.model = energy
|
|
81
|
+
self.parent_model = parent_model
|
|
82
|
+
top_model = get_top_level_model()
|
|
83
|
+
self.REClientModel = top_model.run_engine
|
|
84
|
+
|
|
85
|
+
print("Creating Energy Control layout")
|
|
86
|
+
hbox = QHBoxLayout()
|
|
87
|
+
ebox = QVBoxLayout()
|
|
88
|
+
hbox2 = QHBoxLayout()
|
|
89
|
+
|
|
90
|
+
print("Adding energy control")
|
|
91
|
+
if hasattr(energy, "energy"):
|
|
92
|
+
hbox.addWidget(AutoControl(energy.energy, parent_model))
|
|
93
|
+
|
|
94
|
+
has_slits = (
|
|
95
|
+
hasattr(top_model.beamline, "slits")
|
|
96
|
+
and top_model.beamline.slits is not None
|
|
97
|
+
)
|
|
98
|
+
if has_slits:
|
|
99
|
+
print("Adding exit slit control")
|
|
100
|
+
ebox.addWidget(AutoControl(top_model.beamline.slits, parent_model))
|
|
101
|
+
|
|
102
|
+
print("Adding CFF and grating monitors")
|
|
103
|
+
if hasattr(energy, "cff"):
|
|
104
|
+
hbox2.addWidget(AutoMonitor(energy.cff, parent_model))
|
|
105
|
+
if hasattr(energy, "grating_motor"):
|
|
106
|
+
hbox2.addWidget(AutoMonitor(energy.grating_motor, parent_model))
|
|
107
|
+
|
|
108
|
+
self.advancedControlButton = QPushButton("Advanced Controls")
|
|
109
|
+
self.advancedControlButton.clicked.connect(self.showAdvancedControls)
|
|
110
|
+
hbox2.addWidget(self.advancedControlButton)
|
|
111
|
+
ebox.addLayout(hbox2)
|
|
112
|
+
hbox.addLayout(ebox)
|
|
113
|
+
self.setLayout(hbox)
|
|
114
|
+
print("EnergyControl initialization complete")
|
|
115
|
+
|
|
116
|
+
def showAdvancedControls(self):
|
|
117
|
+
"""Show the Advanced Energy Control dialog."""
|
|
118
|
+
print("Opening advanced controls dialog")
|
|
119
|
+
self.advancedDialog = QDialog(self)
|
|
120
|
+
self.advancedDialog.setWindowTitle("Advanced Energy Control")
|
|
121
|
+
layout = QVBoxLayout()
|
|
122
|
+
advancedControl = AdvancedEnergyControl(
|
|
123
|
+
self.model, self.parent_model, self.advancedDialog
|
|
124
|
+
)
|
|
125
|
+
layout.addWidget(advancedControl)
|
|
126
|
+
self.advancedDialog.setLayout(layout)
|
|
127
|
+
self.advancedDialog.exec_()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class AdvancedEnergyControl(QGroupBox):
|
|
131
|
+
"""Advanced controls for energy-related components.
|
|
132
|
+
|
|
133
|
+
Parameters
|
|
134
|
+
----------
|
|
135
|
+
model : object
|
|
136
|
+
Energy model containing energy, gap, and phase attributes
|
|
137
|
+
parent_model : object
|
|
138
|
+
Parent model containing beamline configuration
|
|
139
|
+
parent : QWidget, optional
|
|
140
|
+
Parent widget
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
def __init__(self, model, parent_model, parent=None):
|
|
144
|
+
print("Initializing AdvancedEnergyControl")
|
|
145
|
+
super().__init__("Advanced Energy Control", parent)
|
|
146
|
+
self.model = model
|
|
147
|
+
self.parent_model = parent_model
|
|
148
|
+
top_model = get_top_level_model()
|
|
149
|
+
self.REClientModel = top_model.run_engine
|
|
150
|
+
|
|
151
|
+
layout = QVBoxLayout()
|
|
152
|
+
|
|
153
|
+
print("Adding CFF control")
|
|
154
|
+
if hasattr(model, "cff"):
|
|
155
|
+
layout.addWidget(AutoControl(model.cff, parent_model))
|
|
156
|
+
|
|
157
|
+
print("Creating grating controls")
|
|
158
|
+
hbox = QHBoxLayout()
|
|
159
|
+
if hasattr(model, "grating_motor"):
|
|
160
|
+
hbox.addWidget(MotorMonitor(model.grating_motor, parent_model))
|
|
161
|
+
|
|
162
|
+
print("Setting up grating selection")
|
|
163
|
+
cb = QComboBox()
|
|
164
|
+
self.cb = cb
|
|
165
|
+
if hasattr(model.grating_motor.obj.setpoint, "enum_strs"):
|
|
166
|
+
for n, s in enumerate(model.grating_motor.obj.setpoint.enum_strs):
|
|
167
|
+
if s != "":
|
|
168
|
+
cb.addItem(s, n)
|
|
169
|
+
|
|
170
|
+
self.button = QPushButton("Change Grating")
|
|
171
|
+
self.button.clicked.connect(self.change_grating)
|
|
172
|
+
hbox.addWidget(cb)
|
|
173
|
+
hbox.addWidget(self.button)
|
|
174
|
+
|
|
175
|
+
self.tuneButton = QPushButton("Tune grating offsets")
|
|
176
|
+
self.tuneButton.clicked.connect(self.tune_grating)
|
|
177
|
+
hbox.addWidget(self.tuneButton)
|
|
178
|
+
|
|
179
|
+
layout.addLayout(hbox)
|
|
180
|
+
self.setLayout(layout)
|
|
181
|
+
print("AdvancedEnergyControl initialization complete")
|
|
182
|
+
|
|
183
|
+
def tune_grating(self):
|
|
184
|
+
"""Execute grating tuning plan."""
|
|
185
|
+
print("Initiating grating tune")
|
|
186
|
+
msg = "Ensure beamline is opened to multimesh reference ladder for tune"
|
|
187
|
+
if self.confirm_dialog(msg):
|
|
188
|
+
plan = BPlan("tune_grating")
|
|
189
|
+
self.REClientModel._client.item_execute(plan)
|
|
190
|
+
|
|
191
|
+
def change_grating(self):
|
|
192
|
+
"""Execute grating change plan."""
|
|
193
|
+
print("Initiating grating change")
|
|
194
|
+
enum = self.cb.currentData()
|
|
195
|
+
print(f"Selected grating enum: {enum}")
|
|
196
|
+
msg = (
|
|
197
|
+
"Are you sure you want to change gratings?\n"
|
|
198
|
+
"Ensure beam is open to the multimesh.\n"
|
|
199
|
+
"Grating change and tune will run as queue item"
|
|
200
|
+
)
|
|
201
|
+
if self.confirm_dialog(msg):
|
|
202
|
+
plan = BPlan("change_grating", enum)
|
|
203
|
+
self.REClientModel._client.item_execute(plan)
|
|
204
|
+
|
|
205
|
+
def confirm_dialog(self, confirm_message):
|
|
206
|
+
"""Show a confirmation dialog.
|
|
207
|
+
|
|
208
|
+
Parameters
|
|
209
|
+
----------
|
|
210
|
+
confirm_message : str
|
|
211
|
+
Message to display in the dialog
|
|
212
|
+
|
|
213
|
+
Returns
|
|
214
|
+
-------
|
|
215
|
+
bool
|
|
216
|
+
True if user confirmed, False otherwise
|
|
217
|
+
"""
|
|
218
|
+
msg = QMessageBox()
|
|
219
|
+
msg.setIcon(QMessageBox.Question)
|
|
220
|
+
msg.setText(confirm_message)
|
|
221
|
+
msg.setStyleSheet("button-layout: 1")
|
|
222
|
+
msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
|
|
223
|
+
msg.setDefaultButton(QMessageBox.No)
|
|
224
|
+
ret = msg.exec_()
|
|
225
|
+
return ret == QMessageBox.Yes
|
nbs_bl/queueserver.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
from .status import StatusDict, StatusContainerBase, RedisStatusDict, StatusList
|
|
2
|
+
from collections import abc
|
|
3
|
+
from ophyd import OphydObject
|
|
4
|
+
import redis
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class GlobalStatusManager:
|
|
8
|
+
"""
|
|
9
|
+
Manager class for handling global status dictionaries and lists with Redis connections
|
|
10
|
+
|
|
11
|
+
Parameters
|
|
12
|
+
----------
|
|
13
|
+
redis_host : str, optional
|
|
14
|
+
Redis server hostname, by default 'localhost'
|
|
15
|
+
redis_port : int, optional
|
|
16
|
+
Redis server port, by default 6379
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, redis_host="localhost", redis_port=6379):
|
|
20
|
+
self._status_dict = StatusDict()
|
|
21
|
+
self._redis_client = None
|
|
22
|
+
self._redis_host = redis_host
|
|
23
|
+
self._redis_port = redis_port
|
|
24
|
+
self._global_prefix = None
|
|
25
|
+
|
|
26
|
+
def init_redis(self, host=None, port=None, db=0, global_prefix="status:"):
|
|
27
|
+
"""
|
|
28
|
+
Initialize Redis connection with optional new host/port and global prefix
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
host : str, optional
|
|
33
|
+
Redis server hostname
|
|
34
|
+
port : int, optional
|
|
35
|
+
Redis server port
|
|
36
|
+
global_prefix : str, optional
|
|
37
|
+
Global prefix for all Redis keys, by default "status:"
|
|
38
|
+
"""
|
|
39
|
+
if host is not None:
|
|
40
|
+
self._redis_host = host
|
|
41
|
+
if port is not None:
|
|
42
|
+
self._redis_port = port
|
|
43
|
+
|
|
44
|
+
self._global_prefix = global_prefix
|
|
45
|
+
print(f"Initializing redis Client with {host}, {port}, {db}")
|
|
46
|
+
self._redis_client = redis.Redis(
|
|
47
|
+
host=self._redis_host, port=self._redis_port, db=db
|
|
48
|
+
)
|
|
49
|
+
return self._redis_client
|
|
50
|
+
|
|
51
|
+
def add_status(self, key, container: StatusContainerBase):
|
|
52
|
+
"""Add a status container to the manager"""
|
|
53
|
+
self._status_dict[key] = container
|
|
54
|
+
|
|
55
|
+
def remove_status(self, key):
|
|
56
|
+
"""Remove a status container from the manager"""
|
|
57
|
+
del self._status_dict[key]
|
|
58
|
+
|
|
59
|
+
def get_status(self):
|
|
60
|
+
"""Get dictionary of all status UIDs"""
|
|
61
|
+
return {k: str(v.get_uid()) for k, v in self._status_dict.items()}
|
|
62
|
+
|
|
63
|
+
def request_status_dict(self, key, use_redis=False, prefix=None):
|
|
64
|
+
"""
|
|
65
|
+
Create and return a new status dictionary
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
key : str
|
|
70
|
+
Key for the status dictionary
|
|
71
|
+
use_redis : bool, optional
|
|
72
|
+
If True, returns RedisStatusDict, otherwise StatusDict
|
|
73
|
+
prefix : str, optional
|
|
74
|
+
Additional prefix for Redis keys if using RedisStatusDict.
|
|
75
|
+
If None, uses the key as prefix.
|
|
76
|
+
|
|
77
|
+
Returns
|
|
78
|
+
-------
|
|
79
|
+
StatusDict or RedisStatusDict
|
|
80
|
+
The requested status dictionary
|
|
81
|
+
|
|
82
|
+
Raises
|
|
83
|
+
------
|
|
84
|
+
RuntimeError
|
|
85
|
+
If Redis is requested but not initialized
|
|
86
|
+
"""
|
|
87
|
+
if use_redis:
|
|
88
|
+
if self._redis_client is None:
|
|
89
|
+
import warnings
|
|
90
|
+
|
|
91
|
+
warnings.warn(f"Redis not initialized. Using plain StatusDict for {key} instead.")
|
|
92
|
+
status_dict = StatusDict()
|
|
93
|
+
else:
|
|
94
|
+
# Construct the full prefix
|
|
95
|
+
if prefix is None:
|
|
96
|
+
prefix = key
|
|
97
|
+
full_prefix = f"{self._global_prefix}{prefix}"
|
|
98
|
+
status_dict = RedisStatusDict(self._redis_client, prefix=full_prefix)
|
|
99
|
+
else:
|
|
100
|
+
status_dict = StatusDict()
|
|
101
|
+
|
|
102
|
+
self.add_status(key, status_dict)
|
|
103
|
+
return status_dict
|
|
104
|
+
|
|
105
|
+
def request_status_list(self, key, use_redis=False):
|
|
106
|
+
"""
|
|
107
|
+
Request a new status list, optionally backed by Redis
|
|
108
|
+
|
|
109
|
+
Parameters
|
|
110
|
+
----------
|
|
111
|
+
key : str
|
|
112
|
+
Key for the status list
|
|
113
|
+
use_redis : bool, optional
|
|
114
|
+
If True, creates list in RedisStatusDict, otherwise returns plain StatusList
|
|
115
|
+
|
|
116
|
+
Returns
|
|
117
|
+
-------
|
|
118
|
+
StatusList
|
|
119
|
+
The requested status list
|
|
120
|
+
|
|
121
|
+
Notes
|
|
122
|
+
-----
|
|
123
|
+
When using Redis, the list is stored in a RedisStatusDict using the global prefix,
|
|
124
|
+
with the provided key used as the dictionary key for the list.
|
|
125
|
+
"""
|
|
126
|
+
if use_redis:
|
|
127
|
+
if self._redis_client is None:
|
|
128
|
+
import warnings
|
|
129
|
+
|
|
130
|
+
warnings.warn(f"Redis not initialized. Using plain StatusList for {key} instead.")
|
|
131
|
+
status_list = StatusList()
|
|
132
|
+
else:
|
|
133
|
+
# Create or get the Redis dict with only global prefix
|
|
134
|
+
redis_dict = self._get_or_create_redis_dict()
|
|
135
|
+
# Create new status list
|
|
136
|
+
status_list = StatusList()
|
|
137
|
+
# Store in Redis dict under the given key
|
|
138
|
+
redis_dict[key] = status_list
|
|
139
|
+
else:
|
|
140
|
+
status_list = StatusList()
|
|
141
|
+
|
|
142
|
+
# Add to status manager
|
|
143
|
+
self.add_status(key, status_list)
|
|
144
|
+
return status_list
|
|
145
|
+
|
|
146
|
+
def _get_or_create_redis_dict(self):
|
|
147
|
+
"""
|
|
148
|
+
Get or create the global RedisStatusDict for storing lists
|
|
149
|
+
|
|
150
|
+
Returns
|
|
151
|
+
-------
|
|
152
|
+
RedisStatusDict
|
|
153
|
+
The global Redis dictionary for storing lists
|
|
154
|
+
"""
|
|
155
|
+
# Use a special key for the global Redis dict
|
|
156
|
+
global_dict_key = "_global_redis_dict"
|
|
157
|
+
|
|
158
|
+
if global_dict_key not in self._status_dict:
|
|
159
|
+
# Create new Redis dict with only global prefix
|
|
160
|
+
redis_dict = RedisStatusDict(self._redis_client, prefix=self._global_prefix)
|
|
161
|
+
self.add_status(global_dict_key, redis_dict)
|
|
162
|
+
|
|
163
|
+
return self._status_dict[global_dict_key]
|
|
164
|
+
|
|
165
|
+
def keys(self):
|
|
166
|
+
return self._status_dict.keys()
|
|
167
|
+
|
|
168
|
+
def __getitem__(self, key):
|
|
169
|
+
return self._status_dict[key]
|
|
170
|
+
|
|
171
|
+
def __setitem__(self, key, value):
|
|
172
|
+
self.add_status(key, value)
|
|
173
|
+
|
|
174
|
+
def __delitem__(self, key):
|
|
175
|
+
self.remove_status(key)
|
|
176
|
+
|
|
177
|
+
def __contains__(self, key):
|
|
178
|
+
return key in self._status_dict
|
|
179
|
+
|
|
180
|
+
def request_update(self, key):
|
|
181
|
+
"""Request an update for a specific status key"""
|
|
182
|
+
if key in self._status_dict:
|
|
183
|
+
sbuffer = self._status_dict[key]
|
|
184
|
+
if isinstance(sbuffer, abc.Sequence):
|
|
185
|
+
return represent_sequence(sbuffer)
|
|
186
|
+
if isinstance(sbuffer, abc.Mapping):
|
|
187
|
+
return represent_mapping(sbuffer)
|
|
188
|
+
if isinstance(sbuffer, abc.Set):
|
|
189
|
+
return represent_set(sbuffer)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# Create global instance
|
|
193
|
+
GLOBAL_USER_STATUS = GlobalStatusManager()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def request_update(key):
|
|
197
|
+
return GLOBAL_USER_STATUS.request_update(key)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def get_status():
|
|
201
|
+
return GLOBAL_USER_STATUS.get_status()
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# Keep existing helper functions
|
|
205
|
+
def represent_item(item):
|
|
206
|
+
if isinstance(item, OphydObject):
|
|
207
|
+
return item.name
|
|
208
|
+
elif isinstance(item, abc.Sequence):
|
|
209
|
+
return represent_sequence(item)
|
|
210
|
+
elif isinstance(item, abc.Mapping):
|
|
211
|
+
return represent_mapping(item)
|
|
212
|
+
elif isinstance(item, abc.Set):
|
|
213
|
+
return represent_set(item)
|
|
214
|
+
else:
|
|
215
|
+
return item
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def represent_mapping(m):
|
|
219
|
+
rep = {}
|
|
220
|
+
for k, v in m.items():
|
|
221
|
+
rep[k] = represent_item(v)
|
|
222
|
+
return rep
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def represent_sequence(s):
|
|
226
|
+
|
|
227
|
+
if isinstance(s, str):
|
|
228
|
+
return s
|
|
229
|
+
else:
|
|
230
|
+
rep = []
|
|
231
|
+
for v in s:
|
|
232
|
+
rep.append(represent_item(v))
|
|
233
|
+
return rep
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def represent_set(s):
|
|
237
|
+
return represent_sequence(s)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def print_mapping(m):
|
|
241
|
+
return str(represent_mapping(m))
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def print_sequence(s):
|
|
245
|
+
return str(represent_sequence(s))
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def print_set(s):
|
|
249
|
+
return str(represent_set(s))
|
nbs_bl/redisDevice.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from ophyd.signal import Signal
|
|
3
|
+
from ophyd.device import Device, Component as Cpt
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class _RedisSignal(Signal):
|
|
8
|
+
"""
|
|
9
|
+
Minimal signal-like wrapper around a Redis-backed USER_STATUS entry.
|
|
10
|
+
|
|
11
|
+
Parameters
|
|
12
|
+
----------
|
|
13
|
+
dict_name : str
|
|
14
|
+
Name of the USER_STATUS dictionary to access.
|
|
15
|
+
key : str
|
|
16
|
+
Key within the dictionary.
|
|
17
|
+
default : any
|
|
18
|
+
Default value to use if key is missing.
|
|
19
|
+
"""
|
|
20
|
+
default_status_provider = None
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def set_default_status_provider(cls, provider):
|
|
24
|
+
cls.default_status_provider = provider
|
|
25
|
+
|
|
26
|
+
def __init__(self, *, dict_name=None, default=None, **kwargs):
|
|
27
|
+
super().__init__(**kwargs)
|
|
28
|
+
parent_prefix = getattr(self.parent, "prefix", None)
|
|
29
|
+
resolved_dict = dict_name or parent_prefix
|
|
30
|
+
if not resolved_dict:
|
|
31
|
+
raise ValueError("RedisSignal requires a dict_name or parent prefix")
|
|
32
|
+
self._dict_name = resolved_dict
|
|
33
|
+
self._key = self.name
|
|
34
|
+
self._default = default
|
|
35
|
+
provider = self._status_provider()
|
|
36
|
+
if self._dict_name not in provider:
|
|
37
|
+
provider.request_status_dict(self._dict_name, use_redis=True)
|
|
38
|
+
self._ensure_key_exists()
|
|
39
|
+
|
|
40
|
+
def _ensure_key_exists(self):
|
|
41
|
+
dct = self._status_provider()[self._dict_name]
|
|
42
|
+
if self._key not in dct:
|
|
43
|
+
dct[self._key] = self._default
|
|
44
|
+
|
|
45
|
+
def get(self, **kwargs):
|
|
46
|
+
try:
|
|
47
|
+
dct = self._status_provider()[self._dict_name]
|
|
48
|
+
return dct.get(self._key, self._default)
|
|
49
|
+
except Exception as e:
|
|
50
|
+
print(f"RedisSignal get error for {self._dict_name}:{self._key}: {e}")
|
|
51
|
+
return self._default
|
|
52
|
+
|
|
53
|
+
def put(self, value, **kwargs):
|
|
54
|
+
try:
|
|
55
|
+
dct = self._status_provider()[self._dict_name]
|
|
56
|
+
dct[self._key] = value
|
|
57
|
+
# Notify subscribers
|
|
58
|
+
self._run_subs(sub_type=self.SUB_VALUE, value=value, **kwargs)
|
|
59
|
+
return value
|
|
60
|
+
except Exception as e:
|
|
61
|
+
print(f"RedisSignal put error for {self._dict_name}:{self._key}: {e}")
|
|
62
|
+
return self._default
|
|
63
|
+
|
|
64
|
+
def _status_provider(self):
|
|
65
|
+
provider = getattr(self, "_provider", None) or _RedisSignal.default_status_provider
|
|
66
|
+
if provider is None:
|
|
67
|
+
raise RuntimeError("No status provider configured for RedisDevice")
|
|
68
|
+
return provider
|
|
69
|
+
|
|
70
|
+
def RedisDevice(prefix, name="", keys=None, status_provider=None, **kwargs):
|
|
71
|
+
"""
|
|
72
|
+
Factory function to create a Redis-backed Device with one Component per key.
|
|
73
|
+
"""
|
|
74
|
+
keys = keys or {}
|
|
75
|
+
|
|
76
|
+
attrs = {
|
|
77
|
+
"_keys_config": keys,
|
|
78
|
+
"_dict_name": None,
|
|
79
|
+
"_status_provider": status_provider or _RedisSignal.default_status_provider,
|
|
80
|
+
}
|
|
81
|
+
for key, default in keys.items():
|
|
82
|
+
attrs[key] = Cpt(_RedisSignal, name=key, default=default)
|
|
83
|
+
|
|
84
|
+
cls = type(f"RedisDevice_{name}", (Device,), attrs)
|
|
85
|
+
obj = cls(prefix=prefix, name=name, **kwargs)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
return obj
|
|
89
|
+
|
|
90
|
+
class RedisModeDevice(Device):
|
|
91
|
+
|
|
92
|
+
mode = Cpt(_RedisSignal, name="mode", default="default")
|
|
93
|
+
|
|
94
|
+
def __init__(self, prefix, name="", status_provider=None, **kwargs):
|
|
95
|
+
super().__init__(prefix=prefix, name=name, **kwargs)
|
|
96
|
+
self._status_provider = status_provider
|
nbs_bl/run_engine.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from bluesky import RunEngine
|
|
3
|
+
from .beamline import GLOBAL_BEAMLINE
|
|
4
|
+
from bluesky_queueserver import is_re_worker_active
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async def generic_cmd(msg):
|
|
8
|
+
"""
|
|
9
|
+
Generic command handler for all commands
|
|
10
|
+
"""
|
|
11
|
+
command, obj, args, kwargs, _ = msg
|
|
12
|
+
ret = getattr(obj, command)(*args, **kwargs)
|
|
13
|
+
return ret
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def call_obj(msg):
|
|
17
|
+
"""
|
|
18
|
+
Call an object's method
|
|
19
|
+
"""
|
|
20
|
+
obj = msg.obj
|
|
21
|
+
kwargs = msg.kwargs
|
|
22
|
+
args = msg.args
|
|
23
|
+
command = kwargs.pop("method")
|
|
24
|
+
ret = getattr(obj, command)(*args, **kwargs)
|
|
25
|
+
return ret
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def _update_plan_status(msg):
|
|
29
|
+
"""
|
|
30
|
+
Update the plan status
|
|
31
|
+
"""
|
|
32
|
+
command, obj, args, kwargs, _ = msg
|
|
33
|
+
GLOBAL_BEAMLINE.plan_status["status"] = args[0]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def _clear_plan_status(msg):
|
|
37
|
+
"""
|
|
38
|
+
Clear the plan status
|
|
39
|
+
"""
|
|
40
|
+
command, obj, args, kwargs, _ = msg
|
|
41
|
+
GLOBAL_BEAMLINE.plan_status["status"] = "idle"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def load_RE_commands(engine):
|
|
45
|
+
engine.register_command("call_obj", call_obj)
|
|
46
|
+
engine.register_command("update_plan_status", _update_plan_status)
|
|
47
|
+
engine.register_command("clear_plan_status", _clear_plan_status)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def setup_run_engine(RE):
|
|
51
|
+
load_RE_commands(RE)
|
|
52
|
+
RE.preprocessors.append(GLOBAL_BEAMLINE.supplemental_data)
|
|
53
|
+
return RE
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def create_run_engine(setup=True):
|
|
57
|
+
if is_re_worker_active():
|
|
58
|
+
RE = RunEngine(call_returns_result=False)
|
|
59
|
+
else:
|
|
60
|
+
RE = RunEngine(call_returns_result=True)
|
|
61
|
+
if setup:
|
|
62
|
+
setup_run_engine(RE)
|
|
63
|
+
return RE
|