MXCuBECore HardwareObject signals#

MXCuBE relies heavily on signals being emitted and listened to by many elements. For example, a hardware object may listen to other lower level hardware objects in order to update values after some calculation. But it is also critical in the UI (both web and Qt), in both cases they expect periodic signal updates for displaying the most recent information to the user (e.g. motor positions, data collection state, etc.)

Implementation#

Depending on the installed modules, signals are emitted using Louie or PyDispatcher. The former being based on the latter. The developer does not need to deal with the differences between those two modules as it is already being handled in the file mxcubecore/dispatcher.py.

Note

Can we remove any of those dependencies?

PyDispatcher provides the Python programmer with a multiple-producer-multiple-consumer signal registration and routing infrastructure for use in multiple contexts.

—[PyDispatcher](https://pypi.org/project/PyDispatcher/2.0.7/)

When certain events or conditions occur within a hardware object, corresponding signals are emitted to inform connected components or modules about these changes.

The mxcubecore.BaseHardwareObjects.HardwareObject class serves as the base class for all hardware objects in MXCuBE. It includes methods for defining and emitting signals, allowing derived classes to customize signal emission based on their specific requirements.

Note

Strictly speaking it is the HardwareObject or HardwareObjectYaml class (both inherit from HardwareObjectMixin). Once we unify the YAML and XML configuration, this distinction should hopefully disappear.

Emit#

Signals are typically emitted when the state of a hardware object changes, such as when it becomes ready for operation, encounters an error, or completes a task. Additionally, signals may be emitted to indicate changes in parameters or settings of the hardware, such as new setpoints, values, or configuration options.

To emit a signal, derived classes can use the mxcubecore.BaseHardwareObjects.HardwareObjectMixin.emit() method provided by the HardwareObject class. This method takes the name of the signal as an argument and optionally includes additional data or parameters to pass along with the signal. This method calls the dispatcher.send method.

From the mxcubecore.BaseHardwareObjects.HardwareObjectMixin class (removing extra lines for brevity):

    def emit(self, signal: Union[str, object, Any], *args) -> None:
        signal = str(signal)

        if len(args) == 1:
            if isinstance(args[0], tuple):
                args = args[0]
        dispatcher.send(signal, self, *args)

So, in a custom hardware object, since it inherits from mxcubecore.BaseHardwareObjects.HardwareObject, one only needs to call:

self.emit('my_signal', new_value)

Receive#

mxcubecore.BaseHardwareObjects.HardwareObjectMixin implements the following connect method, built around the homonymous method of PyDispatcher. Making it more convenient to use. The functions provides syntactic sugar: instead of self.connect(self, "signal", slot) it is possible to do self.connect("signal", slot).

From the HardwareObjectMixin.connect() method (removing extra lines for brevity):

    def connect(
        self,
        sender: Union[str, object, Any],
        signal: Union[str, Any],
        slot: Optional[Callable] = None,
    ) -> None:
        """Connect a signal sent by self to a slot.

        Args:
            sender (Union[str, object, Any]): If a string, interprted as the signal.
            signal (Union[str, Any]): In practice a string, or dispatcher.
            Any if sender is a string interpreted as the slot.
            slot (Optional[Callable], optional): In practice a functon or method.
            Defaults to None.

        Raises:
            ValueError: If slot is None and "sender" parameter is not a string.
        """

        if slot is None:
            if isinstance(sender, str):
                slot = signal
                signal = sender
                sender = self
            else:
                raise ValueError("invalid slot (None)")

        signal = str(signal)

        dispatcher.connect(slot, signal, sender)

        self.connect_dict[sender] = {"signal": signal, "slot": slot}

        if hasattr(sender, "connect_notify"):
            sender.connect_notify(signal)

And an example usage on a custom hardware object would be:

self.connect(some_other_hwobj, "a_signal", callback_method)

This assumes that some_other_hwobj is linked in the custom hardware object initialization, and callback_method must exist, otherwise an exception will happen once the signal is received.

If the sender hardware object has a method named connect_notify, it will be called on connect. Since this connect happens at application initialization, this typically triggers the emission of all signals during initialization, and thus all receivers start with the most recent value.

Basic example#

Given two hardware objects:

from mxcubecore.BaseHardwareObjects import HardwareObject
import gevent
from gevent import monkey; monkey.patch_all(thread=False)
import random
import datetime

class HO1(HardwareObject):
    """
        <object class="HO1">
        </object>
    """

    def __init__(self, name):
        super().__init__(name)
        self._value = 0.0
        self.run = False

    def get_value(self):
        return self._value

    def update_value(self):
        while self.run:
            _new_val = random.random()
            self._value = _new_val
            print(f'{datetime.datetime.now()} | valueChanged emitted, new value: {self._value}')
            self.emit("valueChanged", self._value)
            gevent.sleep(3)

    def start(self):
        self.run = True
        gevent.spawn(self.update_value)

    def stop(self):
        self.run = False

and a data consumer:

from mxcubecore.BaseHardwareObjects import HardwareObject
import datetime

class HO2(HardwareObject):
    """
        <object class="HO2">
          <object hwrid="/ho1" role="ho1"/>
        </object>
    """


    def __init__(self, name):
        super().__init__(name)
        self._value = 0.0
        self.ho1 = None

    def init(self):
        self.ho1 = self.get_object_by_role("ho1")
        self.connect(self.ho1, "valueChanged", self.callback)

    def callback(self, *args):
        print(f"{datetime.datetime.now()} | valueChanged callback, arguments: {args}")

One could run both:

In [1]: from mxcubecore import HardwareRepository as hwr
   ...: hwr_dir='mxcubecore/configuration/mockup/test/'
   ...: hwr.init_hardware_repository(hwr_dir)
   ...: hwrTest = hwr.get_hardware_repository()
   ...: ho1 = hwrTest.get_hardware_object("/ho1")
   ...: ho2 = hwrTest.get_hardware_object("/ho2")
2024-03-18 12:20:18,434 |INFO   | Hardware repository: ['/Users/mikegu/Documents/MXCUBE/mxcubecore_upstream/mxcubecore/configuration/mockup/test']
+======================================================================================+
| role             | Class      | file                   | Time (ms)| Comment
+======================================================================================+
| beamline         | Beamline   | beamline_config.yml    | 9        | Start loading contents:
| mock_procedure   | None       | procedure-mockup.yml   | 0        | File not found
| beamline         | Beamline   | beamline_config.yml    | 9        | Done loading contents
+======================================================================================+

In [2]: ho1.start()

2024-03-18 12:21:15.401871 | valueChanged emitted, new value: 0.7041173058901172
2024-03-18 12:21:15.402110 | valueChanged callback, arguments: (0.7041173058901172,)
2024-03-18 12:21:18.407419 | valueChanged emitted, new value: 0.39293503718591827
2024-03-18 12:21:18.407770 | valueChanged callback, arguments: (0.39293503718591827,)
2024-03-18 12:21:21.411648 | valueChanged emitted, new value: 0.8190801968640632
2024-03-18 12:21:21.411897 | valueChanged callback, arguments: (0.8190801968640632,)
2024-03-18 12:21:24.417379 | valueChanged emitted, new value: 0.5170546126120815
2024-03-18 12:21:24.418428 | valueChanged callback, arguments: (0.5170546126120815,)
2024-03-18 12:21:27.420696 | valueChanged emitted, new value: 0.27400475091220955
2024-03-18 12:21:27.421434 | valueChanged callback, arguments: (0.27400475091220955,)
2024-03-18 12:21:30.426785 | valueChanged emitted, new value: 0.3473955083798488
2024-03-18 12:21:30.427018 | valueChanged callback, arguments: (0.3473955083798488,)
2024-03-18 12:21:33.427715 | valueChanged emitted, new value: 0.9503048610962694
2024-03-18 12:21:33.427902 | valueChanged callback, arguments: (0.9503048610962694,)
In [3]: ho1.stop()

As can be seen, the second hardware object receives and processes first one’s signal.

Note

At least one entry must appear in the beamline’s YAML configuration file. In this case only the procedure mockup is present, all the other mockups are commented. That is why only a few items appear in the loading table.

General signals List#

The following tables list the generic signals as well as the signals from some of the most important hardware objects.

Note

Additional signals could be emitted by other hardware objects. For example, "energyScanFinished" by the energy scan object, and similar. For the sake of keeping this document digestable not all the signals are listed.

Queue#

Signal

Description

Signature

Notes

child_added

“child_added”, (parent_node, child_node)

child_removed

“child_removed”, (parent, child))

