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.
This commit is contained in:
x01dc
2026-07-16 16:23:01 +02:00
parent 6744908983
commit 8a08ec3e9b
2 changed files with 33 additions and 19 deletions
@@ -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
@@ -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