From 3aa4a250f8ac5e64a0d8b1ffe941af2c50f48f4d Mon Sep 17 00:00:00 2001 From: Abigail Emery Date: Fri, 26 Jun 2026 16:52:14 +0100 Subject: [PATCH 1/4] Add devices for tutorials --- src/blueapi/tutorial/devices.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/blueapi/tutorial/devices.py diff --git a/src/blueapi/tutorial/devices.py b/src/blueapi/tutorial/devices.py new file mode 100644 index 000000000..a0b94e8a1 --- /dev/null +++ b/src/blueapi/tutorial/devices.py @@ -0,0 +1,11 @@ +from ophyd_async.sim import SimMotor + +from blueapi.core.protocols import StaticDeviceManager + +devices = StaticDeviceManager() + +x = SimMotor(name="x") +y = SimMotor(name="y") + +devices.devices["x"] = x +devices.devices["y"] = y From b719b0f8841b790c05c38aaa301ee5e7c1845d99 Mon Sep 17 00:00:00 2001 From: Abigail Emery Date: Fri, 26 Jun 2026 16:52:29 +0100 Subject: [PATCH 2/4] Add plans for tutorials --- src/blueapi/tutorial/plans.py | 43 +++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/blueapi/tutorial/plans.py diff --git a/src/blueapi/tutorial/plans.py b/src/blueapi/tutorial/plans.py new file mode 100644 index 000000000..2badffe18 --- /dev/null +++ b/src/blueapi/tutorial/plans.py @@ -0,0 +1,43 @@ +from collections.abc import Sequence +from typing import Annotated, Any + +import bluesky.plans as bp +from bluesky.protocols import Readable +from bluesky.utils import MsgGenerator +from ophyd_async.core import AsyncReadable +from pydantic import Field, NonNegativeFloat, validate_call + + +@validate_call(config={"arbitrary_types_allowed": True}) +def count( + detectors: Annotated[ + Sequence[Readable | AsyncReadable], + Field( + description="Set of readable devices, will take a reading at each point", + min_length=1, + ), + ], + num: Annotated[int, Field(description="Number of frames to collect", ge=1)] = 1, + delay: Annotated[ + NonNegativeFloat | Sequence[NonNegativeFloat], + Field( + description="Delay between readings: if tuple, len(delay) == num - 1 and \ + the delays are between each point, if value or None is the delay for every \ + gap", + json_schema_extra={"units": "s"}, + ), + ] = 0.0, + metadata: dict[str, Any] | None = None, +) -> MsgGenerator: + """Reads from a number of devices. + + Wraps bluesky.plans.count(det, num, delay, md=metadata) exposing only serializable + parameters and metadata. + """ + if isinstance(delay, Sequence): + assert len(delay) == num - 1, ( + f"Number of delays given must be {num - 1}: was given {len(delay)}" + ) + metadata = metadata or {} + metadata["shape"] = (num,) + yield from bp.count(tuple(detectors), num, delay=delay, md=metadata) From 7cf9d51b3522464bc105f3c11995260ab934af4b Mon Sep 17 00:00:00 2001 From: Abigail Emery Date: Fri, 26 Jun 2026 16:52:50 +0100 Subject: [PATCH 3/4] Add config.yaml for tutorials --- src/blueapi/tutorial/config.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/blueapi/tutorial/config.yaml diff --git a/src/blueapi/tutorial/config.yaml b/src/blueapi/tutorial/config.yaml new file mode 100644 index 000000000..a253e476e --- /dev/null +++ b/src/blueapi/tutorial/config.yaml @@ -0,0 +1,11 @@ +env: + metadata: + instrument: tutorial-instrument + sources: + - kind: deviceManager + module: blueapi.tutorial.devices + - kind: planFunctions + module: blueapi.tutorial.plans +stomp: + enabled: true + url: tcp://localhost:61613/ From 223e8cede40d1be6de10e0d730f6edbc43c0718a Mon Sep 17 00:00:00 2001 From: Abigail Emery Date: Fri, 26 Jun 2026 16:53:27 +0100 Subject: [PATCH 4/4] Move StaticDeviceManager from tests to core --- src/blueapi/core/protocols.py | 17 +++++++++++++++++ tests/unit_tests/core/test_context.py | 22 +++------------------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/blueapi/core/protocols.py b/src/blueapi/core/protocols.py index bf84fe7ec..4ec4f88fe 100644 --- a/src/blueapi/core/protocols.py +++ b/src/blueapi/core/protocols.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass, field from typing import Any, Protocol, runtime_checkable @@ -25,3 +26,19 @@ def build_and_connect( timeout: float | None = None, fixtures: dict[str, Any] | None = None, ) -> DeviceConnectResult: ... + + +@dataclass +class StaticDeviceManager: + devices: dict[str, Any] = field(default_factory=dict) + build_errors: dict[str, Exception] = field(default_factory=dict) + connection_errors: dict[str, Exception] = field(default_factory=dict) + + def build_and_connect( + self, + *, + mock: bool = False, + timeout: float | None = None, + fixtures: dict[str, Any] | None = None, + ) -> DeviceConnectResult: + return self diff --git a/tests/unit_tests/core/test_context.py b/tests/unit_tests/core/test_context.py index 23462a148..89450c6cc 100644 --- a/tests/unit_tests/core/test_context.py +++ b/tests/unit_tests/core/test_context.py @@ -1,9 +1,9 @@ from __future__ import annotations -from dataclasses import dataclass, field +from dataclasses import dataclass from pathlib import Path from types import ModuleType, NoneType -from typing import Any, Generic, TypeVar, Union +from typing import Generic, TypeVar, Union from unittest.mock import MagicMock, Mock, patch import pytest @@ -45,7 +45,7 @@ ) from blueapi.core import BlueskyContext, is_bluesky_compatible_device from blueapi.core.context import DefaultFactory, generic_bounds, qualified_name -from blueapi.core.protocols import DeviceConnectResult, DeviceManager +from blueapi.core.protocols import DeviceManager, StaticDeviceManager from blueapi.utils.invalid_config_error import InvalidConfigError SIM_MOTOR_NAME = "sim" @@ -669,22 +669,6 @@ def demo_plan(foo: int | None) -> MsgGenerator: assert "foo" in schema.get("required", []) -@dataclass -class StaticDeviceManager: - devices: dict[str, Any] = field(default_factory=dict) - build_errors: dict[str, Exception] = field(default_factory=dict) - connection_errors: dict[str, Exception] = field(default_factory=dict) - - def build_and_connect( - self, - *, - mock: bool = False, - timeout: float | None = None, - fixtures: dict[str, Any] | None = None, - ) -> DeviceConnectResult: - return self - - def test_empty_device_manager(empty_context: BlueskyContext): sdm = StaticDeviceManager() empty_context.with_device_manager(sdm)