statusMessage

“statusMessage”, (“status”, “message””, “message”)

Unclear purpose, arbitrary usage of the messages

queue_execution_finished

“queue_execution_finished”

queue_execution_finished

“queue_execution_finished”

queue_entry_execute_started

“queue_entry_execute_started”, entry

queue_entry_execute_finished

“queue_entry_execute_finished”, statusMessage

queue_stopped

“queue_stopped”

queue_paused

“queue_paused”

show_workflow_tab

“show_workflow_tab”

Difractometer#

Useful data types:

centring_status = {
    "valid": bool,
    "startTime": str, # "%Y-%m-%d %H:%M:%S"
    "startTime": str, # "%Y-%m-%d %H:%M:%S"
    "angleLimit": bool,
    "motors": MotorsDict, # see below
    "accepted": bool,
}

motor_positions = {
    "phi": float,
    "phiy": float,
    "phiz": float,
    "sampx": float,
    "sampy": float,
    "kappa": float,
    "kappa_phi": float,
    "phi": float,
    "zoom": float?, # optional
    "beam_x": float,
    "beam_y": float
}

GenericDiffractometer.py#

Signal

Description

Signature

Notes

minidiffTransferModeChanged

“minidiffTransferModeChanged”, mode

minidiffPhaseChanged

“minidiffPhaseChanged”, currentPhase

