mx-bluesky 1.5.15__py3-none-any.whl → 1.5.16__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 (72) hide show
  1. mx_bluesky/Getting started.ipynb +1 -0
  2. mx_bluesky/_version.py +2 -2
  3. mx_bluesky/beamlines/i04/callbacks/murko_callback.py +18 -0
  4. mx_bluesky/beamlines/i04/experiment_plans/i04_grid_detect_then_xray_centre_plan.py +3 -2
  5. mx_bluesky/beamlines/i04/thawing_plan.py +1 -0
  6. mx_bluesky/beamlines/i24/jungfrau_commissioning/__init__.py +13 -0
  7. mx_bluesky/beamlines/i24/jungfrau_commissioning/callbacks/__init__.py +0 -0
  8. mx_bluesky/beamlines/i24/jungfrau_commissioning/callbacks/metadata_writer.py +86 -0
  9. mx_bluesky/beamlines/i24/jungfrau_commissioning/composites.py +35 -0
  10. mx_bluesky/beamlines/i24/jungfrau_commissioning/experiment_plans/do_darks.py +18 -19
  11. mx_bluesky/beamlines/i24/jungfrau_commissioning/experiment_plans/rotation_scan_plan.py +292 -0
  12. mx_bluesky/beamlines/i24/jungfrau_commissioning/plan_stubs/do_external_acquisition.py +3 -8
  13. mx_bluesky/beamlines/i24/jungfrau_commissioning/plan_stubs/do_internal_acquisition.py +4 -5
  14. mx_bluesky/beamlines/i24/jungfrau_commissioning/plan_stubs/plan_utils.py +14 -18
  15. mx_bluesky/beamlines/i24/parameters/__init__.py +0 -0
  16. mx_bluesky/beamlines/i24/parameters/constants.py +9 -0
  17. mx_bluesky/beamlines/i24/serial/extruder/i24ssx_extruder_collect_py3v2.py +1 -1
  18. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_moveonclick.py +2 -2
  19. mx_bluesky/common/device_setup_plans/robot_load_unload.py +2 -24
  20. mx_bluesky/common/device_setup_plans/setup_zebra_and_shutter.py +5 -2
  21. mx_bluesky/common/experiment_plans/common_flyscan_xray_centre_plan.py +1 -1
  22. mx_bluesky/common/experiment_plans/inner_plans/read_hardware.py +1 -0
  23. mx_bluesky/common/experiment_plans/rotation/__init__.py +0 -0
  24. mx_bluesky/common/experiment_plans/rotation/rotation_utils.py +127 -0
  25. mx_bluesky/common/external_interaction/callbacks/common/ispyb_callback_base.py +13 -2
  26. mx_bluesky/common/external_interaction/callbacks/common/ispyb_mapping.py +0 -2
  27. mx_bluesky/common/external_interaction/ispyb/data_model.py +1 -1
  28. mx_bluesky/common/external_interaction/ispyb/exp_eye_store.py +1 -1
  29. mx_bluesky/common/parameters/components.py +17 -7
  30. mx_bluesky/common/parameters/constants.py +6 -0
  31. mx_bluesky/{hyperion → common}/parameters/rotation.py +10 -8
  32. mx_bluesky/common/preprocessors/preprocessors.py +98 -36
  33. mx_bluesky/hyperion/__main__.py +55 -22
  34. mx_bluesky/hyperion/baton_handler.py +24 -64
  35. mx_bluesky/hyperion/blueapi_config.yaml +17 -0
  36. mx_bluesky/hyperion/blueapi_dev_config.yaml +16 -0
  37. mx_bluesky/hyperion/blueapi_plans/__init__.py +96 -0
  38. mx_bluesky/hyperion/device_setup_plans/dcm_pitch_roll_mirror_adjuster.py +8 -6
  39. mx_bluesky/hyperion/device_setup_plans/setup_panda.py +1 -1
  40. mx_bluesky/hyperion/experiment_plans/experiment_registry.py +3 -1
  41. mx_bluesky/hyperion/experiment_plans/hyperion_flyscan_xray_centre_plan.py +1 -0
  42. mx_bluesky/hyperion/experiment_plans/hyperion_grid_detect_then_xray_centre_plan.py +2 -2
  43. mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py +3 -1
  44. mx_bluesky/hyperion/experiment_plans/pin_centre_then_xray_centre_plan.py +17 -6
  45. mx_bluesky/hyperion/experiment_plans/robot_load_and_change_energy.py +0 -3
  46. mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +12 -126
  47. mx_bluesky/hyperion/experiment_plans/set_energy_plan.py +2 -2
  48. mx_bluesky/hyperion/external_interaction/agamemnon.py +3 -8
  49. mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +121 -47
  50. mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_callback.py +3 -1
  51. mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_mapping.py +3 -1
  52. mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py +6 -3
  53. mx_bluesky/hyperion/external_interaction/callbacks/stomp/__init__.py +0 -0
  54. mx_bluesky/hyperion/external_interaction/callbacks/stomp/dispatcher.py +33 -0
  55. mx_bluesky/hyperion/in_process_runner.py +132 -0
  56. mx_bluesky/hyperion/parameters/cli.py +43 -4
  57. mx_bluesky/hyperion/parameters/components.py +13 -0
  58. mx_bluesky/hyperion/parameters/constants.py +2 -9
  59. mx_bluesky/hyperion/parameters/load_centre_collect.py +3 -1
  60. mx_bluesky/hyperion/plan_runner.py +45 -66
  61. mx_bluesky/hyperion/plan_runner_api.py +3 -4
  62. mx_bluesky/hyperion/supervisor/__init__.py +3 -0
  63. mx_bluesky/hyperion/supervisor/_supervisor.py +116 -0
  64. mx_bluesky/hyperion/supervisor/client_config.yaml +6 -0
  65. mx_bluesky/hyperion/supervisor/supervisor_config.yaml +10 -0
  66. mx_bluesky/hyperion/supervisor/supervisor_dev_config.yaml +9 -0
  67. {mx_bluesky-1.5.15.dist-info → mx_bluesky-1.5.16.dist-info}/METADATA +3 -31
  68. {mx_bluesky-1.5.15.dist-info → mx_bluesky-1.5.16.dist-info}/RECORD +72 -52
  69. {mx_bluesky-1.5.15.dist-info → mx_bluesky-1.5.16.dist-info}/WHEEL +0 -0
  70. {mx_bluesky-1.5.15.dist-info → mx_bluesky-1.5.16.dist-info}/entry_points.txt +0 -0
  71. {mx_bluesky-1.5.15.dist-info → mx_bluesky-1.5.16.dist-info}/licenses/LICENSE +0 -0
  72. {mx_bluesky-1.5.15.dist-info → mx_bluesky-1.5.16.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,132 @@
1
+ from collections.abc import Callable, Sequence
2
+ from functools import partial
3
+ from typing import Any
4
+
5
+ from blueapi.core import BlueskyContext
6
+ from bluesky import plan_stubs as bps
7
+ from bluesky.utils import MsgGenerator, RequestAbort
8
+ from dodal.devices.aperturescatterguard import ApertureScatterguard
9
+ from dodal.devices.detector.detector_motion import DetectorMotion
10
+ from dodal.devices.motors import XYZStage
11
+ from dodal.devices.robot import BartRobot
12
+ from dodal.devices.smargon import Smargon
13
+
14
+ from mx_bluesky.common.parameters.components import MxBlueskyParameters
15
+ from mx_bluesky.common.parameters.constants import Status
16
+ from mx_bluesky.common.utils.context import (
17
+ device_composite_from_context,
18
+ find_device_in_context,
19
+ )
20
+ from mx_bluesky.common.utils.exceptions import WarningError
21
+ from mx_bluesky.common.utils.log import LOGGER
22
+ from mx_bluesky.hyperion.blueapi_plans import clean_up_udc, move_to_udc_default_state
23
+ from mx_bluesky.hyperion.experiment_plans import load_centre_collect_full
24
+ from mx_bluesky.hyperion.experiment_plans.load_centre_collect_full_plan import (
25
+ create_devices,
26
+ )
27
+ from mx_bluesky.hyperion.experiment_plans.udc_default_state import UDCDefaultDevices
28
+ from mx_bluesky.hyperion.parameters.components import UDCCleanup, UDCDefaultState, Wait
29
+ from mx_bluesky.hyperion.parameters.load_centre_collect import LoadCentreCollect
30
+ from mx_bluesky.hyperion.plan_runner import PlanError, PlanRunner
31
+
32
+
33
+ class InProcessRunner(PlanRunner):
34
+ """Runner that executes experiments from inside a running Bluesky plan"""
35
+
36
+ def __init__(self, context: BlueskyContext, dev_mode: bool) -> None:
37
+ super().__init__(context, dev_mode)
38
+ self._current_status: Status = Status.IDLE
39
+
40
+ def decode_and_execute(
41
+ self, current_visit: str | None, parameter_list: Sequence[MxBlueskyParameters]
42
+ ) -> MsgGenerator:
43
+ for parameters in parameter_list:
44
+ LOGGER.info(
45
+ f"Executing plan with parameters: {parameters.model_dump_json(indent=2)}"
46
+ )
47
+ match parameters:
48
+ case LoadCentreCollect():
49
+ current_visit = parameters.visit
50
+ devices: Any = create_devices(self.context)
51
+ yield from self.execute_plan(
52
+ partial(load_centre_collect_full, devices, parameters)
53
+ )
54
+ case Wait():
55
+ yield from self.execute_plan(partial(_runner_sleep, parameters))
56
+ case UDCDefaultState():
57
+ udc_default_devices: UDCDefaultDevices = (
58
+ device_composite_from_context(self.context, UDCDefaultDevices)
59
+ )
60
+ yield from move_to_udc_default_state(udc_default_devices)
61
+ case UDCCleanup():
62
+ yield from _clean_up_udc(self.context, current_visit)
63
+ case _:
64
+ raise AssertionError(
65
+ f"Unsupported instruction decoded from agamemnon {type(parameters)}"
66
+ )
67
+ return current_visit
68
+
69
+ def execute_plan(
70
+ self,
71
+ experiment: Callable[[], MsgGenerator],
72
+ ) -> MsgGenerator:
73
+ """Execute the specified experiment plan.
74
+ Args:
75
+ experiment: The experiment to run
76
+ Raises:
77
+ PlanError: If the plan raised an exception
78
+ RequestAbort: If the RunEngine aborted during execution"""
79
+
80
+ self._current_status = Status.BUSY
81
+
82
+ try:
83
+ yield from self.check_external_callbacks_are_alive()
84
+ yield from experiment()
85
+ self._current_status = Status.IDLE
86
+ except WarningError as e:
87
+ LOGGER.warning("Plan failed with warning", exc_info=e)
88
+ self._current_status = Status.FAILED
89
+ except RequestAbort:
90
+ # This will occur when the run engine processes an abort when we shut down
91
+ LOGGER.info("UDC Runner aborting")
92
+ raise
93
+ except Exception as e:
94
+ LOGGER.error("Plan failed with exception", exc_info=e)
95
+ self._current_status = Status.FAILED
96
+ raise PlanError("Exception thrown in plan execution") from e
97
+
98
+ def shutdown(self):
99
+ """Performs a prompt shutdown. Aborts the run engine and terminates the loop
100
+ waiting for messages."""
101
+
102
+ LOGGER.info("Shutting down: Stopping the run engine gracefully")
103
+ if self.current_status != Status.ABORTING:
104
+ self._current_status = Status.ABORTING
105
+ self.request_run_engine_abort()
106
+ return
107
+
108
+ @property
109
+ def current_status(self) -> Status:
110
+ return self._current_status
111
+
112
+
113
+ def _runner_sleep(parameters: Wait) -> MsgGenerator:
114
+ yield from bps.sleep(parameters.duration_s)
115
+
116
+
117
+ def _clean_up_udc(context: BlueskyContext, visit: str) -> MsgGenerator:
118
+ robot = find_device_in_context(context, "robot", BartRobot)
119
+ smargon = find_device_in_context(context, "smargon", Smargon)
120
+ aperture_scatterguard = find_device_in_context(
121
+ context, "aperture_scatterguard", ApertureScatterguard
122
+ )
123
+ lower_gonio = find_device_in_context(context, "lower_gonio", XYZStage)
124
+ detector_motion = find_device_in_context(context, "detector_motion", DetectorMotion)
125
+ yield from clean_up_udc(
126
+ visit,
127
+ robot=robot,
128
+ smargon=smargon,
129
+ aperture_scatterguard=aperture_scatterguard,
130
+ lower_gonio=lower_gonio,
131
+ detector_motion=detector_motion,
132
+ )
@@ -1,20 +1,32 @@
1
1
  import argparse
2
2
  from enum import StrEnum
3
+ from pathlib import Path
3
4
 
4
5
  from pydantic.dataclasses import dataclass
5
6
 
6
7
  from mx_bluesky._version import version
8
+ from mx_bluesky.hyperion.parameters.constants import HyperionConstants
7
9
 
8
10
 
9
11
  class HyperionMode(StrEnum):
10
12
  GDA = "gda"
11
13
  UDC = "udc"
14
+ SUPERVISOR = "supervisor"
12
15
 
13
16
 
14
17
  @dataclass
15
18
  class HyperionArgs:
16
19
  mode: HyperionMode
17
20
  dev_mode: bool = False
21
+ client_config: str | None = None
22
+ supervisor_config: str | None = None
23
+
24
+
25
+ @dataclass
26
+ class CallbackArgs:
27
+ dev_mode: bool = False
28
+ watchdog_port: int = HyperionConstants.HYPERION_PORT
29
+ stomp_config: Path | None = None
18
30
 
19
31
 
20
32
  def _add_callback_relevant_args(parser: argparse.ArgumentParser) -> None:
@@ -26,12 +38,27 @@ def _add_callback_relevant_args(parser: argparse.ArgumentParser) -> None:
26
38
  )
