mx-bluesky 1.5.2__py3-none-any.whl → 1.5.3__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.
- mx_bluesky/_version.py +16 -3
- mx_bluesky/beamlines/i04/__init__.py +7 -3
- mx_bluesky/beamlines/i04/experiment_plans/i04_grid_detect_then_xray_centre_plan.py +3 -0
- mx_bluesky/beamlines/i24/serial/blueapi_config.yaml +2 -2
- mx_bluesky/common/experiment_plans/oav_grid_detection_plan.py +12 -2
- mx_bluesky/common/external_interaction/alerting/__init__.py +13 -0
- mx_bluesky/common/external_interaction/alerting/_service.py +82 -0
- mx_bluesky/common/external_interaction/alerting/log_based_service.py +57 -0
- mx_bluesky/common/external_interaction/callbacks/sample_handling/sample_handling_callback.py +28 -4
- mx_bluesky/common/external_interaction/config_server.py +151 -54
- mx_bluesky/common/parameters/constants.py +26 -8
- mx_bluesky/common/parameters/gridscan.py +1 -1
- mx_bluesky/hyperion/__main__.py +50 -178
- mx_bluesky/hyperion/baton_handler.py +125 -69
- mx_bluesky/hyperion/device_setup_plans/dcm_pitch_roll_mirror_adjuster.py +29 -24
- mx_bluesky/hyperion/experiment_plans/hyperion_flyscan_xray_centre_plan.py +4 -1
- mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py +12 -4
- mx_bluesky/hyperion/experiment_plans/pin_tip_centring_plan.py +1 -1
- mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +2 -3
- mx_bluesky/hyperion/external_interaction/agamemnon.py +128 -73
- mx_bluesky/hyperion/external_interaction/alerting/__init__.py +0 -0
- mx_bluesky/hyperion/external_interaction/alerting/constants.py +12 -0
- mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +5 -0
- mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py +2 -2
- mx_bluesky/hyperion/external_interaction/config_server.py +12 -31
- mx_bluesky/hyperion/parameters/cli.py +15 -3
- mx_bluesky/hyperion/parameters/components.py +7 -5
- mx_bluesky/hyperion/parameters/constants.py +20 -4
- mx_bluesky/hyperion/parameters/gridscan.py +22 -14
- mx_bluesky/hyperion/parameters/load_centre_collect.py +1 -14
- mx_bluesky/hyperion/parameters/robot_load.py +1 -4
- mx_bluesky/hyperion/parameters/rotation.py +1 -2
- mx_bluesky/hyperion/plan_runner.py +78 -0
- mx_bluesky/hyperion/runner.py +189 -0
- {mx_bluesky-1.5.2.dist-info → mx_bluesky-1.5.3.dist-info}/METADATA +4 -3
- {mx_bluesky-1.5.2.dist-info → mx_bluesky-1.5.3.dist-info}/RECORD +40 -33
- {mx_bluesky-1.5.2.dist-info → mx_bluesky-1.5.3.dist-info}/entry_points.txt +0 -2
- {mx_bluesky-1.5.2.dist-info → mx_bluesky-1.5.3.dist-info}/WHEEL +0 -0
- {mx_bluesky-1.5.2.dist-info → mx_bluesky-1.5.3.dist-info}/licenses/LICENSE +0 -0
- {mx_bluesky-1.5.2.dist-info → mx_bluesky-1.5.3.dist-info}/top_level.txt +0 -0
mx_bluesky/hyperion/__main__.py
CHANGED
|
@@ -1,33 +1,24 @@
|
|
|
1
|
-
import atexit
|
|
2
1
|
import json
|
|
3
2
|
import threading
|
|
4
|
-
from collections.abc import Callable
|
|
5
3
|
from dataclasses import asdict
|
|
6
|
-
from queue import Queue
|
|
7
4
|
from sys import argv
|
|
8
5
|
from traceback import format_exception
|
|
9
|
-
from typing import Any
|
|
10
6
|
|
|
11
7
|
from blueapi.core import BlueskyContext
|
|
12
|
-
from bluesky.callbacks.zmq import Publisher
|
|
13
|
-
from bluesky.run_engine import RunEngine
|
|
14
|
-
from bluesky.utils import MsgGenerator
|
|
15
8
|
from flask import Flask, request
|
|
16
9
|
from flask_restful import Api, Resource
|
|
17
|
-
from pydantic.dataclasses import dataclass
|
|
18
10
|
|
|
19
|
-
from mx_bluesky.common.external_interaction
|
|
20
|
-
|
|
11
|
+
from mx_bluesky.common.external_interaction import alerting
|
|
12
|
+
from mx_bluesky.common.external_interaction.alerting.log_based_service import (
|
|
13
|
+
LoggingAlertService,
|
|
21
14
|
)
|
|
22
|
-
from mx_bluesky.common.parameters.components import MxBlueskyParameters
|
|
23
15
|
from mx_bluesky.common.parameters.constants import Actions, Status
|
|
24
|
-
from mx_bluesky.common.utils.exceptions import WarningException
|
|
25
16
|
from mx_bluesky.common.utils.log import (
|
|
26
17
|
LOGGER,
|
|
27
18
|
do_default_logging_setup,
|
|
28
19
|
flush_debug_handler,
|
|
29
20
|
)
|
|
30
|
-
from mx_bluesky.
|
|
21
|
+
from mx_bluesky.hyperion.baton_handler import run_forever
|
|
31
22
|
from mx_bluesky.hyperion.experiment_plans.experiment_registry import (
|
|
32
23
|
PLAN_REGISTRY,
|
|
33
24
|
PlanNotFound,
|
|
@@ -36,143 +27,22 @@ from mx_bluesky.hyperion.external_interaction.agamemnon import (
|
|
|
36
27
|
compare_params,
|
|
37
28
|
update_params_from_agamemnon,
|
|
38
29
|
)
|
|
39
|
-
from mx_bluesky.hyperion.parameters.cli import
|
|
30
|
+
from mx_bluesky.hyperion.parameters.cli import (
|
|
31
|
+
HyperionArgs,
|
|
32
|
+
HyperionMode,
|
|
33
|
+
parse_cli_args,
|
|
34
|
+
)
|
|
40
35
|
from mx_bluesky.hyperion.parameters.constants import CONST
|
|
41
36
|
from mx_bluesky.hyperion.parameters.load_centre_collect import LoadCentreCollect
|
|
37
|
+
from mx_bluesky.hyperion.plan_runner import PlanRunner
|
|
38
|
+
from mx_bluesky.hyperion.runner import (
|
|
39
|
+
GDARunner,
|
|
40
|
+
StatusAndMessage,
|
|
41
|
+
make_error_status_and_message,
|
|
42
|
+
)
|
|
42
43
|
from mx_bluesky.hyperion.utils.context import setup_context
|
|
43
44
|
|
|
44
45
|
|
|
45
|
-
@dataclass
|
|
46
|
-
class Command:
|
|
47
|
-
action: Actions
|
|
48
|
-
devices: Any | None = None
|
|
49
|
-
experiment: Callable[[Any, Any], MsgGenerator] | None = None
|
|
50
|
-
parameters: MxBlueskyParameters | None = None
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
@dataclass
|
|
54
|
-
class StatusAndMessage:
|
|
55
|
-
status: str
|
|
56
|
-
message: str = ""
|
|
57
|
-
|
|
58
|
-
def __init__(self, status: Status, message: str = "") -> None:
|
|
59
|
-
self.status = status.value
|
|
60
|
-
self.message = message
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
@dataclass
|
|
64
|
-
class ErrorStatusAndMessage(StatusAndMessage):
|
|
65
|
-
exception_type: str = ""
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def make_error_status_and_message(exception: Exception):
|
|
69
|
-
return ErrorStatusAndMessage(
|
|
70
|
-
status=Status.FAILED.value,
|
|
71
|
-
message=repr(exception),
|
|
72
|
-
exception_type=type(exception).__name__,
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
class BlueskyRunner:
|
|
77
|
-
def __init__(
|
|
78
|
-
self,
|
|
79
|
-
RE: RunEngine,
|
|
80
|
-
context: BlueskyContext,
|
|
81
|
-
) -> None:
|
|
82
|
-
self.command_queue: Queue[Command] = Queue()
|
|
83
|
-
self.current_status: StatusAndMessage = StatusAndMessage(Status.IDLE)
|
|
84
|
-
self.last_run_aborted: bool = False
|
|
85
|
-
self.logging_uid_tag_callback = LogUidTaggingCallback()
|
|
86
|
-
self.context: BlueskyContext
|
|
87
|
-
|
|
88
|
-
self.RE = RE
|
|
89
|
-
self.context = context
|
|
90
|
-
RE.subscribe(self.logging_uid_tag_callback)
|
|
91
|
-
|
|
92
|
-
LOGGER.info("Connecting to external callback ZMQ proxy...")
|
|
93
|
-
self.publisher = Publisher(f"localhost:{CONST.CALLBACK_0MQ_PROXY_PORTS[0]}")
|
|
94
|
-
RE.subscribe(self.publisher)
|
|
95
|
-
|
|
96
|
-
def start(
|
|
97
|
-
self,
|
|
98
|
-
experiment: Callable,
|
|
99
|
-
parameters: MxBlueskyParameters,
|
|
100
|
-
plan_name: str,
|
|
101
|
-
) -> StatusAndMessage:
|
|
102
|
-
LOGGER.info(f"Started with parameters: {parameters.model_dump_json(indent=2)}")
|
|
103
|
-
|
|
104
|
-
devices: Any = PLAN_REGISTRY[plan_name]["setup"](self.context)
|
|
105
|
-
|
|
106
|
-
if (
|
|
107
|
-
self.current_status.status == Status.BUSY.value
|
|
108
|
-
or self.current_status.status == Status.ABORTING.value
|
|
109
|
-
):
|
|
110
|
-
return StatusAndMessage(Status.FAILED, "Bluesky already running")
|
|
111
|
-
else:
|
|
112
|
-
self.current_status = StatusAndMessage(Status.BUSY)
|
|
113
|
-
self.command_queue.put(
|
|
114
|
-
Command(
|
|
115
|
-
action=Actions.START,
|
|
116
|
-
devices=devices,
|
|
117
|
-
experiment=experiment,
|
|
118
|
-
parameters=parameters,
|
|
119
|
-
)
|
|
120
|
-
)
|
|
121
|
-
return StatusAndMessage(Status.SUCCESS)
|
|
122
|
-
|
|
123
|
-
def stopping_thread(self):
|
|
124
|
-
try:
|
|
125
|
-
self.RE.abort()
|
|
126
|
-
self.current_status = StatusAndMessage(Status.IDLE)
|
|
127
|
-
except Exception as e:
|
|
128
|
-
self.current_status = make_error_status_and_message(e)
|
|
129
|
-
|
|
130
|
-
def stop(self) -> StatusAndMessage:
|
|
131
|
-
if self.current_status.status == Status.IDLE.value:
|
|
132
|
-
return StatusAndMessage(Status.FAILED, "Bluesky not running")
|
|
133
|
-
elif self.current_status.status == Status.ABORTING.value:
|
|
134
|
-
return StatusAndMessage(Status.FAILED, "Bluesky already stopping")
|
|
135
|
-
else:
|
|
136
|
-
self.current_status = StatusAndMessage(Status.ABORTING)
|
|
137
|
-
stopping_thread = threading.Thread(target=self.stopping_thread)
|
|
138
|
-
stopping_thread.start()
|
|
139
|
-
self.last_run_aborted = True
|
|
140
|
-
return StatusAndMessage(Status.ABORTING)
|
|
141
|
-
|
|
142
|
-
def shutdown(self):
|
|
143
|
-
"""Stops the run engine and the loop waiting for messages."""
|
|
144
|
-
print("Shutting down: Stopping the run engine gracefully")
|
|
145
|
-
self.stop()
|
|
146
|
-
self.command_queue.put(Command(action=Actions.SHUTDOWN))
|
|
147
|
-
|
|
148
|
-
def wait_on_queue(self):
|
|
149
|
-
while True:
|
|
150
|
-
command = self.command_queue.get()
|
|
151
|
-
if command.action == Actions.SHUTDOWN:
|
|
152
|
-
return
|
|
153
|
-
elif command.action == Actions.START:
|
|
154
|
-
if command.experiment is None:
|
|
155
|
-
raise ValueError("No experiment provided for START")
|
|
156
|
-
try:
|
|
157
|
-
with TRACER.start_span("do_run"):
|
|
158
|
-
self.RE(command.experiment(command.devices, command.parameters))
|
|
159
|
-
|
|
160
|
-
self.current_status = StatusAndMessage(Status.IDLE)
|
|
161
|
-
|
|
162
|
-
self.last_run_aborted = False
|
|
163
|
-
except WarningException as exception:
|
|
164
|
-
LOGGER.warning("Warning Exception", exc_info=True)
|
|
165
|
-
self.current_status = make_error_status_and_message(exception)
|
|
166
|
-
except Exception as exception:
|
|
167
|
-
LOGGER.error("Exception on running plan", exc_info=True)
|
|
168
|
-
|
|
169
|
-
if self.last_run_aborted:
|
|
170
|
-
# Aborting will cause an exception here that we want to swallow
|
|
171
|
-
self.last_run_aborted = False
|
|
172
|
-
else:
|
|
173
|
-
self.current_status = make_error_status_and_message(exception)
|
|
174
|
-
|
|
175
|
-
|
|
176
46
|
def compose_start_args(context: BlueskyContext, plan_name: str, action: Actions):
|
|
177
47
|
experiment_registry_entry = PLAN_REGISTRY.get(plan_name)
|
|
178
48
|
if experiment_registry_entry is None:
|
|
@@ -203,7 +73,7 @@ def compose_start_args(context: BlueskyContext, plan_name: str, action: Actions)
|
|
|
203
73
|
|
|
204
74
|
|
|
205
75
|
class RunExperiment(Resource):
|
|
206
|
-
def __init__(self, runner:
|
|
76
|
+
def __init__(self, runner: GDARunner, context: BlueskyContext) -> None:
|
|
207
77
|
super().__init__()
|
|
208
78
|
self.runner = runner
|
|
209
79
|
self.context = context
|
|
@@ -228,9 +98,9 @@ class RunExperiment(Resource):
|
|
|
228
98
|
|
|
229
99
|
|
|
230
100
|
class StopOrStatus(Resource):
|
|
231
|
-
def __init__(self, runner:
|
|
101
|
+
def __init__(self, runner: GDARunner) -> None:
|
|
232
102
|
super().__init__()
|
|
233
|
-
self.runner:
|
|
103
|
+
self.runner: GDARunner = runner
|
|
234
104
|
|
|
235
105
|
def put(self, action):
|
|
236
106
|
status_and_message = StatusAndMessage(Status.FAILED, f"{action} not understood")
|
|
@@ -252,8 +122,9 @@ class StopOrStatus(Resource):
|
|
|
252
122
|
class FlushLogs(Resource):
|
|
253
123
|
def put(self, **kwargs):
|
|
254
124
|
try:
|
|
125
|
+
log_file = flush_debug_handler()
|
|
255
126
|
status_and_message = StatusAndMessage(
|
|
256
|
-
Status.SUCCESS, f"Flushed debug log to {
|
|
127
|
+
Status.SUCCESS, f"Flushed debug log to {log_file}"
|
|
257
128
|
)
|
|
258
129
|
except Exception as e:
|
|
259
130
|
status_and_message = StatusAndMessage(
|
|
@@ -262,25 +133,18 @@ class FlushLogs(Resource):
|
|
|
262
133
|
return asdict(status_and_message)
|
|
263
134
|
|
|
264
135
|
|
|
265
|
-
def create_app(
|
|
266
|
-
test_config=None,
|
|
267
|
-
RE: RunEngine = RunEngine({}),
|
|
268
|
-
dev_mode: bool = False,
|
|
269
|
-
) -> tuple[Flask, BlueskyRunner]:
|
|
270
|
-
context = setup_context(dev_mode=dev_mode)
|
|
271
|
-
runner = BlueskyRunner(
|
|
272
|
-
RE,
|
|
273
|
-
context=context,
|
|
274
|
-
)
|
|
136
|
+
def create_app(runner: GDARunner, test_config=None) -> Flask:
|
|
275
137
|
app = Flask(__name__)
|
|
276
138
|
if test_config:
|
|
277
139
|
app.config.update(test_config)
|
|
278
140
|
api = Api(app)
|
|
141
|
+
|
|
279
142
|
api.add_resource(
|
|
280
143
|
RunExperiment,
|
|
281
144
|
"/<string:plan_name>/<string:action>",
|
|
282
|
-
resource_class_args=[runner, context],
|
|
145
|
+
resource_class_args=[runner, runner.context],
|
|
283
146
|
)
|
|
147
|
+
|
|
284
148
|
api.add_resource(
|
|
285
149
|
FlushLogs,
|
|
286
150
|
"/flush_debug_log",
|
|
@@ -290,33 +154,41 @@ def create_app(
|
|
|
290
154
|
"/<string:action>",
|
|
291
155
|
resource_class_args=[runner],
|
|
292
156
|
)
|
|
293
|
-
return app
|
|
157
|
+
return app
|
|
294
158
|
|
|
295
159
|
|
|
296
|
-
def
|
|
297
|
-
|
|
298
|
-
args = parse_cli_args()
|
|
160
|
+
def initialise_globals(args: HyperionArgs):
|
|
161
|
+
"""Do all early main low-level application initialisation."""
|
|
299
162
|
do_default_logging_setup(
|
|
300
163
|
CONST.LOG_FILE_NAME, CONST.GRAYLOG_PORT, dev_mode=args.dev_mode
|
|
301
164
|
)
|
|
302
165
|
LOGGER.info(f"Hyperion launched with args:{argv}")
|
|
303
|
-
|
|
304
|
-
return app, runner, hyperion_port, args.dev_mode
|
|
166
|
+
alerting.set_alerting_service(LoggingAlertService(CONST.GRAYLOG_STREAM_ID))
|
|
305
167
|
|
|
306
168
|
|
|
307
169
|
def main():
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
170
|
+
"""Main application entry point."""
|
|
171
|
+
args = parse_cli_args()
|
|
172
|
+
initialise_globals(args)
|
|
173
|
+
hyperion_port = 5005
|
|
174
|
+
context = setup_context(dev_mode=args.dev_mode)
|
|
175
|
+
|
|
176
|
+
if args.mode == HyperionMode.GDA:
|
|
177
|
+
runner = GDARunner(context=context)
|
|
178
|
+
app = create_app(runner)
|
|
179
|
+
flask_thread = threading.Thread(
|
|
180
|
+
target=lambda: app.run(
|
|
181
|
+
host="0.0.0.0", port=hyperion_port, debug=True, use_reloader=False
|
|
182
|
+
),
|
|
183
|
+
daemon=True,
|
|
184
|
+
)
|
|
185
|
+
flask_thread.start()
|
|
186
|
+
LOGGER.info(
|
|
187
|
+
f"Hyperion now listening on {hyperion_port} ({'IN DEV' if args.dev_mode else ''})"
|
|
188
|
+
)
|
|
189
|
+
runner.wait_on_queue()
|
|
190
|
+
else:
|
|
191
|
+
run_forever(PlanRunner(context))
|
|
320
192
|
|
|
321
193
|
|
|
322
194
|
if __name__ == "__main__":
|
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
from collections.abc import Sequence
|
|
2
|
+
from functools import partial
|
|
3
|
+
from typing import Any
|
|
2
4
|
|
|
3
|
-
from blueapi.core import BlueskyContext
|
|
5
|
+
from blueapi.core.context import BlueskyContext
|
|
4
6
|
from bluesky import plan_stubs as bps
|
|
5
7
|
from bluesky import preprocessors as bpp
|
|
8
|
+
from bluesky.utils import MsgGenerator, RunEngineInterrupted
|
|
6
9
|
from dodal.devices.baton import Baton
|
|
7
10
|
|
|
8
|
-
from mx_bluesky.common.
|
|
9
|
-
from mx_bluesky.common.utils.
|
|
11
|
+
from mx_bluesky.common.parameters.components import MxBlueskyParameters
|
|
12
|
+
from mx_bluesky.common.utils.context import (
|
|
13
|
+
find_device_in_context,
|
|
14
|
+
)
|
|
10
15
|
from mx_bluesky.common.utils.log import LOGGER
|
|
11
16
|
from mx_bluesky.hyperion.experiment_plans.load_centre_collect_full_plan import (
|
|
12
|
-
LoadCentreCollectComposite,
|
|
13
17
|
create_devices,
|
|
14
18
|
load_centre_collect_full,
|
|
15
19
|
)
|
|
16
20
|
from mx_bluesky.hyperion.external_interaction.agamemnon import (
|
|
17
21
|
create_parameters_from_agamemnon,
|
|
18
22
|
)
|
|
23
|
+
from mx_bluesky.hyperion.parameters.components import Wait
|
|
19
24
|
from mx_bluesky.hyperion.parameters.load_centre_collect import LoadCentreCollect
|
|
25
|
+
from mx_bluesky.hyperion.plan_runner import PlanException, PlanRunner
|
|
20
26
|
from mx_bluesky.hyperion.utils.context import (
|
|
21
27
|
clear_all_device_caches,
|
|
22
28
|
setup_devices,
|
|
@@ -26,48 +32,78 @@ HYPERION_USER = "Hyperion"
|
|
|
26
32
|
NO_USER = "None"
|
|
27
33
|
|
|
28
34
|
|
|
29
|
-
def
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
def run_forever(runner: PlanRunner):
|
|
36
|
+
try:
|
|
37
|
+
while True:
|
|
38
|
+
try:
|
|
39
|
+
run_udc_when_requested(runner.context, runner)
|
|
40
|
+
except PlanException as e:
|
|
41
|
+
LOGGER.info(
|
|
42
|
+
"Caught exception during plan execution, stopped and waiting for baton.",
|
|
43
|
+
exc_info=e,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
except RunEngineInterrupted:
|
|
47
|
+
# In the event that BlueskyRunner.stop() or shutdown() was called then
|
|
48
|
+
# RunEngine.abort() will have been called and we will get RunEngineInterrupted
|
|
49
|
+
LOGGER.info(
|
|
50
|
+
f"RunEngine was interrupted. Runner state is {runner.current_status}, "
|
|
51
|
+
f"run engine is {runner.RE.state}"
|
|
52
|
+
)
|
|
36
53
|
|
|
37
54
|
|
|
38
|
-
def
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
55
|
+
def run_udc_when_requested(context: BlueskyContext, runner: PlanRunner):
|
|
56
|
+
"""This will wait for the baton to be handed to hyperion and then run through the
|
|
57
|
+
UDC queue from agamemnon until:
|
|
58
|
+
1. There are no more instructions from agamemnon
|
|
59
|
+
2. There is an error on the beamline
|
|
60
|
+
3. The baton is requested by another party
|
|
61
|
+
4. A shutdown is requested
|
|
62
|
+
|
|
63
|
+
In the case of 1. 2. or 4. hyperion will immediately release the baton. In the case of
|
|
64
|
+
3. the baton will be released after the next collection has finished."""
|
|
43
65
|
|
|
66
|
+
baton = _get_baton(context)
|
|
44
67
|
|
|
45
|
-
def
|
|
46
|
-
|
|
47
|
-
|
|
68
|
+
def acquire_baton() -> MsgGenerator:
|
|
69
|
+
yield from _wait_for_hyperion_requested(baton)
|
|
70
|
+
yield from bps.abs_set(baton.current_user, HYPERION_USER)
|
|
71
|
+
|
|
72
|
+
def collect() -> MsgGenerator:
|
|
73
|
+
"""
|
|
74
|
+
Move to the default state for collection, then enter a loop fetching instructions
|
|
75
|
+
from Agamemnon and continue the loop until any of the following occur:
|
|
76
|
+
* A user requests the baton away from Hyperion
|
|
77
|
+
* Hyperion releases the baton when Agamemnon has no more instructions
|
|
78
|
+
* The RunEngine raises a RequestAbort exception, most likely due to a shutdown command
|
|
79
|
+
* A plan raises an exception not of type WarningException (which is then wrapped as a PlanException)
|
|
80
|
+
Args:
|
|
81
|
+
baton: The baton device
|
|
82
|
+
runner: The runner
|
|
83
|
+
"""
|
|
84
|
+
yield from _move_to_default_state()
|
|
48
85
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
)
|
|
53
|
-
if parameter_list:
|
|
54
|
-
for parameters in parameter_list:
|
|
55
|
-
yield from load_centre_collect_full(composite, parameters)
|
|
56
|
-
else:
|
|
57
|
-
yield from bps.mv(baton.requested_user, NO_USER)
|
|
58
|
-
|
|
59
|
-
yield from bpp.contingency_wrapper(
|
|
60
|
-
inner_loop(), except_plan=ignore_sample_errors, auto_raise=False
|
|
61
|
-
)
|
|
62
|
-
requested_user = yield from bps.rd(baton.requested_user)
|
|
86
|
+
# re-fetch the baton because the device has been reinstantiated
|
|
87
|
+
baton = _get_baton(context)
|
|
88
|
+
while (yield from _is_requesting_baton(baton)):
|
|
89
|
+
yield from _fetch_and_process_agamemnon_instruction(baton, runner)
|
|
63
90
|
|
|
91
|
+
def release_baton() -> MsgGenerator:
|
|
92
|
+
# If hyperion has given up the baton itself we need to also release requested
|
|
93
|
+
# user so that hyperion doesn't think we're requested again
|
|
94
|
+
baton = _get_baton(context)
|
|
95
|
+
yield from _safely_release_baton(baton)
|
|
96
|
+
yield from bps.abs_set(baton.current_user, NO_USER)
|
|
64
97
|
|
|
65
|
-
def
|
|
66
|
-
|
|
67
|
-
|
|
98
|
+
def collect_then_release() -> MsgGenerator:
|
|
99
|
+
yield from bpp.contingency_wrapper(collect(), final_plan=release_baton)
|
|
100
|
+
|
|
101
|
+
context.run_engine(acquire_baton())
|
|
102
|
+
_initialise_udc(context)
|
|
103
|
+
context.run_engine(collect_then_release())
|
|
68
104
|
|
|
69
105
|
|
|
70
|
-
def
|
|
106
|
+
def _initialise_udc(context: BlueskyContext):
|
|
71
107
|
"""
|
|
72
108
|
Perform all initialisation that happens at the start of UDC just after the
|
|
73
109
|
baton is acquired, but before we execute any plans or move hardware.
|
|
@@ -77,45 +113,65 @@ def initialise_udc(context: BlueskyContext, dev_mode: bool = False):
|
|
|
77
113
|
"""
|
|
78
114
|
LOGGER.info("Initialising mx-bluesky for UDC start...")
|
|
79
115
|
clear_all_device_caches(context)
|
|
80
|
-
setup_devices(context,
|
|
116
|
+
setup_devices(context, False)
|
|
81
117
|
|
|
82
118
|
|
|
83
|
-
def
|
|
84
|
-
|
|
119
|
+
def _wait_for_hyperion_requested(baton: Baton):
|
|
120
|
+
SLEEP_PER_CHECK = 0.1
|
|
121
|
+
while True:
|
|
122
|
+
requested_user = yield from bps.rd(baton.requested_user)
|
|
123
|
+
if requested_user == HYPERION_USER:
|
|
124
|
+
break
|
|
125
|
+
yield from bps.sleep(SLEEP_PER_CHECK)
|
|
85
126
|
|
|
86
127
|
|
|
87
|
-
def
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
128
|
+
def _fetch_and_process_agamemnon_instruction(
|
|
129
|
+
baton: Baton, runner: PlanRunner
|
|
130
|
+
) -> MsgGenerator:
|
|
131
|
+
parameter_list: Sequence[MxBlueskyParameters] = create_parameters_from_agamemnon()
|
|
132
|
+
if parameter_list:
|
|
133
|
+
for parameters in parameter_list:
|
|
134
|
+
LOGGER.info(
|
|
135
|
+
f"Executing plan with parameters: {parameters.model_dump_json(indent=2)}"
|
|
136
|
+
)
|
|
137
|
+
match parameters:
|
|
138
|
+
case LoadCentreCollect():
|
|
139
|
+
devices: Any = create_devices(runner.context)
|
|
140
|
+
yield from runner.execute_plan(
|
|
141
|
+
partial(load_centre_collect_full, devices, parameters)
|
|
142
|
+
)
|
|
143
|
+
case Wait():
|
|
144
|
+
yield from runner.execute_plan(partial(_runner_sleep, parameters))
|
|
145
|
+
case _:
|
|
146
|
+
raise AssertionError(
|
|
147
|
+
f"Unsupported instruction decoded from agamemnon {type(parameters)}"
|
|
148
|
+
)
|
|
149
|
+
else:
|
|
150
|
+
# Release the baton for orderly exit from the instruction loop
|
|
151
|
+
yield from _safely_release_baton(baton)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _runner_sleep(parameters: Wait) -> MsgGenerator:
|
|
155
|
+
yield from bps.sleep(parameters.duration_s)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _is_requesting_baton(baton: Baton) -> MsgGenerator:
|
|
159
|
+
requested_user = yield from bps.rd(baton.requested_user)
|
|
160
|
+
return requested_user == HYPERION_USER
|
|
93
161
|
|
|
94
|
-
In the case of 1. or 2. hyperion will immediately release the baton. In the case of
|
|
95
|
-
3. the baton will be released after the next collection has finished."""
|
|
96
162
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
yield from bps.
|
|
163
|
+
def _move_to_default_state() -> MsgGenerator:
|
|
164
|
+
# To be filled in in https://github.com/DiamondLightSource/mx-bluesky/issues/396
|
|
165
|
+
yield from bps.null()
|
|
100
166
|
|
|
101
|
-
def initialise_then_collect():
|
|
102
|
-
initialise_udc(context, dev_mode)
|
|
103
|
-
yield from move_to_default_state()
|
|
104
167
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
composite = create_devices(context)
|
|
108
|
-
yield from main_hyperion_loop(new_baton, composite)
|
|
168
|
+
def _get_baton(context: BlueskyContext) -> Baton:
|
|
169
|
+
return find_device_in_context(context, "baton", Baton)
|
|
109
170
|
|
|
110
|
-
def release_baton():
|
|
111
|
-
# If hyperion has given up the baton itself we need to also release requested
|
|
112
|
-
# user so that hyperion doesn't think we're requested again
|
|
113
|
-
baton = _get_baton(context)
|
|
114
|
-
requested_user = yield from bps.rd(baton.requested_user)
|
|
115
|
-
if requested_user == HYPERION_USER:
|
|
116
|
-
yield from bps.abs_set(baton.requested_user, NO_USER)
|
|
117
|
-
yield from bps.abs_set(baton.current_user, NO_USER)
|
|
118
171
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
172
|
+
def _safely_release_baton(baton: Baton) -> MsgGenerator:
|
|
173
|
+
"""Relinquish the requested user of the baton if it is not already requested
|
|
174
|
+
by another user."""
|
|
175
|
+
requested_user = yield from bps.rd(baton.requested_user)
|
|
176
|
+
if requested_user == HYPERION_USER:
|
|
177
|
+
yield from bps.abs_set(baton.requested_user, NO_USER)
|
|
@@ -59,31 +59,41 @@ def _apply_and_wait_for_voltages_to_settle(
|
|
|
59
59
|
def adjust_mirror_stripe(
|
|
60
60
|
energy_kev, mirror: FocusingMirrorWithStripes, mirror_voltages: MirrorVoltages
|
|
61
61
|
):
|
|
62
|
-
"""
|
|
62
|
+
"""Adjusts the mirror stripe based on the new energy.
|
|
63
|
+
|
|
64
|
+
Changing this takes some time and moves motors that are liable to overheating so we
|
|
65
|
+
check whether its required first.
|
|
66
|
+
|
|
67
|
+
Feedback should be OFF prior to entry, in order to prevent
|
|
63
68
|
feedback from making unnecessary corrections while beam is being adjusted."""
|
|
64
69
|
mirror_config = mirror.energy_to_stripe(energy_kev)
|
|
65
70
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
)
|
|
69
|
-
yield from bps.abs_set(mirror.stripe, mirror_config["stripe"], wait=True)
|
|
70
|
-
yield from bps.trigger(mirror.apply_stripe)
|
|
71
|
+
current_mirror_stripe = yield from bps.rd(mirror.stripe)
|
|
72
|
+
new_stripe = mirror_config["stripe"]
|
|
71
73
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
if current_mirror_stripe != new_stripe:
|
|
75
|
+
LOGGER.info(
|
|
76
|
+
f"Adjusting mirror stripe for {energy_kev}keV selecting {new_stripe} stripe"
|
|
77
|
+
)
|
|
78
|
+
yield from bps.abs_set(mirror.stripe, new_stripe, wait=True)
|
|
79
|
+
yield from bps.trigger(mirror.apply_stripe)
|
|
77
80
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
# yaw, lat cannot be done simultaneously
|
|
82
|
+
LOGGER.info(f"Adjusting {mirror.name} lat to {mirror_config['lat_mm']}")
|
|
83
|
+
yield from bps.abs_set(
|
|
84
|
+
mirror.x_mm, mirror_config["lat_mm"], wait=True, timeout=YAW_LAT_TIMEOUT_S
|
|
85
|
+
)
|
|
82
86
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
LOGGER.info(f"Adjusting {mirror.name} yaw to {mirror_config['yaw_mrad']}")
|
|
88
|
+
yield from bps.abs_set(
|
|
89
|
+
mirror.yaw_mrad,
|
|
90
|
+
mirror_config["yaw_mrad"],
|
|
91
|
+
wait=True,
|
|
92
|
+
timeout=YAW_LAT_TIMEOUT_S,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
LOGGER.info("Adjusting mirror voltages...")
|
|
96
|
+
yield from _apply_and_wait_for_voltages_to_settle(new_stripe, mirror_voltages)
|
|
87
97
|
|
|
88
98
|
|
|
89
99
|
def adjust_dcm_pitch_roll_vfm_from_lut(
|
|
@@ -128,9 +138,4 @@ def adjust_dcm_pitch_roll_vfm_from_lut(
|
|
|
128
138
|
yield from dcm_roll_adjuster(DCM_GROUP)
|
|
129
139
|
LOGGER.info("Waiting for DCM roll adjust to complete...")
|
|
130
140
|
|
|
131
|
-
#
|
|
132
|
-
# Adjust vfm mirror stripe and mirror voltages
|
|
133
|
-
#
|
|
134
|
-
|
|
135
|
-
# VFM Stripe selection
|
|
136
141
|
yield from adjust_mirror_stripe(energy_kev, vfm, mirror_voltages)
|
|
@@ -22,6 +22,9 @@ from mx_bluesky.hyperion.device_setup_plans.setup_panda import (
|
|
|
22
22
|
from mx_bluesky.hyperion.device_setup_plans.setup_zebra import (
|
|
23
23
|
setup_zebra_for_panda_flyscan,
|
|
24
24
|
)
|
|
25
|
+
from mx_bluesky.hyperion.external_interaction.config_server import (
|
|
26
|
+
get_hyperion_config_client,
|
|
27
|
+
)
|
|
25
28
|
from mx_bluesky.hyperion.parameters.device_composites import (
|
|
26
29
|
HyperionFlyScanXRayCentreComposite,
|
|
27
30
|
)
|
|
@@ -64,7 +67,7 @@ def construct_hyperion_specific_features(
|
|
|
64
67
|
|
|
65
68
|
setup_trigger_plan: Callable[..., MsgGenerator]
|
|
66
69
|
|
|
67
|
-
if
|
|
70
|
+
if get_hyperion_config_client().get_feature_flags().USE_PANDA_FOR_GRIDSCAN:
|
|
68
71
|
setup_trigger_plan = _panda_triggering_setup
|
|
69
72
|
tidy_plan = partial(_panda_tidy, xrc_composite)
|
|
70
73
|
set_flyscan_params_plan = partial(
|