newAutomaticCentringPoint

“newAutomaticCentringPoint”, motorPositions

centringInvalid

“centringInvalid”

centringStarted

“centringStarted”, (method, False)

centringMoving

“centringMoving”

centringFailed

“centringFailed”, (method, dict:centring_status)

centringSuccessful

“centringSuccessful”, (method, dict:centring_status)

newAutomaticCentringPoint

“newAutomaticCentringPoint”, motorPos

centringAccepted

“centringAccepted”, (bool: accepted, dict:centring_status)

fsmConditionChanged

“fsmConditionChanged”, (message, bool)

Also emitted in collect so unclear purpose

progressMessage

“progressMessage”, msg

diffractometerMoved

“diffractometerMoved”

pixelsPerMmChanged

“pixelsPerMmChanged”, (pixels_per_mm_x, pixels_per_mm_y)

zoomMotorStateChanged

“zoomMotorStateChanged”, state

zoomMotorPredefinedPositionChanged

“zoomMotorPredefinedPositionChanged”, (position_name, offset)

minidiffStateChanged

“minidiffStateChanged”, state

minidiffReady

“minidiffReady”

minidiffNotReady

“minidiffNotReady”

minidiffSampleIsLoadedChanged

“minidiffSampleIsLoadedChanged”, sampleIsLoaded

minidiffHeadTypeChanged

“minidiffHeadTypeChanged”, headType

minidiffNotReady

“minidiffNotReady”

Minidiff.py#

Signal

Description

Signature

Notes

diffractometerMoved

“diffractometerMoved”

minidiffReady

“minidiffReady”

minidiffNotReady

“minidiffNotReady”

minidiffStateChanged

“minidiffStateChanged”, state

zoomMotorStateChanged

“zoomMotorStateChanged”, state

zoomMotorPredefinedPositionChanged

“zoomMotorPredefinedPositionChanged”, (position_name, offset)

phiMotorStateChanged

“phiMotorStateChanged”, state

phizMotorStateChanged

“phizMotorStateChanged”, state

phiyMotorStateChanged

“phiyMotorStateChanged”, state

sampxMotorStateChanged

“sampxMotorStateChanged”, state

sampyMotorStateChanged

“sampyMotorStateChanged”, state

centringInvalid

“centringInvalid”

centringStarted

“centringStarted”, (method, False)

centringMoving

“centringMoving”

centringFailed

“centringFailed”, (method, dict:centring_status)

centringSuccessful

“centringSuccessful”, (method, dict:centring_status)

newAutomaticCentringPoint

“newAutomaticCentringPoint”, motorPos

centringAccepted

“centringAccepted”, (bool: accepted, dict:centring_status)

centringSnapshots

“centringSnapshots”, boolean

progressMessage

“progressMessage”, msg