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#
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.name
read-only property to read it’s namea
Device.parent
read-write property to read it’s parent Device if it existsa
Device.children
to iterate through the Device attributes, yielding the(name, child)
child Devicesa
setattr
override that detects whether the attribute is also a Device and sets its parenta
Device.set_name
method to set its name and also set the names of its children using the parent name as a prefixa
Device.connect
method 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_annotations
that is called during__init__
to turn annotations into concrete child DevicesDeviceConnector.connect_mock
that is called ifconnect(mock=True)
is called, and should connect the child Devices in mock mode for testing without a control systemDeviceConnector.connect_real
that 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#
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:
SignalR
is a signal with a read-only value that supports the Readable and Subscribable protocols. It also adds theSignalR.get_value
andSignalR.subscribe_value
methods that are used to interact with the Signal in the parent Device.SignalW
is a signal with a write-only value that supports the Movable protocol.SignalRW
is a signal with a read-write value that inherits from SignalR and SignalW and adds the Locatable protocolSignalX
is 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==False
then it callsSignalBackend.connect
to connect it to the control system, and wires all theSignal
methods to use itif
mock==True
then it creates aMockSignalBackend
for test purposes and wires all theSignal
methods 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#
classDiagram Device <|-- StandardReadable Device <|-- StandardDetector Device <|-- StandardFlyer
There are also some Device
subclasses that provide helpers for implementing particular protocols, namely:
StandardReadable
that supports the Readable protocol using the values of its childrenStandardDetector
that supports the WritesStreamAssets protocol using logic classes for the detector driver and writerStandardFlyer
that supports the Flyable protocol for motion and trigger systems