Skip to content

Commit 8a05a26

Browse files
authored
feat: Add support for device manager beamline modules (#1267)
1 parent cef2b55 commit 8a05a26

File tree

8 files changed

+279
-6
lines changed

8 files changed

+279
-6
lines changed

helm/blueapi/config_schema.json

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,39 @@
6464
"title": "CORSConfig",
6565
"type": "object"
6666
},
67+
"DeviceManagerSource": {
68+
"additionalProperties": false,
69+
"properties": {
70+
"module": {
71+
"description": "Module to be imported",
72+
"title": "Module",
73+
"type": "string"
74+
},
75+
"kind": {
76+
"const": "deviceManager",
77+
"default": "deviceManager",
78+
"title": "Kind",
79+
"type": "string"
80+
},
81+
"mock": {
82+
"default": false,
83+
"description": "If true, ophyd_async device connections are mocked",
84+
"title": "Mock",
85+
"type": "boolean"
86+
},
87+
"name": {
88+
"default": "devices",
89+
"description": "Name of the device manager in the module",
90+
"title": "Name",
91+
"type": "string"
92+
}
93+
},
94+
"required": [
95+
"module"
96+
],
97+
"title": "DeviceManagerSource",
98+
"type": "object"
99+
},
67100
"DeviceSource": {
68101
"additionalProperties": false,
69102
"properties": {
@@ -131,6 +164,7 @@
131164
"discriminator": {
132165
"mapping": {
133166
"deviceFunctions": "#/$defs/DeviceSource",
167+
"deviceManager": "#/$defs/DeviceManagerSource",
134168
"dodal": "#/$defs/DodalSource",
135169
"planFunctions": "#/$defs/PlanSource"
136170
},
@@ -145,6 +179,9 @@
145179
},
146180
{
147181
"$ref": "#/$defs/DodalSource"
182+
},
183+
{
184+
"$ref": "#/$defs/DeviceManagerSource"
148185
}
149186
]
150187
},

helm/blueapi/values.schema.json

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,38 @@
553553
},
554554
"additionalProperties": false
555555
},
556+
"DeviceManagerSource": {
557+
"title": "DeviceManagerSource",
558+
"type": "object",
559+
"required": [
560+
"module"
561+
],
562+
"properties": {
563+
"kind": {
564+
"title": "Kind",
565+
"default": "deviceManager",
566+
"const": "deviceManager"
567+
},
568+
"mock": {
569+
"title": "Mock",
570+
"description": "If true, ophyd_async device connections are mocked",
571+
"default": false,
572+
"type": "boolean"
573+
},
574+
"module": {
575+
"title": "Module",
576+
"description": "Module to be imported",
577+
"type": "string"
578+
},
579+
"name": {
580+
"title": "Name",
581+
"description": "Name of the device manager in the module",
582+
"default": "devices",
583+
"type": "string"
584+
}
585+
},
586+
"additionalProperties": false
587+
},
556588
"DeviceSource": {
557589
"title": "DeviceSource",
558590
"type": "object",
@@ -640,6 +672,9 @@
640672
},
641673
{
642674
"$ref": "#/$defs/DodalSource"
675+
},
676+
{
677+
"$ref": "#/$defs/DeviceManagerSource"
643678
}
644679
]
645680
}

src/blueapi/config.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ class SourceKind(str, Enum):
4747
PLAN_FUNCTIONS = "planFunctions"
4848
DEVICE_FUNCTIONS = "deviceFunctions"
4949
DODAL = "dodal"
50+
DEVICE_MANAGER = "deviceManager"
5051

5152

5253
class Source(BlueapiBaseModel):
@@ -72,6 +73,18 @@ class DodalSource(Source):
7273
)
7374

7475

76+
class DeviceManagerSource(Source):
77+
kind: Literal[SourceKind.DEVICE_MANAGER] = Field(
78+
SourceKind.DEVICE_MANAGER, init=False
79+
)
80+
mock: bool = Field(
81+
description="If true, ophyd_async device connections are mocked", default=False
82+
)
83+
name: str = Field(
84+
default="devices", description="Name of the device manager in the module"
85+
)
86+
87+
7588
class TcpUrl(AnyUrl):
7689
_constraints = UrlConstraints(allowed_schemes=["tcp"])
7790

@@ -121,7 +134,7 @@ class EnvironmentConfig(BlueapiBaseModel):
121134

122135
sources: list[
123136
Annotated[
124-
PlanSource | DeviceSource | DodalSource,
137+
PlanSource | DeviceSource | DodalSource | DeviceManagerSource,
125138
Field(discriminator="kind"),
126139
]
127140
] = [

src/blueapi/core/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
)
1616
from .context import BlueskyContext
1717
from .event import EventPublisher, EventStream
18+
from .protocols import DeviceManager
1819

1920
OTLP_EXPORT_ENABLED = environ.get("OTLP_EXPORT_ENABLED") == "true"
2021