27
39
 
28
40
 
29
- def parse_callback_dev_mode_arg() -> bool:
30
- """Returns the bool representing the 'dev_mode' argument."""
41
+ def parse_callback_args() -> CallbackArgs:
42
+ """Parse the CLI arguments for the watchdog port and dev mode into a CallbackArgs instance."""
31
43
  parser = argparse.ArgumentParser()
32
44
  _add_callback_relevant_args(parser)
45
+ parser.add_argument(
46
+ "--watchdog-port",
47
+ type=int,
48
+ help="Liveness port for callbacks to ping regularly",
49
+ )
50
+ parser.add_argument(
51
+ "--stomp-config",
52
+ type=Path,
53
+ default=None,
54
+ help="Specify config yaml for the STOMP backend (default is 0MQ)",
55
+ )
33
56
  args = parser.parse_args()
34
- return args.dev
57
+ return CallbackArgs(
58
+ dev_mode=args.dev,
59
+ watchdog_port=args.watchdog_port,
60
+ stomp_config=args.stomp_config,
61
+ )
35
62
 
36
63
 
37
64
  def parse_cli_args() -> HyperionArgs:
@@ -53,5 +80,17 @@ def parse_cli_args() -> HyperionArgs:
53
80
  type=HyperionMode,
54
81
  choices=HyperionMode.__members__.values(),
