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 |