Devices, Signals and their Backends#
The bluesky.run_engine.RunEngine facing interface is defined by the bluesky protocols that a Device implements, but to implement that interface ophyd-async uses some helper classes. This document details how those helper classes fit together to provide particular Device behavior.
Device and DeviceConnector#
---
config:
theme: neutral
---
classDiagram
Device *-- DeviceConnector
Device : connect(mock)
Device <|-- EpicsDevice
Device <|-- TangoDevice
EpicsDevice *-- EpicsDeviceConnector
TangoDevice *-- TangoDeviceConnector
DeviceConnector <|-- EpicsDeviceConnector
DeviceConnector <|-- TangoDeviceConnector
The Device class is the base of all ophyd-async objects that are published to bluesky. It provides:
a
Device.nameread-only property to read it’s namea
Device.parentread-write property to read it’s parent Device if it existsa
Device.childrento iterate through the Device attributes, yielding the(name, child)child Devicesa
setattroverride that detects whether the attribute is also a Device and sets its parenta
Device.set_namemethod to set its name and also set the names of its children using the parent name as a prefix, called at init and also when a new child is attached to an already named Devicea
Device.connectmethod that connects it and its children
All the above methods are concrete, but connect() calls out to a DeviceConnector to actually do the connection, only handling caching itself. This enables plug-in behavior on connect (like the introspection of child Attributes in Tango or PVI, or the special case for Signal we will see later).
A DeviceConnector provides the ability to:
DeviceConnector.create_children_from_annotationsthat is called during__init__to turn annotations into concrete child DevicesDeviceConnector.connect_mockthat is called ifconnect(mock=True)is called, and should connect the child Devices in mock mode for testing without a control systemDeviceConnector.connect_realthat is called ifconnect(mock=False)is called, and should connect the child Devices to the control system in parallel
The base DeviceConnector provides suitable methods for use with non-introspected Devices, but there are various control system specific connectors that handle filling annotations in declarative Devices.
Signal and SignalBackend#
---
config:
theme: neutral
---
classDiagram
Device <|-- Signal
Signal : source
Signal <|-- SignalR
SignalR : read()
SignalR : subscribe()
SignalR : get_value()
Signal <|-- SignalW
SignalW : set()
SignalR <|-- SignalRW
SignalW <|-- SignalRW
SignalRW : locate()
Signal <|-- SignalX
SignalX : trigger()
Signal *-- SignalConnector
SignalConnector *-- SignalBackend
SignalBackend <|-- CaSignalConnector
SignalBackend <|-- PvaSignalConnector
SignalBackend <|-- TangoSignalConnector
If a Device with children is like a branch in a tree, a Signal is like a leaf. It has no children, but represents a single value or action in the control system. There are 4 types of signal:
SignalRis a signal with a read-only value that supports the Readable and Subscribable protocols. It also adds theSignalR.get_valueandSignalR.subscribe_valuemethods that are used to interact with the Signal in the parent Device.SignalWis a signal with a write-only value that supports the Movable protocol.SignalRWis a signal with a read-write value that inherits from SignalR and SignalW and adds the Locatable protocolSignalXis a signal that performs an action, and supports the Triggerable protocol
These are all concrete classes, but delegate their actions to a SignalBackend:
class SignalBackend(Generic[SignalDatatypeT]):
"""A read/write/monitor backend for a Signals."""
def __init__(self, datatype: type[SignalDatatypeT] | None):
self.datatype = datatype
@abstractmethod
def source(self, name: str, read: bool) -> str:
"""Return source of signal.
:param name: The name of the signal, which can be used or discarded.
:param read: True if we want the source for reading, False if writing.
"""
@abstractmethod
async def connect(self, timeout: float):
"""Connect to underlying hardware."""
@abstractmethod
async def put(self, value: SignalDatatypeT | None, wait: bool):
"""Put a value to the PV, if wait then wait for completion."""
@abstractmethod
async def get_datakey(self, source: str) -> DataKey:
"""Metadata like source, dtype, shape, precision, units."""
@abstractmethod
async def get_reading(self) -> Reading[SignalDatatypeT]:
"""Return the current value, timestamp and severity."""
@abstractmethod
async def get_value(self) -> SignalDatatypeT:
"""Return the current value."""
@abstractmethod
async def get_setpoint(self) -> SignalDatatypeT:
"""Return the point that a signal was requested to move to."""
@abstractmethod
def set_callback(self, callback: Callback[Reading[SignalDatatypeT]] | None) -> None:
"""Observe changes to the current value, timestamp and severity."""
Each control system implements its own concrete SignalBackend subclass with those methods filled in. It is these subclasses that take control system specific parameters like EPICS PV or Tango TRL. The instance is passed to Signal.__init__, which passes it to a generic SignalConnector.
At connect() time this SignalConnector does one of two things:
if
mock==Falsethen it callsSignalBackend.connectto connect it to the control system, and wires all theSignalmethods to use itif
mock==Truethen it creates aMockSignalBackendfor test purposes and wires all theSignalmethods to use it
This means that to construct a Signal you need to do something like:
my_signal = SignalR(MyControlSystemBackend(int, cs_param="something"))
This is a little verbose, so instead we provide helpers like soft_signal_rw to make it a little shorter. The above might look like:
my_signal = my_cs_signal_r(int, "something")
“Standard” Device subclasses#
---
config:
theme: neutral
---
classDiagram
Device <|-- StandardReadable
Device <|-- StandardDetector
Device <|-- StandardFlyer
There are also some Device subclasses that provide helpers for implementing particular protocols, namely:
StandardReadablethat supports the Readable protocol using the values of its childrenStandardDetectorthat supports the WritesStreamAssets protocol using logic classes for the detector driver and writerStandardFlyerthat supports the Flyable protocol for motion and trigger systems