55
82
  )
83
+ parser.add_argument(
84
+ "--client-config", help="Specify the blueapi client configuration file."
85
+ )
86
+ parser.add_argument(
87
+ "--supervisor-config",
88
+ help="Specify the supervisor bluesky context configuration file.",
89
+ )
56
90
  args = parser.parse_args()
57
- return HyperionArgs(dev_mode=args.dev or False, mode=args.mode)
91
+ return HyperionArgs(
92
+ dev_mode=args.dev or False,
93
+ mode=args.mode,
94
+ supervisor_config=args.supervisor_config,
95
+ client_config=args.client_config,
96
+ )
@@ -8,3 +8,16 @@ class Wait(MxBlueskyParameters):
8
8
  """
9
9
 
10
10
  duration_s: float
11
+
12
+
13
+ class UDCDefaultState(MxBlueskyParameters):
14
+ """Represents an instruction to execute the UDC default state plan."""
15
+
16
+ pass
17
+
18
+
19
+ class UDCCleanup(MxBlueskyParameters):
20
+ """Represents an instruction to perform UDC Cleanup,
21
+ in which the detector shutter is closed and a robot unload is performed."""
22
+
23
+ pass
@@ -7,7 +7,6 @@ from mx_bluesky.common.parameters.constants import (
7
7
  DeviceSettingsConstants,
8
8
  DocDescriptorNames,
9
9
  EnvironmentConstants,
10
- ExperimentParamConstants,
11
10
  FeatureSettings,
12
11
  FeatureSettingSources,
13
12
  HardwareConstants,
@@ -27,7 +26,6 @@ class I03Constants:
27
26
  OAV_CENTRING_FILE = OavConstants.OAV_CONFIG_JSON
28
27
  SHUTTER_TIME_S = 0.06
29
28
  USE_GPU_RESULTS = True
30
- OMEGA_FLIP = True
31
29
  ALTERNATE_ROTATION_DIRECTION = True
32
30
 
33
31
 
@@ -59,22 +57,17 @@ class HyperionFeatureSettings(FeatureSettings):
59
57
  class HyperionConstants:
60
58
  ZOCALO_ENV = EnvironmentConstants.ZOCALO_ENV
61
59
  HARDWARE = HardwareConstants()
62
- I03 = I03Constants()
63
- PARAM = ExperimentParamConstants()
64
60
  PLAN = PlanNameConstants()
65
61
  WAIT = PlanGroupCheckpointConstants()
66
62
  HYPERION_PORT = 5005
63
+ SUPERVISOR_PORT = 5006
67
64
  CALLBACK_0MQ_PROXY_PORTS = (5577, 5578)
68
65
  DESCRIPTORS = DocDescriptorNames()
69
- CONFIG_SERVER_URL = (
70
- "http://fake-url-not-real"
71
- if TEST_MODE
72
- else "https://daq-config.diamond.ac.uk/api"
73
- )
74
66
  GRAYLOG_PORT = 12232 # Hyperion stream
75
67
  GRAYLOG_STREAM_ID = "66264f5519ccca6d1c9e4e03"
76
68
  PARAMETER_SCHEMA_DIRECTORY = "src/hyperion/parameters/schemas/"
77
69
  LOG_FILE_NAME = "hyperion.log"
70
+ SUPERVISOR_LOG_FILE_NAME = "hyperion-supervisor.log"
78
71
  DEVICE_SETTINGS_CONSTANTS = DeviceSettingsConstants()
79
72
 
80
73
 
@@ -8,10 +8,12 @@ from mx_bluesky.common.parameters.components import (
8
8
  WithSample,
9
9
  WithVisit,
10
10
  )
11
+ from mx_bluesky.common.parameters.rotation import (
12
+ RotationScan,
13
+ )
11
14
  from mx_bluesky.hyperion.parameters.robot_load import (
12
15
  RobotLoadThenCentre,
13
16
  )
14
- from mx_bluesky.hyperion.parameters.rotation import RotationScan
15
17
 
16
18
  T = TypeVar("T", bound=BaseModel)
17
19
 
@@ -1,13 +1,14 @@
1
1
  import threading
2
2
  import time
3
- from collections.abc import Callable
3
+ from abc import abstractmethod
4
+ from collections.abc import Sequence
4
5
 
5
6
  from blueapi.core import BlueskyContext
6
7
  from bluesky import plan_stubs as bps
7
- from bluesky.utils import MsgGenerator, RequestAbort
8
+ from bluesky.utils import MsgGenerator
8
9
 
10
+ from mx_bluesky.common.parameters.components import MxBlueskyParameters
9
11
  from mx_bluesky.common.parameters.constants import Status
10
- from mx_bluesky.common.utils.exceptions import WarningError
11
12
  from mx_bluesky.common.utils.log import LOGGER
12
13
  from mx_bluesky.hyperion.runner import BaseRunner
13
14
 
@@ -19,63 +20,52 @@ class PlanError(Exception):
19
20
 
20
21
 
21
22
  class PlanRunner(BaseRunner):
22
- """Runner that executes experiments from inside a running Bluesky plan"""
23
-
24
- EXTERNAL_CALLBACK_WATCHDOG_TIMER_S = 60
25
23
  EXTERNAL_CALLBACK_POLL_INTERVAL_S = 1
24
+ EXTERNAL_CALLBACK_WATCHDOG_TIMER_S = 60
26
25
 
27
- def __init__(self, context: BlueskyContext, dev_mode: bool) -> None:
26
+ def __init__(self, context: BlueskyContext, dev_mode: bool):
28
27
  super().__init__(context)
29
- self.current_status: Status = Status.IDLE
30
- self.is_dev_mode = dev_mode
31
28
  self._callbacks_started = False
32
29
  self._callback_watchdog_expiry = time.monotonic()
30
+ self.is_dev_mode = dev_mode
33
31
 
34
- def execute_plan(
35
- self,
36
- experiment: Callable[[], MsgGenerator],
32
+ @abstractmethod
33
+ def decode_and_execute(
34
+ self, current_visit: str | None, parameter_list: Sequence[MxBlueskyParameters]
37
35
  ) -> MsgGenerator:
38
- """Execute the specified experiment plan.
39
- Args:
40
- experiment: The experiment to run
41
- Raises:
42
- PlanError: If the plan raised an exception
43
- RequestAbort: If the RunEngine aborted during execution"""
44
-
45
- self.current_status = Status.BUSY
46
-
47
- try:
48
- callback_expiry = time.monotonic() + self.EXTERNAL_CALLBACK_WATCHDOG_TIMER_S
49
- while time.monotonic() < callback_expiry:
50
- if self._callbacks_started:
51
- break
52
- # If on first launch the external callbacks aren't started yet, wait until they are
53
- LOGGER.info("Waiting for external callbacks to start")
54
- yield from bps.sleep(self.EXTERNAL_CALLBACK_POLL_INTERVAL_S)
55
- else:
56
- raise RuntimeError("External callbacks not running - try restarting")
57
-
58
- if not self._external_callbacks_are_alive():
59
- raise RuntimeError(
60
- "External callback watchdog timer expired, check external callbacks are running."
61
- )
62
- yield from experiment()
63
- self.current_status = Status.IDLE
64
- except WarningError as e:
65
- LOGGER.warning("Plan failed with warning", exc_info=e)
66
- self.current_status = Status.FAILED
67
- except RequestAbort:
68
- # This will occur when the run engine processes an abort when we shut down
69
- LOGGER.info("UDC Runner aborting")
70
- raise
71
- except Exception as e:
72
- LOGGER.error("Plan failed with exception", exc_info=e)
73
- self.current_status = Status.FAILED
74
- raise PlanError("Exception thrown in plan execution") from e
75
-
76
- def shutdown(self):
77
- """Performs a prompt shutdown. Aborts the run engine and terminates the loop
78
- waiting for messages."""
36
+ pass
37
+
38
+ def reset_callback_watchdog_timer(self):
39
+ """Called periodically to reset the watchdog timer when the external callbacks ping us."""
40
+ self._callbacks_started = True
41
+ self._callback_watchdog_expiry = (
42
+ time.monotonic() + self.EXTERNAL_CALLBACK_WATCHDOG_TIMER_S
43
+ )
44
+
45
+ @property
46
+ @abstractmethod
47
+ def current_status(self) -> Status:
48
+ pass
49
+
50
+ def check_external_callbacks_are_alive(self):
51
+ callback_expiry = time.monotonic() + self.EXTERNAL_CALLBACK_WATCHDOG_TIMER_S
52
+ while time.monotonic() < callback_expiry:
53
+ if self._callbacks_started:
54
+ break
55
+ # If on first launch the external callbacks aren't started yet, wait until they are
56
+ LOGGER.info("Waiting for external callbacks to start")
57
+ yield from bps.sleep(self.EXTERNAL_CALLBACK_POLL_INTERVAL_S)
58
+ else:
59
+ raise RuntimeError("External callbacks not running - try restarting")
60
+
61
+ if not self._external_callbacks_are_alive():
62
+ raise RuntimeError(
63
+ "External callback watchdog timer expired, check external callbacks are running."
64
+ )
65
+
66
+ def request_run_engine_abort(self):
67
+ """Asynchronously request an abort from the run engine. This cannot be done from
68
+ inside the main thread."""
79
69
 
80
70
  def issue_abort():
81
71
  try:
@@ -89,19 +79,8 @@ class PlanRunner(BaseRunner):
89
79
  exc_info=e,
90
80
  )
91
81
 
92
- LOGGER.info("Shutting down: Stopping the run engine gracefully")
93
- if self.current_status != Status.ABORTING:
94
- self.current_status = Status.ABORTING
95
- stopping_thread = threading.Thread(target=issue_abort)
96
- stopping_thread.start()
97
- return
98
-
99
- def reset_callback_watchdog_timer(self):
100
- """Called periodically to reset the watchdog timer when the external callbacks ping us."""
101
- self._callbacks_started = True
102
- self._callback_watchdog_expiry = (
103
- time.monotonic() + self.EXTERNAL_CALLBACK_WATCHDOG_TIMER_S
104
- )
82
+ stopping_thread = threading.Thread(target=issue_abort)
83
+ stopping_thread.start()
105
84
 
106
85
  def _external_callbacks_are_alive(self) -> bool:
107
86
  return time.monotonic() < self._callback_watchdog_expiry
@@ -4,23 +4,22 @@ from flask import Flask
4
4
  from flask_restful import Api, Resource
5
5
 
6
6
  from mx_bluesky.common.utils.log import LOGGER
7
- from mx_bluesky.hyperion.parameters.constants import HyperionConstants
8
7
  from mx_bluesky.hyperion.plan_runner import PlanRunner
9
8
 
10
9
 
11
10
  # Ignore this function for code coverage as there is no way to shut down
12
11
  # a server once it is started.
13
- def create_server_for_udc(runner: PlanRunner) -> Thread: # pragma: no cover
12
+ def create_server_for_udc(runner: PlanRunner, port: int) -> Thread: # pragma: no cover
14
13
  """Create a minimal API for Hyperion UDC mode"""
15
14
  app = create_app_for_udc(runner)
16
15
 
17
16
  flask_thread = Thread(
18
17
  target=app.run,
19
- kwargs={"host": "0.0.0.0", "port": HyperionConstants.HYPERION_PORT},
18
+ kwargs={"host": "0.0.0.0", "port": port},
20
19
  daemon=True,
21
20
  )
22
21
  flask_thread.start()
23
- LOGGER.info(f"Hyperion now listening on {HyperionConstants.HYPERION_PORT}")
22
+ LOGGER.info(f"Hyperion now listening on {port}")
24
23
  return flask_thread
25
24
 
26
25
 
@@ -0,0 +1,3 @@
1
+ from mx_bluesky.hyperion.supervisor._supervisor import SupervisorRunner
2
+
3
+ __all__ = ["SupervisorRunner"]
@@ -0,0 +1,116 @@
1
+ from collections.abc import Sequence
2
+
3
+ from blueapi.client.client import BlueapiClient
4
+ from blueapi.client.event_bus import BlueskyStreamingError
5
+ from blueapi.config import ApplicationConfig
6
+ from blueapi.core import BlueskyContext
7
+ from blueapi.service.model import TaskRequest
8
+ from bluesky import plan_stubs as bps
9
+ from bluesky.utils import MsgGenerator
10
+
11
+ from mx_bluesky.common.parameters.components import MxBlueskyParameters
12
+ from mx_bluesky.common.parameters.constants import Status
13
+ from mx_bluesky.common.utils.log import LOGGER
14
+ from mx_bluesky.hyperion.parameters.components import UDCCleanup, UDCDefaultState, Wait
15
+ from mx_bluesky.hyperion.parameters.load_centre_collect import LoadCentreCollect
16
+ from mx_bluesky.hyperion.plan_runner import PlanError, PlanRunner
17
+
18
+
19
+ class SupervisorRunner(PlanRunner):
20
+ """Runner that executes plans by delegating to a remote blueapi instance"""
21
+
22
+ def __init__(
23
+ self,
24
+ bluesky_context: BlueskyContext,
25
+ client_config: ApplicationConfig,
26
+ dev_mode: bool,
27
+ ):
28
+ super().__init__(bluesky_context, dev_mode)
29
+ self.blueapi_client = BlueapiClient.from_config(client_config)
30
+ self._current_status = Status.IDLE
31
+
32
+ def decode_and_execute(
33
+ self, current_visit: str | None, parameter_list: Sequence[MxBlueskyParameters]
34
+ ) -> MsgGenerator:
35
+ try:
36
+ yield from self.check_external_callbacks_are_alive()
37
+ except Exception as e:
38
+ raise PlanError(f"Exception raised during plan execution: {e}") from e
39
+ instrument_session = current_visit or "NO_VISIT"
40
+ try:
41
+ if self._current_status == Status.ABORTING:
42
+ raise PlanError("Plan execution cancelled, supervisor is shutting down")
43
+ self._current_status = Status.BUSY
44
+ for parameters in parameter_list:
45
+ LOGGER.info(
46
+ f"Executing plan with parameters: {parameters.model_dump_json(indent=2)}"
47
+ )
48
+ match parameters:
49
+ case LoadCentreCollect():
50
+ task_request = TaskRequest(
51
+ name="load_centre_collect",
52
+ params={"parameters": parameters},
53
+ instrument_session=instrument_session,
54
+ )
55
+ self._run_task_remotely(task_request)
56
+ case Wait():
57
+ yield from bps.sleep(parameters.duration_s)
58
+ case UDCDefaultState():
59
+ task_request = TaskRequest(
60
+ name="move_to_udc_default_state",
61
+ params={},
62
+ instrument_session=instrument_session,
63
+ )
64
+ self._run_task_remotely(task_request)
65
+ case UDCCleanup():
66
+ task_request = TaskRequest(
67
+ name="clean_up_udc",
68
+ params={"visit": current_visit},
69
+ instrument_session=instrument_session,
70
+ )
71
+ self._run_task_remotely(task_request)
72
+ case _:
73
+ raise AssertionError(
74
+ f"Unsupported instruction decoded from agamemnon {type(parameters)}"
75
+ )
76
+ except:
77
+ self._current_status = Status.FAILED
78
+ raise
79
+ else:
80
+ self._current_status = Status.IDLE
81
+ return current_visit
82
+
83
+ @property
84
+ def current_status(self) -> Status:
85
+ return self._current_status
86
+
87
+ def is_connected(self) -> bool:
88
+ try:
89
+ self.blueapi_client.get_state()
90
+ except Exception as e:
91
+ LOGGER.debug(f"Failed to get worker state: {e}")
92
+ return False
93
+ return True
94
+
95
+ def shutdown(self):
96
+ LOGGER.info(
97
+ "Hyperion supervisor received shutdown request, signalling abort to BlueAPI server..."
98
+ )
99
+ if self.current_status != Status.BUSY:
100
+ self.request_run_engine_abort()
101
+ else:
102
+ self._current_status = Status.ABORTING
103
+ self.blueapi_client.abort()
104
+
105
+ def _run_task_remotely(self, task_request: TaskRequest):
106
+ try:
107
+ self.blueapi_client.run_task(task_request)
108
+ except BlueskyStreamingError as e:
109
+ # We will receive a BlueskyStreamingError if the remote server
110
+ # processed an abort during plan execution, but this is not
111
+ # the only possible cause.
112
+ if self.current_status == Status.ABORTING:
113
+ LOGGER.info("Aborting local runner...")
114
+ self.request_run_engine_abort()
115
+ else:
116
+ raise PlanError(f"Exception raised during plan execution: {e}") from e
@@ -0,0 +1,6 @@
1
+ # Configuration for the BlueAPI client running in the hyperion supervisor
2
+ api:
3
+ url: http://localhost:5005
4
+ stomp:
5
+ enabled: true
6
+ url: tcp://localhost:61613
@@ -0,0 +1,10 @@
1
+ # Configuration for the supervisor BlueAPI context
2
+ # to access the beamline baton device
3
+ env:
4
+ sources:
5
+ - kind: deviceManager
6
+ module: dodal.beamlines.i03_supervisor
7
+ logging:
8
+ graylog:
9
+ url: "tcp://graylog-log-target.diamond.ac.uk:12232"
10
+ enabled: true
@@ -0,0 +1,9 @@
1
+ # Configuration for the supervisor BlueAPI context
2
+ # to access the beamline baton device
3
+ env:
4
+ sources:
5
+ - kind: deviceManager
6
+ module: dodal.beamlines.i03_supervisor
7
+ mock: true
8
+ events:
9
+ broadcast_status_events: false
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mx-bluesky
3
- Version: 1.5.15
3
+ Version: 1.5.16
4
4
  Summary: Bluesky tools for MX Beamlines at DLS
5
5
  Author-email: Dominic Oram <dominic.oram@diamond.ac.uk>
6
6
  License: Apache License
@@ -236,41 +236,13 @@ Requires-Dist: semver
236
236
  Requires-Dist: deepdiff
237
237
  Requires-Dist: matplotlib
238
238
  Requires-Dist: cachetools
239
+ Requires-Dist: bluesky-stomp>=0.2.0
239
240
  Requires-Dist: daq-config-server>=v1.0.0-rc.2
240
241
  Requires-Dist: blueapi>=1.8.0
241
242
  Requires-Dist: ophyd>=1.10.5
242
243
  Requires-Dist: ophyd-async>=0.14.0
243
244
  Requires-Dist: bluesky>=1.14.6
244
- Requires-Dist: dls-dodal==1.68.0
245
- Provides-Extra: dev
246
- Requires-Dist: black; extra == "dev"
247
- Requires-Dist: build; extra == "dev"
248
- Requires-Dist: diff-cover; extra == "dev"
249
- Requires-Dist: GitPython; extra == "dev"
250
- Requires-Dist: import-linter; extra == "dev"
251
- Requires-Dist: ispyb; extra == "dev"
252
- Requires-Dist: ipython; extra == "dev"
253
- Requires-Dist: mypy; extra == "dev"
254
- Requires-Dist: myst-parser; extra == "dev"
255
- Requires-Dist: pipdeptree; extra == "dev"
256
- Requires-Dist: plantweb; extra == "dev"
257
- Requires-Dist: pre-commit; extra == "dev"
258
- Requires-Dist: pydata-sphinx-theme>=0.12; extra == "dev"
259
- Requires-Dist: pyright==1.1.406; extra == "dev"
260
- Requires-Dist: pytest-asyncio; extra == "dev"
261
- Requires-Dist: pytest-cov; extra == "dev"
262
- Requires-Dist: pytest-random-order; extra == "dev"
263
- Requires-Dist: pytest-timeout; extra == "dev"
264
- Requires-Dist: pytest; extra == "dev"
265
- Requires-Dist: responses; extra == "dev"
266
- Requires-Dist: ruff; extra == "dev"
267
- Requires-Dist: sphinx-autobuild; extra == "dev"
268
- Requires-Dist: sphinx-copybutton; extra == "dev"
269
- Requires-Dist: sphinx-design; extra == "dev"
270
- Requires-Dist: tox-direct; extra == "dev"
271
- Requires-Dist: tox; extra == "dev"
272
- Requires-Dist: types-mock; extra == "dev"
273
- Requires-Dist: types-requests; extra == "dev"
245
+ Requires-Dist: dls-dodal==1.69.0
274
246
  Dynamic: license-file
275
247
 
276
248
  mx-bluesky