Skip to content

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).

export PYMM_SIGNALS_BACKEND=psygnal
set PYMM_SIGNALS_BACKEND=psygnal
# before instantiating CMMCorePlus
import os

os.environ["PYMM_SIGNALS_BACKEND"] = "psygnal"
$env:PYMM_SIGNALS_BACKEND = "psygnal"

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:

import numpy as np
import useq

from pymmcore_plus import CMMCorePlus

core = CMMCorePlus()

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:

@core.mda.events.frameReady.connect
def on_image_captured(data: np.ndarray, event: useq.MDAEvent, metadata: dict):
    print(f"Event index {event.index} captured with shape {data.shape}")

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 at CMMCorePlus.events when the state of the microscope changes.
  • PMDASignaler: events emitted at CMMCorePlus.mda.events as 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.

Example: Disconnecting callbacks

core.events.propertyChanged.disconnect(on_property_changed)

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 frameReady callback will delay the next event, so do heavy processing elsewhere (handler objects passed to output= 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):

properties_and_state_events.py
from pymmcore_plus import CMMCorePlus

core = CMMCorePlus.instance()
core.loadSystemConfiguration()


@core.events.propertyChanged.connect
def _on_prop_changed(dev, prop, value):
    # Note that when state devices change either the state or the label
    # TWO propertyChanged events will be emitted, one for prop 'State'
    # and one for prop 'Label'. Probably, best to check the property
    # name and only respond to one of them
    if dev == "Objective" and prop == "Label":
        print("new objective is", value)


core.setState("Objective", 3)

... 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.

propertyChanged: Objective State 3
propertyChanged: Objective Label Nikon 20X Plan Fluor ELWD

So it's usually best to check the property name and only respond to one of them, as the example above does.