mx-bluesky 1.5.2__py3-none-any.whl → 1.5.4__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 (64) hide show
  1. mx_bluesky/_version.py +16 -3
  2. mx_bluesky/beamlines/aithre_lasershaping/__init__.py +2 -0
  3. mx_bluesky/beamlines/aithre_lasershaping/beamline_safe.py +17 -0
  4. mx_bluesky/beamlines/i04/__init__.py +7 -3
  5. mx_bluesky/beamlines/i04/experiment_plans/i04_grid_detect_then_xray_centre_plan.py +3 -8
  6. mx_bluesky/beamlines/i04/thawing_plan.py +3 -3
  7. mx_bluesky/beamlines/i24/serial/blueapi_config.yaml +2 -2
  8. mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/CustomChip_py3v1.edl +10 -10
  9. mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/DetStage.edl +1 -1
  10. mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/DiamondChipI24-py3v1.edl +68 -68
  11. mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/ME14E-GeneralPurpose.edl +120 -120
  12. mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/MappingLite-oxford_py3v1.edl +135 -135
  13. mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/PMAC_Command.edl +2 -2
  14. mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/Shutter_Control.edl +3 -3
  15. mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/nudgechip.edl +24 -24
  16. mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/pumpprobe-py3v1.edl +12 -12
  17. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Manager_py3v1.py +13 -12
  18. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_moveonclick.py +10 -10
  19. mx_bluesky/beamlines/i24/serial/parameters/utils.py +1 -1
  20. mx_bluesky/beamlines/i24/serial/set_visit_directory.sh +1 -1
  21. mx_bluesky/beamlines/i24/serial/setup_beamline/pv.py +142 -135
  22. mx_bluesky/common/device_setup_plans/manipulate_sample.py +2 -2
  23. mx_bluesky/common/experiment_plans/common_grid_detect_then_xray_centre_plan.py +2 -2
  24. mx_bluesky/common/experiment_plans/inner_plans/udc_default_state.py +65 -0
  25. mx_bluesky/common/experiment_plans/oav_grid_detection_plan.py +12 -2
  26. mx_bluesky/common/experiment_plans/oav_snapshot_plan.py +2 -2
  27. mx_bluesky/common/external_interaction/alerting/__init__.py +13 -0
  28. mx_bluesky/common/external_interaction/alerting/_service.py +82 -0
  29. mx_bluesky/common/external_interaction/alerting/log_based_service.py +57 -0
  30. mx_bluesky/common/external_interaction/callbacks/common/grid_detection_callback.py +35 -17
  31. mx_bluesky/common/external_interaction/callbacks/sample_handling/sample_handling_callback.py +31 -6
  32. mx_bluesky/common/external_interaction/config_server.py +151 -54
  33. mx_bluesky/common/parameters/constants.py +27 -8
  34. mx_bluesky/common/parameters/gridscan.py +1 -1
  35. mx_bluesky/hyperion/__main__.py +50 -178
  36. mx_bluesky/hyperion/baton_handler.py +130 -69
  37. mx_bluesky/hyperion/device_setup_plans/dcm_pitch_roll_mirror_adjuster.py +29 -24
  38. mx_bluesky/hyperion/experiment_plans/hyperion_flyscan_xray_centre_plan.py +4 -1
  39. mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py +17 -5
  40. mx_bluesky/hyperion/experiment_plans/pin_tip_centring_plan.py +1 -1
  41. mx_bluesky/hyperion/experiment_plans/robot_load_and_change_energy.py +6 -2
  42. mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +2 -3
  43. mx_bluesky/hyperion/external_interaction/agamemnon.py +128 -73
  44. mx_bluesky/hyperion/external_interaction/alerting/__init__.py +0 -0
  45. mx_bluesky/hyperion/external_interaction/alerting/constants.py +12 -0
  46. mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +9 -0
  47. mx_bluesky/hyperion/external_interaction/callbacks/alert_on_container_change.py +54 -0
  48. mx_bluesky/hyperion/external_interaction/callbacks/rotation/nexus_callback.py +2 -2
  49. mx_bluesky/hyperion/external_interaction/config_server.py +12 -31
  50. mx_bluesky/hyperion/parameters/cli.py +15 -3
  51. mx_bluesky/hyperion/parameters/components.py +7 -5
  52. mx_bluesky/hyperion/parameters/constants.py +20 -4
  53. mx_bluesky/hyperion/parameters/gridscan.py +22 -14
  54. mx_bluesky/hyperion/parameters/load_centre_collect.py +1 -14
  55. mx_bluesky/hyperion/parameters/robot_load.py +1 -4
  56. mx_bluesky/hyperion/parameters/rotation.py +1 -2
  57. mx_bluesky/hyperion/plan_runner.py +78 -0
  58. mx_bluesky/hyperion/runner.py +189 -0
  59. {mx_bluesky-1.5.2.dist-info → mx_bluesky-1.5.4.dist-info}/METADATA +4 -3
  60. {mx_bluesky-1.5.2.dist-info → mx_bluesky-1.5.4.dist-info}/RECORD +64 -55
  61. {mx_bluesky-1.5.2.dist-info → mx_bluesky-1.5.4.dist-info}/entry_points.txt +0 -2
  62. {mx_bluesky-1.5.2.dist-info → mx_bluesky-1.5.4.dist-info}/WHEEL +0 -0
  63. {mx_bluesky-1.5.2.dist-info → mx_bluesky-1.5.4.dist-info}/licenses/LICENSE +0 -0
  64. {mx_bluesky-1.5.2.dist-info → mx_bluesky-1.5.4.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
1
- from typing import TypedDict
1
+ from typing import Generic, TypedDict, TypeVar
2
2
 
3
3
  import numpy as np
4
4
  from bluesky.callbacks import CallbackBase
@@ -7,6 +7,8 @@ from event_model.documents import Event
7
7
 
8
8
  from mx_bluesky.common.utils.log import LOGGER
9
9
 
10
+ T = TypeVar("T", int, float)
11
+
10
12
 
11
13
  class GridParamUpdate(TypedDict):
12
14
  """
@@ -40,14 +42,22 @@ class GridParamUpdate(TypedDict):
40
42
  z_step_size_um: float
41
43
 
42
44
 
45
+ class XYZParams(TypedDict, Generic[T]):
46
+ x: T
47
+ y: T
48
+ z: T
49
+
50
+
43
51
  class GridDetectionCallback(CallbackBase):
52
+ OMEGA_TOLERANCE = 1
53
+
44
54
  def __init__(
45
55
  self,
46
56
  *args,
47
57
  ) -> None:
48
58
  super().__init__(*args)
49
- self.start_positions_mm: list = []
50
- self.box_numbers: list = []
59
+ self.start_positions_um: XYZParams[float] = XYZParams(x=0, y=0, z=0)
60
+ self.box_numbers: XYZParams[int] = XYZParams(x=0, y=0, z=0)
51
61
 
52
62
  def event(self, doc: Event):
53
63
  data = doc.get("data")
@@ -82,13 +92,21 @@ class GridDetectionCallback(CallbackBase):
82
92
  )
83
93
  LOGGER.info(f"Calculated start position {position_grid_start_mm}")
84
94
 
85
- self.start_positions_mm.append(position_grid_start_mm)
86
- self.box_numbers.append(
87
- (
88
- data["oav-grid_snapshot-num_boxes_x"],
89
- data["oav-grid_snapshot-num_boxes_y"],
95
+ # If data is taken at omega=~0 then it gives us x-y info, at omega=~-90 it is x-z
96
+ if abs(smargon_omega) < self.OMEGA_TOLERANCE:
97
+ self.start_positions_um["x"] = position_grid_start_mm[0] * 1000
98
+ self.start_positions_um["y"] = position_grid_start_mm[1] * 1000
99
+ self.box_numbers["x"] = data["oav-grid_snapshot-num_boxes_x"]
100
+ self.box_numbers["y"] = data["oav-grid_snapshot-num_boxes_y"]
101
+ elif abs(smargon_omega + 90) < self.OMEGA_TOLERANCE:
102
+ self.start_positions_um["x"] = position_grid_start_mm[0] * 1000
103
+ self.start_positions_um["z"] = position_grid_start_mm[2] * 1000
104
+ self.box_numbers["x"] = data["oav-grid_snapshot-num_boxes_x"]
105
+ self.box_numbers["z"] = data["oav-grid_snapshot-num_boxes_y"]
106
+ else:
107
+ raise ValueError(
108
+ f"Grid detection only works at omegas of 0 or -90, omega of {smargon_omega} given."
90
109
  )
91
- )
92
110
 
93
111
  self.x_step_size_um = box_width_px * microns_per_pixel_x
94
112
  self.y_step_size_um = box_width_px * microns_per_pixel_y
@@ -97,14 +115,14 @@ class GridDetectionCallback(CallbackBase):
97
115
 
98
116
  def get_grid_parameters(self) -> GridParamUpdate:
99
117
  return {
100
- "x_start_um": self.start_positions_mm[0][0] * 1000,
101
- "y_start_um": self.start_positions_mm[0][1] * 1000,
102
- "y2_start_um": self.start_positions_mm[0][1] * 1000,
103
- "z_start_um": self.start_positions_mm[1][2] * 1000,
104
- "z2_start_um": self.start_positions_mm[1][2] * 1000,
105
- "x_steps": self.box_numbers[0][0],
106
- "y_steps": self.box_numbers[0][1],
107
- "z_steps": self.box_numbers[1][1],
118
+ "x_start_um": self.start_positions_um["x"],
119
+ "y_start_um": self.start_positions_um["y"],
120
+ "y2_start_um": self.start_positions_um["y"],
121
+ "z_start_um": self.start_positions_um["z"],
122
+ "z2_start_um": self.start_positions_um["z"],
123
+ "x_steps": self.box_numbers["x"],
124
+ "y_steps": self.box_numbers["y"],
125
+ "z_steps": self.box_numbers["z"],
108
126
  "x_step_size_um": self.x_step_size_um,
109
127
  "y_step_size_um": self.y_step_size_um,
110
128
  "z_step_size_um": self.z_step_size_um,
@@ -1,5 +1,10 @@
1
+ from dodal.utils import get_beamline_name
1
2
  from event_model import RunStart, RunStop
2
3
 
4
+ from mx_bluesky.common.external_interaction.alerting import (
5
+ Metadata,
6
+ get_alerting_service,
7
+ )
3
8
  from mx_bluesky.common.external_interaction.callbacks.common.plan_reactive_callback import (
4
9
  PlanReactiveCallback,
5
10
  )
@@ -12,36 +17,43 @@ from mx_bluesky.common.utils.log import ISPYB_ZOCALO_CALLBACK_LOGGER
12
17
 
13
18
 
14
19
  class SampleHandlingCallback(PlanReactiveCallback):
15
- """Intercepts exceptions from experiment plans and updates the ISPyB BLSampleStatus
16
- field according to the type of exception raised."""
20
+ """Intercepts exceptions from experiment plans and:
21
+ * Updates the ISPyB BLSampleStatus field according to the type of exception raised.
22
+ * Triggers an alert with details of the error."""
17
23
 
18
24
  def __init__(self, record_loaded_on_success=False):
19
25
  super().__init__(log=ISPYB_ZOCALO_CALLBACK_LOGGER)
20
26
  self._sample_id: int | None = None
27
+ self._visit: str | None = None
21
28
  self._descriptor: str | None = None
22
29
  self._run_id: str | None = None
30
+ self._container: int | None = None
23
31
 
24
32
  # Record 'sample loaded' if document successfully stops
25
33
  self.record_loaded_on_success = record_loaded_on_success
26
34
 
27
35
  def activity_gated_start(self, doc: RunStart):
28
36
  if not self._sample_id and self.active:
29
- sample_id = doc.get("metadata", {}).get("sample_id")
37
+ metadata = doc.get("metadata", {})
38
+ sample_id = metadata.get("sample_id")
30
39
  self.log.info(f"Recording sample ID at run start {sample_id}")
31
40
  self._sample_id = sample_id
41
+ self._visit = metadata.get("visit")
32
42
  self._run_id = self.activity_uid
43
+ self._container = metadata.get("container")
33
44
 
34
45
  def activity_gated_stop(self, doc: RunStop) -> RunStop:
35
46
  if self._run_id == doc.get("run_start"):
36
47
  expeye = ExpeyeInteraction()
37
48
  if doc["exit_status"] != "success":
49
+ reason = doc.get("reason", "")
38
50
  exception_type, message = SampleException.type_and_message_from_reason(
39
- doc.get("reason", "")
51
+ reason
40
52
  )
41
53
  self.log.info(
42
54
  f"Sample handling callback intercepted exception of type {exception_type}: {message}"
43
55
  )
44
- self._record_exception(exception_type, expeye)
56
+ self._record_exception(exception_type, expeye, reason)
45
57
 
46
58
  elif self.record_loaded_on_success:
47
59
  self._record_loaded(expeye)
@@ -51,10 +63,23 @@ class SampleHandlingCallback(PlanReactiveCallback):
51
63
 
52
64
  return doc
53
65
 
54
- def _record_exception(self, exception_type: str, expeye: ExpeyeInteraction):
66
+ def _record_exception(
67
+ self, exception_type: str, expeye: ExpeyeInteraction, reason: str
68
+ ):
55
69
  assert self._sample_id, "Unable to record exception due to no sample ID"
56
70
  sample_status = self._decode_sample_status(exception_type)
57
71
  expeye.update_sample_status(self._sample_id, sample_status)
72
+ if sample_status == BLSampleStatus.ERROR_BEAMLINE:
73
+ beamline = get_beamline_name("")
74
+ get_alerting_service().raise_alert(
75
+ f"UDC encountered an error on {beamline}",
76
+ f"Hyperion encountered the following beamline error: {reason}",
77
+ {
78
+ Metadata.SAMPLE_ID: str(self._sample_id),
79
+ Metadata.VISIT: self._visit or "",
80
+ Metadata.CONTAINER: str(self._container),
81
+ },
82
+ )
58
83
 
59
84
  def _decode_sample_status(self, exception_type: str) -> BLSampleStatus:
60
85
  match exception_type:
@@ -1,57 +1,154 @@
1
- from abc import ABC, abstractmethod
2
- from functools import cache
1
+ import json
2
+ from dataclasses import fields
3
+ from enum import Enum
4
+ from pathlib import Path
5
+ from typing import Any, Generic, TypeVar
3
6
 
4
7
  from daq_config_server.client import ConfigServer
5
- from pydantic import BaseModel, Field, model_validator
6
-
7
-
8
- class FeatureFlags(BaseModel, ABC):
9
- """Abstract class to use ConfigServer to toggle features for an experiment
10
-
11
- A module wanting to use FeatureFlags should inherit this class, add boolean features
12
- as attributes, and implement a get_config_server method, which returns a cached creation of
13
- ConfigServer. See HyperionFeatureFlags for an example
14
-
15
- Values supplied upon class instantiation will always take priority over the config server. If connection to the server cannot
16
- be made AND values were not supplied, attributes will use their default values
17
- """
18
-
19
- # Feature values supplied at construction will override values from the config server
20
- overriden_features: dict = Field(default_factory=dict, exclude=True)
21
-
22
- @staticmethod
23
- @cache
24
- @abstractmethod
25
- def get_config_server() -> ConfigServer: ...
26
-
27
- @model_validator(mode="before")
28
- @classmethod
29
- def mark_overridden_features(cls, values):
30
- assert isinstance(values, dict)
31
- values["overriden_features"] = values.copy()
32
- cls._validate_overridden_features(values)
33
- return values
34
-
35
- @classmethod
36
- def _validate_overridden_features(cls, values: dict):
37
- """Validates overridden features to ensure they are defined in the model fields."""
38
- defined_fields = cls.model_fields.keys()
39
- invalid_features = [key for key in values.keys() if key not in defined_fields]
40
-
41
- if invalid_features:
42
- message = f"Invalid feature toggle(s) supplied: {invalid_features}. "
43
- raise ValueError(message)
44
-
45
- def _get_flags(self):
46
- flags = type(self).get_config_server().best_effort_get_all_feature_flags()
47
- return {f: flags[f] for f in flags if f in self.model_fields.keys()}
48
-
49
- def update_self_from_server(self):
50
- """Used to update the feature flags from the server during a plan. Where there are flags which were explicitly set from externally supplied parameters, these values will be used instead."""
51
- for flag, value in self._get_flags().items():
52
- updated_value = (
53
- value
54
- if flag not in self.overriden_features.keys()
55
- else self.overriden_features[flag]
8
+ from pydantic import TypeAdapter
9
+
10
+ from mx_bluesky.common.parameters.constants import (
11
+ GDA_DOMAIN_PROPERTIES_PATH,
12
+ FeatureSetting,
13
+ FeatureSettingources,
14
+ OavConstants,
15
+ )
16
+ from mx_bluesky.common.utils.log import LOGGER
17
+
18
+ FEATURE_FLAG_CACHE_LENGTH_S = 60 * 5
19
+ # Used by the config server when refreshing its cache
20
+ _JSON_CONFIG_PATHS = [OavConstants.OAV_CONFIG_JSON]
21
+
22
+ T = TypeVar("T", bound=FeatureSetting)
23
+
24
+
25
+ class MXConfigClient(ConfigServer, Generic[T]):
26
+ def __init__(
27
+ self,
28
+ feature_sources: type[FeatureSettingources],
29
+ feature_dc: type[T],
30
+ url: str = "https://daq-config.diamond.ac.uk",
31
+ ):
32
+ """MX implementation of the config server client. Makes requests to the config server to retrieve config while falling back to
33
+ the filesystem in the case that the request failed.
34
+
35
+ See mx_bluesky/hyperion/external_interaction/config_server.py for example implementation.
36
+
37
+ Args:
38
+ feature_sources: A StrEnum containing available features, where the string is the name of that feature toggle in a beamline's GDA
39
+ domain.properties.
40
+
41
+ feature_dc: A dataclass containing available features along with their default flags. This dataclass must contain the same keys
42
+ as the feature_sources parameter. These defaults are used when a server request fails.
43
+ """
44
+
45
+ self.feature_sources = feature_sources
46
+ self.feature_dc: type[T] = feature_dc
47
+ self._cached_features: T | None = None
48
+ self._cached_json_config: dict[str, dict[str, Any]] = {}
49
+ self._time_of_last_feature_get: float = 0
50
+ self._verify_feature_parameters()
51
+ super().__init__(url)
52
+
53
+ def get_feature_flags(self) -> T:
54
+ """Get feature flags by making a request to the config server. If the request fails, use the hardcoded defaults. Store results in a cache
55
+ which should be updated at the start of a plan using self.refresh_cache()
56
+ """
57
+ return self._get_feature_flags()
58
+
59
+ def get_json_config(self, path_to_json: Path | str) -> dict[str, Any]:
60
+ """Get the OAV config in the form of a python dictionary. Store results in a cache
61
+ which should be updated at the start of a plan using self.refresh_cache().
62
+
63
+ Note that this method replicates the functionality of the config server, but provides
64
+ a fallback incase of server. It will be removed once the config server has been run reliably
65
+ for several months.
66
+
67
+ Args:
68
+ path_to_json: Absolute file path to json file
69
+ """
70
+ return self._get_json_config(path_to_json)
71
+
72
+ def refresh_cache(self):
73
+ """Refresh the client's cache. Use at the beginning of a plan"""
74
+ self._get_feature_flags(reset_cached_result=True)
75
+ for path in _JSON_CONFIG_PATHS:
76
+ self._get_json_config(path, reset_cached_result=True)
77
+
78
+ def _verify_feature_parameters(self):
79
+ sources_keys = {feature.name for feature in self.feature_sources}
80
+ feature_dc_keys = {key.name for key in fields(self.feature_dc)}
81
+ assert sources_keys == feature_dc_keys, (
82
+ f"MXConfig server feature_sources names do not match feature_dc keys: {sources_keys} != {feature_dc_keys}"
83
+ )
84
+
85
+ def _get_feature_flags(self, reset_cached_result=False) -> T:
86
+ """
87
+ Args:
88
+ reset_cached_result (bool): Force refresh the cache for this request
89
+ """
90
+ try:
91
+ if reset_cached_result:
92
+ self._cached_features = None
93
+ if not self._cached_features:
94
+ self._cached_features = None
95
+ # Construct self.feature_dc by reading the domain.properties file
96
+ all_features = list(self.feature_sources)
97
+ feature_dict = {}
98
+ domain_properties = self.get_file_contents(
99
+ GDA_DOMAIN_PROPERTIES_PATH, reset_cached_result=reset_cached_result
100
+ ).splitlines()
101
+ for line in domain_properties:
102
+ line = line.strip()
103
+ if not line or line.startswith("#"):
104
+ continue
105
+ line = line.split("#", 1)[0].strip() # Remove inline comments
106
+ if "=" in line:
107
+ key, value = map(str.strip, line.split("=", 1))
108
+ for feature in all_features:
109
+ assert isinstance(feature, Enum)
110
+ if key == feature.value:
111
+ feature_dict[feature.name] = value
112
+ self._check_missing_fields(
113
+ {f.name for f in fields(self.feature_dc)}, set(feature_dict.keys())
114
+ )
115
+ self._cached_features = self.feature_dc(**feature_dict)
116
+ return self._cached_features
117
+ except Exception as e:
118
+ LOGGER.warning(
119
+ f"Failed to get feature flags from config server: {e} \nUsing defaults..."
120
+ )
121
+ return self.feature_dc()
122
+
123
+ def _check_missing_fields(self, expected: set, actual: set):
124
+ missing = expected - actual
125
+ if missing:
126
+ LOGGER.warning(
127
+ f"Missing features from domain.properties: {missing}.\n Using defaults for missing features"
56
128
  )
57
- setattr(self, flag, updated_value)
129
+
130
+ def _get_json_config(
131
+ self, path_to_json: Path | str, reset_cached_result=False
132
+ ) -> dict[str, Any]:
133
+ """
134
+ Args:
135
+ reset_cached_result (bool): Force refresh the cache for this request
136
+ """
137
+ if reset_cached_result:
138
+ self._cached_json_config = {}
139
+ str_to_json = str(path_to_json)
140
+ if str_to_json not in self._cached_json_config:
141
+ try:
142
+ self._cached_json_config[str_to_json] = self.get_file_contents(
143
+ path_to_json, dict, reset_cached_result=reset_cached_result
144
+ )
145
+
146
+ except Exception as e:
147
+ LOGGER.warning(
148
+ f"Failed to get json config from config server: {e} \nReading the file directly..."
149
+ )
150
+ with open(path_to_json) as f:
151
+ self._cached_json_config[str_to_json] = TypeAdapter(
152
+ dict[str, Any]
153
+ ).validate_python(json.loads(f.read()))
154
+ return self._cached_json_config[str_to_json]
@@ -1,5 +1,5 @@
1
1
  import os
2
- from enum import Enum
2
+ from enum import Enum, StrEnum
3
3
 
4
4
  from dodal.devices.aperturescatterguard import ApertureValue
5
5
  from dodal.devices.detector import EIGER2_X_16M_SIZE
@@ -13,10 +13,17 @@ BEAMLINE = get_beamline_name("test")
13
13
  TEST_MODE = BEAMLINE == "test"
14
14
  ZEBRA_STATUS_TIMEOUT = 30
15
15
 
16
+ GDA_DOMAIN_PROPERTIES_PATH = (
17
+ "tests/test_data/test_domain_properties"
18
+ if TEST_MODE
19
+ else (f"/dls_sw/{BEAMLINE}/software/daq_configuration/domain/domain.properties")
20
+ )
21
+
16
22
 
17
23
  @dataclass(frozen=True)
18
24
  class DocDescriptorNames:
19
25
  # Robot load/unload event descriptor
26
+ ROBOT_PRE_LOAD = "robot_update_pre_load"
20
27
  ROBOT_UPDATE = "robot_update"
21
28
  # For callbacks to use
22
29
  OAV_ROTATION_SNAPSHOT_TRIGGERED = "rotation_snapshot_triggered"
@@ -28,15 +35,18 @@ class DocDescriptorNames:
28
35
  FLYSCAN_RESULTS = "flyscan_results_obtained"
29
36
 
30
37
 
38
+ def _get_oav_config_json_path():
39
+ if TEST_MODE:
40
+ return "tests/test_data/test_OAVCentring.json"
41
+ elif BEAMLINE == "i03":
42
+ return f"/dls_sw/{BEAMLINE}/software/daq_configuration/json/OAVCentring_hyperion.json"
43
+ else:
44
+ return f"/dls_sw/{BEAMLINE}/software/daq_configuration/json/OAVCentring.json"
45
+
46
+
31
47
  @dataclass(frozen=True)
32
48
  class OavConstants:
33
- OAV_CONFIG_JSON = (
34
- "tests/test_data/test_OAVCentring.json"
35
- if TEST_MODE
36
- else (
37
- f"/dls_sw/{BEAMLINE}/software/daq_configuration/json/OAVCentring_hyperion.json"
38
- )
39
- )
49
+ OAV_CONFIG_JSON = _get_oav_config_json_path()
40
50
 
41
51
 
42
52
  @dataclass(frozen=True)
@@ -150,3 +160,12 @@ class Status(Enum):
150
160
  BUSY = "Busy"
151
161
  ABORTING = "Aborting"
152
162
  IDLE = "Idle"
163
+
164
+
165
+ @dataclass
166
+ class FeatureSetting: ... # List of features and their default values. Subclasses must also be a pydantic dataclass
167
+
168
+
169
+ class FeatureSettingources(
170
+ StrEnum
171
+ ): ... # List of features and the name of that property in domain.properties
@@ -102,7 +102,7 @@ class SpecifiedThreeDGridScan(
102
102
  """Parameters representing a so-called 3D grid scan, which consists of doing a
103
103
  gridscan in X and Y, followed by one in X and Z."""
104
104
 
105
- grid1_omega_deg: float = Field(default=GridscanParamConstants.OMEGA_1) # type: ignore
105
+ grid1_omega_deg: float = Field(default=GridscanParamConstants.OMEGA_1)
106
106
  grid2_omega_deg: float = Field(default=GridscanParamConstants.OMEGA_2)
107
107
  x_step_size_um: float = Field(default=GridscanParamConstants.BOX_WIDTH_UM)
108
108
  y_step_size_um: float = Field(default=GridscanParamConstants.BOX_WIDTH_UM)