Events and Callbacks¶
Terminology confusion!
A quick warning on terminology: the name "event" may refer to two different things in pymmcore-plus, which are not to be confused.
This page discusses the "events" that are emitted by the CMMCorePlus and
MDARunner objects. These are occurrences that your program can react to
by registering callback functions. These are also known as "signals" in the
context of the Qt framework or psygnal.
The term "event" may also be used to refer to the useq.MDAEvent objects that
are consumed by the Acquisition Engine.
These are not the same as the "events" discussed on this page.
Both the CMMCorePlus object and the
CMMCorePlus.mda (MDARunner) objects have
events attributes that can be used to register callbacks for events that occur
as the state of the microscope changes, or as an acquisition sequence progresses.
Event backends¶
pymmcore-plus supports both Qt-based and
psygnal-based event signaling.
The default behavior is to use Qt-backed signals when a global QApplication
instance exists at the time the CMMCorePlus object is created, and psygnal
otherwise. The backend is chosen once, when the core is created, so if you're
building a Qt application, create the QApplication before the CMMCorePlus.
(The difference matters mostly for threads, see below.)
If you would like to force a specific event backend, you can do so by
setting the PYMM_SIGNALS_BACKEND environment variable to qt,
psygnal, or auto (for the default behavior).
Connecting callbacks to events¶
To connect a callback to an event, use the connect method of the specific
event emitter that you would like to listen to. (All event backends
support the same connection/disconnection API.)
Example: Connecting callbacks
Given a CMMCorePlus object core:
Register a callback to listen to property changes on the CMMCorePlus:
@core.events.propertyChanged.connect
def on_property_changed(dev: str, prop: str, value: str):
print(f"Property {prop!r} on device {dev!r} changed to {value}")
Register a callback to process data during an acquisition sequence:
The signature of the callback function should match the signature of the event emitter's signal (although it may accept fewer arguments than the signal emits, in which case the extra ones are dropped). The available events and their signatures are listed in the events reference, and are defined by two protocols:
PCoreSignaler: events emitted atCMMCorePlus.eventswhen the state of the microscope changes.PMDASignaler: events emitted atCMMCorePlus.mda.eventsas an acquisition sequence progresses.
You may disconnect a callback from an event by calling the disconnect method
of the event emitter with the callback function as the argument.
Listening to a single device or property¶
propertyChanged fires for every property on every device. If you only care
about one device (or one property on one device), use
devicePropertyChanged.
Note that the callback then receives fewer arguments:
def on_gain_changed(value: str):
print(f"Gain is now {value}")
def on_camera_changed(prop: str, value: str):
print(f"Camera {prop} is now {value}")
# called with (value) when the camera's Gain changes
core.events.devicePropertyChanged("Camera", "Gain").connect(on_gain_changed)
# called with (property_name, value) when any camera property changes
core.events.devicePropertyChanged("Camera").connect(on_camera_changed)
Threads¶
run_mda runs the acquisition on a new
thread, and all of the core.mda.events signals are emitted from that thread.
(core.mda.run() blocks and runs the acquisition in the thread you called it
from, so the signals are emitted there.) What happens next depends on the event
backend:
- With psygnal, callbacks are called synchronously, in the acquisition
thread. A slow
frameReadycallback will delay the next event, so do heavy processing elsewhere (handler objects passed tooutput=are called from a separate thread, so they don't hold up the acquisition). Don't touch Qt widgets from these callbacks. - With Qt, callbacks connected from the main thread are delivered on the main thread (through Qt's event loop), which is what you want for updating a GUI.
The same goes for core.events: with psygnal, callbacks run in whichever thread
made the change (usually yours, but some device adapters notify the core from
their own threads).
See processing frames yourself for handing frames off to another thread.
Following changes in the core¶
Here's a small script that reacts to a change in the microscope's state (the same pattern you'd use to keep a GUI in sync with the hardware):
... which prints new objective is Nikon 20X Plan Fluor ELWD.
State devices emit two events
When a state device (like a filter wheel or objective turret) changes, two
propertyChanged events are emitted: one for the State property and one for
the Label property.
So it's usually best to check the property name and only respond to one of them, as the example above does.