From 8a08ec3e9bf9fec955a874ea479bdbb9dc7c514d Mon Sep 17 00:00:00 2001 From: x01dc Date: Thu, 16 Jul 2026 16:23:01 +0200 Subject: [PATCH] fix(bec_widgets): use cached device reads for sample-storage & slit polling Both widgets' polling loops were issuing live EPICS round-trips every 2s even though the underlying signals are auto_monitor=True and BEC already keeps a fresh monitored value locally. Switching to read(cached=True) removes that redundant hardware traffic; for the slit widget this also makes the old round-robin "selected slit every tick, others in rotation" strategy unnecessary, so it now polls all six slits every tick. --- .../widgets/sample_storage/sample_storage.py | 8 +++- .../widgets/slit_control/slit_control.py | 44 +++++++++++-------- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/csaxs_bec/bec_widgets/widgets/sample_storage/sample_storage.py b/csaxs_bec/bec_widgets/widgets/sample_storage/sample_storage.py index b57aa49..1735c3d 100644 --- a/csaxs_bec/bec_widgets/widgets/sample_storage/sample_storage.py +++ b/csaxs_bec/bec_widgets/widgets/sample_storage/sample_storage.py @@ -256,10 +256,16 @@ class SampleStorageWidget(BECWidget, QWidget): ``flomni_samples_sample_in_gripper_name``), the same keys the CLI iterated over in ftransfer_sample_change. The raw name signal packs name+owner (see ``sample_desc_codec``), unpacked here. + + Every underlying signal is an ``auto_monitor=True`` ``EpicsSignal`` + (see ``FlomniSampleStorage``), so BEC already keeps a fresh cached + value per signal via CA monitor callbacks. We pass ``cached=True`` + so the poll returns those monitored values instead of issuing a + live EPICS round-trip for all ~23 signals every 2 s. """ result: dict[int, tuple[bool, str, str]] = {} try: - data = self._samples.read() + data = self._samples.read(cached=True) except Exception as exc: logger.warning(f"SampleStorageWidget: bulk read failed: {exc}") # keep whatever is currently displayed rather than blanking out diff --git a/csaxs_bec/bec_widgets/widgets/slit_control/slit_control.py b/csaxs_bec/bec_widgets/widgets/slit_control/slit_control.py index 3dc3658..ad9960e 100644 --- a/csaxs_bec/bec_widgets/widgets/slit_control/slit_control.py +++ b/csaxs_bec/bec_widgets/widgets/slit_control/slit_control.py @@ -130,7 +130,6 @@ class SlitControlWidget(BECWidget, QWidget): self.step = self._STEP_DEFAULT self._ov_items: dict[tuple[int, int], QTableWidgetItem] = {} self._shortcuts: list[QShortcut] = [] - self._slow_poll_idx: int = 0 self._fetch_thread: Optional[threading.Thread] = None self._slit_data_ready.connect(self._apply_slit_data) self._locked: bool = False @@ -478,11 +477,17 @@ class SlitControlWidget(BECWidget, QWidget): sc.setEnabled(True) def _populate_target_spins(self) -> None: - """Pre-fill target spinboxes with current device values.""" + """Pre-fill target spinboxes with current device values. + + Uses ``cached=True``: the motor's readback signal is + ``auto_monitor=True``, so BEC already holds a fresh value pushed via + CA monitor and a live round-trip isn't needed for this display-only + prefill. + """ for suffix, spin in self._target_spins.items(): try: device = getattr(self.dev, self._dev_name(suffix)) - reading = device.read() + reading = device.read(cached=True) value = float(reading[self._dev_name(suffix)]["value"]) spin.setValue(value) except Exception: @@ -527,14 +532,16 @@ class SlitControlWidget(BECWidget, QWidget): def _refresh_overview(self) -> None: """ Kick off a background thread to fetch slit values without blocking the - Qt main thread. device.read() on virtual slit devices triggers RPC - calls to the device server; doing those in the main thread stalls the UI. - The thread emits _slit_data_ready when done; _apply_slit_data updates - the table and target spinboxes safely from the main thread. + Qt main thread. The thread emits _slit_data_ready when done; + _apply_slit_data updates the table and target spinboxes safely from + the main thread. - Per-tick strategy (2 s timer): - - Selected slit: every tick. - - Other slits: one per tick in rotation (~10 s per non-selected slit). + The fetch uses ``cached=True`` (see ``_fetch_one_slit``): readback + signals are ``auto_monitor=True``, so each read is a local lookup of + BEC's monitor-updated cache rather than a live EPICS round-trip. + That makes it cheap enough to read all six slits every 2 s tick — + no need for the round-robin "selected slit every tick, others in + rotation" strategy a live read would have required. """ if self._fetch_thread is not None and self._fetch_thread.is_alive(): return # previous fetch still running – skip this tick @@ -544,21 +551,22 @@ class SlitControlWidget(BECWidget, QWidget): def _fetch_slit_data(self) -> None: """Runs in background thread – NO Qt widget access here.""" data: dict[tuple[int, str], Optional[float]] = {} - self._fetch_one_slit(self.slit, data) - others = [n for n in self._SLIT_LABELS if n != self.slit] - if others: - self._slow_poll_idx = self._slow_poll_idx % len(others) - self._fetch_one_slit(others[self._slow_poll_idx], data) - self._slow_poll_idx += 1 + for slit_num in self._SLIT_LABELS: + self._fetch_one_slit(slit_num, data) self._slit_data_ready.emit(data) # thread-safe: Qt queues it to main thread def _fetch_one_slit(self, slit_num: int, data: dict[tuple[int, str], Optional[float]]) -> None: - """Fetch four axis values for one slit into data (background thread).""" + """Fetch four axis values for one slit into data (background thread). + + Uses ``cached=True`` — the motor readback signals are + ``auto_monitor=True``, so this display poll reads BEC's + monitor-updated cache instead of issuing a live EPICS round-trip. + """ for suffix in ("xc", "xs", "yc", "ys"): dev_name = f"sl{slit_num}{suffix}" try: device = getattr(self.dev, dev_name) - reading = device.read() + reading = device.read(cached=True) data[(slit_num, suffix)] = float(reading[dev_name]["value"]) except Exception: data[(slit_num, suffix)] = None