The session makes each component by calling its constructor and passing every
argument by keyword. Here is where each parameter of this presenter gets its
value:
the declared namemotor_ctrlwhat the session holds,looked up by typeWhat exists before any component does. The settings (SessionConfig, Settings), the devices (DeviceMapping, DevicesOf[P]), the path provider, the catalog address, and the values the session's providers share.session fileor Declare(...)the default1.0MotorPresenter(...)namedevices: DeviceMappingstep: float = 1.0by its typeif it gives stepotherwiseWhat exists before any component does. The settings (SessionConfig, Settings), the devices (DeviceMapping, DevicesOf[P]), the path provider, the catalog address, and the values the session's providers share.
The session can't see types imported under if TYPE_CHECKING:
The session reads the annotations while the program runs, when a type
imported only under if TYPE_CHECKING: doesn't exist, so it leaves the
component out and logs a TypeError. Import those types normally;
Limitations
lists where this applies.
A constructor runs before the other components exist, so a component that
needs another, or a value another shares, asks for it in an optional setup:
classMotorReadings:"""The last position read from each motor."""def__init__(self)->None:self.positions:dict[str,float]={}classRoiPresenter:def__init__(self,name:str)->None:self.name=namedefsetup(self,readings:MotorReadings)->None:self.readings=readings
Every setup runs once all presenters and views exist, filled by type, so a
component can ask for one declared after it. The calls run in declaration
order, though: a setup reading what another setup assigns sees it only if
that component is declared first. An async def setup gets the component left
out. When setup can't get what it asks for, the outcome depends on whose
mistake it is:
When setup can't get what it asks for
setup raisesasks for a componentthat failed to buildasks for somethingnothing declaresasks for a componentof a later layerthe constructor asksfor a componentcomponent kept,listed under Not set upThe session logs the error and runs the component without what setup was going to give it.the build stopswith TypeErrorNo component failed. The mistake is in how the session is written, so you fix the session. The error says what to change, such as moving a constructor parameter to setup.The session logs the error and runs the component without what setup was going to give it.No component failed. The mistake is in how the session is written, so you fix the session. The error says what to change, such as moving a constructor parameter to setup.
setup raisesasks for a componentthat failed to buildasks for somethingnothing declaresasks for a componentof a later layerthe constructor asksfor a componentcomponent kept,listed under Not set upThe session logs the error and runs the component without what setup was going to give it.the build stopswith TypeErrorNo component failed. The mistake is in how the session is written, so you fix the session. The error says what to change, such as moving a constructor parameter to setup.The session logs the error and runs the component without what setup was going to give it.No component failed. The mistake is in how the session is written, so you fix the session. The error says what to change, such as moving a constructor parameter to setup.
The build checks a constructor that asks for a component, and a setup that
asks for one of a later layer, before anything is built.
The session checks a component only by its members' names and signatures,
never by where a protocol came from. So a
plugin can satisfy a redsun protocol, or copy its
definition for its own type checker, without importing redsun. A copy works
alone only when the types its members name do:
protocol
names
a copy works alone
Axis, Light
ophyd-async and bluesky types only
yes
DescribesAxes, DescribesLights
AxisInfo or LightInfo, and Configuration, from redsun.utils.devices
no
HasPlans
PlanEntry
no
HasActions
ActionManager
no
DescribesPlans
PlanSpec and CallbackType
no
A shared value is found by its exact type too, so asking for the RunEngine
or for Deferrals, which applies a setting change
during a plan without corrupting what the plan records, needs redsun's own
classes.
A device is an ophyd-async
device, a subclass of ophyd_async.core.Device; redsun adds nothing to the
device layer, so the ophyd-async documentation covers
signals and detectors. Your devices model your whole setup as a tree, and
reaching the hardware is best left to a service, as
Devices and services explains. Step
through the cases to see which devices end up in the session:
How the session makes and connects a device
Every device is made from its declaration, then connected. Step through what can happen to it.
declarationAsDevice[MyCamera]makecls(name=..., **kwargs)The arguments come from the session file or Declare(...), plus the prefix of the device's service and the path provider when the constructor takes them.connectall at once, up to 10 s eachin the sessionleft outconstructor raised,or no service prefixdidn't connectautoconnect=FalseThe arguments come from the session file or Declare(...), plus the prefix of the device's service and the path provider when the constructor takes them.
declarationAsDevice[MyCamera]makecls(name=..., **kwargs)The arguments come from the session file or Declare(...), plus the prefix of the device's service and the path provider when the constructor takes them.connectall at once, up to 10 s eachin the sessionleft outconstructor raised,or no service prefixdidn't connectautoconnect=FalseThe arguments come from the session file or Declare(...), plus the prefix of the device's service and the path provider when the constructor takes them.
The usual case: the device is made, connects within 10 seconds, and takes its place in the session.
declarationAsDevice[MyCamera]makecls(name=..., **kwargs)The arguments come from the session file or Declare(...), plus the prefix of the device's service and the path provider when the constructor takes them.connectall at once, up to 10 s eachin the sessionleft outconstructor raised,or no service prefixdidn't connectautoconnect=FalseThe arguments come from the session file or Declare(...), plus the prefix of the device's service and the path provider when the constructor takes them.
declarationAsDevice[MyCamera]makecls(name=..., **kwargs)The arguments come from the session file or Declare(...), plus the prefix of the device's service and the path provider when the constructor takes them.connectall at once, up to 10 s eachin the sessionleft outconstructor raised,or no service prefixdidn't connectautoconnect=FalseThe arguments come from the session file or Declare(...), plus the prefix of the device's service and the path provider when the constructor takes them.
Its constructor raises, or it names a service with no prefix: the session logs it as Failed to build device and leaves it out.
declarationAsDevice[MyCamera]makeconstructor raisedThe arguments come from the session file or Declare(...), plus the prefix of the device's service and the path provider when the constructor takes them.connectall at once, up to 10 s eachin the sessionleft outFailed to build deviceconstructor raised,or no service prefixdidn't connectautoconnect=FalseThe arguments come from the session file or Declare(...), plus the prefix of the device's service and the path provider when the constructor takes them.
declarationAsDevice[MyCamera]makeconstructor raisedThe arguments come from the session file or Declare(...), plus the prefix of the device's service and the path provider when the constructor takes them.connectall at once, up to 10 s eachin the sessionleft outFailed to build deviceconstructor raised,or no service prefixdidn't connectautoconnect=FalseThe arguments come from the session file or Declare(...), plus the prefix of the device's service and the path provider when the constructor takes them.
It doesn't answer within 10 seconds: it is left out, and the summary lists it as not connected.
declarationAsDevice[MyCamera]makecls(name=..., **kwargs)The arguments come from the session file or Declare(...), plus the prefix of the device's service and the path provider when the constructor takes them.connectno answer in 10 sin the sessionleft outcamera (device, not connected)constructor raised,or no service prefixdidn't connectautoconnect=FalseThe arguments come from the session file or Declare(...), plus the prefix of the device's service and the path provider when the constructor takes them.
declarationAsDevice[MyCamera]makecls(name=..., **kwargs)The arguments come from the session file or Declare(...), plus the prefix of the device's service and the path provider when the constructor takes them.connectno answer in 10 sin the sessionleft outcamera (device, not connected)constructor raised,or no service prefixdidn't connectautoconnect=FalseThe arguments come from the session file or Declare(...), plus the prefix of the device's service and the path provider when the constructor takes them.
Declared with autoconnect=False, it skips the connection and stays in the session for your code to connect.
declarationAsDevice[MyCamera]makecls(name=..., **kwargs)The arguments come from the session file or Declare(...), plus the prefix of the device's service and the path provider when the constructor takes them.connectall at once, up to 10 s eachin the sessionnot connectedleft outconstructor raised,or no service prefixdidn't connectautoconnect=FalseThe arguments come from the session file or Declare(...), plus the prefix of the device's service and the path provider when the constructor takes them.
declarationAsDevice[MyCamera]makecls(name=..., **kwargs)The arguments come from the session file or Declare(...), plus the prefix of the device's service and the path provider when the constructor takes them.connectall at once, up to 10 s eachin the sessionnot connectedleft outconstructor raised,or no service prefixdidn't connectautoconnect=FalseThe arguments come from the session file or Declare(...), plus the prefix of the device's service and the path provider when the constructor takes them.
Making a device as cls(name=<name>, **kwargs) works for every ophyd-async
device, including one whose first parameter is prefix, except one that takes
name only by position (after a /), which is left out.
A device declared with autoconnect=False stays
unconnected for your code to connect when it chooses, so the build can't leave
it out for missing hardware: a component decides what to do when the
connection fails. See
How to connect a device on demand.
A device declared with service="stage_ioc" gets that
service'sprefix as its prefix, and is
left out when the service isn't declared, didn't start or has no prefix.
A device writes its own data files, in the format it or its service chooses.
A constructor that takes path_provider gets the session's
path provider, which puts every file of a session
in one folder, named after the session, the day, the
data key and the plan.
How to choose where acquisition files go
sets the folder and the names, and
ADR 13 records
why the device writes the data and not redsun.
A service holding hardware, such as a camera, can let go of it and keep
running if it offers a command for that as a
process variable. When the user asks, your
presenter triggers that command through the service's devices, which stay
connected; the session has no standby step of its own.
A presenter holds the session's application logic. It
may run blueskyplans, react to the
documents a run produces, move a device directly, or
talk to another program. Because it never touches a widget, it works without a
screen.
Any class can be a presenter if its constructor takes name as a keyword and
its instances keep that name. It doesn't inherit anything from redsun, and
it can't be an ophyd-async device.
A view holds the widgets, and says where it wants to be
shown with a placement, such as Dock("left").
The placement is what makes a class a view. Before the build, the
frontend checks that it can show the placement and
that the view is the right kind of object for it; Frontends
covers the Qt rules, and How to place a view
shows how to set one.
MotorPresenter.sig_movedSignal(str, float)MotorView.refreshmarked with @slotRuns on the main thread, because a Qt widget may only be used from there. @slot(thread=...) picks another thread.the session connects them,in wire or the wiring sectionRuns on the main thread, because a Qt widget may only be used from there. @slot(thread=...) picks another thread.
MotorPresenter.sig_movedSignal(str, float)MotorView.refreshmarked with @slotRuns on the main thread, because a Qt widget may only be used from there. @slot(thread=...) picks another thread.the session connects them,in wire or the wiring sectionRuns on the main thread, because a Qt widget may only be used from there. @slot(thread=...) picks another thread.
Signal names start with sig_ by convention. A slot is marked with slot,
may be async def, and is part of the component's public interface, since
other code connects to it by name. Components never connect themselves;
Wire components together shows wire and the
wiring section. A presenter and a view written for each other can name the
signals that reach their slots, and the session connects them with one
pairing (Offer a pairing).
If a component needs to clean up, give it a shutdown method, plain or
async, and the session calls it when the session shuts down. A device can
define one too, to leave its hardware in a safe state:
presenters and viewsnewest firstdevicesEvery device that connected, and every device declared with autoconnect=False, since a component may have connected it. A device that didn't connect is left out of the session and never shut down, since it would write to hardware that never answered.services stopEvery device that connected, and every device declared with autoconnect=False, since a component may have connected it. A device that didn't connect is left out of the session and never shut down, since it would write to hardware that never answered.
presenters and viewsnewest firstdevicesEvery device that connected, and every device declared with autoconnect=False, since a component may have connected it. A device that didn't connect is left out of the session and never shut down, since it would write to hardware that never answered.services stopEvery device that connected, and every device declared with autoconnect=False, since a component may have connected it. A device that didn't connect is left out of the session and never shut down, since it would write to hardware that never answered.
The devices go after every presenter and view, which may still use them in
their own shutdown, and before the services stop, so each device can still
reach its service.
On a pydantic model, a signal must be a ClassVar, since pydantic refuses
a class attribute without an annotation. A value that setup assigns
shouldn't be a field: use field(init=False) on a dataclass, or a private
attribute on a model.
A class with __slots__ and a signal is left out
psygnal refers to the component weakly, so a class with __slots__
that owns a signal needs __weakref__ among its slots, or the component
is never freed. The session leaves such a class out, and the error names
the fix: add __weakref__ to the slots, or pass weakref_slot=True to a
dataclass.
A Qt view can't be a pydantic model or a dataclass, because Qt needs its own
QWidget.__init__ to run.
Your code reaches each one by its name, as self.stage_x. A component that
needs every stage, however many there are, asks the session by what they can
do: see Questions.