Presenters¶
A presenter holds the logic of a session.
Devices do the work on hardware. A presenter can decide the order of that work, through bluesky plans, but it is not limited to that. A presenter can also:
- consume
blueskydocuments to process them on the fly, store intermediate results or forward them to the interface (for example, computing the FFT of an image and sending it to a view); - control devices by hand: move a motor stage or change a camera's exposure from the interface, calling the device directly without going through the
RunEngine; - talk to external applications over a protocol of its own, sending commands or waiting for them.
Presenters talk to each other and to views through the VirtualContainer, which routes commands and documents to whoever receives them.
The presenter contract¶
A presenter is recognised by shape, through the
PPresenter protocol: any class whose
instances expose a name (str) and devices (Mapping[str, Device])
satisfies it. The protocol declares both as read-only properties, so an
implementer may use instance attributes, class attributes or properties, and
devices may be any Mapping, such as a dict.
A presenter is checked twice. When the class is declared, its constructor's
leading positional parameters must be exactly (name, devices): the container
calls cls(name, devices, **config_kwargs). When it is built, the instance is
checked against the protocol, and a TypeError names the missing members.
Attributes assigned in __init__ do not exist before the instance does, which
is why the second check runs on the instance
(see ADR 0003).
The Presenter ABC is an optional base with the
usual constructor:
- a
name, the presenter's unique identifier; - a
Mapping[str, Device]of the session's devices; - keyword arguments from the session configuration file.
The ABC does not inherit the protocol; it satisfies it by shape, like any other implementer.
A presenter reaches the virtual container by implementing
IsProvider or
IsInjectable, and is shut down through
HasShutdown.
Consuming documents¶
A presenter processing acquisition data subscribes to the RunEngine's
documents, directly or through the callback registry of the
VirtualContainer. Document callbacks run
synchronously on the engine's event loop thread and cannot await. A presenter
storing a derived result, such as a median over Event documents, adds it to the
store named in the StreamResource document; the acquisition itself belongs
to the device
(ADR 0013).
The session's path provider¶
The container builds one
SessionPathProvider per session
and passes it to every device taking a path_provider keyword, so all of a
session's files share one root:
Each data key gets its own directory and counter, so two detectors in one run
are both <plan>_00003. The <session> folder is the session name with
characters unsafe in a path replaced.
The root comes from the session file, and defaults to the user data directory:
session: my-session
storage:
base_dir: "D:/experiments/2026-09" # optional
max_digits: 5 # optional, width of the counter
The provider is wired as path_provider, and resolved through
PATH_PROVIDER:
provider = container.require(PATH_PROVIDER)
provider.base_dir # where files go now
provider.set_base_dir("E:/other-disk") # where they go from the next run on
A component named path_provider is refused: it would shadow the provider.
The plan lifecycle¶
Three slots, connected to whatever announces a run:
wiring:
- from: acquisition.sig_pre_launch_notify
to: path_provider.set_plan
- from: acquisition.sig_plan_done
to: path_provider.reset_plan
- from: output_dir_widget.sig_directory_chosen
to: path_provider.set_base_dir
set_plan names files after the upcoming run; reset_plan sets the name back
to unknown. Only the application knows which signals mark a run, so nothing
is connected by default.
set_base_dir raises RuntimeError while a plan runs, and for good once the
session's catalog has started, since the catalog reads only the directories it
started with. A GUI offering the root catches the error. Without set_plan
and reset_plan wired, a running plan goes unnoticed.
What is not supported¶
A device cannot get a root of its own from configuration: path_provider is
reserved, as service and autoconnect are. One root per session keeps a
session archivable as a unit and its catalog's readable directories correct. A
device writing elsewhere, to a scratch disk or inside a container, skips the
keyword and owns its paths:
class FastCamera(Device):
def __init__(self, name: str, scratch: str) -> None:
super().__init__(name=name)
self._provider = StaticPathProvider(UUIDFilenameProvider(), scratch)
Two devices must not share a data key: they would write the same filenames in one place, and the last writer to close wins.
A device's own provider cannot be retargeted from outside: ophyd-async's
PathProvider has only __call__. set_base_dir exists because redsun
built its provider itself.