@@ -28,6 +29,7 @@
2829
"EventPublisher",
2930
"EventStream",
3031
"DataEvent",
32+
"DeviceManager",
3133
"WatchableStatus",
3234
"is_bluesky_compatible_device",
3335
"is_bluesky_plan_generator",

src/blueapi/core/context.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from bluesky.run_engine import RunEngine
1212
from dodal.common.beamlines.beamline_utils import get_path_provider, set_path_provider
1313
from dodal.utils import AnyDevice, make_all_devices
14-
from ophyd_async.core import NotConnectedError
14+
from ophyd_async.core import NotConnectedError, PathProvider
1515
from pydantic import BaseModel, GetCoreSchemaHandler, GetJsonSchemaHandler, create_model
1616
from pydantic.fields import FieldInfo
1717
from pydantic.json_schema import JsonSchemaValue, SkipJsonSchema
@@ -21,11 +21,13 @@
2121
from blueapi.client.numtracker import NumtrackerClient
2222
from blueapi.config import (
2323
ApplicationConfig,
24+
DeviceManagerSource,
2425
DeviceSource,
2526
DodalSource,
2627
EnvironmentConfig,
2728
PlanSource,
2829
)
30+
from blueapi.core.protocols import DeviceManager
2931
from blueapi.utils import (
3032
BlueapiPlanModelConfig,
3133
is_function_sourced_from_module,
@@ -115,6 +117,7 @@ class BlueskyContext:
115117
default_factory=lambda: RunEngine(context_managers=[])
116118
)
117119
numtracker: NumtrackerClient | None = field(default=None, init=False, repr=False)
120+
path_provider: PathProvider | None = None
118121
plans: dict[str, Plan] = field(default_factory=dict)
119122
devices: dict[str, Device] = field(default_factory=dict)
120123
plan_functions: dict[str, PlanGenerator] = field(default_factory=dict)
@@ -135,10 +138,12 @@ def __post_init__(self, configuration: ApplicationConfig | None):
135138
)
136139

137140
path_provider = StartDocumentPathProvider()
141+
# TODO: Remove this when device manager is rolled out
138142
set_path_provider(path_provider)
139143

140144
self.run_engine.subscribe(path_provider.run_start, "start")
141145
self.run_engine.subscribe(path_provider.run_stop, "stop")
146+
self.path_provider = path_provider
142147

143148
# local reference so it's available in _update_scan_num
144149
numtracker = self.numtracker
@@ -194,6 +199,13 @@ def with_config(self, config: EnvironmentConfig) -> None:
194199
self.with_device_module(mod)
195200
case DodalSource(mock=mock):
196201
self.with_dodal_module(mod, mock=mock)
202+
case DeviceManagerSource(mock=mock, name=name):
203+
manager = getattr(mod, name)
204+
if not isinstance(manager, DeviceManager):
205+
raise ValueError(
206+
f"{name} in module {mod} is not a device manager"
207+
)
208+
self.with_device_manager(manager, mock)
197209

198210
def with_plan_module(self, module: ModuleType) -> None:
199211
"""
@@ -227,6 +239,30 @@ def plan_2(...) -> MsgGenerator:
227239
):
228240
self.register_plan(obj)
229241

242+
def with_device_manager(self, manager: DeviceManager, mock: bool = False):
243+
fixtures = {"path_provider": self.path_provider} if self.path_provider else {}
244+
build_result = manager.build_and_connect(mock=mock, fixtures=fixtures)
245+
246+
for device in build_result.devices.values():
247+
self.register_device(device)
248+
249+
if errs := build_result.build_errors:
250+
LOGGER.warning(
251+
f"{errs} errors while building devices",
252+
exc_info=ExceptionGroup(
253+
"Errors while building devices", list(errs.values())
254+
),
255+
)
256+
if errs := build_result.connection_errors:
257+
LOGGER.warning(
258+
f"{len(errs)} errors while connecting devices",
259+
exc_info=NotConnectedError(errs),
260+
)
261+
return build_result.devices, {
262+
**build_result.build_errors,
263+
**build_result.connection_errors,
264+
}
265+
230266
def with_device_module(self, module: ModuleType) -> None:
231267
self.with_dodal_module(module)
232268

src/blueapi/core/protocols.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from typing import Any, Protocol, runtime_checkable
2+
3+
4+
@runtime_checkable
5+
class DeviceConnectResult(Protocol):
6+
devices: dict[str, Any]
7+
build_errors: dict[str, Exception]
8+
connection_errors: dict[str, Exception]
9+
10+
11+
@runtime_checkable
12+
class DeviceBuildResult(Protocol):
13+
devices: dict[str, Any]
14+
errors: dict[str, Exception]
15+
16+
def connect(self, timeout: float) -> DeviceConnectResult: ...
17+
18+
19+
@runtime_checkable
20+
class DeviceManager(Protocol):
21+
def build_and_connect(
22+
self,
23+
*,
24+
mock: bool = False,
25+
timeout: float | None = None,
26+
fixtures: dict[str, Any] | None = None,
27+
) -> DeviceConnectResult: ...

0 commit comments

Comments
 (0)