How to change a device setting while a plan runs¶
A user may change a device setting from a view while the engine runs a plan,
and the change has to be applied without corrupting what the plan records. A
camera's region of interest is the usual case. If it changes halfway through a
point, one event stream carries frames of two shapes, and the store the service
writes no longer matches the
StreamResource describing it.
Any setting a plan's readings depend on has the same problem.
Deferrals avoids it by applying the change between
two messages of the plan. Once the message under way completes, every change
queued by then runs before the next message is sent. The plan contains nothing
about it, so every plan gets the behaviour, and the plan is neither suspended
nor rewound. A change asked for during a plan's last message runs before the
plan returns, so whoever waits on the plan's result finds it applied.
Create it beside the engine¶
Whoever owns the RunEngine builds the
Deferrals and shares it:
from redsun import provides
from redsun.engine import Deferrals, RunEngine
class AcquisitionPresenter:
def __init__(self, name: str) -> None:
self.name = name
self.engine = RunEngine()
self._deferrals = Deferrals(self.engine)
@provides
def deferrals(self) -> Deferrals:
return self._deferrals
Ask for it where the setting is written¶
A component writing a setting asks for Deferrals in setup, and hands the
write over as a coroutine function:
from redsun import DevicesOf, slot
from redsun.engine import Deferrals
class DetectorPresenter:
def __init__(self, name: str, *, detectors: DevicesOf[HasRoi]) -> None:
self.name = name
self.detectors = detectors
def setup(self, deferrals: Deferrals) -> None:
self._deferrals = deferrals
@slot
async def set(self, detector: str, value: str) -> None:
signal = self.detectors[detector].roi
async def apply() -> None:
await signal.set(value)
self._deferrals.request(apply)
request returns at once, with a future that is done once the change ran on
the engine's loop. While a plan runs, the change waits for the next message
boundary, and while none runs, it is applied straight away. Several changes
asked for during one message are applied together, in the order asked. A change
that raises is logged under redsun, and the ones after it still run.
A coroutine that blocks on the future stalls its loop
A thread may wait on the future, but a coroutine must not block on it,
because that stops the coroutine's event loop while it waits. From a
coroutine, await asyncio.wrap_future(future) instead.
What a plan sees¶
The plan sees nothing. Deferrals is a preprocessor on the engine, and it runs
each queued change as a wait_for inserted before the plan's next message.
No message runs twice. A bluesky
suspension would rewind to the last
checkpoint and replay what came after
it, which is why Deferrals doesn't use one. A change waits as long as the
message under way takes, so a plan that sleeps or waits for a long move delays
it by that much.