How to write a component¶
This page shows how to write each kind of component and add it to a session. Components explains the rules behind it.
A device¶
Write an ophyd-async device. Its
constructor must accept name as a keyword, as every ophyd-async base class
does:
from ophyd_async.core import StandardReadable, soft_signal_rw
class MyStage(StandardReadable):
def __init__(self, name: str = "", *, units: str = "mm") -> None:
with self.add_children_as_readables():
self.position = soft_signal_rw(float, units=units)
super().__init__(name=name)
Every argument after name can come from the session file.
A presenter¶
A presenter is any class that takes name as a keyword and keeps it:
from psygnal import Signal
from redsun import DeviceMapping, slot
class StagePresenter:
sig_moved = Signal(float)
def __init__(self, name: str, *, devices: DeviceMapping, step: float = 1.0) -> None:
self.name = name
self.stage = devices["stage"]
self.step = step
@slot
async def nudge(self) -> None:
position = await self.stage.position.get_value()
await self.stage.position.set(position + self.step)
self.sig_moved.emit(position + self.step)
devicesis filled by type:DeviceMappingis every device of the session, by name.stepcomes from the session file if it is there, and is1.0if not.nudgeis a slot, so the session can connect a signal to it. It may beasync.
A view¶
A Qt view is a QWidget with a
placement, and a constructor starting
with (name: str, parent: QWidget):
from psygnal import Signal
from qtpy.QtWidgets import QLabel, QPushButton, QVBoxLayout, QWidget
from redsun import Placement, slot
from redsun.qt import Dock
class StageView(QWidget):
placement: Placement = Dock("left")
sig_nudge = Signal()
def __init__(self, name: str, parent: QWidget) -> None:
super().__init__(parent)
self.name = name
button = QPushButton("Nudge")
button.clicked.connect(self.sig_nudge.emit)
self.label = QLabel("position: 0.0")
layout = QVBoxLayout(self)
layout.addWidget(button)
layout.addWidget(self.label)
@slot
def show_position(self, position: float) -> None:
self.label.setText(f"position: {position}")
Add them to a session¶
Declare each component on a session class, and connect the view's button to
the presenter, and the presenter back to the view, in wire:
from collections.abc import Iterator
from redsun import AsDevice, AsPresenter, AsView, Link
from redsun.qt import QtSession
class MyApp(QtSession):
stage: AsDevice[MyStage]
stage_ctrl: AsPresenter[StagePresenter]
stage_view: AsView[StageView]
def wire(self) -> Iterator[Link]:
yield self.stage_view.sig_nudge, self.stage_ctrl.nudge
yield self.stage_ctrl.sig_moved, self.stage_view.show_position
To give a component arguments in Python rather than in the file, use
Declare:
from typing import Annotated
from redsun import Declare
class MyApp(QtSession):
stage_ctrl: Annotated[AsPresenter[StagePresenter], Declare(step=0.5)]
If the file sets the same argument, Declare wins.
A component reads its arguments from the file under its own name. To read
them under another key, use FromConfig:
from redsun import FromConfig
class MyApp(QtSession):
stage: Annotated[AsDevice[MyStage], FromConfig("xy_stage")]
The device is still called stage. Only the key its arguments are read from
changes, which helps when that key isn't a valid Python name.
Alias gives the component another name:
from redsun import Alias
class MyApp(QtSession):
stage_ctrl: Annotated[AsPresenter[StagePresenter], Alias("ctrl")]
The alias is the name the component receives, the name the session lists it
under and the name a session file wires it by. The attribute you declared it
under still holds it, and its arguments are still read from the entry named
after that attribute, here stage_ctrl.
Use another component¶
A constructor runs before the other components exist, so you ask for another
component, or a value it shares, in setup:
class MotorReadings:
"""The last position read from each motor."""
def __init__(self) -> None:
self.positions: dict[str, float] = {}
class RoiPresenter:
def __init__(self, name: str) -> None:
self.name = name
def setup(self, readings: MotorReadings) -> None:
self.readings = readings
MotorReadings stands for any class of yours that another component shares.
To share a value, see Share a value. To ask for "every component that can do X", see Questions.
Clean up¶
Define shutdown, plain or async, and the session calls it when it shuts
down:
Test it¶
A component takes plain values, so a test can make it directly:
from redsun.aio import run_coro
def test_nudge_moves_by_one_step() -> None:
stage = MyStage(name="stage")
run_coro(stage.connect(mock=True))
ctrl = StagePresenter("ctrl", devices={"stage": stage}, step=2.0)
run_coro(ctrl.nudge())
assert run_coro(stage.position.get_value()) == 2.0
To test it inside a session without a window, declare the same components on
a plain Session, which makes every component and shows
nothing. Give it mock: true, and its devices connect as
connect(mock=True) does, with no service launched: