A session is one running application. It knows which
components to make, makes them in the right order,
connects them, and takes them apart again when it ends.
Each annotated line is a declaration: the name on
the left becomes the component's name, and AsDevice, AsPresenter or
AsView says which layer it belongs to and which class to
make. Once the session is built, self.motor_ctrl holds the MotorPresenter
it made, and your editor and mypy see it as one.
A session has three layers of components, the DVP pattern,
and the services below them reach the hardware. Point at a
layer to read what it holds:
The layers of a session
session processservicestheir own programs, reached by prefixover Channel Access or PVAccessA service talks to the hardware and offers it to the devices under a prefix. The session starts the ones it launches before anything else and stops them after every component. A device that needs no hardware, such as a simulated stage, needs no service.hardwareviewswhat the user sees and touchesA view holds the widgets. It shows what devices and presenters report, and turns what the user does into a signal a presenter acts on. It may hold presenters and devices.presentersdecide what happens and whenA presenter runs plans, computes results from the documents a run produces, moves devices when asked, and keeps the state the application needs. It may hold devices, never a view.devicesthe signals of your setupEach device is an ophyd-async device, a set of signals such as a position or an exposure time. It knows what can be controlled, not when or why.A service talks to the hardware and offers it to the devices under a prefix. The session starts the ones it launches before anything else and stops them after every component. A device that needs no hardware, such as a simulated stage, needs no service.A view holds the widgets. It shows what devices and presenters report, and turns what the user does into a signal a presenter acts on. It may hold presenters and devices.A presenter runs plans, computes results from the documents a run produces, moves devices when asked, and keeps the state the application needs. It may hold devices, never a view.Each device is an ophyd-async device, a set of signals such as a position or an exposure time. It knows what can be controlled, not when or why.
The session builds the layers in that order, and a component's constructor or
setup can take only what its own layer or an earlier one owns. So a view may
take presenters and devices, while a presenter never holds a view, which is why
a presenter runs without a screen, in a test for example. Signals and slots
are not bound by the order: they connect components across layers in either
direction. Components explains what each layer may contain.
Services are separate programs, not components the session builds. The
session starts the ones it launches before anything else and stops them after
every component.
The session reads its files and mappings in order, merges them, and checks the result once, listing every problem it finds.
read theconfigurationget thefrontend readyservicesdevicesconnectsealsetupviewspresentersregistrywiringpresentationreport# common.yamlpresenters:motor_ctrl:step:2.0# common.yamlpresenters:motor_ctrl:step:2.0# simulation.yamlsession:my-lab# simulation.yamlsession:my-labmerged and checkedthen
read theconfigurationget thefrontend readyservicesdevicesconnectsealsetupviewspresentersregistrywiringpresentationreport# common.yamlpresenters:motor_ctrl:step:2.0# common.yamlpresenters:motor_ctrl:step:2.0# simulation.yamlsession:my-lab# simulation.yamlsession:my-labmerged and checkedthen
Before any component exists, the session sets the backend that runs coroutine slots. A QtSession also makes the QApplication here.
read theconfigurationget thefrontend readyservicesdevicesconnectsealsetupviewspresentersregistrywiringpresentationreportbackend forcoroutine slotsQApplicationQtSession only
read theconfigurationget thefrontend readyservicesdevicesconnectsealsetupviewspresentersregistrywiringpresentationreportbackend forcoroutine slotsQApplicationQtSession only
The session runs the module of every launched service at once, as its own process, and waits up to 15 seconds for the ready text the declaration names. A session whose storage names a catalog also starts its tiled server.
read theconfigurationget thefrontend readyservicesdevicesconnectsealsetupviewspresentersregistrywiringpresentationreportcamera_ioc: Annotated[ AsService, Launch("mylab.iocs.camera", ready="Server startup complete."),]camera_ioc:Annotated[AsService,Launch("mylab.iocs.camera",ready="Server startup complete."),]$ python -m mylab.iocs.camera
...
Server startup complete.$ python -m mylab.iocs.camera...Server startup complete.starts it, then waits for the ready line
read theconfigurationget thefrontend readyservicesdevicesconnectsealsetupviewspresentersregistrywiringpresentationreportcamera_ioc: Annotated[ AsService, Launch("mylab.iocs.camera", ready="Server startup complete."),]camera_ioc:Annotated[AsService,Launch("mylab.iocs.camera",ready="Server startup complete."),]$ python -m mylab.iocs.camera
...
Server startup complete.$ python -m mylab.iocs.camera...Server startup complete.starts it, then waits for the ready line
Each device is made with its name and its settings, plus its service's prefix and the path provider when its constructor takes them. One that raises is left out.
The session records what was built, notes the settings it would save, and closes itself to further building.
read theconfigurationget thefrontend readyservicesdevicesconnectsealsetupviewspresentersregistrywiringpresentationreportrecord whatwas builtnote the settingsit would saveclose to morebuilding
read theconfigurationget thefrontend readyservicesdevicesconnectsealsetupviewspresentersregistrywiringpresentationreportrecord whatwas builtnote the settingsit would saveclose to morebuilding
Signals are connected to slots: the links wire yields, then the wiring section of the files, then the pairs.
The shared values registry collects come from the session's
providers, classes made only to share
values. The settings seal notes let the session
tell later whether you changed any.
The order never changes. A frontend can change what happens inside a step,
never which steps run: presentation does nothing in a plain Session, while
QtSession shows the main window and its views there.
Here the camera's constructor can't find the serial port it talks through:
classMyCamera(StandardReadable):def__init__(self,name:str="",*,port:str="COM3")->None:raiseOSError(f"serial port {port} not found")classMyApp(Session):stage:AsDevice[MyStage]camera:AsDevice[MyCamera]stage_ctrl:AsPresenter[StagePresenter]
The session logs the error, and the summary at the end names what is missing:
A mistake in the session itself, such as a malformed wiring section, still
stops the build, and a strict session stops
whenever something is missing.
How to find out why a component is missing
shows how to read the summary and make a session strict.
ADR 11 records
why the session carries on.
Two cases only log a warning: a component that shares nothing, asks for nothing
and isn't wired to anything, and a shared value no component asks for. Neither
is a mistake while you're still putting a session together, or when a
plugin ships more than your session uses.
The connections go first, so no signal calls a slot of a component that is
shutting down. A build that fails halfway runs the same releases, so it never
leaves a service running. You can call shutdown twice, and build the session
again afterwards.
You can describe a session in one of two ways: declare its components in a
Python class, or list them all in a session file.
Either way, the settings come from the files and mappings the session reads.
The components and their classes live in your code, where your editor and
mypy can check them. Files are optional. When the class lists some in
config, they supply the session name and the arguments of the components
the class declares, each under the component's name:
The frontend key picks the class the session is built on, so a file
naming qt comes up as a QtSession. With no class name to fall back on,
the files must set session.
Run a session without a GUI shows a file
that names no frontend.
A session reads its sources in order and merges them: a later source wins, and
nested sections merge key by key, except for a component's entry, which a later
source replaces whole:
Two files merged
common.yaml, the first source, gives motor_ctrl a step and a speed.
The merged result is checked once, before anything is built.
# common.yamlsession:my-labpresenters:motor_ctrl:step:2.0speed:1.0# common.yamlsession:my-labpresenters:motor_ctrl:step:2.0speed:1.0# simulation.yamlpresenters:motor_ctrl:step:0.5# simulation.yamlpresenters:motor_ctrl:step:0.5checked beforeanything is built# mergedsession:my-labpresenters:motor_ctrl:step:0.5# mergedsession:my-labpresenters:motor_ctrl:step:0.5thenmotor_ctrl replacedwhole: speed is gone
# common.yamlsession:my-labpresenters:motor_ctrl:step:2.0speed:1.0# common.yamlsession:my-labpresenters:motor_ctrl:step:2.0speed:1.0# simulation.yamlpresenters:motor_ctrl:step:0.5# simulation.yamlpresenters:motor_ctrl:step:0.5checked beforeanything is built# mergedsession:my-labpresenters:motor_ctrl:step:0.5# mergedsession:my-labpresenters:motor_ctrl:step:0.5thenmotor_ctrl replacedwhole: speed is gone
schema_version, frontend and services.transport say what kind of session
this is, so every source must agree on them; a different value is an error. A
subclass adds its sources after its base class's, so you write shared settings
once:
classInstrument(QtSession):config="common.yaml"classSimulation(Instrument):config="simulation.yaml"# read after common.yaml
Every mistake in the configuration at once
The check runs on the merged result. A misspelled key or a value of the
wrong type raises ConfigurationError, which
lists every problem as section.key: what.
Session file lists every key.
Each run writes its log to a file under logs/<session> in the root folder the
session's data goes under, and each launched service writes to a file of its
own beside it. The files follow the root if it moves while the session runs,
and close last at shutdown.
Configure logging
shows the layout.
A session is written against two protocols, and the
classes redsun ships fill them in:
Session protocols and classes
BuildableSession+one method per build stepDesktopSession+window+run()build, show the window, start the event loopSession+start_runtime()set the backend of coroutine slots+present()does nothingQtSession+start_runtime()also make the QApplication+present()show the main window and viewsimplementsextendsextendsimplements
BuildableSession+one method per build stepDesktopSession+window+run()build, show the window, start the event loopSession+start_runtime()set the backend of coroutine slots+present()does nothingQtSession+start_runtime()also make the QApplication+present()show the main window and viewsimplementsextendsextendsimplements
A session with no frontend shows nothing, which is what a test wants: build()
alone gives you every component without opening a window.