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.
Files changed (40) hide show
  1. mx_bluesky/_version.py +16 -3
  2. mx_bluesky/beamlines/i04/__init__.py +7 -3
  3. mx_bluesky/beamlines/i04/experiment_plans/i04_grid_detect_then_xray_centre_plan.py +3 -0
  4. mx_bluesky/beamlines/i24/serial/blueapi_config.yaml +2 -2
  5. mx_bluesky/common/experiment_plans/oav_grid_detection_plan.py +12 -2
  6. mx_bluesky/common/external_interaction/alerting/__init__.py +13 -0
  7. mx_bluesky/common/external_interaction/alerting/_service.py +82 -0
  8. mx_bluesky/common/external_interaction/alerting/log_based_service.py +57 -0
  9. mx_bluesky/common/external_interaction/callbacks/sample_handling/sample_handling_callback.py +28 -4
  10. mx_bluesky/common/external_interaction/config_server.py +151 -54
  11. mx_bluesky/common/parameters/constants.py +26 -8
  12. mx_bluesky/common/parameters/gridscan.py +1 -1
  13. mx_bluesky/hyperion/__main__.py +50 -178
  14. mx_bluesky/hyperion/baton_handler.py +125 -69
  15. mx_bluesky/hyperion/device_setup_plans/dcm_pitch_roll_mirror_adjuster.py +29 -24
  16. mx_bluesky/hyperion/experiment_plans/hyperion_flyscan_xray_centre_plan.py +4 -1
  17. mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py +12 -4
  18. mx_bluesky/hyperion/experiment_plans/pin_tip_centring_plan.py +1 -1
  19. mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +2 -3
  20. mx_bluesky/hyperion/external_interaction/agamemnon.py +128 -73
  21. mx_bluesky/hyperion/external_interaction/alerting/__init__.py +0 -0
  22. mx_bluesky/hyperion/external_interaction/alerting/constants.py +12 -0
  23. mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +5 -0
  24. mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py +2 -2
  25. mx_bluesky/hyperion/external_interaction/config_server.py +12 -31
  26. mx_bluesky/hyperion/parameters/cli.py +15 -3
  27. mx_bluesky/hyperion/parameters/components.py +7 -5
  28. mx_bluesky/hyperion/parameters/constants.py +20 -4
  29. mx_bluesky/hyperion/parameters/gridscan.py +22 -14
  30. mx_bluesky/hyperion/parameters/load_centre_collect.py +1 -14
  31. mx_bluesky/hyperion/parameters/robot_load.py +1 -4
  32. mx_bluesky/hyperion/parameters/rotation.py +1 -2
  33. mx_bluesky/hyperion/plan_runner.py +78 -0
  34. mx_bluesky/hyperion/runner.py +189 -0
  35. {mx_bluesky-1.5.2.dist-info → mx_bluesky-1.5.3.dist-info}/METADATA +4 -3
  36. {mx_bluesky-1.5.2.dist-info → mx_bluesky-1.5.3.dist-info}/RECORD +40 -33
  37. {mx_bluesky-1.5.2.dist-info → mx_bluesky-1.5.3.dist-info}/entry_points.txt +0 -2
  38. {mx_bluesky-1.5.2.dist-info → mx_bluesky-1.5.3.dist-info}/WHEEL +0 -0
  39. {mx_bluesky-1.5.2.dist-info → mx_bluesky-1.5.3.dist-info}/licenses/LICENSE +0 -0
  40. {mx_bluesky-1.5.2.dist-info → mx_bluesky-1.5.3.dist-info}/top_level.txt +0 -0
@@ -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.callbacks.common.log_uid_tag_callback import (
20
- LogUidTaggingCallback,
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.common.utils.tracing import TRACER
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 parse_cli_args
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: BlueskyRunner, context: BlueskyContext) -> None:
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: BlueskyRunner) -> None:
101
+ def __init__(self, runner: GDARunner) -> None:
232
102
  super().__init__()
