Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion src/blueapi/core/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ class BlueskyContext:
plan_functions: dict[str, PlanGenerator] = field(default_factory=dict)

_reference_cache: dict[type, type] = field(default_factory=dict)
_warned_name_clash: bool = field(default=False, init=False, repr=False)

def __post_init__(self, configuration: ApplicationConfig | None):
if not configuration:
Expand Down Expand Up @@ -341,6 +342,11 @@ def register_device(self, device: Device, name: str | None = None) -> None:
`name` parameter here. If the device conforms to a different protocol then
the parameter must be used to name it.

Children of ophyd-async devices, including signals, are also registered
under their own names, e.g. registering "stage" also registers "stage-x"
and "stage-x-velocity". If a name is already in use, the existing device
is kept and the new one and its children are not registered.

Args:
device (Device): The device to register
name (Optional[str], optional): A name for the device. Defaults to None.
Expand All @@ -358,7 +364,39 @@ def register_device(self, device: Device, name: str | None = None) -> None:
else:
raise KeyError(f"Must supply a name for this device: {device}")

self.devices[name] = device
if self._add_device(name, device):
self._register_children(device)

def _register_children(self, device: Device) -> None:
"""Register the children of an ophyd-async device under their own names."""

if not isinstance(device, AsyncDevice):
return

for _, child in device.children():
if not child.name or self._add_device(child.name, child):
self._register_children(child)

def _add_device(self, name: str, device: Device) -> bool:
"""Add a device unless a different one is already registered as name."""

existing = self.devices.setdefault(name, device)
if existing is not device:
# Expected for e.g. a motor's user_readback, which has the motor's name.
# The caller skips its children too, as their names start with this one
if not self._warned_name_clash:
LOGGER.warning(
"Some devices were not registered as their names are in use, "
"e.g. motor readbacks. Enable debug logging to see which"
)
self._warned_name_clash = True
LOGGER.debug(
"Not registering %s or its children, name '%s' is in use",
device,
name,
)
return False
return True

def unregister_all_devices(self):
"""Unregister all devices from the context."""
Expand Down
13 changes: 10 additions & 3 deletions src/blueapi/service/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,15 +191,22 @@ def get_plan(name: str) -> PlanModel:

def get_devices() -> list[DeviceModel]:
"""Get all available devices in the BlueskyContext"""
return [DeviceModel.from_device(device) for device in context().devices.values()]
# Report each device under its registered name. A device registered under an
# override name would otherwise show its own name, which one of its children
# may be registered as, e.g. a motor's user_readback
return [
DeviceModel.from_device(device, name)
for name, device in context().devices.items()
]


def get_device(name: str) -> DeviceModel:
"""Retrieve device by name from the BlueskyContext"""
if not (device := context().find_device(name)):
ctx = context()
if not (device := ctx.find_device(name)):
raise KeyError(name)

return DeviceModel.from_device(device)
return DeviceModel.from_device(device, name if name in ctx.devices else None)


def submit_task(
Expand Down
5 changes: 3 additions & 2 deletions src/blueapi/service/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ class DeviceModel(BlueapiBaseModel):
)

@classmethod
def from_device(cls, device: Device) -> "DeviceModel":
name = device.name if isinstance(device, HasName) else _UNKNOWN_NAME
def from_device(cls, device: Device, name: str | None = None) -> "DeviceModel":
if name is None:
name = device.name if isinstance(device, HasName) else _UNKNOWN_NAME
return cls(name=name, protocols=list(_protocol_info(device)))


Expand Down
Loading
Loading