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.
- mx_bluesky/Getting started.ipynb +1 -0
- mx_bluesky/_version.py +2 -2
- mx_bluesky/beamlines/i04/callbacks/murko_callback.py +18 -0
- mx_bluesky/beamlines/i04/experiment_plans/i04_grid_detect_then_xray_centre_plan.py +3 -2
- mx_bluesky/beamlines/i04/thawing_plan.py +1 -0
- mx_bluesky/beamlines/i24/jungfrau_commissioning/__init__.py +13 -0
- mx_bluesky/beamlines/i24/jungfrau_commissioning/callbacks/__init__.py +0 -0
- mx_bluesky/beamlines/i24/jungfrau_commissioning/callbacks/metadata_writer.py +86 -0
- mx_bluesky/beamlines/i24/jungfrau_commissioning/composites.py +35 -0
- mx_bluesky/beamlines/i24/jungfrau_commissioning/experiment_plans/do_darks.py +18 -19
- mx_bluesky/beamlines/i24/jungfrau_commissioning/experiment_plans/rotation_scan_plan.py +292 -0
- mx_bluesky/beamlines/i24/jungfrau_commissioning/plan_stubs/do_external_acquisition.py +3 -8
- mx_bluesky/beamlines/i24/jungfrau_commissioning/plan_stubs/do_internal_acquisition.py +4 -5
- mx_bluesky/beamlines/i24/jungfrau_commissioning/plan_stubs/plan_utils.py +14 -18
- mx_bluesky/beamlines/i24/parameters/__init__.py +0 -0
- mx_bluesky/beamlines/i24/parameters/constants.py +9 -0
- mx_bluesky/beamlines/i24/serial/extruder/i24ssx_extruder_collect_py3v2.py +1 -1
- mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_moveonclick.py +2 -2
- mx_bluesky/common/device_setup_plans/robot_load_unload.py +2 -24
- mx_bluesky/common/device_setup_plans/setup_zebra_and_shutter.py +5 -2
- mx_bluesky/common/experiment_plans/common_flyscan_xray_centre_plan.py +1 -1
- mx_bluesky/common/experiment_plans/inner_plans/read_hardware.py +1 -0
- mx_bluesky/common/experiment_plans/rotation/__init__.py +0 -0
- mx_bluesky/common/experiment_plans/rotation/rotation_utils.py +127 -0
- mx_bluesky/common/external_interaction/callbacks/common/ispyb_callback_base.py +13 -2
- mx_bluesky/common/external_interaction/callbacks/common/ispyb_mapping.py +0 -2
- mx_bluesky/common/external_interaction/ispyb/data_model.py +1 -1
- mx_bluesky/common/external_interaction/ispyb/exp_eye_store.py +1 -1
- mx_bluesky/common/parameters/components.py +17 -7
- mx_bluesky/common/parameters/constants.py +6 -0
- mx_bluesky/{hyperion → common}/parameters/rotation.py +10 -8
- mx_bluesky/common/preprocessors/preprocessors.py +98 -36
- mx_bluesky/hyperion/__main__.py +55 -22
- mx_bluesky/hyperion/baton_handler.py +24 -64
- mx_bluesky/hyperion/blueapi_config.yaml +17 -0
- mx_bluesky/hyperion/blueapi_dev_config.yaml +16 -0
- mx_bluesky/hyperion/blueapi_plans/__init__.py +96 -0
- mx_bluesky/hyperion/device_setup_plans/dcm_pitch_roll_mirror_adjuster.py +8 -6
- mx_bluesky/hyperion/device_setup_plans/setup_panda.py +1 -1
- mx_bluesky/hyperion/experiment_plans/experiment_registry.py +3 -1
- mx_bluesky/hyperion/experiment_plans/hyperion_flyscan_xray_centre_plan.py +1 -0
- mx_bluesky/hyperion/experiment_plans/hyperion_grid_detect_then_xray_centre_plan.py +2 -2
- mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py +3 -1
- mx_bluesky/hyperion/experiment_plans/pin_centre_then_xray_centre_plan.py +17 -6
- mx_bluesky/hyperion/experiment_plans/robot_load_and_change_energy.py +0 -3
- mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +12 -126
- mx_bluesky/hyperion/experiment_plans/set_energy_plan.py +2 -2
- mx_bluesky/hyperion/external_interaction/agamemnon.py +3 -8
- mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +121 -47
- mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_callback.py +3 -1
- mx_bluesky/hyperion/external_interaction/callbacks/rotation/ispyb_mapping.py +3 -1
- mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py +6 -3
- mx_bluesky/hyperion/external_interaction/callbacks/stomp/__init__.py +0 -0
- mx_bluesky/hyperion/external_interaction/callbacks/stomp/dispatcher.py +33 -0
- mx_bluesky/hyperion/in_process_runner.py +132 -0
- mx_bluesky/hyperion/parameters/cli.py +43 -4
- mx_bluesky/hyperion/parameters/components.py +13 -0
- mx_bluesky/hyperion/parameters/constants.py +2 -9
- mx_bluesky/hyperion/parameters/load_centre_collect.py +3 -1
- mx_bluesky/hyperion/plan_runner.py +45 -66
- mx_bluesky/hyperion/plan_runner_api.py +3 -4
- mx_bluesky/hyperion/supervisor/__init__.py +3 -0
- mx_bluesky/hyperion/supervisor/_supervisor.py +116 -0
- mx_bluesky/hyperion/supervisor/client_config.yaml +6 -0
- mx_bluesky/hyperion/supervisor/supervisor_config.yaml +10 -0
- mx_bluesky/hyperion/supervisor/supervisor_dev_config.yaml +9 -0
- {mx_bluesky-1.5.15.dist-info → mx_bluesky-1.5.16.dist-info}/METADATA +3 -31
- {mx_bluesky-1.5.15.dist-info → mx_bluesky-1.5.16.dist-info}/RECORD +72 -52
- {mx_bluesky-1.5.15.dist-info → mx_bluesky-1.5.16.dist-info}/WHEEL +0 -0
- {mx_bluesky-1.5.15.dist-info → mx_bluesky-1.5.16.dist-info}/entry_points.txt +0 -0
- {mx_bluesky-1.5.15.dist-info → mx_bluesky-1.5.16.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
30
|
-
"""
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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)
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def decode_and_execute(
|
|
34
|
+
self, current_visit: str | None, parameter_list: Sequence[MxBlueskyParameters]
|
|
37
35
|
) -> MsgGenerator:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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":
|
|
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 {
|
|
22
|
+
LOGGER.info(f"Hyperion now listening on {port}")
|
|
24
23
|
return flask_thread
|
|
25
24
|
|
|
26
25
|
|
|
@@ -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,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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mx-bluesky
|
|
3
|
-
Version: 1.5.
|
|
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.
|
|
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
|