233
- self.runner: BlueskyRunner = 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 {flush_debug_handler()}"
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, runner
157
+ return app
294
158
 
295
159
 
296
- def create_targets():
297
- hyperion_port = 5005
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
- app, runner = create_app(dev_mode=args.dev_mode)
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
- app, runner, port, dev_mode = create_targets()
309
- atexit.register(runner.shutdown)
310
- flask_thread = threading.Thread(
311
- target=lambda: app.run(
312
- host="0.0.0.0", port=port, debug=True, use_reloader=False
313
- ),
314
- daemon=True,
315
- )
316
- flask_thread.start()
317
- LOGGER.info(f"Hyperion now listening on {port} ({'IN DEV' if dev_mode else ''})")
318
- runner.wait_on_queue()
319
- flask_thread.join()
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.utils.context import find_device_in_context
9
- from mx_bluesky.common.utils.exceptions import WarningException
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 wait_for_hyperion_requested(baton: Baton):
30
- SLEEP_PER_CHECK = 0.1
31
- while True:
32
- requested_user = yield from bps.rd(baton.requested_user)
33
- if requested_user == HYPERION_USER:
34
- break
35
- yield from bps.sleep(SLEEP_PER_CHECK)
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 ignore_sample_errors(exception: Exception):
39
- yield from bps.null()
40
- # For sample errors we want to continue the loop
41
- if not isinstance(exception, WarningException):
42
- raise exception
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 main_hyperion_loop(baton: Baton, composite: LoadCentreCollectComposite):
46
- requested_user = yield from bps.rd(baton.requested_user)
47
- while requested_user == HYPERION_USER:
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
- def inner_loop():
50
- parameter_list: Sequence[LoadCentreCollect] = (
51
- create_parameters_from_agamemnon()
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 move_to_default_state():
66
- # To be filled in in https://github.com/DiamondLightSource/mx-bluesky/issues/396
67
- yield from bps.null()
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 initialise_udc(context: BlueskyContext, dev_mode: bool = False):
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, dev_mode)
116
+ setup_devices(context, False)
81
117
 
82
118
 
83
- def _get_baton(context: BlueskyContext) -> Baton:
84
- return find_device_in_context(context, "baton", Baton)
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 run_udc_when_requested(context: BlueskyContext, dev_mode: bool = False):
88
- """This will wait for the baton to be handed to hyperion and then run through the
89
- UDC queue from agamemnon until:
90
- 1. There are no more instructions from agamemnon
91
- 2. There is an error on the beamline
92
- 3. The baton is requested by another party
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
- baton = _get_baton(context)
98
- yield from wait_for_hyperion_requested(baton)
99
- yield from bps.abs_set(baton.current_user, HYPERION_USER)
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
- # re-fetch the baton because the device has been reinstantiated
106
- new_baton = _get_baton(context)
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
- yield from bpp.contingency_wrapper(
120
- initialise_then_collect(), final_plan=release_baton
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
- """Feedback should be OFF prior to entry, in order to prevent
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
- LOGGER.info(
67
- f"Adjusting mirror stripe for {energy_kev}keV selecting {mirror_config['stripe']} stripe"
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
- # yaw, lat cannot be done simultaneously
73
- LOGGER.info(f"Adjusting {mirror.name} lat to {mirror_config['lat_mm']}")
74
- yield from bps.abs_set(
75
- mirror.x_mm, mirror_config["lat_mm"], wait=True, timeout=YAW_LAT_TIMEOUT_S
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
- LOGGER.info(f"Adjusting {mirror.name} yaw to {mirror_config['yaw_mrad']}")
79
- yield from bps.abs_set(
80
- mirror.yaw_mrad, mirror_config["yaw_mrad"], wait=True, timeout=YAW_LAT_TIMEOUT_S
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
- LOGGER.info("Adjusting mirror voltages...")
84
- yield from _apply_and_wait_for_voltages_to_settle(
85
- mirror_config["stripe"], mirror_voltages
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 xrc_parameters.features.use_panda_for_gridscan:
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(