From 64a48240546846fdf4541c2adf3a0a5a0829f948 Mon Sep 17 00:00:00 2001 From: appel_c Date: Thu, 1 May 2025 19:49:10 +0200 Subject: [PATCH] fix(waveform): Ignore callbacks for on_async_readback from QtSender objects that are already destroyed; closes #497 --- bec_widgets/utils/bec_dispatcher.py | 8 +++++--- bec_widgets/widgets/plots/waveform/waveform.py | 12 +++++++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/bec_widgets/utils/bec_dispatcher.py b/bec_widgets/utils/bec_dispatcher.py index 5f722c8b..4e9c072b 100644 --- a/bec_widgets/utils/bec_dispatcher.py +++ b/bec_widgets/utils/bec_dispatcher.py @@ -27,8 +27,9 @@ if TYPE_CHECKING: # pragma: no cover class QtThreadSafeCallback(QObject): cb_signal = pyqtSignal(dict, dict) - def __init__(self, cb): + def __init__(self, cb: Callable, cb_info: dict | None = None): super().__init__() + self.cb_info = cb_info self.cb = cb self.cb_signal.connect(self.cb) @@ -37,7 +38,7 @@ class QtThreadSafeCallback(QObject): # make 2 differents QtThreadSafeCallback to look # identical when used as dictionary keys, if the # callback is the same - return id(self.cb) + return f"{id(self.cb)}{self.cb_info}".__hash__() def __call__(self, msg_content, metadata): self.cb_signal.emit(msg_content, metadata) @@ -141,6 +142,7 @@ class BECDispatcher: self, slot: Callable, topics: Union[EndpointInfo, str, list[Union[EndpointInfo, str]]], + cb_info: dict | None = None, **kwargs, ) -> None: """Connect widget's qt slot, so that it is called on new pub/sub topic message. @@ -150,7 +152,7 @@ class BECDispatcher: the corresponding pub/sub message topics (EndpointInfo | str | list): A topic or list of topics that can typically be acquired via bec_lib.MessageEndpoints """ - slot = QtThreadSafeCallback(slot) + slot = QtThreadSafeCallback(cb=slot, cb_info=cb_info) self.client.connector.register(topics, cb=slot, **kwargs) topics_str, _ = self.client.connector._convert_endpointinfo(topics) self._slots[slot].update(set(topics_str)) diff --git a/bec_widgets/widgets/plots/waveform/waveform.py b/bec_widgets/widgets/plots/waveform/waveform.py index e74b2133..489447e0 100644 --- a/bec_widgets/widgets/plots/waveform/waveform.py +++ b/bec_widgets/widgets/plots/waveform/waveform.py @@ -1182,10 +1182,11 @@ class Waveform(PlotBase): self.on_async_readback, MessageEndpoints.device_async_readback(self.scan_id, name), from_start=True, + cb_info={"scan_id": self.scan_id}, ) logger.info(f"Setup async curve {name}") - @SafeSlot(dict, dict) + @SafeSlot(dict, dict, verify_sender=True) def on_async_readback(self, msg, metadata): """ Get async data readback. This code needs to be fast, therefor we try @@ -1204,6 +1205,14 @@ class Waveform(PlotBase): msg(dict): Message with the async data. metadata(dict): Metadata of the message. """ + sender = self.sender() + if not hasattr(sender, "cb_info"): + logger.info(f"Sender {sender} has no cb_info.") + return + scan_id = sender.cb_info.get("scan_id", None) + if scan_id != self.scan_id: + logger.info("Scan ID mismatch, ignoring async readback.") + instruction = metadata.get("async_update", {}).get("type") if instruction not in ["add", "add_slice", "replace"]: logger.warning(f"Invalid async update instruction: {instruction}") @@ -1212,6 +1221,7 @@ class Waveform(PlotBase): plot_mode = self.x_axis_mode["name"] for curve in self._async_curves: x_data = None # Reset x_data + y_data = None # Reset y_data # Get the curve data async_data = msg["signals"].get(curve.config.signal.entry, None) if async_data is None: